edaa95fec0
Move three pure helpers from src/shared/ into @oh-my-opencode/model-core so the package can stand alone without depending on plugin internals:
- buildModelCapabilitiesSnapshotFromModelsDev + fetchModelCapabilitiesSnapshot (models.dev normalization)
- parseModelSuggestion (cross-provider ProviderModelNotFoundError suggestion extraction)
- resolveActualContextLimit (Anthropic GA 1M context override)
Split provider-model-id-transform into two variants exposed by model-core:
- transformModelForProvider keeps the runtime dash to dot Anthropic rewrite used by the SDK
- transformModelForProviderDisplay preserves hyphenated Anthropic IDs so the installer writes registry-compatible model strings, fixing the ProviderModelNotFoundError fresh installs hit when the dotted form leaks into the config
src/shared/* and src/cli/provider-model-id-transform.ts collapse to re-export shims that point at the new core modules. Stale src/shared/{known-variants,model-capability-aliases,model-capability-guardrails,model-capability-heuristics}.ts re-export files plus the duplicated context-limit-resolver test are removed in favor of the canonical model-core copies.
Tests: bun test packages/model-core src/shared/model-capabilities-cache.test.ts src/cli/provider-model-id-transform.test.ts
221 lines
6.9 KiB
TypeScript
221 lines
6.9 KiB
TypeScript
import type { createOpencodeClient } from "@opencode-ai/sdk"
|
|
import { parseModelSuggestion as parseModelSuggestionFromCore } from "@oh-my-opencode/model-core"
|
|
import { log } from "./logger"
|
|
import {
|
|
createPromptTimeoutContext,
|
|
PROMPT_TIMEOUT_MS,
|
|
type PromptRetryOptions,
|
|
} from "./prompt-timeout-context"
|
|
import {
|
|
dispatchInternalPrompt,
|
|
isInternalPromptDispatchAccepted,
|
|
releasePromptAsyncReservation,
|
|
} from "./prompt-async-gate"
|
|
import { isAmbiguousPostDispatchPromptFailure } from "./prompt-failure-classifier"
|
|
|
|
type Client = ReturnType<typeof createOpencodeClient>
|
|
|
|
export type { ModelSuggestionInfo } from "@oh-my-opencode/model-core"
|
|
export { parseModelSuggestionFromCore as parseModelSuggestion }
|
|
|
|
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<string, unknown>
|
|
if (typeof obj.message === "string") return obj.message
|
|
try {
|
|
return JSON.stringify(error)
|
|
} catch {
|
|
return ""
|
|
}
|
|
}
|
|
return String(error)
|
|
}
|
|
|
|
function isAgentResolutionError(error: unknown): boolean {
|
|
const message = extractMessage(error)
|
|
return message.includes("Agent not found") || message.includes("agent.name")
|
|
}
|
|
|
|
function shouldReleaseReservationAfterFailedAsyncPrompt(error: unknown): boolean {
|
|
return parseModelSuggestionFromCore(error) !== null || isAgentResolutionError(error)
|
|
}
|
|
|
|
interface PromptBody {
|
|
model?: { providerID: string; modelID: string }
|
|
[key: string]: unknown
|
|
}
|
|
|
|
interface PromptArgs {
|
|
path: { id: string }
|
|
body: PromptBody
|
|
signal?: AbortSignal
|
|
[key: string]: unknown
|
|
}
|
|
|
|
export async function promptWithModelSuggestionRetry(
|
|
client: Client,
|
|
args: PromptArgs,
|
|
options: PromptRetryOptions = {},
|
|
): Promise<void> {
|
|
const timeoutMs = options.timeoutMs ?? PROMPT_TIMEOUT_MS
|
|
const timeoutContext = createPromptTimeoutContext(args, timeoutMs)
|
|
|
|
try {
|
|
const promptResult = await dispatchInternalPrompt({
|
|
mode: "async",
|
|
client,
|
|
sessionID: args.path.id,
|
|
input: {
|
|
...args,
|
|
signal: timeoutContext.signal,
|
|
} as Parameters<typeof client.session.promptAsync>[0],
|
|
source: "model-suggestion-retry",
|
|
settleMs: 0,
|
|
...(options.queueBehavior ? { queueBehavior: options.queueBehavior } : {}),
|
|
})
|
|
if (promptResult.status === "failed") {
|
|
if (timeoutContext.wasTimedOut()) {
|
|
throw new Error(`promptAsync timed out after ${timeoutMs}ms`)
|
|
}
|
|
if (isAmbiguousPostDispatchPromptFailure(promptResult)) {
|
|
return
|
|
}
|
|
throw promptResult.error
|
|
}
|
|
if (!isInternalPromptDispatchAccepted(promptResult)) {
|
|
throw new Error(`promptAsync skipped by gate: ${promptResult.status}`)
|
|
}
|
|
if (timeoutContext.wasTimedOut()) {
|
|
throw new Error(`promptAsync timed out after ${timeoutMs}ms`)
|
|
}
|
|
} catch (error) {
|
|
if (timeoutContext.wasTimedOut()) {
|
|
throw new Error(`promptAsync timed out after ${timeoutMs}ms`)
|
|
}
|
|
if (shouldReleaseReservationAfterFailedAsyncPrompt(error)) {
|
|
releasePromptAsyncReservation(args.path.id, "model-suggestion-retry")
|
|
}
|
|
throw error
|
|
} finally {
|
|
timeoutContext.cleanup()
|
|
}
|
|
}
|
|
|
|
export async function promptSyncWithModelSuggestionRetry(
|
|
client: Client,
|
|
args: PromptArgs,
|
|
options: PromptRetryOptions = {},
|
|
): Promise<void> {
|
|
const timeoutMs = options.timeoutMs ?? PROMPT_TIMEOUT_MS
|
|
|
|
try {
|
|
const timeoutContext = createPromptTimeoutContext(args, timeoutMs)
|
|
try {
|
|
const promptResult = await dispatchInternalPrompt({
|
|
mode: "sync",
|
|
client,
|
|
sessionID: args.path.id,
|
|
input: {
|
|
...args,
|
|
signal: timeoutContext.signal,
|
|
} as Parameters<typeof client.session.prompt>[0],
|
|
source: "model-suggestion-retry:sync",
|
|
settleMs: 0,
|
|
checkStatus: false,
|
|
checkToolState: false,
|
|
...(options.queueBehavior ? { queueBehavior: options.queueBehavior } : {}),
|
|
})
|
|
if (promptResult.status === "failed") {
|
|
if (timeoutContext.wasTimedOut()) {
|
|
throw new Error(`prompt timed out after ${timeoutMs}ms`)
|
|
}
|
|
if (isAmbiguousPostDispatchPromptFailure(promptResult)) {
|
|
return
|
|
}
|
|
throw promptResult.error
|
|
}
|
|
if (!isInternalPromptDispatchAccepted(promptResult)) {
|
|
throw new Error(`prompt skipped by gate: ${promptResult.status}`)
|
|
}
|
|
if (timeoutContext.wasTimedOut()) {
|
|
throw new Error(`prompt timed out after ${timeoutMs}ms`)
|
|
}
|
|
} catch (error) {
|
|
if (timeoutContext.wasTimedOut()) {
|
|
throw new Error(`prompt timed out after ${timeoutMs}ms`)
|
|
}
|
|
throw error
|
|
} finally {
|
|
timeoutContext.cleanup()
|
|
}
|
|
} catch (error) {
|
|
const suggestion = parseModelSuggestionFromCore(error)
|
|
if (!suggestion || !args.body.model) {
|
|
throw error
|
|
}
|
|
|
|
// The first attempt failed synchronously with ProviderModelNotFoundError, which means the
|
|
// prompt did not reach the server. Release the post-dispatch reservation hold so the
|
|
// immediate retry can dispatch without waiting for the hold window to expire.
|
|
releasePromptAsyncReservation(args.path.id, "model-suggestion-retry:sync")
|
|
|
|
log("[model-suggestion-retry] Model not found, retrying with suggestion", {
|
|
original: `${suggestion.providerID}/${suggestion.modelID}`,
|
|
suggested: suggestion.suggestion,
|
|
})
|
|
|
|
const retryArgs: PromptArgs = {
|
|
...args,
|
|
body: {
|
|
...args.body,
|
|
model: {
|
|
providerID: suggestion.providerID,
|
|
modelID: suggestion.suggestion,
|
|
},
|
|
},
|
|
}
|
|
|
|
const timeoutContext = createPromptTimeoutContext(retryArgs, timeoutMs)
|
|
try {
|
|
const promptResult = await dispatchInternalPrompt({
|
|
mode: "sync",
|
|
client,
|
|
sessionID: retryArgs.path.id,
|
|
input: {
|
|
...retryArgs,
|
|
signal: timeoutContext.signal,
|
|
} as Parameters<typeof client.session.prompt>[0],
|
|
source: "model-suggestion-retry:sync-retry",
|
|
settleMs: 0,
|
|
checkStatus: false,
|
|
checkToolState: false,
|
|
...(options.queueBehavior ? { queueBehavior: options.queueBehavior } : {}),
|
|
})
|
|
if (promptResult.status === "failed") {
|
|
if (timeoutContext.wasTimedOut()) {
|
|
throw new Error(`prompt timed out after ${timeoutMs}ms`)
|
|
}
|
|
if (isAmbiguousPostDispatchPromptFailure(promptResult)) {
|
|
return
|
|
}
|
|
throw promptResult.error
|
|
}
|
|
if (!isInternalPromptDispatchAccepted(promptResult)) {
|
|
throw new Error(`prompt skipped by gate: ${promptResult.status}`)
|
|
}
|
|
if (timeoutContext.wasTimedOut()) {
|
|
throw new Error(`prompt timed out after ${timeoutMs}ms`)
|
|
}
|
|
} catch (retryError) {
|
|
if (timeoutContext.wasTimedOut()) {
|
|
throw new Error(`prompt timed out after ${timeoutMs}ms`)
|
|
}
|
|
throw retryError
|
|
} finally {
|
|
timeoutContext.cleanup()
|
|
}
|
|
}
|
|
}
|