diff --git a/changelog.d/fixes/8610-opencode-go-effort-catalog.md b/changelog.d/fixes/8610-opencode-go-effort-catalog.md new file mode 100644 index 0000000000..4228bdc40c --- /dev/null +++ b/changelog.d/fixes/8610-opencode-go-effort-catalog.md @@ -0,0 +1 @@ +- **fix(providers):** keep registered OpenCode Go effort aliases in the Playground catalog when synced base models are present, and preserve Hunyuan3's 256K context metadata for combo routing ([#8610](https://github.com/diegosouzapw/OmniRoute/pull/8610)). diff --git a/open-sse/config/providers/registry/opencode/go/index.ts b/open-sse/config/providers/registry/opencode/go/index.ts index 4ead5d8823..9ff9deda62 100644 --- a/open-sse/config/providers/registry/opencode/go/index.ts +++ b/open-sse/config/providers/registry/opencode/go/index.ts @@ -106,10 +106,25 @@ export const opencode_goProvider: RegistryEntry = { }, { id: "qwen3.5-plus", name: "Qwen3.5 Plus", targetFormat: "claude", supportsVision: false }, // #8353: hy3 is the Go-tier base id (distinct from hy3-preview / hy3-free). - { id: "hy3", name: "Hunyuan3", supportsReasoning: true }, - { id: "hy3-none", name: "Hunyuan3 (none effort)", supportsReasoning: true }, - { id: "hy3-low", name: "Hunyuan3 (low effort)", supportsReasoning: true }, - { id: "hy3-high", name: "Hunyuan3 (high effort)", supportsReasoning: true }, + { id: "hy3", name: "Hunyuan3", contextLength: 256000, supportsReasoning: true }, + { + id: "hy3-none", + name: "Hunyuan3 (none effort)", + contextLength: 256000, + supportsReasoning: true, + }, + { + id: "hy3-low", + name: "Hunyuan3 (low effort)", + contextLength: 256000, + supportsReasoning: true, + }, + { + id: "hy3-high", + name: "Hunyuan3 (high effort)", + contextLength: 256000, + supportsReasoning: true, + }, { id: "hy3-preview", name: "Hunyuan3 Preview" }, // #8353: Grok 4.5 + effort tiers from the OpenCode Go registry. { id: "grok-4.5", name: "Grok 4.5", supportsReasoning: true }, diff --git a/src/app/api/v1/models/catalog.ts b/src/app/api/v1/models/catalog.ts index 5339e10801..34ccbdfbaf 100644 --- a/src/app/api/v1/models/catalog.ts +++ b/src/app/api/v1/models/catalog.ts @@ -662,6 +662,18 @@ async function buildUnifiedModelsResponseCore( return Array.isArray(models) && models.length > 0; }) ); + const isRegisteredEffortVariant = ( + providerModels: Array<{ id: string }>, + modelId: string + ): boolean => { + for (const suffix of ["none", "low", "medium", "high", "max", "xhigh"]) { + const suffixWithSeparator = `-${suffix}`; + if (!modelId.endsWith(suffixWithSeparator)) continue; + const baseModelId = modelId.slice(0, -suffixWithSeparator.length); + return providerModels.some((candidate) => candidate.id === baseModelId); + } + return false; + }; // Add provider models (chat) for (const [alias, providerModels] of Object.entries(PROVIDER_MODELS)) { @@ -680,9 +692,14 @@ async function buildUnifiedModelsResponseCore( continue; } - if (providersWithSyncedModels.has(canonicalProviderId)) continue; - for (const model of providerModels) { + // Synced models replace static base entries, but they do not carry aliases + // registered for provider-specific reasoning variants. + if ( + providersWithSyncedModels.has(canonicalProviderId) && + !isRegisteredEffortVariant(providerModels, model.id) + ) + continue; if (!providerSupportsModel(canonicalProviderId, model.id)) continue; const aliasId = `${alias}/${model.id}`; if (getModelIsHidden(canonicalProviderId, model.id)) continue; diff --git a/tests/unit/models-catalog-route.test.ts b/tests/unit/models-catalog-route.test.ts index ed76e9a59e..2db47dbf9c 100644 --- a/tests/unit/models-catalog-route.test.ts +++ b/tests/unit/models-catalog-route.test.ts @@ -851,6 +851,34 @@ test("v1 models catalog includes synced non-Gemini provider models from discover assert.equal(syncedModel.context_length, 262144); }); +test("v1 models catalog retains registered effort aliases beside synced OpenCode Go bases", async () => { + const connection = await seedConnection("opencode-go", { + name: "opencode-go-effort-aliases", + apiKey: "go-key", + }); + + await modelsDb.replaceSyncedAvailableModelsForConnection("opencode-go", connection.id, [ + { + id: "hy3", + name: "Hunyuan3", + source: "imported", + supportedEndpoints: ["chat"], + }, + ]); + + const response = await v1ModelsCatalog.getUnifiedModelsResponse( + new Request("http://localhost/api/v1/models") + ); + const body = (await response.json()) as { data: Array<{ id: string }> }; + const ids = body.data.map((item: { id: string }) => item.id); + + assert.equal(response.status, 200); + assert.ok(ids.includes("opencode-go/hy3")); + assert.ok(ids.includes("opencode-go/hy3-none")); + assert.ok(ids.includes("opencode-go/hy3-low")); + assert.ok(ids.includes("opencode-go/hy3-high")); +}); + test("v1 models catalog advertises GLM-5.2 provider aliases with hosted context limits", async () => { const hfConnection = await seedConnection("huggingface", { name: "huggingface-glm52", diff --git a/tests/unit/opencode-go-catalog-alignment.test.ts b/tests/unit/opencode-go-catalog-alignment.test.ts index 6b9bd3b68b..ba7dba12c6 100644 --- a/tests/unit/opencode-go-catalog-alignment.test.ts +++ b/tests/unit/opencode-go-catalog-alignment.test.ts @@ -13,6 +13,7 @@ import assert from "node:assert/strict"; const { opencode_goProvider } = await import("../../open-sse/config/providers/registry/opencode/go/index.ts"); +const { getResolvedModelCapabilities } = await import("../../src/lib/modelCapabilities.ts"); function modelIds(): string[] { return (opencode_goProvider.models ?? []).map((m) => m.id); @@ -51,3 +52,13 @@ test("opencode-go preserves the pre-existing minimax-m3 and qwen routing via tar assert.equal(byId.get("minimax-m3")?.targetFormat, "claude"); assert.equal(byId.get("qwen3.7-max")?.targetFormat, "claude"); }); + +test("opencode-go hy3 variants expose their context window to combo compatibility filtering", () => { + for (const modelId of ["hy3", "hy3-none", "hy3-low", "hy3-high"]) { + assert.equal( + getResolvedModelCapabilities(`opencode-go/${modelId}`).contextWindow, + 256000, + `${modelId} should resolve a 256K context window` + ); + } +});