diff --git a/.github/pr-assets/4823-opper-provider-catalog.png b/.github/pr-assets/4823-opper-provider-catalog.png new file mode 100644 index 0000000000..374c7b3924 Binary files /dev/null and b/.github/pr-assets/4823-opper-provider-catalog.png differ diff --git a/.github/pr-assets/4823-opper-provider-note.png b/.github/pr-assets/4823-opper-provider-note.png new file mode 100644 index 0000000000..a37239de6e Binary files /dev/null and b/.github/pr-assets/4823-opper-provider-note.png differ diff --git a/docs-site/src/content/docs/guides/providers.md b/docs-site/src/content/docs/guides/providers.md index b7e8438c3c..90e192a97a 100644 --- a/docs-site/src/content/docs/guides/providers.md +++ b/docs-site/src/content/docs/guides/providers.md @@ -395,7 +395,7 @@ selectors, then retry. Signing in from a machine with no existing `kiro-cli` ses ## 3. API-key catalog -opencodex ships 94 built-in presets: 78 key-based, 12 OAuth, three local, and one default +opencodex ships 95 built-in presets: 79 key-based, 12 OAuth, three local, and one default ChatGPT-forward preset. The dashboard's **Add provider** picker opens a key provider's dashboard, validates the key, and stores it; validation is provider-specific. Notable entries: @@ -428,6 +428,15 @@ token group allows (`gpt-5.5` and `gpt-5.1-codex` are seeded). Register at pins the row near the top of the Add provider picker and marks it as a sponsor, and nothing else about routing or defaults changes. +**Opper** is the EU-hosted AI gateway from Opper AI (Stockholm): one key (created at +[platform.opper.ai](https://platform.opper.ai)) and one OpenAI-compatible endpoint in front of 700+ +models from 30+ providers. Bare model ids such as `claude-sonnet-4-6` or `gpt-5.5` are *pools*: Opper +picks the provider and region per request, so the seeded ids stay valid as routes come and go. A +`vendor/model` id (`anthropic/claude-sonnet-4-6`, `aws/claude-sonnet-4-6-eu`) pins one route instead. +The catalogue is discovered live from `/v3/compat/models` with your key; the public list, including +region-pinned EU routes, is at [opper.ai/models](https://opper.ai/models). Opper is also a +[models.dev](https://models.dev) provider (`opper`), which uses the same bare pool ids. + | Provider | Base URL | | --- | --- | | **OpenAI (API key)** | `https://api.openai.com/v1` | @@ -476,6 +485,7 @@ routing or defaults changes. | Xiaomi MiMo | `https://api.xiaomimimo.com/anthropic` | | Xiaomi MiMo (OpenAI Chat) | `https://api.xiaomimimo.com/v1` | | Kilo | `https://api.kilo.ai/api/gateway` | +| Opper | `https://api.opper.ai/v3/compat` | | GitLab Duo | `https://cloud.gitlab.com/ai/v1/proxy/openai/v1` | | Cloudflare AI Gateway | `https://gateway.ai.cloudflare.com/v1/{account-id}/{gateway}/anthropic` | | …and more | opencode zen, Vercel AI Gateway, Venice, NanoGPT, Synthetic, Qianfan, Alibaba, Parallel, ZenMux, LiteLLM | diff --git a/gui/public/provider-icons/README.md b/gui/public/provider-icons/README.md index 22d329d513..3f0878f924 100644 --- a/gui/public/provider-icons/README.md +++ b/gui/public/provider-icons/README.md @@ -208,6 +208,7 @@ Published as SVG and committed with only comments, ``/`<desc>` and - `digitalocean.svg` — `digitalocean.com` favicon, 32x32. - `featherless.svg` — `featherless.ai/favicon.svg`, 256x256. - `kilo.svg` — `kilo.ai/favicon/favicon.svg`, 32x32. Keeps its `oklch()` plate. +- `opper.svg` — supplied in PR #4823 by an Opper employee, 315x315 single-ink mark. - `nanogpt.svg` — `nano-gpt.com/logo.svg`, 181x187, gradient. - `nebius.svg` — `nebius.com/favicon/favicon.svg`, 96x96. - `neuralwatt.svg` — the site's Webflow-hosted brand asset, 32x32. diff --git a/gui/public/provider-icons/opper.svg b/gui/public/provider-icons/opper.svg new file mode 100644 index 0000000000..45500826c3 --- /dev/null +++ b/gui/public/provider-icons/opper.svg @@ -0,0 +1,3 @@ +<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 315 315" fill="#000000"> + <path fill-rule="evenodd" clip-rule="evenodd" d="M159.78 315C71.53 315 0 244.49 0 157.5C0 -18.9499 159.78 0.650075 159.78 0.650075C159.78 87.2201 88.36 157.4 0.2 157.5C149.8 157.64 159.78 315 159.78 315ZM160.52 217.98C160.52 217.98 156.94 161.65 105.04 157.52C120.6 157.34 160.52 151.54 160.52 96.5601C160.52 151.54 200.44 157.34 216 157.52C164.1 161.63 160.52 217.98 160.52 217.98Z"/> +</svg> diff --git a/gui/src/provider-icons.ts b/gui/src/provider-icons.ts index 78055e5df1..e7906e773c 100644 --- a/gui/src/provider-icons.ts +++ b/gui/src/provider-icons.ts @@ -51,6 +51,7 @@ const PROVIDER_ICON_ALIASES: Record<string, string> = { "opencode-go": "opencode.svg", "opencode-zen": "opencode.svg", openrouter: "openrouter-color.svg", + opper: "opper.svg", qianfan: "qianfan-color.svg", /* * Qoder Global and Qoder CN are one brand on two operators (BRIGHT ZENITH @@ -144,6 +145,7 @@ const PROVIDER_DISPLAY_NAMES: Record<string, string> = { "github-copilot": "GitHub Copilot", "gitlab-duo": "GitLab Duo", openrouter: "OpenRouter", + opper: "Opper", "opencode-go": "OpenCode Go", "opencode-free": "OpenCode Free", "opencode-zen": "OpenCode Zen", @@ -231,6 +233,7 @@ const MASKED_PROVIDER_ICONS: ReadonlySet<string> = new Set([ "nous.svg", "novita.svg", "packycode.svg", + "opper.svg", "siliconflow.svg", "synthetic.svg", "zenmux.svg", diff --git a/scripts/test-layout/layout.json b/scripts/test-layout/layout.json index c361c4c0c8..5446a94e51 100644 --- a/scripts/test-layout/layout.json +++ b/scripts/test-layout/layout.json @@ -1055,6 +1055,7 @@ "opencode-zen-rate-limit.test.ts": "providers", "openrouter-provider-routing.test.ts": "providers", "openrouter-quota-reset-cooldown-4024.test.ts": "providers", + "opper-provider.test.ts": "providers", "optional-shutdown-hooks.test.ts": "lib", "orcarouter-provider.test.ts": "providers", "outbound-body-guard.test.ts": "server", diff --git a/src/providers/registry/entries-extended.ts b/src/providers/registry/entries-extended.ts index 068d5087a2..6a8a6d1ee8 100644 --- a/src/providers/registry/entries-extended.ts +++ b/src/providers/registry/entries-extended.ts @@ -98,6 +98,10 @@ import { DIGITALOCEAN_CHAT_COMPLETION_MODELS, SCALEWAY_SERVERLESS_CHAT_MODELS, SCALEWAY_MODEL_INPUT_MODALITIES, + OPPER_MODELS, + OPPER_MODEL_CONTEXT_WINDOWS, + OPPER_MODEL_MAX_OUTPUT_TOKENS, + OPPER_MODEL_INPUT_MODALITIES, } from "./model-seeds"; export const PROVIDER_REGISTRY_EXTENDED: readonly ProviderRegistryEntry[] = [ @@ -1023,6 +1027,30 @@ export const PROVIDER_REGISTRY_EXTENDED: readonly ProviderRegistryEntry[] = [ noJsonSchemaModels: [...DEEPSEEK_GATEWAY_THINKING_MODELS, ...OPENCODE_FREE_DEEPSEEK_MODELS], }, { id: "vercel-ai-gateway", label: "Vercel AI Gateway", baseUrl: "https://ai-gateway.vercel.sh/v1", adapter: "openai-chat", authKind: "key", dashboardUrl: "https://vercel.com/dashboard" }, + { + // Opper: EU-hosted AI gateway (Opper AI AB, Stockholm). One OpenAI-compatible endpoint and one + // key in front of 30+ upstream providers. Seeded ids are Opper *pools* (bare names such as + // `claude-sonnet-4-6`): the gateway chooses the provider/region per request, and a + // `vendor/model` id (`anthropic/claude-sonnet-4-6`, `aws/claude-sonnet-4-6-eu`) pins one route. + // The original provider author reported on 2026-09-08 that GET /v3/compat/models answers 401 + // without a key, so the default discovery URL doubles as key validation. Windows, output caps + // and modalities live in model-seeds.ts (smallest value / shared modality across each pool's + // members); live discovery owns which models exist. + id: "opper", + label: "Opper", + adapter: "openai-chat", + baseUrl: "https://api.opper.ai/v3/compat", + authKind: "key", + dashboardUrl: "https://platform.opper.ai", + liveModels: true, + preserveCustomDestination: true, + defaultModel: "claude-sonnet-4-6", + models: OPPER_MODELS, + modelContextWindows: OPPER_MODEL_CONTEXT_WINDOWS, + modelMaxOutputTokens: OPPER_MODEL_MAX_OUTPUT_TOKENS, + modelInputModalities: OPPER_MODEL_INPUT_MODALITIES, + note: "EU-hosted AI gateway: one OpenAI-compatible endpoint and one key in front of 30+ providers. Bare model ids are pools (claude-sonnet-4-6, gpt-5.5) and Opper picks the route per request; vendor/model ids (anthropic/claude-sonnet-4-6) pin one provider. The catalogue is discovered live from /v3/compat/models with your key; the public list is at opper.ai/models. Token rates are the model providers' rates with no markup; Opper charges a 3% fee when you buy credits.", + }, { id: "opencode-free", label: "OpenCode Free", diff --git a/src/providers/registry/model-seeds.ts b/src/providers/registry/model-seeds.ts index f18b73cb57..ed5f6b1c4a 100644 --- a/src/providers/registry/model-seeds.ts +++ b/src/providers/registry/model-seeds.ts @@ -960,3 +960,47 @@ export const CLINE_PASS_TEXT_ONLY_MODELS = CLINE_PASS_MODALITY_KNOWN_MODELS.filt export const CLINE_PASS_MODEL_INPUT_MODALITIES: Record<string, string[]> = Object.fromEntries( CLINE_PASS_MODALITY_KNOWN_MODELS.map(id => [id, CLINE_PASS_IMAGE_MODELS.has(id) ? ["text", "image"] : ["text"]]), ); + +// Opper seed: bare *pool* names. A pool is every provider Opper serves that model through; Opper +// picks the route per request. Each name is the `.model` of a `pooled: true` entry in the public +// catalogue snapshot supplied by the original provider author +// (https://api.opper.ai/v3/models?limit=2000, captured 2026-09-14); `vendor/model` ids +// (anthropic/claude-sonnet-4-6) pin one route and stay valid, they are just not seeded. +export const OPPER_MODELS = [ + "claude-sonnet-4-6", + "claude-opus-5", + "gpt-5.5", + "gpt-5.4-mini", + "gemini-3.8-flash", + "deepseek-v4-pro", + "kimi-k3", + "mistral-large-2512", +]; +// Smallest value across each pool's members in that snapshot, capped at the lab model's own limit +// (kimi-k3 output); live discovery owns which models exist. +export const OPPER_MODEL_CONTEXT_WINDOWS: Record<string, number> = { + "claude-sonnet-4-6": 1_000_000, + "claude-opus-5": 1_000_000, + "gpt-5.5": 1_050_000, + "gpt-5.4-mini": 400_000, + "gemini-3.8-flash": 1_048_576, + "deepseek-v4-pro": 1_000_000, + "kimi-k3": 1_048_576, + "mistral-large-2512": 256_000, +}; +export const OPPER_MODEL_MAX_OUTPUT_TOKENS: Record<string, number> = { + "claude-sonnet-4-6": 64_000, + "claude-opus-5": 128_000, + "gpt-5.5": 128_000, + "gpt-5.4-mini": 128_000, + "gemini-3.8-flash": 65_536, + "deepseek-v4-pro": 65_536, + "kimi-k3": 131_072, + "mistral-large-2512": 8_192, +}; +// Pools whose members do not all accept image input (deepseek-v4-pro: no member does; kimi-k3: the +// sference route is text-only), so the shared modality set is text. +export const OPPER_TEXT_ONLY_MODELS = ["deepseek-v4-pro", "kimi-k3"]; +export const OPPER_MODEL_INPUT_MODALITIES: Record<string, string[]> = Object.fromEntries( + OPPER_MODELS.map(id => [id, OPPER_TEXT_ONLY_MODELS.includes(id) ? ["text"] : ["text", "image"]]), +); diff --git a/structure/providers/xai-grok.md b/structure/providers/xai-grok.md index 5bd4a83a55..0a6b292c7a 100644 --- a/structure/providers/xai-grok.md +++ b/structure/providers/xai-grok.md @@ -127,6 +127,9 @@ overrides remain authoritative. First-party `deepseek-chat`, `deepseek-reasoner` The Crusoe fixed-key registry row, discovery predicate, effort ladder, and input-modality map are also provider-scoped and do not alter xAI model metadata, OAuth routing, or wire behavior. +The Opper pool seeds in `src/providers/registry/entries-extended.ts` are also provider-scoped and +do not alter xAI discovery, model metadata, or transport behavior. + Native Chat applies qualifying effort ceilings independently of model pins; pin selection precedes the cap and only pins or cap rewrites enter wire mapping. The [catalog effort contract](../catalog.md#ultra-reasoning-level) records the V1/compaction exemptions and caller-preservation boundary. Combo child requests normalize effort and thinking controls against the selected target while retaining reasoning summaries; strict unknown targets preserve caller controls. The [Responses transport owner](../transports/responses.md) documents this boundary, and native Chat removes effort only for an explicit empty declaration or no-reasoning model. diff --git a/structure/subagents.md b/structure/subagents.md index 91ea815fbe..89d9e85848 100644 --- a/structure/subagents.md +++ b/structure/subagents.md @@ -390,6 +390,10 @@ vision support from a provider-wide multimodal label. [Anthropic seed image metadata](runtime.md#capability-aware-image-admission) supplies missing capability evidence; subagent selection and eligibility rules remain unchanged. +Opper's fallback pool seeds carry provider-scoped text/image declarations from +`src/providers/registry/model-seeds.ts`. They feed the same capability-aware image admission and +do not change subagent selection, roster order, or eligibility. + Provider-scoped approval reviewer settings are projected by the [catalog owner](catalog.md#provider-scoped-approval-reviewer); this surface retains its existing routing, transport and account-selection behavior. Renamed fixed-key providers receive [missing reasoning metadata](catalog.md#renamed-destination-reasoning-metadata) during derivation; explicit per-model entries and provider defaults retain precedence. diff --git a/structure/transports/inventory.md b/structure/transports/inventory.md index 55dec2695d..71563d6c2e 100644 --- a/structure/transports/inventory.md +++ b/structure/transports/inventory.md @@ -40,7 +40,7 @@ surface is listed here so a maintainer can find the owner without grepping: | OAuth account failover | `src/oauth/generic-account-failover.ts`, `src/oauth/anthropic-routing.ts` | Reactive pre-output 429 recovery is presence-driven with 2+ eligible accounts. Pool and `oauthAccountFailover` flags govern proactive routing, not the reactive retry: a disabled Anthropic pool recovers through quota ordering rather than its dormant strategy, and a per-provider `enabled` beats the global default in either direction. | | OAuth login callback (inbound) | `src/oauth/callback-server.ts` | Every response, including non-callback 404s, closes its connection so a pooled socket cannot deliver a later login to a retired flow on the same callback port. | | Alibaba regions | `src/providers/alibaba-region-backup.ts`, `src/providers/alibaba-region-migration.ts`, `src/providers/alibaba-region-startup.ts` | Region migration backs up before rewriting and is idempotent across restarts. | -| Discovery and quota | `src/providers/model-discovery.ts`, `src/providers/quota.ts`, `src/providers/registry.ts` | Discovery rejects a response over 4 MiB or past 2,000 raw rows before caching it. Provider-scoped hints fill capabilities omitted by live rosters; OpenCode Go's `deepseek-v4.1-flash` keeps its 1,048,576-token context window. Codex quota DTOs suppress retired Spark evidence under the [OpenAI scope contract](../providers/openai-tiers.md#public-provider-contract), retaining ordinary custom windows. | +| Discovery and quota | `src/providers/model-discovery.ts`, `src/providers/quota.ts`, `src/providers/registry.ts` | Discovery rejects a response over 4 MiB or past 2,000 raw rows before caching it. Provider-scoped hints fill capabilities omitted by live rosters; OpenCode Go's `deepseek-v4.1-flash` keeps its 1,048,576-token context window. The fixed-key Opper preset uses the shared OpenAI Chat adapter at `https://api.opper.ai/v3/compat`, discovers models through its conventional authenticated `/models` path, preserves an older same-named custom destination, and falls back to bare pool ids while passing vendor-prefixed ids through unchanged. Codex quota DTOs suppress retired Spark evidence under the [OpenAI scope contract](../providers/openai-tiers.md#public-provider-contract), retaining ordinary custom windows. | The registry's first-party `deepseek-flash` row declares native `text` and `image` input, so image requests bypass the vision sidecar by default; explicit `noVisionModels` or text-only declarations diff --git a/tests/fixtures/test-layout-expected.json b/tests/fixtures/test-layout-expected.json index d59eeb0c83..9564514726 100644 --- a/tests/fixtures/test-layout-expected.json +++ b/tests/fixtures/test-layout-expected.json @@ -881,6 +881,7 @@ "opencode-zen-rate-limit.test.ts": "providers", "openrouter-provider-routing.test.ts": "providers", "openrouter-quota-reset-cooldown-4024.test.ts": "providers", + "opper-provider.test.ts": "providers", "optional-shutdown-hooks.test.ts": "lib", "orcarouter-provider.test.ts": "providers", "outbound-body-guard.test.ts": "server", diff --git a/tests/providers/opper-provider.test.ts b/tests/providers/opper-provider.test.ts new file mode 100644 index 0000000000..61eabc9f21 --- /dev/null +++ b/tests/providers/opper-provider.test.ts @@ -0,0 +1,106 @@ +import { describe, expect, test } from "bun:test"; +import { KEY_LOGIN_PROVIDERS } from "../../src/oauth/key-providers"; +import { deriveProviderPresets, providerConfigSeed } from "../../src/providers/derive"; +import { resolveProviderModelDiscoveryUrl } from "../../src/providers/model-discovery"; +import { PROVIDER_REGISTRY } from "../../src/providers/registry"; +import { routeModel } from "../../src/router"; +import type { OcxConfig } from "../../src/types"; +import { en } from "../../gui/src/i18n/en"; +import { interpolate, type TFn } from "../../gui/src/i18n/shared"; +import { formatProviderDisplayName, providerIconPaint, providerIconSrc } from "../../gui/src/provider-icons"; + +const englishT: TFn = (key, vars) => interpolate(en[key], vars); +const OPPER_BASE_URL = "https://api.opper.ai/v3/compat"; + +function registryEntry() { + const entry = PROVIDER_REGISTRY.find(row => row.id === "opper"); + if (!entry) throw new Error("missing opper registry entry"); + return entry; +} + +describe("Opper gateway provider", () => { + test("is a fixed OpenAI-compatible API-key gateway seeded with bare pool ids", () => { + const entry = registryEntry(); + expect(entry).toMatchObject({ + label: "Opper", + adapter: "openai-chat", + baseUrl: OPPER_BASE_URL, + authKind: "key", + liveModels: true, + preserveCustomDestination: true, + defaultModel: "claude-sonnet-4-6", + }); + expect(entry.allowBaseUrlOverride).toBeUndefined(); + expect(entry.models).toContain(entry.defaultModel!); + for (const id of entry.models ?? []) { + // Pool names carry no vendor prefix; Opper resolves the route per request. + expect(id).toMatch(/^[a-z0-9.-]+$/); + expect(entry.modelContextWindows?.[id]).toBeGreaterThan(0); + expect(entry.modelMaxOutputTokens?.[id]).toBeGreaterThan(0); + expect(entry.modelMaxOutputTokens?.[id]).toBeLessThanOrEqual(entry.modelContextWindows?.[id] ?? 0); + expect(entry.modelInputModalities?.[id]?.[0]).toBe("text"); + } + // Only pools whose every member takes images advertise the image modality. + expect(entry.modelInputModalities?.["claude-sonnet-4-6"]).toEqual(["text", "image"]); + expect(entry.modelInputModalities?.["deepseek-v4-pro"]).toEqual(["text"]); + expect(entry.modelInputModalities?.["kimi-k3"]).toEqual(["text"]); + }); + + test("derives the key-login preset and config seed from the registry row", () => { + const entry = registryEntry(); + expect(KEY_LOGIN_PROVIDERS.opper).toMatchObject({ + label: "Opper", + adapter: "openai-chat", + baseUrl: OPPER_BASE_URL, + defaultModel: "claude-sonnet-4-6", + }); + expect(deriveProviderPresets().find(row => row.id === "opper")).toMatchObject({ auth: "key" }); + const seed = providerConfigSeed(entry); + expect(seed).toMatchObject({ + adapter: "openai-chat", + baseUrl: OPPER_BASE_URL, + liveModels: true, + models: entry.models, + modelContextWindows: entry.modelContextWindows, + }); + expect(seed).not.toHaveProperty("preserveCustomDestination"); + expect(seed).not.toHaveProperty("note"); + }); + + test("uses the authenticated compat model list for key validation", () => { + // The original provider author reported that /v3/compat/models answers 401 without a key; + // this test statically pins the resulting registry policy and URL, not that upstream behavior. + expect(registryEntry().apiKeyValidation).toBeUndefined(); + expect(resolveProviderModelDiscoveryUrl( + "opper", + { adapter: "openai-chat", baseUrl: OPPER_BASE_URL, authMode: "key" }, + OPPER_BASE_URL, + `${OPPER_BASE_URL}/models`, + )).toBe("https://api.opper.ai/v3/compat/models"); + }); + + test("routes the provider prefix and forwards pool and pinned model ids unchanged", () => { + const config: OcxConfig = { + port: 10100, + defaultProvider: "opper", + providers: { + opper: { ...providerConfigSeed(registryEntry()), apiKey: "test-key" }, + }, + }; + // Bare pool id: Opper picks the provider. + const pooled = routeModel(config, "opper/claude-sonnet-4-6"); + expect(pooled.modelId).toBe("claude-sonnet-4-6"); + expect(pooled.provider.baseUrl).toBe(OPPER_BASE_URL); + expect(pooled.provider.adapter).toBe("openai-chat"); + // Vendor-prefixed id pins one route; the extra slash must survive the provider split. + const pinned = routeModel(config, "opper/anthropic/claude-sonnet-4-6"); + expect(pinned.modelId).toBe("anthropic/claude-sonnet-4-6"); + expect(pinned.provider.baseUrl).toBe(OPPER_BASE_URL); + }); + + test("ships a masked single-ink mark and a brand display name in the dashboard", () => { + expect(providerIconSrc("opper")).toBe("/provider-icons/opper.svg"); + expect(providerIconPaint(providerIconSrc("opper"))).toBe("mask"); + expect(formatProviderDisplayName("opper", englishT)).toBe("Opper"); + }); +}); diff --git a/tests/providers/provider-registry-parity.test.ts b/tests/providers/provider-registry-parity.test.ts index 21ff647de0..5e53824a84 100644 --- a/tests/providers/provider-registry-parity.test.ts +++ b/tests/providers/provider-registry-parity.test.ts @@ -38,7 +38,7 @@ const EXPECTED_KEY_PROVIDER_IDS = [ "deepseek", "cerebras", "chutes", "deepinfra", "hyperbolic", "nscale", "vultr", "baseten", "commandcode", "sambanova", "nebius", "crusoe", "digitalocean", "scaleway", "featherless", "novita", "together", "fireworks", "firepass", "moonshot", "huggingface", "nvidia", "venice", "zai", "zhipu-bigmodel", "zhipu-bigmodel-coding", "zhipu-bigmodel-responses", "nanogpt", "synthetic", "siliconflow", "qwen-cloud", "tencent-coding-plan", "volcengine", "volcengine-coding-plan", "volcengine-agent-plan", "qianfan", "alibaba", "alibaba-token-plan", "alibaba-token-plan-intl", "parallel", "zenmux", "litellm", "ollama-cloud", "mistral", - "minimax", "minimax-cn", "kimi-code", "opencode-zen", "vercel-ai-gateway", + "minimax", "minimax-cn", "kimi-code", "opencode-zen", "vercel-ai-gateway", "opper", "opencode-free", "xiaomi", "xiaomi-mimo", "kilo", "mimo-free", "mimo", "cloudflare-ai-gateway", "cloudflare-workers-ai", "gitlab-duo", "qoder", "qoder-cn", "codebuddy", "codebuddy-cn", ];