-
Notifications
You must be signed in to change notification settings - Fork 1.2k
feat(providers): add Opper as a built-in API-key gateway provider #4981
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
cae5a1d
b10a304
d89be48
72f6076
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -208,6 +208,7 @@ Published as SVG and committed with only comments, `<title>`/`<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. | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
The commit explicitly says it carries #4823 by @Felixkw12, but its message contains no AGENTS.md reference: AGENTS.md:L288-L292 Useful? React with 👍 / 👎. |
||
| - `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. | ||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -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. | ||||||||||
|
Comment on lines
+130
to
+131
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win Correct the Opper seed source path. The Opper pool seeds are exported by Proposed fix-The Opper pool seeds in `src/providers/registry/entries-extended.ts` are also provider-scoped and
+The Opper pool seeds in `src/providers/registry/model-seeds.ts` are also provider-scoped and📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||
|
|
||||||||||
| 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. | ||||||||||
|
|
||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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"); | ||
| }); | ||
| }); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
🔎 Supported by static analysis
🏁 Script executed:
Repository: lidge-jun/opencodex
Length of output: 783
🏁 Script executed:
Repository: lidge-jun/opencodex
Length of output: 50377
🏁 Script executed:
Repository: lidge-jun/opencodex
Length of output: 19420
Update all translated provider guides.
The Japanese, Korean, Russian, and Simplified Chinese guides still report 94 presets and 78 key-based presets at
ja/guides/providers.md:171,ko/guides/providers.md:169,ru/guides/providers.md:182, andzh-cn/guides/providers.md:162. None includes the new Opper catalog entry. Update these catalog sections to match the English guide.🤖 Prompt for AI Agents
Source: Path instructions