import type { createOpencodeClient } from "@opencode-ai/sdk" import { log } from "./logger" type Client = ReturnType export interface ModelSuggestionInfo { providerID: string modelID: string suggestion: string } function extractMessage(error: unknown): string { if (typeof error === "string") return error if (error instanceof Error) return error.message if (typeof error === "object" && error !== null) { const obj = error as Record if (typeof obj.message === "string") return obj.message try { return JSON.stringify(error) } catch { return "" } } return String(error) } export function parseModelSuggestion(error: unknown): ModelSuggestionInfo | null { if (!error) return null if (typeof error === "object") { const errObj = error as Record if (errObj.name === "ProviderModelNotFoundError" && typeof errObj.data === "object" && errObj.data !== null) { const data = errObj.data as Record const suggestions = data.suggestions if (Array.isArray(suggestions) && suggestions.length > 0 && typeof suggestions[0] === "string") { return { providerID: String(data.providerID ?? ""), modelID: String(data.modelID ?? ""), suggestion: suggestions[0], } } return null } for (const key of ["data", "error", "cause"] as const) { const nested = errObj[key] if (nested && typeof nested === "object") { const result = parseModelSuggestion(nested) if (result) return result } } } const message = extractMessage(error) if (!message) return null const modelMatch = message.match(/model not found:\s*([^/\s]+)\s*\/\s*([^.\s]+)/i) const suggestionMatch = message.match(/did you mean:\s*([^,?]+)/i) if (modelMatch && suggestionMatch) { return { providerID: modelMatch[1].trim(), modelID: modelMatch[2].trim(), suggestion: suggestionMatch[1].trim(), } } return null } interface PromptBody { model?: { providerID: string; modelID: string } [key: string]: unknown } interface PromptArgs { path: { id: string } body: PromptBody [key: string]: unknown } export async function promptWithModelSuggestionRetry( client: Client, args: PromptArgs, ): Promise { // NOTE: Model suggestion retry removed — promptAsync returns 204 immediately, // model errors happen asynchronously server-side and cannot be caught here const promptPromise = client.session.promptAsync( args as Parameters[0], ) let timeoutID: ReturnType | null = null const timeoutPromise = new Promise((_, reject) => { timeoutID = setTimeout(() => { reject(new Error("promptAsync timed out after 120000ms")) }, 120000) }) try { await Promise.race([promptPromise, timeoutPromise]) } finally { if (timeoutID !== null) clearTimeout(timeoutID) } } /** * Synchronous variant of promptWithModelSuggestionRetry. * * Uses `session.prompt` (blocking HTTP call that waits for the LLM response) * instead of `promptAsync` (fire-and-forget HTTP 204). * * Required by callers that need the response to be available immediately after * the call returns — e.g. look_at, which reads session messages right away. */ export async function promptSyncWithModelSuggestionRetry( client: Client, args: PromptArgs, ): Promise { try { await client.session.prompt(args as Parameters[0]) } catch (error) { const suggestion = parseModelSuggestion(error) if (!suggestion || !args.body.model) { throw error } log("[model-suggestion-retry] Model not found, retrying with suggestion", { original: `${suggestion.providerID}/${suggestion.modelID}`, suggested: suggestion.suggestion, }) await client.session.prompt({ ...args, body: { ...args.body, model: { providerID: suggestion.providerID, modelID: suggestion.suggestion, }, }, } as Parameters[0]) } }