Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ Unattended runs never self-approve: their asks park in an inbox until a human an

Model access is yours: pick a provider, paste your key, switch anytime. Supported out of the box:

**OpenAI · Anthropic · Google Gemini · BytePlus Ark · Volcengine Ark Agent Plan · Inkling (Thinking Machines) · GLM (Z.ai) · DeepSeek · Kimi (Moonshot) · Qwen · MiniMax · Mistral · Grok (xAI)** - plus open-weight models via **Together** and **Fireworks**, and fully local models via **Ollama**.
**OpenAI · Anthropic · Google Gemini · BytePlus Ark · Volcengine Ark Agent Plan · Inkling (Thinking Machines) · GLM (Z.ai) · DeepSeek · Kimi (Moonshot) · Qwen · MiniMax · Mistral · Grok (xAI)** - plus many labs behind a single key via **aimlapi.com**, **Together** and **Fireworks**, and fully local models via **Ollama**.

A curated model list marks what we've verified for tool-calling work. Adding any model string works at your own risk.

Expand Down
30 changes: 29 additions & 1 deletion coworker/providers/matrix.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
showing a made-up denominator. Values entered 2026-07-28 from vendor docs; verify alongside
the id refresh.

Resellers: Together + Fireworks + OpenRouter. TODO: add Groq entries here AND its
Resellers: Together + Fireworks + OpenRouter + aimlapi.com. TODO: add Groq entries here AND its
descriptor in ``registry.py`` once the current provider surface is tested — deliberately
deferred to bound how much needs verifying at once.
"""
Expand All @@ -37,6 +37,12 @@
_AGENTIC_VISION = ModelCapabilities(
tools=True, vision=True, pdf=True, parallel_tool_calls=True, streaming=True
)
# A reseller row whose image input is verified live but whose PDF part is not: images go
# on the wire as usual, PDFs still take the pdf_support.py fallback. Kept separate from
# _AGENTIC_VISION so no reseller silently claims native PDF ingestion it hasn't shown.
_AGENTIC_VISION_NO_PDF = ModelCapabilities(
tools=True, vision=True, parallel_tool_calls=True, streaming=True
)


@dataclass(frozen=True)
Expand Down Expand Up @@ -228,6 +234,28 @@ class ModelEntry:
"openrouter:stealth/ox-alpha": ModelEntry(
"Ox Alpha · via OpenRouter", _AGENTIC, 1_048_576
),
# aimlapi.com uses its OWN id namespace — do NOT reuse the OpenRouter slugs above,
# they are not aliases there. Every id below was checked against
# `GET /v1/models?include=all` on 2026-09-03 (present as an id, `type ==
# "openai/chat-completions"`) AND round-tripped with a live completion whose echoed
# `model` came back as the same model, because an id there can also be an alias of a
# DIFFERENT model. Context windows are that catalog's `info.contextLength` — note it
# is nested, there is no top-level `context_length`. Prefer the dotted Anthropic
# spelling: the dashed `claude-sonnet-4-6` is a separate, streaming-only entry.
# Four rows on purpose — same budget the other three resellers get, and the matrix
# size cap in tests is a ceiling to prune under, not one to raise for a newcomer.
"aimlapi:openai/gpt-5.6-sol": ModelEntry(
"GPT-5.6 Sol · via aimlapi.com", _AGENTIC_VISION_NO_PDF, 1_050_000
),
"aimlapi:anthropic/claude-sonnet-4.6": ModelEntry(
"Claude Sonnet 4.6 · via aimlapi.com", _AGENTIC_VISION_NO_PDF, 200_000
),
"aimlapi:zhipu/glm-5.2": ModelEntry(
"GLM-5.2 · via aimlapi.com", _AGENTIC, 1_000_000
),
"aimlapi:deepseek/deepseek-v4-pro": ModelEntry(
"DeepSeek V4 Pro · via aimlapi.com", _AGENTIC, 1_000_000
),
# -- cloud accounts (models running in the user's own AWS/GCP) ----------------
# Bedrock ids carry a family segment (claude/ → native Anthropic path, other/ →
# Converse) plus AWS's own `-v<n>:<m>` version suffix. Some regions require the
Expand Down
9 changes: 9 additions & 0 deletions coworker/providers/openai_provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,7 @@ def __init__(
api_key: Optional[str] = None,
base_url: Optional[str] = None,
secrets: Any = None,
default_headers: Optional[dict[str, str]] = None,
):
# The SDK client is built lazily on first use, NOT at construction. This lets an engine
# be assembled before any key exists — the desktop app lets you enter the key in Settings
Expand All @@ -155,10 +156,16 @@ def __init__(
# `base_url` points the same OpenAI SDK at any OpenAI-compatible endpoint — used by the
# provider router for Ollama (`http://localhost:11434/v1`, with a placeholder key) and,
# later, other OpenAI-shaped backends. When None, behavior is identical to stock OpenAI.
#
# `default_headers` rides on every request this client makes — the same knob
# CodexProvider already uses for its backend headers. Endpoint-specific extras only
# (a gateway's app-attribution headers); the caller decides which endpoint earns
# them, and we copy the mapping so a shared constant can never be mutated here.
self._client = client
self._api_key = api_key
self._base_url = base_url
self._secrets = secrets
self._default_headers = dict(default_headers) if default_headers else None
self.default_model = default_model

def _ensure_client(self) -> Any:
Expand All @@ -175,6 +182,8 @@ def _ensure_client(self) -> Any:
kwargs: dict[str, Any] = {"api_key": key}
if self._base_url:
kwargs["base_url"] = self._base_url
if self._default_headers:
kwargs["default_headers"] = dict(self._default_headers)
self._client = OpenAI(**kwargs)
return self._client

Expand Down
80 changes: 77 additions & 3 deletions coworker/providers/registry.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,35 @@

DEFAULT_OLLAMA_URL = "http://localhost:11434"

# aimlapi.com identifies the calling app with OpenRouter's `HTTP-Referer`/`X-Title` pair
# plus two headers of its own. They name OpenWorker (the app making the call), not the
# gateway, and they carry no user data — they exist so the gateway can tell OpenWorker
# traffic apart from everyone else's. Deliberately NOT applied to any other vendor.
AIMLAPI_BASE_URL = "https://api.aimlapi.com/v1"
_AIMLAPI_ORIGIN = "https://api.aimlapi.com"
AIMLAPI_ATTRIBUTION_HEADERS: dict[str, str] = {
"HTTP-Referer": "https://github.com/andrewyng/openworker",
"X-Title": "OpenWorker",
"X-AIMLAPI-Partner-ID": "part_nLTCEqZPnFgQDrMIuu7M5CoQ",
"X-AIMLAPI-Source": "agent/openworker",
}


def _aimlapi_headers(base_url: str) -> Optional[dict[str, str]]:
"""Attribution headers, but only when the request is actually going to aimlapi.com.

`base_url` is a user-editable field, so a key repointed at a corporate proxy — or at
a different vendor entirely — must not carry these along. Scoping on the resolved
ORIGIN (not on the provider name) is what makes that impossible. Returns a fresh dict
so the module constant is never handed out to be mutated.
"""
from urllib.parse import urlsplit

parts = urlsplit((base_url or "").strip())
if f"{parts.scheme}://{parts.netloc}".lower() != _AIMLAPI_ORIGIN:
return None
return dict(AIMLAPI_ATTRIBUTION_HEADERS)


@dataclass(frozen=True)
class ProviderField:
Expand Down Expand Up @@ -201,12 +230,22 @@ def _build_ollama(profile: dict[str, Any], secrets: Any) -> ProviderClient:
return OpenAIProvider(api_key="ollama", base_url=base_url)


def _openai_compat(vendor: str, default_base_url: str, env_key: Optional[str] = None):
def _openai_compat(
vendor: str,
default_base_url: str,
env_key: Optional[str] = None,
headers_for: Optional[Callable[[str], Optional[dict[str, str]]]] = None,
):
"""Builder factory for vendors reached through their OpenAI-compatible API (Z AI, DeepSeek,
Kimi, MiniMax, Qwen, xAI, Mistral). The key is resolved from the vendor's OWN profile (or its
env var) — deliberately NOT from the OpenAI env/SecretStore fallback, so a configured OpenAI
key is never silently sent to a different vendor's endpoint. Missing key ⇒ fail fast with a
vendor-named error (these are only built on demand, when one of their models is selected).

`headers_for(base_url)` is the opt-in hook for a vendor that wants app-identifying headers
on its own endpoint (aimlapi.com). It receives the RESOLVED base URL so it can decline when
the user has repointed the provider elsewhere; returning None keeps the request byte-identical
to every other compat vendor's.
"""

def build(profile: dict[str, Any], secrets: Any) -> ProviderClient:
Expand All @@ -218,7 +257,11 @@ def build(profile: dict[str, Any], secrets: Any) -> ProviderClient:
raise RuntimeError(
f"No {vendor} API key configured — add it in Settings ▸ Models."
)
return OpenAIProvider(api_key=api_key, base_url=base_url)
return OpenAIProvider(
api_key=api_key,
base_url=base_url,
default_headers=headers_for(base_url) if headers_for else None,
)

return build

Expand Down Expand Up @@ -262,6 +305,7 @@ def _compat(
recommended_model: str,
env_key: str,
endpoint_help: str = "",
headers_for: Optional[Callable[[str], Optional[dict[str, str]]]] = None,
) -> ProviderDescriptor:
"""Descriptor for an OpenAI-compatible vendor: key + a prefilled, editable endpoint."""
vendor = title.split(" (")[0]
Expand All @@ -285,7 +329,7 @@ def _compat(
or f"Prefilled with {vendor}'s official endpoint; edit only for a regional or proxy variant.",
),
],
build=_openai_compat(vendor, base_url, env_key),
build=_openai_compat(vendor, base_url, env_key, headers_for),
recommended_model=recommended_model,
env_key=env_key,
blurb=f"Uses {vendor}'s OpenAI-compatible API — the endpoint is prefilled, just add your key.",
Expand Down Expand Up @@ -336,6 +380,17 @@ def _responses_compat(


DESCRIPTORS: list[ProviderDescriptor] = [
# Model ids here are aimlapi.com's OWN namespace and do NOT match OpenRouter's slugs
# even where the model is identical — checked against their live catalog 2026-09-03,
# three of OpenRouter's four ids resolve to nothing there. See matrix.py.
_compat(
"aimlapi",
"aimlapi.com",
base_url=AIMLAPI_BASE_URL,
recommended_model="zhipu/glm-5.2",
env_key="AIMLAPI_API_KEY",
headers_for=_aimlapi_headers,
),
ProviderDescriptor(
name="openai",
title="OpenAI",
Expand Down Expand Up @@ -986,6 +1041,25 @@ def verify_provider_key(
},
timeout=timeout,
)
elif name == "aimlapi":
# aimlapi.com's /models is PUBLIC: it answers 200 to a bogus key, an empty key
# and no Authorization header at all (verified 2026-09-03), so the usual
# list-models probe would green-light a typo'd key and leave the user to
# discover it at the first real turn. A one-token chat completion is the
# cheapest call that actually exercises the credential (401 on a bad key).
base = (base_url or "").strip().rstrip("/") or AIMLAPI_BASE_URL
headers = {"Authorization": f"Bearer {key}"}
headers.update(_aimlapi_headers(base) or {})
resp = httpx.post(
base + "/chat/completions",
headers=headers,
json={
"model": d.recommended_model,
"messages": [{"role": "user", "content": "Reply with OK."}],
"max_tokens": 1,
},
timeout=timeout,
)
else: # openai + any OpenAI-compatible endpoint (Azure, OpenRouter, vendors, vLLM…)
default_base = next(
(f.default for f in d.fields if f.key == "base_url" and f.default), ""
Expand Down
5 changes: 5 additions & 0 deletions surfaces/gui/e2e/fixtures.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,8 @@ const SETTINGS = {
"ark:dola-seed-2-1-turbo-260628": "Dola Seed 2.1 Turbo · BytePlus Ark",
"ark-agent-plan-cn:doubao-seed-evolving": "Doubao Seed Evolving · Volcengine Agent Plan",
"ark-agent-plan-cn:doubao-seed-2.1-turbo": "Doubao Seed 2.1 Turbo · Volcengine Agent Plan",
"aimlapi:zhipu/glm-5.2": "GLM-5.2 · via aimlapi.com",
"aimlapi:deepseek/deepseek-v4-pro": "DeepSeek V4 Pro · via aimlapi.com",
},
// Context windows (subset — mirrors /v1/settings.model_context_windows); drives the
// composer usage chip's context-fill meter.
Expand Down Expand Up @@ -367,6 +369,9 @@ const PROVIDERS = [
// have independent credentials, endpoints, and strict curated model lists.
{ name: "ark", title: "BytePlus Ark", needs_key: true, blurb: "Uses BytePlus Ark's OpenAI-compatible Responses API — the endpoint is prefilled, just add your key.", fields: [{ key: "api_key", label: "BytePlus Ark API key", secret: true, required: true, help: "", placeholder: "" }, { key: "base_url", label: "Endpoint", secret: false, required: false, help: "BytePlus Ark's Asia Pacific endpoint.", placeholder: "https://ark.ap-southeast.bytepluses.com/api/v3", default: "https://ark.ap-southeast.bytepluses.com/api/v3" }], configured: false, values: {}, suggested_models: ["dola-seed-evolving-latest-version", "dola-seed-2-1-turbo-260628"], key_set_at: null, last_used_at: null },
{ name: "ark-agent-plan-cn", title: "Volcengine Ark Agent Plan", needs_key: true, blurb: "Uses Volcengine Ark Agent Plan's OpenAI-compatible Responses API — the endpoint is prefilled, just add your key.", fields: [{ key: "api_key", label: "Volcengine Ark Agent Plan API key", secret: true, required: true, help: "", placeholder: "" }, { key: "base_url", label: "Endpoint", secret: false, required: false, help: "Volcengine Ark Agent Plan's China (Beijing) endpoint.", placeholder: "https://ark.cn-beijing.volces.com/api/plan/v3", default: "https://ark.cn-beijing.volces.com/api/plan/v3" }], configured: false, values: {}, suggested_models: ["doubao-seed-evolving", "doubao-seed-2.1-turbo"], key_set_at: null, last_used_at: null },
// aimlapi: a reseller — many labs' models behind one key, in ITS OWN id namespace
// (not OpenRouter's slugs). Unconfigured, prefilled endpoint, curated model preview.
{ name: "aimlapi", title: "aimlapi.com", needs_key: true, blurb: "Uses aimlapi.com's OpenAI-compatible API — the endpoint is prefilled, just add your key.", fields: [{ key: "api_key", label: "aimlapi.com API key", secret: true, required: true, help: "", placeholder: "" }, { key: "base_url", label: "Endpoint", secret: false, required: false, help: "Prefilled with aimlapi.com's official endpoint; edit only for a regional or proxy variant.", placeholder: "https://api.aimlapi.com/v1", default: "https://api.aimlapi.com/v1" }], configured: false, values: {}, suggested_models: ["zhipu/glm-5.2", "deepseek/deepseek-v4-pro"], key_set_at: null, last_used_at: null },
// ollama: keyless local provider — "configured" without proving anything runs; the
// onboarding gallery shows "No key needed" and its form is endpoint + Detect (§39).
{ name: "ollama", title: "Ollama (local models)", needs_key: false, fields: [{ key: "base_url", label: "Endpoint", secret: false, required: false, help: "", placeholder: "http://127.0.0.1:11434", default: "http://127.0.0.1:11434" }], configured: true, values: {}, suggested_models: ["qwen3-coder:30b"], key_set_at: null, last_used_at: null },
Expand Down
23 changes: 23 additions & 0 deletions surfaces/gui/e2e/settings.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,29 @@ test("Models: BytePlus and Volcengine Ark stay visually and operationally separa
await expect(preview).not.toContainText("Dola Seed");
});

test("Models: aimlapi.com is named aimlapi.com and previews its own model ids", async ({ page }) => {
await page.goto("/");
await page.getByTestId("account-row").click();
await page.getByRole("button", { name: "Settings", exact: true }).click();
await page.getByRole("button", { name: "Models", exact: true }).click();

// One display name everywhere, and its own brand mark rather than the fallback monogram.
const card = page.getByTestId("set-provider-aimlapi");
await expect(card).toContainText("aimlapi.com");
await expect(card).toContainText("Not set up");
expect(await card.locator("img").getAttribute("src")).toBeTruthy();

await card.click();
await page.getByTestId("set-endpoint-link").click();
await expect(page.getByTestId("set-field-base_url")).toHaveValue("https://api.aimlapi.com/v1");

// The reseller's ids are its own; OpenRouter's slug for the same model is not shown.
const preview = page.getByTestId("model-preview");
await expect(preview).toContainText("GLM-5.2 · via aimlapi.com");
await expect(preview).toContainText("DeepSeek V4 Pro · via aimlapi.com");
await expect(preview).not.toContainText("via OpenRouter");
});

// UX-021: a configured provider's form shows the in-field saved state and the Remove key…
// affordance; removing reverts the card to "Not set up".
test("Models: Remove key reverts a configured provider", async ({ page }) => {
Expand Down
12 changes: 12 additions & 0 deletions surfaces/gui/src/providers/ProviderSetup.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -120,3 +120,15 @@ describe("Ark provider presentation", () => {
);
});
});

describe("aimlapi.com provider presentation", () => {
it("carries its own brand mark rather than the fallback monogram", () => {
const { container } = render(<ProviderMark name="aimlapi" title="aimlapi.com" />);
expect(container.querySelector("img")).toBeTruthy();
});

it("links to its own API key console", () => {
expect(KEY_HELP.aimlapi.url).toBe("https://aimlapi.com/app/keys");
expect(KEY_HELP.aimlapi.label).toBe("aimlapi.com");
});
});
1 change: 1 addition & 0 deletions surfaces/gui/src/providers/ProviderSetup.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ export const KEY_HELP: Record<string, { url: string; label: string }> = {
qwen: { url: "https://modelstudio.console.alibabacloud.com", label: "alibabacloud.com" },
minimax: { url: "https://platform.minimax.io", label: "platform.minimax.io" },
xai: { url: "https://console.x.ai", label: "console.x.ai" },
aimlapi: { url: "https://aimlapi.com/app/keys", label: "aimlapi.com" },
};

export type Verify = { state: "idle" | "testing" | "ok" | "error"; msg?: string };
Expand Down
6 changes: 5 additions & 1 deletion surfaces/gui/src/providers/logos.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// Provider logo registry (UX-DECISIONS §39): official brand marks for the onboarding
// provider gallery. Most are vendored from the MIT-licensed lobe-icons set; BytePlus is
// its official website mark, used with permission. All stay bundled like connector assets
// its official website mark, used with permission, and aimlapi.com is its own hexagon
// mark. All stay bundled like connector assets
// (no CDN at runtime). Keys are /v1/providers names; unknown names get no mark (the gallery
// falls back to a neutral monogram). PROVIDER_ORDER is the gallery order — recognition
// first, long tail behind the scroll fold.
Expand All @@ -24,6 +25,7 @@ import qwen from "./logos/qwen.svg";
import minimax from "./logos/minimax.svg";
import xai from "./logos/xai.svg";
import meta from "./logos/meta.svg";
import aimlapi from "./logos/aimlapi.svg";

export const PROVIDER_LOGOS: Record<string, string> = {
anthropic,
Expand All @@ -48,9 +50,11 @@ export const PROVIDER_LOGOS: Record<string, string> = {
qwen,
minimax,
xai,
aimlapi,
};

export const PROVIDER_ORDER = [
"aimlapi",
"anthropic",
"openai",
"gemini",
Expand Down
1 change: 1 addition & 0 deletions surfaces/gui/src/providers/logos/aimlapi.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading