mirror of
https://github.com/anomalyco/opencode.git
synced 2026-08-24 14:43:37 -04:00
fix(tui): show the default model for sessions stored without one
Sessions created through the raw API with no model (agent: null, model: null) run fine on the server's default model, but the prompt status line rendered only the stored session model and claimed 'No provider selected - Connect a provider' even with providers connected. The session selection now falls back to GET /api/model/default, gated on the model still being available, so 'No provider selected' appears only when there genuinely is no usable provider.
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
import { createStore } from "solid-js/store"
|
||||
import { dedupeWith } from "effect/Array"
|
||||
import { createSimpleContext } from "./helper"
|
||||
import { batch, createMemo, onCleanup } from "solid-js"
|
||||
import { batch, createMemo, createResource, onCleanup } from "solid-js"
|
||||
import { useEvent } from "./event"
|
||||
import path from "path"
|
||||
import { useTuiPaths } from "./runtime"
|
||||
@@ -32,6 +32,22 @@ export function parseModel(model: string) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A session stored without a model runs on the server's default model, so the
|
||||
* status line shows that effective model instead of claiming no provider is
|
||||
* selected. "No provider selected" remains only when no usable default exists.
|
||||
*/
|
||||
export function withDefaultModelFallback(options: {
|
||||
selection: (ModelPreferenceModel & { variant?: string }) | undefined
|
||||
defaultModel: ModelPreferenceModel | undefined
|
||||
isValid: (model: ModelPreferenceModel) => boolean
|
||||
variantPreference: (model: ModelPreferenceModel) => string | undefined
|
||||
}) {
|
||||
if (options.selection) return options.selection
|
||||
if (!options.defaultModel || !options.isValid(options.defaultModel)) return undefined
|
||||
return { ...options.defaultModel, variant: normalizeModelVariant(options.variantPreference(options.defaultModel)) }
|
||||
}
|
||||
|
||||
export function recentModels(model: ModelPreferenceModel, recent: ModelPreferenceModel[]) {
|
||||
const seen = new Set<string>()
|
||||
return [model, ...recent]
|
||||
@@ -217,8 +233,30 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
|
||||
)
|
||||
})
|
||||
|
||||
const [serverDefaultModel] = createResource(
|
||||
() => {
|
||||
const ref = location.ref ?? data.location.default()
|
||||
// Refetch when the catalog changes, such as a provider connecting.
|
||||
return JSON.stringify([ref.directory, ref.workspaceID, models()?.length ?? -1])
|
||||
},
|
||||
async () => {
|
||||
const ref = location.ref ?? data.location.default()
|
||||
const response = await client.api.model
|
||||
.default({ location: { directory: ref.directory, workspace: ref.workspaceID } })
|
||||
.catch(() => undefined)
|
||||
if (!response?.data) return undefined
|
||||
return { providerID: response.data.providerID, modelID: response.data.id }
|
||||
},
|
||||
)
|
||||
|
||||
const currentSelection = createMemo<ModelSelection | undefined>(() => {
|
||||
if (route.data.type === "session") return sessionSelection(route.data.sessionID)
|
||||
if (route.data.type === "session")
|
||||
return withDefaultModelFallback({
|
||||
selection: sessionSelection(route.data.sessionID),
|
||||
defaultModel: serverDefaultModel(),
|
||||
isValid: isModelValid,
|
||||
variantPreference: (model) => preferences.variant[modelPreferenceKey(model)],
|
||||
})
|
||||
const model = newSessionModel()
|
||||
if (!model) return
|
||||
return { ...model, variant: normalizeModelVariant(preferences.variant[modelPreferenceKey(model)]) }
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { expect, test } from "bun:test"
|
||||
import { parseModel, recentModels } from "../../src/context/local"
|
||||
import { parseModel, recentModels, withDefaultModelFallback } from "../../src/context/local"
|
||||
|
||||
test("parses model IDs containing slashes", () => {
|
||||
expect(parseModel("provider/family/model")).toEqual({
|
||||
@@ -20,3 +20,45 @@ test("moves a model to the front, deduplicates, and limits recents", () => {
|
||||
...recent.slice(6, 10),
|
||||
])
|
||||
})
|
||||
|
||||
test("session selection wins over the default model", () => {
|
||||
const selection = { providerID: "openai", modelID: "gpt", variant: "high" }
|
||||
expect(
|
||||
withDefaultModelFallback({
|
||||
selection,
|
||||
defaultModel: { providerID: "opencode", modelID: "fable" },
|
||||
isValid: () => true,
|
||||
variantPreference: () => undefined,
|
||||
}),
|
||||
).toBe(selection)
|
||||
})
|
||||
|
||||
test("sessions without a stored model fall back to the server default", () => {
|
||||
expect(
|
||||
withDefaultModelFallback({
|
||||
selection: undefined,
|
||||
defaultModel: { providerID: "opencode", modelID: "fable" },
|
||||
isValid: () => true,
|
||||
variantPreference: (model) => (model.modelID === "fable" ? "max" : undefined),
|
||||
}),
|
||||
).toEqual({ providerID: "opencode", modelID: "fable", variant: "max" })
|
||||
})
|
||||
|
||||
test("no provider is reported only without a usable default", () => {
|
||||
expect(
|
||||
withDefaultModelFallback({
|
||||
selection: undefined,
|
||||
defaultModel: undefined,
|
||||
isValid: () => true,
|
||||
variantPreference: () => undefined,
|
||||
}),
|
||||
).toBeUndefined()
|
||||
expect(
|
||||
withDefaultModelFallback({
|
||||
selection: undefined,
|
||||
defaultModel: { providerID: "gone", modelID: "model" },
|
||||
isValid: () => false,
|
||||
variantPreference: () => undefined,
|
||||
}),
|
||||
).toBeUndefined()
|
||||
})
|
||||
|
||||
@@ -152,6 +152,11 @@ export function createFetch(override?: FetchHandler, events?: ReturnType<typeof
|
||||
location: { directory, project: { id: "proj_test", directory: worktree, canonical: worktree } },
|
||||
data: [],
|
||||
})
|
||||
if (url.pathname === "/api/model/default")
|
||||
return json({
|
||||
location: { directory, project: { id: "proj_test", directory: worktree, canonical: worktree } },
|
||||
data: null,
|
||||
})
|
||||
if (url.pathname === "/api/reference")
|
||||
return json({ location: { directory, project: { id: "proj_test", directory, canonical: directory } }, data: [] })
|
||||
if (url.pathname === "/api/websearch/provider") {
|
||||
|
||||
Reference in New Issue
Block a user