From c4140fe95771d871489ab180f9dbd734f86de22c Mon Sep 17 00:00:00 2001 From: "Ralph Sto. Domingo" Date: Tue, 13 Oct 2026 13:03:02 +0800 Subject: [PATCH] fix(run): report a failed provider list as itself, not "Model not found" `run --model` read a failed `provider.list` call as an empty list, so any server error was reported as "Model not found". It now throws an error naming the HTTP status and detail, which also makes the intermittent subprocess failure diagnosable. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_0174HUYiaceApqHP7EKMNgnb --- packages/opencode/src/cli/cmd/run.ts | 5 +- .../opencode/src/cli/cmd/run/provider-list.ts | 30 ++++++++++++ .../test/cli/run/provider-list.test.ts | 48 +++++++++++++++++++ 3 files changed, 82 insertions(+), 1 deletion(-) create mode 100644 packages/opencode/src/cli/cmd/run/provider-list.ts create mode 100644 packages/opencode/test/cli/run/provider-list.test.ts diff --git a/packages/opencode/src/cli/cmd/run.ts b/packages/opencode/src/cli/cmd/run.ts index edf037e2bf..36d1c31b11 100644 --- a/packages/opencode/src/cli/cmd/run.ts +++ b/packages/opencode/src/cli/cmd/run.ts @@ -48,6 +48,9 @@ import { MessageID } from "../../session/schema" // altimate_change start — run implies run mode (fork-only module) import { applyRunModeDefault } from "./run/run-mode" // altimate_change end +// altimate_change start — a failed provider list is reported as itself, not "Model not found" (fork-only module) +import { providerListOrThrow } from "./run/provider-list" +// altimate_change end // altimate_change start — run-mode-only idle-done fallback (fork-only modules). // Detection lives in idle-done.ts; the confirm-DONE challenge text and the DONE // token contract live in session/termination.ts; delivery goes through the nudge @@ -622,7 +625,7 @@ You are speaking to a non-technical business executive. Follow these rules stric // `run` waiting until the process-level timeout kills it. if (args.model) { const parsed = Provider.parseModel(args.model) - const providers = (await sdk.provider.list()).data?.all ?? [] + const providers = providerListOrThrow(await sdk.provider.list(), args.model) const provider = providers.find((item) => item.id === parsed.providerID) if (!provider?.models?.[parsed.modelID]) { throw new Provider.ModelNotFoundError({ diff --git a/packages/opencode/src/cli/cmd/run/provider-list.ts b/packages/opencode/src/cli/cmd/run/provider-list.ts new file mode 100644 index 0000000000..524951e9e7 --- /dev/null +++ b/packages/opencode/src/cli/cmd/run/provider-list.ts @@ -0,0 +1,30 @@ +// `run --model` checks the model against the server's provider list before it starts a session. +// A list call that failed used to read as an empty list, so a server error was reported as +// "Model not found" and looked like a typo in --model. This returns the list, or throws an error +// that says the call failed and why. + +type ListResult = { + data?: { all: T[] } + error?: unknown + response?: Response +} + +export function providerListOrThrow(result: ListResult, model: string): T[] { + // Checked at runtime: a body that is not the expected JSON (an HTML page, say) still arrives as `data`. + const all: unknown = (result.data as { all?: unknown } | undefined)?.all + if (Array.isArray(all)) return all as T[] + throw new Error(`Could not load the provider list to check --model ${model} (${describeFailure(result)})`) +} + +function describeFailure(result: ListResult): string { + const status = result.response ? `HTTP ${result.response.status}` : "no response" + const detail = errorDetail(result.error) ?? (result.response?.ok ? "unexpected response body" : undefined) + return detail ? `${status}: ${detail.slice(0, 500)}` : status +} + +function errorDetail(error: unknown): string | undefined { + if (error instanceof Error) return error.message + if (typeof error === "string") return error || undefined + if (error && typeof error === "object" && Object.keys(error).length > 0) return JSON.stringify(error) + return undefined +} diff --git a/packages/opencode/test/cli/run/provider-list.test.ts b/packages/opencode/test/cli/run/provider-list.test.ts new file mode 100644 index 0000000000..e34b95a824 --- /dev/null +++ b/packages/opencode/test/cli/run/provider-list.test.ts @@ -0,0 +1,48 @@ +import { describe, expect, test } from "bun:test" +import { providerListOrThrow } from "../../../src/cli/cmd/run/provider-list" + +const MODEL = "test/test-model" + +function failure(result: Parameters[0]): string { + try { + providerListOrThrow(result, MODEL) + } catch (error) { + return (error as Error).message + } + throw new Error("expected providerListOrThrow to throw") +} + +describe("run --model provider list check", () => { + test("returns the providers from a successful list", () => { + const all = [{ id: "test" }] + expect(providerListOrThrow({ data: { all }, response: new Response(null, { status: 200 }) }, MODEL)).toBe(all) + }) + + test("an empty list is still a list, so an unknown model stays a model error", () => { + expect(providerListOrThrow({ data: { all: [] }, response: new Response(null, { status: 200 }) }, MODEL)).toEqual([]) + }) + + // Each failed call names the model and says what failed, never "Model not found". + const cases: Array<[string, Parameters[0], string]> = [ + [ + "a server error with a JSON body", + { error: { name: "UnknownError", message: "boom" }, response: new Response(null, { status: 500 }) }, + 'HTTP 500: {"name":"UnknownError","message":"boom"}', + ], + ["a request that never got a response", { error: new Error("socket closed") }, "no response: socket closed"], + ["a request that failed without any detail", { error: {} }, "no response"], + [ + "a 200 whose body is not the provider list", + { data: "" as never, response: new Response(null, { status: 200 }) }, + "HTTP 200: unexpected response body", + ], + ["a non-2xx with an empty body", { error: "", response: new Response(null, { status: 503 }) }, "HTTP 503"], + ] + for (const [name, result, detail] of cases) { + test(name, () => { + const message = failure(result) + expect(message).toBe(`Could not load the provider list to check --model ${MODEL} (${detail})`) + expect(message).not.toContain("Model not found") + }) + } +})