diff --git a/packages/first-class-providers/src/providers.test.ts b/packages/first-class-providers/src/providers.test.ts index 02192da94..5b6415ef4 100644 --- a/packages/first-class-providers/src/providers.test.ts +++ b/packages/first-class-providers/src/providers.test.ts @@ -1,7 +1,6 @@ import { describe, expect, test } from "bun:test"; import { FIRST_CLASS_PROVIDERS, - connectListProviders, firstClassPathAsProvider, firstClassProviderById, } from "./providers.js"; @@ -21,10 +20,6 @@ describe("FIRST_CLASS_PROVIDERS", () => { ]); }); - test("connectListProviders matches FIRST_CLASS_PROVIDERS", () => { - expect(connectListProviders()).toBe(FIRST_CLASS_PROVIDERS); - }); - test("has no separate Codex connect row", () => { expect(FIRST_CLASS_PROVIDERS.some((p) => p.id === "codex")).toBe(false); expect(FIRST_CLASS_PROVIDERS.map((p) => p.label)).not.toContain( diff --git a/packages/first-class-providers/src/providers.ts b/packages/first-class-providers/src/providers.ts index 3242d8c2b..d93c1b067 100644 --- a/packages/first-class-providers/src/providers.ts +++ b/packages/first-class-providers/src/providers.ts @@ -164,11 +164,6 @@ export const FIRST_CLASS_PROVIDERS: readonly FirstClassProviderDef[] = [ }, ] as const; -/** Connect-list rows (same as FIRST_CLASS_PROVIDERS; explicit export for hosts). */ -export function connectListProviders(): readonly FirstClassProviderDef[] { - return FIRST_CLASS_PROVIDERS; -} - export function firstClassProviderById( id: string, ): FirstClassProviderDef | undefined { diff --git a/packages/opencode-go/src/catalog.ts b/packages/opencode-go/src/catalog.ts index c8b4e7449..d5f4b4b16 100644 --- a/packages/opencode-go/src/catalog.ts +++ b/packages/opencode-go/src/catalog.ts @@ -7,7 +7,7 @@ import { import { OPENCODE_GO_DEFAULT_MODEL, OPENCODE_GO_MODEL_IDS, - OPENCODE_GO_MODELS, + PROTOCOL_BY_ID, type GoProtocol, } from "./models.js"; @@ -23,10 +23,8 @@ interface GoCatalogEntry { } export function buildGoCatalogEntry(): GoCatalogEntry { - const protocols: Record = {}; - for (const m of OPENCODE_GO_MODELS) { - protocols[m.id] = m.protocol; - } + const protocols: Record = + Object.fromEntries(PROTOCOL_BY_ID); return { name: OPENCODE_GO_PROVIDER_ID, displayName: OPENCODE_GO_DISPLAY_NAME, diff --git a/packages/opencode-go/src/identity.ts b/packages/opencode-go/src/identity.ts index 21329b25d..be1509303 100644 --- a/packages/opencode-go/src/identity.ts +++ b/packages/opencode-go/src/identity.ts @@ -3,6 +3,12 @@ import { OPENCODE_GO_PROVIDER_ID, } from "./constants.js"; +/** True when `host` is opencode.ai or a subdomain of it (case-insensitive). */ +export function hostMatchesOpencodeDotAi(host: string): boolean { + const normalized = host.trim().toLowerCase(); + return normalized === "opencode.ai" || normalized.endsWith(".opencode.ai"); +} + /** * True when the URL or base is the public OpenCode Go gateway. * @@ -21,10 +27,7 @@ export function isOpenCodeGoURL(urlOrBase: string | undefined): boolean { if (trimmed.length === 0) return false; try { const url = new URL(trimmed); - if ( - url.hostname !== "opencode.ai" && - !url.hostname.endsWith(".opencode.ai") - ) { + if (!hostMatchesOpencodeDotAi(url.hostname)) { return false; } // Strip trailing slashes, then compare path segments case-insensitively. diff --git a/packages/opencode-go/src/index.ts b/packages/opencode-go/src/index.ts index 615e2e7b4..c76d0d226 100644 --- a/packages/opencode-go/src/index.ts +++ b/packages/opencode-go/src/index.ts @@ -19,5 +19,6 @@ export { isOpenCodeGoProvider, isOpenCodeGoProviderId, isOpenCodeGoURL, + hostMatchesOpencodeDotAi, } from "./identity.js"; export { parseGoAPIError } from "./errors.js"; diff --git a/packages/opencode-go/src/models.ts b/packages/opencode-go/src/models.ts index 4da311297..4d49c6b29 100644 --- a/packages/opencode-go/src/models.ts +++ b/packages/opencode-go/src/models.ts @@ -63,9 +63,10 @@ export const OPENCODE_GO_MODEL_IDS: readonly string[] = OPENCODE_GO_MODELS.map( export const OPENCODE_GO_DEFAULT_MODEL: GoModelId = "kimi-k2.7-code"; -const PROTOCOL_BY_ID = new Map( - OPENCODE_GO_MODELS.map((m) => [m.id, m.protocol]), -); +export const PROTOCOL_BY_ID: ReadonlyMap = new Map< + string, + GoProtocol +>(OPENCODE_GO_MODELS.map((m) => [m.id, m.protocol])); export function protocolForGoModel(modelId: string): GoProtocol | undefined { return PROTOCOL_BY_ID.get(modelId); diff --git a/packages/opencode-go/src/usage.ts b/packages/opencode-go/src/usage.ts index c9548697f..898502c63 100644 --- a/packages/opencode-go/src/usage.ts +++ b/packages/opencode-go/src/usage.ts @@ -99,6 +99,20 @@ export async function fetchGoUsage( } } +function formatWindowUsage(w: GoUsageWindow): string { + if (w.usagePercent !== undefined) return `${Math.round(w.usagePercent)}%`; + if (w.usageDollars !== undefined && w.limitDollars !== undefined) { + return `$${w.usageDollars.toFixed(2)}/$${w.limitDollars.toFixed(0)}`; + } + return "ok"; +} + +function usageWindowLabel(usage: GoUsage): string { + if (usage.rolling5h !== undefined) return "5h"; + if (usage.weekly !== undefined) return "week"; + return "month"; +} + export function formatGoUsage(usage: GoUsage): string { if (usage.status !== "ok") { if (usage.status === "unavailable") return "Go usage unavailable"; @@ -109,17 +123,5 @@ export function formatGoUsage(usage: GoUsage): string { } const w = usage.rolling5h ?? usage.weekly ?? usage.monthly; if (w === undefined) return "Go usage ok"; - const pct = - w.usagePercent !== undefined - ? `${Math.round(w.usagePercent)}%` - : w.usageDollars !== undefined && w.limitDollars !== undefined - ? `$${w.usageDollars.toFixed(2)}/$${w.limitDollars.toFixed(0)}` - : "ok"; - const window = - usage.rolling5h !== undefined - ? "5h" - : usage.weekly !== undefined - ? "week" - : "month"; - return `Go ${window} ${pct}`; + return `Go ${usageWindowLabel(usage)} ${formatWindowUsage(w)}`; } diff --git a/packages/zen/src/identity.ts b/packages/zen/src/identity.ts index 83d22ba77..301264261 100644 --- a/packages/zen/src/identity.ts +++ b/packages/zen/src/identity.ts @@ -3,11 +3,7 @@ // helpers; Go's own URL and id keep matching Go first, never Zen. import { ZEN_DISPLAY_NAME, ZEN_PROVIDER_ID } from "./constants.js"; - -function hostMatchesOpencodeDotAi(host: string): boolean { - const normalized = host.trim().toLowerCase(); - return normalized === "opencode.ai" || normalized.endsWith(".opencode.ai"); -} +import { hostMatchesOpencodeDotAi } from "../../opencode-go/src/index.js"; /** * Match a bare Zen base URL: an opencode.ai URL whose first path segment is diff --git a/src/provider/billing-product.ts b/src/provider/billing-product.ts index e2d89c912..998142d38 100644 --- a/src/provider/billing-product.ts +++ b/src/provider/billing-product.ts @@ -1,4 +1,5 @@ import { + hostMatchesOpencodeDotAi, isKnownGoModel, isOpenCodeGoProvider, } from "../../packages/opencode-go/src/index.js"; @@ -25,10 +26,7 @@ export function isBareZenBaseURL(baseURL: string): boolean { if (BARE_ZEN_BASES.has(trimmed)) return true; try { const url = new URL(trimmed); - if ( - url.hostname !== "opencode.ai" && - !url.hostname.endsWith(".opencode.ai") - ) { + if (!hostMatchesOpencodeDotAi(url.hostname)) { return false; } const path = url.pathname.replace(/\/+$/, "");