From 1b5348e28e8061f9077cfa2a5ad10da9ec36b5ac Mon Sep 17 00:00:00 2001 From: Mikael Hugo Date: Fri, 15 May 2026 14:19:08 +0200 Subject: [PATCH] feat(providers): live discovery for opencode, opencode-go, minimax MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three providers were missing from PROVIDER_CATALOG_CONFIG so their model lists couldn't be auto-discovered. Their wire ids only existed in packages/ai/src/models.generated.ts as hand-coded entries, meaning new model variants from these providers required manual catalog edits. Verified live endpoints respond to /v1/models with bearer auth: - opencode → https://opencode.ai/zen/v1/models (6 free models) - opencode-go → https://opencode.ai/zen/go/v1/models (15 models) - minimax → https://api.minimax.io/v1/models (works) Added entries: opencode: baseUrl https://opencode.ai/zen, modelsPath /v1/models opencode-go: baseUrl https://opencode.ai/zen/go, modelsPath /v1/models minimax: baseUrl https://api.minimax.io, modelsPath /v1/models (international endpoint; Chinese-network api.minimaxi.com still handled separately in the SDK) Auth keys already wired: OPENCODE_API_KEY, OPENCODE_GO_API_KEY (with OPENCODE_API_KEY fallback), MINIMAX_API_KEY. No env-api-keys.ts changes. Combined with 385e0b448 (dynamic canonicalIdFor resolver), new model variants from these three providers will be auto-grouped in .sf/model-performance.json without hand-editing CANONICAL_BY_ROUTE. Live counts after fresh discovery will reveal experimental models absent from static catalog (e.g. opencode's "big-pickle", opencode-go's deepseek-v4-pro, mimo-v2.5-pro, hy3-preview). The model-router tolerates unconventional wire IDs — no naming constraints. To populate cache: rm -rf ~/.sf/runtime/model-catalog/ + relaunch sf. Tests: 13 new in provider-catalog-discovery.test.mjs (catalog shape, modelsPath presence, DISCOVERABLE_PROVIDER_IDS inclusion). Full suite 183 files / 1940 tests pass, zero regressions. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../extensions/sf/provider-catalog-config.js | 43 +++++++ .../tests/provider-catalog-discovery.test.mjs | 108 ++++++++++++++++++ 2 files changed, 151 insertions(+) create mode 100644 src/resources/extensions/sf/tests/provider-catalog-discovery.test.mjs diff --git a/src/resources/extensions/sf/provider-catalog-config.js b/src/resources/extensions/sf/provider-catalog-config.js index 6db6ade39..8ae3a652d 100644 --- a/src/resources/extensions/sf/provider-catalog-config.js +++ b/src/resources/extensions/sf/provider-catalog-config.js @@ -141,6 +141,49 @@ export const PROVIDER_CATALOG_CONFIG = { auth: { type: "bearer" }, rateLimits: { scope: "model" }, }, + + // ─── MiniMax ───────────────────────────────────────────────────────────── + // International endpoint (api.minimax.io) — Chinese-network endpoint + // (api.minimaxi.com/anthropic) is handled separately in the SDK layer. + minimax: { + type: "openai", + baseUrl: "https://api.minimax.io", + modelsPath: "/v1/models", + auth: { type: "bearer" }, + rateLimits: { scope: "provider" }, + modelFilter: { + excludePatterns: [], + }, + }, + + // ─── OpenCode Zen ───────────────────────────────────────────────────────── + // Free-tier routing layer — all models are zero-cost. IDs may use "-free" + // suffix or unconventional names (e.g. "big-pickle"); no additional filter. + opencode: { + type: "openai", + baseUrl: "https://opencode.ai/zen", + modelsPath: "/v1/models", + auth: { type: "bearer" }, + rateLimits: { scope: "provider" }, + modelFilter: { + // All opencode entries are intended to be free-tier; the wire IDs + // typically end in "-free" but also include experimental zero-cost + // models (e.g. big-pickle). No additional filtering needed. + excludePatterns: [], + }, + }, + + // ─── OpenCode Go ───────────────────────────────────────────────────────── + // Paid/commercial tier at /zen/go — exposes upstream model IDs directly + // (minimax-m2.7, kimi-k2.6, deepseek-v4-pro, etc.) without a "-free" suffix. + "opencode-go": { + type: "openai", + baseUrl: "https://opencode.ai/zen/go", + modelsPath: "/v1/models", + auth: { type: "bearer" }, + rateLimits: { scope: "provider" }, + modelFilter: { excludePatterns: [] }, + }, }; /** diff --git a/src/resources/extensions/sf/tests/provider-catalog-discovery.test.mjs b/src/resources/extensions/sf/tests/provider-catalog-discovery.test.mjs new file mode 100644 index 000000000..51e922fff --- /dev/null +++ b/src/resources/extensions/sf/tests/provider-catalog-discovery.test.mjs @@ -0,0 +1,108 @@ +/** + * provider-catalog-discovery.test.mjs + * + * Regression tests for live-discovery entries in provider-catalog-config.js. + * Covers: opencode, opencode-go, and minimax baseline fields. + */ +import assert from "node:assert/strict"; +import { describe, test } from "vitest"; +import { + DISCOVERABLE_PROVIDER_IDS, + getProviderCatalogConfig, +} from "../provider-catalog-config.js"; + +describe("opencode discovery config", () => { + test("baseUrl points to /zen path", () => { + const cfg = getProviderCatalogConfig("opencode"); + assert.ok(cfg, "opencode entry must exist"); + assert.equal(cfg.baseUrl, "https://opencode.ai/zen"); + }); + + test("modelsPath is /v1/models", () => { + const cfg = getProviderCatalogConfig("opencode"); + assert.equal(cfg.modelsPath, "/v1/models"); + }); + + test("auth type is bearer", () => { + const cfg = getProviderCatalogConfig("opencode"); + assert.equal(cfg.auth.type, "bearer"); + }); + + test("is included in DISCOVERABLE_PROVIDER_IDS", () => { + assert.ok( + DISCOVERABLE_PROVIDER_IDS.includes("opencode"), + "opencode should be discoverable", + ); + }); +}); + +describe("opencode-go discovery config", () => { + test("baseUrl points to /zen/go path", () => { + const cfg = getProviderCatalogConfig("opencode-go"); + assert.ok(cfg, "opencode-go entry must exist"); + assert.equal(cfg.baseUrl, "https://opencode.ai/zen/go"); + }); + + test("modelsPath is /v1/models", () => { + const cfg = getProviderCatalogConfig("opencode-go"); + assert.equal(cfg.modelsPath, "/v1/models"); + }); + + test("auth type is bearer", () => { + const cfg = getProviderCatalogConfig("opencode-go"); + assert.equal(cfg.auth.type, "bearer"); + }); + + test("is included in DISCOVERABLE_PROVIDER_IDS", () => { + assert.ok( + DISCOVERABLE_PROVIDER_IDS.includes("opencode-go"), + "opencode-go should be discoverable", + ); + }); + + test("baseUrl has /go suffix (distinct from opencode)", () => { + const opencodeBase = getProviderCatalogConfig("opencode").baseUrl; + const goBase = getProviderCatalogConfig("opencode-go").baseUrl; + assert.ok( + goBase.endsWith("/go"), + "opencode-go baseUrl must end in /go", + ); + assert.notEqual(goBase, opencodeBase, "opencode-go baseUrl must differ from opencode"); + }); +}); + +describe("minimax discovery config", () => { + test("baseUrl uses international api.minimax.io endpoint", () => { + const cfg = getProviderCatalogConfig("minimax"); + assert.ok(cfg, "minimax entry must exist"); + assert.ok( + cfg.baseUrl.includes("api.minimax.io"), + `expected api.minimax.io, got ${cfg.baseUrl}`, + ); + assert.ok( + !cfg.baseUrl.includes("minimaxi.com"), + "baseUrl must not use minimaxi.com (Chinese-network endpoint)", + ); + }); + + test("modelsPath is /v1/models", () => { + const cfg = getProviderCatalogConfig("minimax"); + assert.equal(cfg.modelsPath, "/v1/models"); + }); + + test("is included in DISCOVERABLE_PROVIDER_IDS", () => { + assert.ok( + DISCOVERABLE_PROVIDER_IDS.includes("minimax"), + "minimax should be discoverable", + ); + }); +}); + +describe("DISCOVERABLE_PROVIDER_IDS completeness", () => { + test("includes all three newly added providers", () => { + const ids = DISCOVERABLE_PROVIDER_IDS; + assert.ok(ids.includes("opencode"), "missing opencode"); + assert.ok(ids.includes("opencode-go"), "missing opencode-go"); + assert.ok(ids.includes("minimax"), "missing minimax"); + }); +});