From fe7e7842e61e07baeacf8bc055179227ab66a168 Mon Sep 17 00:00:00 2001 From: minhn4 Date: Thu, 30 Jul 2026 19:04:26 +0700 Subject: [PATCH 1/2] fix(tui): stop pinning a default model during gateway setup MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The configure flow wrote the chosen model as a top-level `model` in the global config. That pin outranks every other source on startup — Provider defaultModel checks config before the state dir's model.json recents — so each `/models` switch reverted on the next launch and only hand-editing codev.jsonc undid it. buildGatewayConfig no longer emits `model`; the flow seeds the choice into the persisted recents instead, so it still starts as the default and a later switch replaces it. The flow now awaits instance disposal before bootstrapping (config.update only forks the reload) so the reloaded provider list contains netgate before the seed. Hub attribution is unaffected: it probes `provider.`, never `model`. Co-Authored-By: Claude Opus 5 (1M context) --- CHANGELOG.md | 11 +++++++++++ packages/codev-gateway/src/config-block.ts | 16 ++++++++++++---- packages/codev-gateway/test/config-block.test.ts | 8 +++++++- packages/tui/src/component/dialog-configure.tsx | 14 ++++++++++++++ 4 files changed, 44 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3e51f69928..450eecab88 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,17 @@ file is the human-readable summary of what changed and why. ## Unreleased +### TUI + +- **The model you pick stays picked** — setup no longer pins its "Choose default + model" answer as a top-level `model` in the global config. That pin outranked + everything else on startup (config `model` beats the saved recent models), so + every `/models` switch silently reverted on the next launch and the only fix + was hand-editing `codev.jsonc`. The chosen model is now seeded as the most + recent selection instead, so it still starts as the default and a later switch + simply replaces it. Existing configs keep their pin — delete the `"model"` + line from `~/.config/codev/codev.jsonc` to pick up the new behavior. + ## 1.18.4-3 ### TUI diff --git a/packages/codev-gateway/src/config-block.ts b/packages/codev-gateway/src/config-block.ts index a744696cc2..0e0b54da99 100644 --- a/packages/codev-gateway/src/config-block.ts +++ b/packages/codev-gateway/src/config-block.ts @@ -18,19 +18,27 @@ const MODEL_MODALITIES: { attachment: boolean; modalities: { input: Modality[]; }, } -// The exact codev.json fragment the codev hub writes for CoDev Code (hub -// src/lib/configure.ts configureOpenCodeKind), so hub-managed and -// self-configured installs are indistinguishable — the hub marks a config as +// The codev.json fragment the codev hub writes for CoDev Code (hub +// src/lib/configure.ts configureOpenCodeKind), minus the default-model pin +// below, so hub-managed and self-configured installs still attribute the same +// way — the hub marks a config as // CoDev-managed by probing `provider.` against its candidate set (saved // id, `netgate`, `ai-gateway`, legacy `aigateway`; hub PR #212), so hubs older // than that PR cannot attribute a `netgate` config. The per-model `limit` is // mandatory: without it opencode sizes the context window to 0 and disables // auto-compaction; the modality block is mandatory for the same reason (hub PR // #219) — see MODEL_MODALITIES. +// +// Deliberately no top-level `model`, unlike the hub fragment: a pinned default +// outranks the TUI's persisted model selection every startup (Provider +// defaultModel checks config first, then the state dir's model.json recents), +// so the wizard's one-time choice would silently undo every later /models +// switch. The configure flow seeds that choice into the recents instead, which +// the next switch displaces. Attribution is unaffected — the hub probes +// `provider.`, never `model`. export function buildGatewayConfig(input: { apiKey: string; baseUrl: string; model: string; models?: string[] }) { const ids = [...new Set([input.model, ...(input.models ?? [])])] return { - model: `${GATEWAY_PROVIDER.id}/${input.model}`, compaction: { auto: true, reserved: GATEWAY_COMPACT_RESERVED, diff --git a/packages/codev-gateway/test/config-block.test.ts b/packages/codev-gateway/test/config-block.test.ts index ee4447bd0a..7335e78f11 100644 --- a/packages/codev-gateway/test/config-block.test.ts +++ b/packages/codev-gateway/test/config-block.test.ts @@ -22,7 +22,6 @@ describe("buildGatewayConfig", () => { models: ["MiniMax/MiniMax-M2.7", "other/model"], }) expect(config).toEqual({ - model: "netgate/MiniMax/MiniMax-M2.7", compaction: { auto: true, reserved: 29491 }, provider: { netgate: { @@ -65,6 +64,13 @@ describe("buildGatewayConfig", () => { const withDupe = buildGatewayConfig({ apiKey: "k", baseUrl: "https://gw.test", model: "m", models: ["m", "n"] }) expect(Object.keys(withDupe.provider.netgate.models)).toEqual(["m", "n"]) }) + + // A pinned default outranks the TUI's persisted selection on every startup, + // so the chosen model is seeded into the recents by the configure flow + // instead of written here. + test("pins no top-level default model", () => { + expect(buildGatewayConfig({ apiKey: "k", baseUrl: "https://gw.test", model: "m" })).not.toHaveProperty("model") + }) }) describe("buildCustomProviderConfig", () => { diff --git a/packages/tui/src/component/dialog-configure.tsx b/packages/tui/src/component/dialog-configure.tsx index d18ef544b0..feb1837fd2 100644 --- a/packages/tui/src/component/dialog-configure.tsx +++ b/packages/tui/src/component/dialog-configure.tsx @@ -3,6 +3,7 @@ import { fetchApiKey, fetchCodevConfig, fetchModels, + GATEWAY_PROVIDER, login, saveApiKey, saveCodevConfig, @@ -13,6 +14,7 @@ import open from "open" import { createSignal, onMount, Show, type Accessor } from "solid-js" import { useClipboard } from "../context/clipboard" import { useEpilogue } from "../context/epilogue" +import { useLocal } from "../context/local" import { runCustomProviderFlow, RETRIES, StatusBox, withRetries } from "./dialog-custom-provider" import { DialogProvider } from "./dialog-provider" import { useExit } from "../context/exit" @@ -46,6 +48,7 @@ interface FlowContext { toast: ReturnType exit: ReturnType epilogue: ReturnType + local: ReturnType } export function DialogConfigure() { @@ -56,6 +59,7 @@ export function DialogConfigure() { toast: useToast(), exit: useExit(), epilogue: useEpilogue(), + local: useLocal(), } // The entry chooser is a non-dismissable setup gate: no escape, no @@ -321,7 +325,17 @@ async function runModelFlow(ctx: FlowContext, creds: GatewayCreds) { }) if (result.error) throw new Error(JSON.stringify(result.error)) }) + // The config block deliberately pins no top-level `model` (it would outrank + // every later /models switch on restart), so persist the choice as the most + // recent selection instead — the highest-priority source a switch can still + // replace. Dispose first like the custom-provider flow does: config.update + // only forks the instance reload, so a bootstrap can otherwise race it and + // come back without the new provider. Skip the seed rather than let + // model.set warn about an unknown provider mid-onboarding if it does. + await ctx.sdk.client.instance.dispose() await ctx.sync.bootstrap() + if (ctx.sync.data.provider.some((item) => item.id === GATEWAY_PROVIDER.id && !!item.models[model])) + ctx.local.model.set({ providerID: GATEWAY_PROVIDER.id, modelID: model }, { recent: true }) ctx.toast.show({ variant: "info", message: `CoDev Code configured — using ${model}` }) ctx.dialog.clear() } catch (err) { From b77e3e58c4f5bb6b3d6cdfe2961d71d97cb07e10 Mon Sep 17 00:00:00 2001 From: minhn4 Date: Thu, 30 Jul 2026 20:28:24 +0700 Subject: [PATCH 2/2] docs(gateway): note the hub dropped the model pin too MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The block comment claimed this fragment deliberately differs from the hub's. codev-hub#221 removes the same pin from configureOpenCodeKind, so the two writers match again — say that instead, and keep the caveat that a config written by an older hub still carries a pin. Co-Authored-By: Claude Opus 5 (1M context) --- packages/codev-gateway/src/config-block.ts | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/packages/codev-gateway/src/config-block.ts b/packages/codev-gateway/src/config-block.ts index 0e0b54da99..c1df582484 100644 --- a/packages/codev-gateway/src/config-block.ts +++ b/packages/codev-gateway/src/config-block.ts @@ -18,10 +18,9 @@ const MODEL_MODALITIES: { attachment: boolean; modalities: { input: Modality[]; }, } -// The codev.json fragment the codev hub writes for CoDev Code (hub -// src/lib/configure.ts configureOpenCodeKind), minus the default-model pin -// below, so hub-managed and self-configured installs still attribute the same -// way — the hub marks a config as +// The exact codev.json fragment the codev hub writes for CoDev Code (hub +// src/lib/configure.ts configureOpenCodeKind), so hub-managed and +// self-configured installs are indistinguishable — the hub marks a config as // CoDev-managed by probing `provider.` against its candidate set (saved // id, `netgate`, `ai-gateway`, legacy `aigateway`; hub PR #212), so hubs older // than that PR cannot attribute a `netgate` config. The per-model `limit` is @@ -29,13 +28,14 @@ const MODEL_MODALITIES: { attachment: boolean; modalities: { input: Modality[]; // auto-compaction; the modality block is mandatory for the same reason (hub PR // #219) — see MODEL_MODALITIES. // -// Deliberately no top-level `model`, unlike the hub fragment: a pinned default -// outranks the TUI's persisted model selection every startup (Provider -// defaultModel checks config first, then the state dir's model.json recents), -// so the wizard's one-time choice would silently undo every later /models -// switch. The configure flow seeds that choice into the recents instead, which -// the next switch displaces. Attribution is unaffected — the hub probes -// `provider.`, never `model`. +// Deliberately no top-level `model`, and the hub fragment dropped it in the +// same round (hub PR #221): a pinned default outranks the TUI's persisted +// model selection every startup (Provider defaultModel checks config first, +// then the state dir's model.json recents), so the wizard's one-time choice +// would silently undo every later /models switch. The configure flow seeds +// that choice into the recents instead, which the next switch displaces; the +// hub leads its models map with the chosen id for the same effect. A config +// written by an older hub still carries a pin — nothing here removes one. export function buildGatewayConfig(input: { apiKey: string; baseUrl: string; model: string; models?: string[] }) { const ids = [...new Set([input.model, ...(input.models ?? [])])] return {