diff --git a/README.md b/README.md index 60172b41d0..24a26abf9c 100644 --- a/README.md +++ b/README.md @@ -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. diff --git a/coworker/providers/matrix.py b/coworker/providers/matrix.py index 052a19ff00..dc84d070ad 100644 --- a/coworker/providers/matrix.py +++ b/coworker/providers/matrix.py @@ -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. """ @@ -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) @@ -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:` version suffix. Some regions require the diff --git a/coworker/providers/openai_provider.py b/coworker/providers/openai_provider.py index 3e7ca306b3..341ec17076 100644 --- a/coworker/providers/openai_provider.py +++ b/coworker/providers/openai_provider.py @@ -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 @@ -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: @@ -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 diff --git a/coworker/providers/registry.py b/coworker/providers/registry.py index 397efee10f..b556c8efb4 100644 --- a/coworker/providers/registry.py +++ b/coworker/providers/registry.py @@ -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: @@ -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: @@ -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 @@ -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] @@ -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.", @@ -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", @@ -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), "" diff --git a/surfaces/gui/e2e/fixtures.ts b/surfaces/gui/e2e/fixtures.ts index 9c2d444bd4..64d855e13a 100644 --- a/surfaces/gui/e2e/fixtures.ts +++ b/surfaces/gui/e2e/fixtures.ts @@ -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. @@ -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 }, diff --git a/surfaces/gui/e2e/settings.spec.ts b/surfaces/gui/e2e/settings.spec.ts index 2c77c59f6b..0817465cf3 100644 --- a/surfaces/gui/e2e/settings.spec.ts +++ b/surfaces/gui/e2e/settings.spec.ts @@ -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 }) => { diff --git a/surfaces/gui/src/providers/ProviderSetup.test.tsx b/surfaces/gui/src/providers/ProviderSetup.test.tsx index 89d0e8d2d2..e1237b22c1 100644 --- a/surfaces/gui/src/providers/ProviderSetup.test.tsx +++ b/surfaces/gui/src/providers/ProviderSetup.test.tsx @@ -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(); + 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"); + }); +}); diff --git a/surfaces/gui/src/providers/ProviderSetup.tsx b/surfaces/gui/src/providers/ProviderSetup.tsx index 1f7559c95a..d1a6d5ae54 100644 --- a/surfaces/gui/src/providers/ProviderSetup.tsx +++ b/surfaces/gui/src/providers/ProviderSetup.tsx @@ -39,6 +39,7 @@ export const KEY_HELP: Record = { 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 }; diff --git a/surfaces/gui/src/providers/logos.ts b/surfaces/gui/src/providers/logos.ts index cbedcfb7f9..e9c6f9735d 100644 --- a/surfaces/gui/src/providers/logos.ts +++ b/surfaces/gui/src/providers/logos.ts @@ -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. @@ -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 = { anthropic, @@ -48,9 +50,11 @@ export const PROVIDER_LOGOS: Record = { qwen, minimax, xai, + aimlapi, }; export const PROVIDER_ORDER = [ + "aimlapi", "anthropic", "openai", "gemini", diff --git a/surfaces/gui/src/providers/logos/aimlapi.svg b/surfaces/gui/src/providers/logos/aimlapi.svg new file mode 100644 index 0000000000..4cd60ce44f --- /dev/null +++ b/surfaces/gui/src/providers/logos/aimlapi.svg @@ -0,0 +1 @@ +aimlapi.com diff --git a/tests/test_provider_verify.py b/tests/test_provider_verify.py index a6e553afa3..c210d5b12e 100644 --- a/tests/test_provider_verify.py +++ b/tests/test_provider_verify.py @@ -161,3 +161,48 @@ def test_verify_unexpected_status(monkeypatch): res = verify_provider_key("anthropic", api_key="sk-ant-x") assert res["ok"] is False assert "500" in res["error"] + + +def test_verify_aimlapi_probes_chat_not_the_public_models_list(monkeypatch): + """aimlapi.com's /models answers 200 to a bogus key, an empty key and no key at all + (verified 2026-09-03), so the generic list-models probe would green-light a typo. + Test must exercise the credential — one max_tokens=1 chat completion does.""" + cap: dict = {} + _patch_post(monkeypatch, status=200, capture=cap) + + assert verify_provider_key("aimlapi", api_key="aiml-key") == {"ok": True} + assert cap["url"] == "https://api.aimlapi.com/v1/chat/completions" + assert cap["headers"]["Authorization"] == "Bearer aiml-key" + assert cap["json"] == { + "model": "zhipu/glm-5.2", + "messages": [{"role": "user", "content": "Reply with OK."}], + "max_tokens": 1, + } + + +def test_verify_aimlapi_probe_carries_attribution(monkeypatch): + cap: dict = {} + _patch_post(monkeypatch, status=200, capture=cap) + verify_provider_key("aimlapi", api_key="aiml-key") + assert cap["headers"]["X-AIMLAPI-Partner-ID"].startswith("part_") + assert cap["headers"]["X-AIMLAPI-Source"] == "agent/openworker" + + +def test_verify_aimlapi_override_endpoint_drops_attribution(monkeypatch): + """Same origin scoping as the live client: a user-supplied endpoint is somebody + else's server until proven otherwise.""" + cap: dict = {} + _patch_post(monkeypatch, status=200, capture=cap) + verify_provider_key( + "aimlapi", api_key="aiml-key", base_url="https://gateway.example/v1/" + ) + assert cap["url"] == "https://gateway.example/v1/chat/completions" + assert "X-AIMLAPI-Partner-ID" not in cap["headers"] + + +def test_verify_aimlapi_bad_key_is_invalid(monkeypatch): + _patch_post(monkeypatch, status=401) + assert verify_provider_key("aimlapi", api_key="nope") == { + "ok": False, + "error": "Invalid API key.", + } diff --git a/tests/test_providers.py b/tests/test_providers.py index 99df73c3d2..f4a17b4673 100644 --- a/tests/test_providers.py +++ b/tests/test_providers.py @@ -617,3 +617,172 @@ def create(self, **kwargs): calls = client.chat.completions.calls assert turn.text == "ok" and len(calls) == 2 assert "max_tokens" not in calls[1] + + +# -- aimlapi.com -------------------------------------------------------------- +# A reseller like the three above, plus one thing none of them do: it reads app +# attribution headers. Those are the only reason OpenAIProvider grew a +# `default_headers` kwarg, so they get pinned here — a malformed partner id is +# accepted by the gateway and silently earns nothing, which no runtime check catches. +AIMLAPI_BASE_URL = "https://api.aimlapi.com/v1" + + +def test_aimlapi_descriptor_matches_the_reseller_shape(): + from coworker.providers.registry import get_descriptor + + d = get_descriptor("aimlapi") + assert d is not None and d.needs_key + assert d.title == "aimlapi.com" # the name users see, everywhere + assert d.env_key == "AIMLAPI_API_KEY" + assert d.recommended_model == "zhipu/glm-5.2" + base = next(f for f in d.fields if f.key == "base_url") + assert base.default == AIMLAPI_BASE_URL and not base.required + + +def test_aimlapi_attribution_headers_are_wellformed(): + """`X-AIMLAPI-Partner-ID` is validated as `part_` server-side and a value that + fails the pattern is dropped silently — no error, no attribution. Only a test catches + a typo here. `X-AIMLAPI-Source` is `/` with a closed channel enum.""" + import re + + from coworker.providers.registry import AIMLAPI_ATTRIBUTION_HEADERS as H + + assert re.fullmatch(r"part_[A-Za-z0-9]{1,64}", H["X-AIMLAPI-Partner-ID"]) + channel, _, client = H["X-AIMLAPI-Source"].partition("/") + assert channel in ("web", "agent", "mcp") + assert re.fullmatch(r"[a-z0-9-]{1,32}", client) + # HTTP-Referer/X-Title identify the CALLING app — OpenWorker, not the gateway. + assert H["X-Title"] == "OpenWorker" + assert "openworker" in H["HTTP-Referer"] + + +def test_aimlapi_client_carries_attribution_and_never_mutates_the_constant(): + from coworker.providers.registry import AIMLAPI_ATTRIBUTION_HEADERS as H + from coworker.providers.registry import build_provider_client + + before = dict(H) + p = build_provider_client("aimlapi", {"api_key": "aiml-key"}, None) + assert isinstance(p, OpenAIProvider) + assert p._base_url == AIMLAPI_BASE_URL + assert p._default_headers == H + p._default_headers["X-Title"] = "tampered" # a per-instance copy, not the constant + assert H == before + + +def test_aimlapi_attribution_is_scoped_to_its_own_origin(): + """The endpoint field is user-editable. A key repointed at a proxy — or at another + vendor entirely — must not carry our partner tag onto someone else's wire.""" + from coworker.providers.registry import build_provider_client + + for elsewhere in ( + "https://gateway.example/v1", + "https://api.openai.com/v1", + "http://api.aimlapi.com.evil.test/v1", + ): + p = build_provider_client( + "aimlapi", {"api_key": "k", "base_url": elsewhere}, None + ) + assert p._default_headers is None, elsewhere + + +def test_other_compat_vendors_send_no_attribution_headers(): + """Lockdown: the new kwarg is opt-in per descriptor, not a global default.""" + from coworker.providers.registry import build_provider_client + + for name in ("openrouter", "together", "fireworks", "deepseek"): + p = build_provider_client(name, {"api_key": "k"}, None) + assert p._default_headers is None, name + + +def test_aimlapi_requests_omit_unset_params_rather_than_sending_null(): + """aimlapi.com's validator 400s on an explicit null for temperature, top_p, seed, + tools, tool_choice, response_format, stream, stream_options, parallel_tool_calls, + max_tokens and max_completion_tokens (swept live 2026-09-03) — precisely the shape a + provider produces when it forwards its unset optionals. Both wire paths here build + kwargs from what the caller actually passed and gate `tools` on truthiness, so no key + may reach the wire holding None. + + `tools` is the one that bites in an agent loop rather than on the first call: a host + that clears tools between turns by nulling the field succeeds on turn 1 and fails on + turn 2, every time. + """ + for tools in (None, [], [{"type": "function", "function": {"name": "f"}}]): + client = _FakeClient(_response(content="ok")) + provider = OpenAIProvider( + client=client, base_url=AIMLAPI_BASE_URL, api_key="aiml-key" + ) + provider.complete( + model="zhipu/glm-5.2", + messages=[{"role": "user", "content": "hi"}], + tools=tools, + ) + sent = client.chat.completions.calls[0] + assert [k for k, v in sent.items() if v is None] == [], tools + assert "temperature" not in sent and "top_p" not in sent + assert ("tools" in sent) is bool(tools) + + +def test_aimlapi_streaming_requests_send_no_null_params(): + """Same contract on the streaming path, which additionally always sets `stream` and + `stream_options` — both of which aimlapi.com rejects as null.""" + + class _StreamingFake: + def __init__(self): + self.calls: list[dict] = [] + + def create(self, **kwargs): + self.calls.append(kwargs) + return iter(()) + + client = _FakeClient(_response(content="ok")) + client.chat.completions = _StreamingFake() + provider = OpenAIProvider( + client=client, base_url=AIMLAPI_BASE_URL, api_key="aiml-key" + ) + list(provider.stream(model="zhipu/glm-5.2", messages=[], tools=None)) + + sent = client.chat.completions.calls[0] + assert [k for k, v in sent.items() if v is None] == [] + assert sent["stream"] is True and sent["stream_options"] == {"include_usage": True} + assert "tools" not in sent + + +def test_aimlapi_curated_models_are_its_own_namespace(): + """Every id here was checked against aimlapi.com's live catalog AND round-tripped; + OpenRouter's slugs for the same models do not resolve there, so none are reused.""" + from coworker.providers.matrix import models_for_provider + + ours = models_for_provider("aimlapi") + assert ours == [ + "openai/gpt-5.6-sol", + "anthropic/claude-sonnet-4.6", + "zhipu/glm-5.2", + "deepseek/deepseek-v4-pro", + ] + # These three OpenRouter slugs resolve to nothing on aimlapi.com — neither an id nor + # an alias (checked 2026-09-03). Copying a reseller's array across is the failure + # mode this guards; where the two namespaces genuinely agree, reuse is fine. + assert not { + "z-ai/glm-5.2", + "moonshotai/kimi-k2.6", + "meta-llama/llama-4-maverick", + } & set(ours) + + +def test_aimlapi_recommended_model_is_curated_and_routes(): + from coworker.providers.matrix import models_for_provider + from coworker.providers.registry import get_descriptor + from coworker.providers.router import ProviderRouter + + recommended = get_descriptor("aimlapi").recommended_model + assert recommended in models_for_provider("aimlapi") + + router = ProviderRouter.__new__(ProviderRouter) + for bare in models_for_provider("aimlapi"): + full = f"aimlapi:{bare}" + assert router._provider_name(full) == "aimlapi" + assert ProviderRouter._bare(full) == bare + caps = capabilities_for(full) + assert caps.tools and caps.parallel_tool_calls and caps.streaming + # No reseller row claims native PDF ingestion — pdf_support.py handles those. + assert not caps.pdf