diff options
| author | Adam Malczewski <[email protected]> | 2026-08-12 22:01:59 +0900 |
|---|---|---|
| committer | Adam Malczewski <[email protected]> | 2026-08-12 22:01:59 +0900 |
| commit | 5f10a40b26dbfdc10b2c02cdf23ec3a7edfa9443 (patch) | |
| tree | 2f1d5ee2674d1dcc9909e89f06a82a7b10b4191a /src/features/chat/model-select.ts | |
| parent | 774519e052fd869952cc5e05aca159e4bf978fa1 (diff) | |
| download | dispatch-web-5f10a40b26dbfdc10b2c02cdf23ec3a7edfa9443.tar.gz dispatch-web-5f10a40b26dbfdc10b2c02cdf23ec3a7edfa9443.zip | |
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-<n> keys (#2 before #10); friendly
OpenCode #N labels (raw opencode-N/<model> value still selected/persisted);
migrateLegacyModelName rewrites opencode/<model> -> opencode-1/<model>.
- 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/<model> tab models idempotently.
- concurrency: providerLabel renders opencode-<n> as OpenCode #<n> 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).
Diffstat (limited to 'src/features/chat/model-select.ts')
| -rw-r--r-- | src/features/chat/model-select.ts | 58 |
1 files changed, 52 insertions, 6 deletions
diff --git a/src/features/chat/model-select.ts b/src/features/chat/model-select.ts index 602e0ef..5da6e7b 100644 --- a/src/features/chat/model-select.ts +++ b/src/features/chat/model-select.ts @@ -29,15 +29,61 @@ export function joinModelName(key: string, model: string): string { /** Distinct keys across all models, in first-seen order. */ export function modelKeys(models: readonly string[]): string[] { const seen = new Set<string>(); - const out: string[] = []; + const openCodeKeys: Array<{ key: string; n: number }> = []; + const otherKeys: string[] = []; for (const full of models) { const { key } = splitModelName(full); - if (!seen.has(key)) { - seen.add(key); - out.push(key); - } + if (seen.has(key)) continue; + seen.add(key); + const n = openCodeNumber(key); + if (n !== null) openCodeKeys.push({ key, n }); + else otherKeys.push(key); } - return out; + // Numbered OpenCode credentials sort NUMERICALLY (#2 before #10), placed + // before other providers (the primary credential in this app). Others keep + // first-seen order. + openCodeKeys.sort((a, b) => a.n - b.n); + return [...openCodeKeys.map((e) => e.key), ...otherKeys]; +} + +/** + * The stable numeric identity of a numbered OpenCode provider id/key + * (`opencode-1` → 1, `opencode-10` → 10), or `null` when the id is not a + * numbered OpenCode key. Numbers are canonical positive integers with no + * leading zeros. + */ +export function openCodeNumber(key: string): number | null { + const m = /^opencode-([1-9][0-9]*)$/.exec(key); + return m === null ? null : Number(m[1]); +} + +/** True iff `id` is a numbered OpenCode provider/key (`opencode-<n>`). */ +export function isNumberedOpenCode(id: string): boolean { + return openCodeNumber(id) !== null; +} + +/** + * A friendly human label for a model key/provider id: `opencode-<n>` → + * "OpenCode #<n>"; any other id is returned unchanged. The RAW id is still what + * gets selected/persisted — this only affects display. + */ +export function modelKeyLabel(key: string): string { + const n = openCodeNumber(key); + return n === null ? key : `OpenCode #${n}`; +} + +/** + * Migration helper: rewrite a legacy `opencode/<model>` full model name to the + * numbered `opencode-1/<model>` namespace. Returns the input unchanged for + * anything that is not `opencode/`-prefixed (already-numbered names, other + * providers). Idempotent. This is the FE-side persisted-state migration; the + * backend performs its own equivalent on its own store. + */ +export function migrateLegacyModelName(full: string): string { + const legacyPrefix = "opencode/"; + if (!full.startsWith(legacyPrefix)) return full; + const model = full.slice(legacyPrefix.length); + return model.length === 0 ? full : `opencode-1/${model}`; } /** The model suffixes available under a given key, in order. */ |
