From 5f10a40b26dbfdc10b2c02cdf23ec3a7edfa9443 Mon Sep 17 00:00:00 2001 From: Adam Malczewski Date: Wed, 12 Aug 2026 22:01:59 +0900 Subject: feat(models): numbered OpenCode credential selection, labels, migration Adopt the numbered OPENCODE_API_KEY_N scheme in the model picker and concurrency surfaces. - model-select: numeric sort of opencode- keys (#2 before #10); friendly OpenCode #N labels (raw opencode-N/ value still selected/persisted); migrateLegacyModelName rewrites opencode/ -> opencode-1/. - ModelSelector: renders friendly key labels; surfaces a disappeared saved credential as UNAVAILABLE (disabled option + notice) without silently switching to another model. - app store: DEFAULT_MODEL -> opencode-1/...; catalogs no longer auto-fallback the active model when the selected credential is absent; boot restore migrates persisted legacy opencode/ tab models idempotently. - concurrency: providerLabel renders opencode- as OpenCode # in the limit rows and Add dropdown (raw provider id preserved on the wire). - Tests: parsing/sorting/labels/migration, unavailable-state selector behavior, no-silent-fallback, and concurrency provider label; fixtures updated to the numbered scheme. Verification: bun install, bun run typecheck (0 errors), bun run test (1179 passed), bun run check (clean, exit 0), bun run build (vite OK). --- src/features/concurrency/logic/view-model.test.ts | 28 ++++++++++++++++++---- src/features/concurrency/logic/view-model.ts | 12 ++++++++++ .../concurrency/ui/ConcurrencyLimitRow.svelte | 5 ++-- src/features/concurrency/ui/ConcurrencyView.svelte | 3 ++- .../concurrency/ui/ConcurrencyView.test.ts | 4 ++-- 5 files changed, 42 insertions(+), 10 deletions(-) (limited to 'src/features/concurrency') diff --git a/src/features/concurrency/logic/view-model.test.ts b/src/features/concurrency/logic/view-model.test.ts index 6e6b770..9c557f5 100644 --- a/src/features/concurrency/logic/view-model.test.ts +++ b/src/features/concurrency/logic/view-model.test.ts @@ -13,6 +13,7 @@ import { parseLimitInput, pauseLabel, providerFromModel, + providerLabel, providerOptions, statusLabel, summarizeLimits, @@ -95,7 +96,7 @@ describe("cooldownLabel", () => { describe("providerFromModel", () => { it("takes the part before the first slash", () => { expect(providerFromModel("openai/gpt-4o")).toBe("openai"); - expect(providerFromModel("openai-compat/gpt-4o-mini")).toBe("openai-compat"); + expect(providerFromModel("opencode-1/gpt-4o-mini")).toBe("opencode-1"); }); it("returns the whole string when there is no slash", () => { expect(providerFromModel("umans")).toBe("umans"); @@ -127,6 +128,23 @@ describe("providerOptions", () => { }); }); +describe("providerLabel", () => { + it("labels a numbered OpenCode provider as OpenCode #N", () => { + expect(providerLabel("opencode-2")).toBe("OpenCode #2"); + expect(providerLabel("opencode-10")).toBe("OpenCode #10"); + }); + + it("leaves non-OpenCode provider ids unchanged", () => { + expect(providerLabel("umans")).toBe("umans"); + expect(providerLabel("anthropic")).toBe("anthropic"); + }); + + it("rejects noncanonical numbered ids (label passthrough)", () => { + expect(providerLabel("opencode")).toBe("opencode"); + expect(providerLabel("opencode-0")).toBe("opencode-0"); + }); +}); + // ── pauseLabel + formatPauseDuration ─────────────────────────────────────────── describe("formatPauseDuration", () => { @@ -450,12 +468,12 @@ describe("normalizeConcurrencyLimits", () => { const limits = normalizeConcurrencyLimits({ limits: [ { providerId: "umans", limit: 4 }, - { providerId: "openai-compat", limit: 5 }, + { providerId: "opencode-1", limit: 5 }, ], }); expect(limits).toEqual([ { providerId: "umans", limit: 4 }, - { providerId: "openai-compat", limit: 5 }, + { providerId: "opencode-1", limit: 5 }, ]); }); @@ -510,7 +528,7 @@ describe("normalizeConcurrencyStatus", () => { providers: [ { providerId: "umans", limit: 4, inFlight: 2, queued: 1, paused: false }, { - providerId: "openai-compat", + providerId: "opencode-1", limit: 5, inFlight: 5, queued: 3, @@ -532,7 +550,7 @@ describe("normalizeConcurrencyStatus", () => { }); expect(first !== undefined && !("pausedUntil" in first)).toBe(true); expect(second).toEqual({ - providerId: "openai-compat", + providerId: "opencode-1", limit: 5, inFlight: 5, queued: 3, diff --git a/src/features/concurrency/logic/view-model.ts b/src/features/concurrency/logic/view-model.ts index 8a37198..80c155c 100644 --- a/src/features/concurrency/logic/view-model.ts +++ b/src/features/concurrency/logic/view-model.ts @@ -170,6 +170,18 @@ export function providerOptions( return out; } +/** + * Friendly display label for a concurrency provider id. A numbered OpenCode + * provider (`opencode-`) renders as "OpenCode #" (so "opencode-2" reads + * as a coherent credential identity, matching the model picker); any other id is + * shown unchanged. The RAW id is still what is sent to / displayed-with the + * backend — this only affects presentation. + */ +export function providerLabel(providerId: string): string { + const m = /^opencode-([1-9][0-9]*)$/.exec(providerId); + return m === null ? providerId : `OpenCode #${m[1]}`; +} + // ── Status → display view ────────────────────────────────────────────────────── const NO_LIMITS = "No limits configured"; diff --git a/src/features/concurrency/ui/ConcurrencyLimitRow.svelte b/src/features/concurrency/ui/ConcurrencyLimitRow.svelte index bf06ac0..c8f6c8f 100644 --- a/src/features/concurrency/ui/ConcurrencyLimitRow.svelte +++ b/src/features/concurrency/ui/ConcurrencyLimitRow.svelte @@ -4,6 +4,7 @@ DEFAULT_COOLDOWN_MS, parseCooldownInput, parseLimitInput, + providerLabel, statusLabel, type Badge, type ConcurrencyLimitView, @@ -132,8 +133,8 @@ the buttons never wrap; tight gap + narrow inputs keep the provider name visible; the provider shrinks via flex-1 + min-w-0). -->
- {limit.providerId}{providerLabel(limit.providerId)} No providers available {:else} {#each providerOpts as provider (provider)} - + {/each} {/if} diff --git a/src/features/concurrency/ui/ConcurrencyView.test.ts b/src/features/concurrency/ui/ConcurrencyView.test.ts index a8163c2..e214692 100644 --- a/src/features/concurrency/ui/ConcurrencyView.test.ts +++ b/src/features/concurrency/ui/ConcurrencyView.test.ts @@ -11,8 +11,8 @@ import type { } from "../logic/types"; import ConcurrencyView from "./ConcurrencyView.svelte"; -// Available models → provider ids are "umans", "anthropic", "openai-compat". -const MODELS = ["umans/umans-glm-5.2", "anthropic/claude-sonnet", "openai-compat/gpt-4o"] as const; +// Available models → provider ids are "umans", "anthropic", "opencode-1". +const MODELS = ["umans/umans-glm-5.2", "anthropic/claude-sonnet", "opencode-1/gpt-4o"] as const; // A status entry factory (defaults to a healthy limited provider). The new // concurrency-fixes fields (`cooldownMs`, `autoReduced`) are always present. -- cgit v1.2.3