From 3cc9e8bc30495ca45847ccdb114087f3cde3d13e Mon Sep 17 00:00:00 2001 From: YeonGyu-Kim Date: Fri, 3 Apr 2026 19:36:56 +0900 Subject: [PATCH 1/3] refactor(shared): extract shared cache factory to deduplicate cache patterns --- src/shared/connected-providers-cache.ts | 122 ++++++------------------ src/shared/json-file-cache-store.ts | 98 +++++++++++++++++++ src/shared/model-capabilities-cache.ts | 65 +++---------- 3 files changed, 142 insertions(+), 143 deletions(-) create mode 100644 src/shared/json-file-cache-store.ts diff --git a/src/shared/connected-providers-cache.ts b/src/shared/connected-providers-cache.ts index 444c93943..582c26f01 100644 --- a/src/shared/connected-providers-cache.ts +++ b/src/shared/connected-providers-cache.ts @@ -1,7 +1,6 @@ -import { existsSync, readFileSync, writeFileSync, mkdirSync } from "fs" -import { join } from "path" import { log } from "./logger" import * as dataPath from "./data-path" +import { createJsonFileCacheStore } from "./json-file-cache-store" const CONNECTED_PROVIDERS_CACHE_FILE = "connected-providers.json" const PROVIDER_MODELS_CACHE_FILE = "provider-models.json" @@ -47,115 +46,52 @@ function isRecord(value: unknown): value is Record { export function createConnectedProvidersCacheStore( getCacheDir: () => string = dataPath.getOmoOpenCodeCacheDir ) { - function getCacheFilePath(filename: string): string { - return join(getCacheDir(), filename) - } - - let memConnected: string[] | null | undefined - let memProviderModels: ProviderModelsCache | null | undefined - - function ensureCacheDir(): void { - const cacheDir = getCacheDir() - if (!existsSync(cacheDir)) { - mkdirSync(cacheDir, { recursive: true }) - } - } + const connectedProvidersCacheStore = createJsonFileCacheStore({ + getCacheDir, + filename: CONNECTED_PROVIDERS_CACHE_FILE, + logPrefix: "connected-providers-cache", + cacheLabel: "Cache", + describe: (value) => ({ count: value.connected.length, updatedAt: value.updatedAt }), + }) + const providerModelsCacheStore = createJsonFileCacheStore({ + getCacheDir, + filename: PROVIDER_MODELS_CACHE_FILE, + logPrefix: "connected-providers-cache", + cacheLabel: "Provider-models cache", + describe: (value) => ({ + providerCount: Object.keys(value.models).length, + updatedAt: value.updatedAt, + }), + }) function readConnectedProvidersCache(): string[] | null { - if (memConnected !== undefined) return memConnected - const cacheFile = getCacheFilePath(CONNECTED_PROVIDERS_CACHE_FILE) - - if (!existsSync(cacheFile)) { - log("[connected-providers-cache] Cache file not found", { cacheFile }) - memConnected = null - return null - } - - try { - const content = readFileSync(cacheFile, "utf-8") - const data = JSON.parse(content) as ConnectedProvidersCache - log("[connected-providers-cache] Read cache", { count: data.connected.length, updatedAt: data.updatedAt }) - memConnected = data.connected - return data.connected - } catch (err) { - log("[connected-providers-cache] Error reading cache", { error: String(err) }) - memConnected = null - return null - } + return connectedProvidersCacheStore.read()?.connected ?? null } function hasConnectedProvidersCache(): boolean { - const cacheFile = getCacheFilePath(CONNECTED_PROVIDERS_CACHE_FILE) - return existsSync(cacheFile) + return connectedProvidersCacheStore.has() } function writeConnectedProvidersCache(connected: string[]): void { - ensureCacheDir() - const cacheFile = getCacheFilePath(CONNECTED_PROVIDERS_CACHE_FILE) - - const data: ConnectedProvidersCache = { + connectedProvidersCacheStore.write({ connected, updatedAt: new Date().toISOString(), - } - - try { - writeFileSync(cacheFile, JSON.stringify(data, null, 2)) - memConnected = connected - log("[connected-providers-cache] Cache written", { count: connected.length }) - } catch (err) { - log("[connected-providers-cache] Error writing cache", { error: String(err) }) - } + }) } function readProviderModelsCache(): ProviderModelsCache | null { - if (memProviderModels !== undefined) return memProviderModels - const cacheFile = getCacheFilePath(PROVIDER_MODELS_CACHE_FILE) - - if (!existsSync(cacheFile)) { - log("[connected-providers-cache] Provider-models cache file not found", { cacheFile }) - memProviderModels = null - return null - } - - try { - const content = readFileSync(cacheFile, "utf-8") - const data = JSON.parse(content) as ProviderModelsCache - log("[connected-providers-cache] Read provider-models cache", { - providerCount: Object.keys(data.models).length, - updatedAt: data.updatedAt, - }) - memProviderModels = data - return data - } catch (err) { - log("[connected-providers-cache] Error reading provider-models cache", { error: String(err) }) - memProviderModels = null - return null - } + return providerModelsCacheStore.read() } function hasProviderModelsCache(): boolean { - const cacheFile = getCacheFilePath(PROVIDER_MODELS_CACHE_FILE) - return existsSync(cacheFile) + return providerModelsCacheStore.has() } function writeProviderModelsCache(data: { models: Record; connected: string[] }): void { - ensureCacheDir() - const cacheFile = getCacheFilePath(PROVIDER_MODELS_CACHE_FILE) - - const cacheData: ProviderModelsCache = { + providerModelsCacheStore.write({ ...data, updatedAt: new Date().toISOString(), - } - - try { - writeFileSync(cacheFile, JSON.stringify(cacheData, null, 2)) - memProviderModels = cacheData - log("[connected-providers-cache] Provider-models cache written", { - providerCount: Object.keys(data.models).length, - }) - } catch (err) { - log("[connected-providers-cache] Error writing provider-models cache", { error: String(err) }) - } + }) } async function updateConnectedProvidersCache(client: { @@ -223,8 +159,8 @@ export function createConnectedProvidersCacheStore( } function _resetMemCacheForTesting(): void { - memConnected = undefined - memProviderModels = undefined + connectedProvidersCacheStore.resetMemory() + providerModelsCacheStore.resetMemory() } return { @@ -256,7 +192,7 @@ export function findProviderModelMetadata( continue } - if (entry?.id === modelID) { + if (entry.id === modelID) { return entry } } diff --git a/src/shared/json-file-cache-store.ts b/src/shared/json-file-cache-store.ts new file mode 100644 index 000000000..5561a66b9 --- /dev/null +++ b/src/shared/json-file-cache-store.ts @@ -0,0 +1,98 @@ +import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs" +import { join } from "node:path" + +import { log } from "./logger" + +type JsonFileCacheStoreOptions = { + getCacheDir: () => string + filename: string + logPrefix: string + cacheLabel: string + describe: (value: TValue) => Record + serialize?: (value: TValue) => string +} + +type JsonFileCacheStore = { + read: () => TValue | null + has: () => boolean + write: (value: TValue) => void + resetMemory: () => void +} + +function toLogLabel(cacheLabel: string): string { + return cacheLabel.toLowerCase() +} + +export function createJsonFileCacheStore( + options: JsonFileCacheStoreOptions, +): JsonFileCacheStore { + let memoryValue: TValue | null | undefined + + function getCacheFilePath(): string { + return join(options.getCacheDir(), options.filename) + } + + function ensureCacheDir(): void { + const cacheDir = options.getCacheDir() + if (!existsSync(cacheDir)) { + mkdirSync(cacheDir, { recursive: true }) + } + } + + function read(): TValue | null { + if (memoryValue !== undefined) { + return memoryValue + } + + const cacheFile = getCacheFilePath() + if (!existsSync(cacheFile)) { + memoryValue = null + log(`[${options.logPrefix}] ${options.cacheLabel} file not found`, { cacheFile }) + return null + } + + try { + const content = readFileSync(cacheFile, "utf-8") + const value = JSON.parse(content) as TValue + memoryValue = value + log(`[${options.logPrefix}] Read ${toLogLabel(options.cacheLabel)}`, options.describe(value)) + return value + } catch (error) { + memoryValue = null + log(`[${options.logPrefix}] Error reading ${toLogLabel(options.cacheLabel)}`, { + error: String(error), + }) + return null + } + } + + function has(): boolean { + return existsSync(getCacheFilePath()) + } + + function write(value: TValue): void { + ensureCacheDir() + const cacheFile = getCacheFilePath() + + try { + writeFileSync(cacheFile, options.serialize?.(value) ?? JSON.stringify(value, null, 2)) + memoryValue = value + log(`[${options.logPrefix}] ${options.cacheLabel} written`, options.describe(value)) + } catch (error) { + log(`[${options.logPrefix}] Error writing ${toLogLabel(options.cacheLabel)}`, { + error: String(error), + }) + } + } + + function resetMemory(): void { + memoryValue = undefined + } + + return { + read, + has, + write, + resetMemory, + } +} diff --git a/src/shared/model-capabilities-cache.ts b/src/shared/model-capabilities-cache.ts index bff841c68..37d6b6429 100644 --- a/src/shared/model-capabilities-cache.ts +++ b/src/shared/model-capabilities-cache.ts @@ -1,7 +1,5 @@ -import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs" -import { join } from "path" import * as dataPath from "./data-path" -import { log } from "./logger" +import { createJsonFileCacheStore } from "./json-file-cache-store" import type { ModelCapabilitiesSnapshot, ModelCapabilitiesSnapshotEntry } from "./model-capabilities" export const MODELS_DEV_SOURCE_URL = "https://models.dev/api.json" @@ -162,61 +160,28 @@ export async function fetchModelCapabilitiesSnapshot(args: { export function createModelCapabilitiesCacheStore( getCacheDir: () => string = dataPath.getOmoOpenCodeCacheDir, ) { - let memSnapshot: ModelCapabilitiesSnapshot | null | undefined - - function getCacheFilePath(): string { - return join(getCacheDir(), MODEL_CAPABILITIES_CACHE_FILE) - } - - function ensureCacheDir(): void { - const cacheDir = getCacheDir() - if (!existsSync(cacheDir)) { - mkdirSync(cacheDir, { recursive: true }) - } - } + const snapshotCacheStore = createJsonFileCacheStore({ + getCacheDir, + filename: MODEL_CAPABILITIES_CACHE_FILE, + logPrefix: "model-capabilities-cache", + cacheLabel: "Cache", + describe: (snapshot) => ({ + modelCount: Object.keys(snapshot.models).length, + generatedAt: snapshot.generatedAt, + }), + serialize: (snapshot) => `${JSON.stringify(snapshot, null, 2)}\n`, + }) function readModelCapabilitiesCache(): ModelCapabilitiesSnapshot | null { - if (memSnapshot !== undefined) { - return memSnapshot - } - - const cacheFile = getCacheFilePath() - if (!existsSync(cacheFile)) { - memSnapshot = null - log("[model-capabilities-cache] Cache file not found", { cacheFile }) - return null - } - - try { - const content = readFileSync(cacheFile, "utf-8") - const snapshot = JSON.parse(content) as ModelCapabilitiesSnapshot - memSnapshot = snapshot - log("[model-capabilities-cache] Read cache", { - modelCount: Object.keys(snapshot.models).length, - generatedAt: snapshot.generatedAt, - }) - return snapshot - } catch (error) { - memSnapshot = null - log("[model-capabilities-cache] Error reading cache", { error: String(error) }) - return null - } + return snapshotCacheStore.read() } function hasModelCapabilitiesCache(): boolean { - return existsSync(getCacheFilePath()) + return snapshotCacheStore.has() } function writeModelCapabilitiesCache(snapshot: ModelCapabilitiesSnapshot): void { - ensureCacheDir() - const cacheFile = getCacheFilePath() - - writeFileSync(cacheFile, JSON.stringify(snapshot, null, 2) + "\n") - memSnapshot = snapshot - log("[model-capabilities-cache] Cache written", { - modelCount: Object.keys(snapshot.models).length, - generatedAt: snapshot.generatedAt, - }) + snapshotCacheStore.write(snapshot) } async function refreshModelCapabilitiesCache(args: { From d0f795dd8f168648c73937ea7d161d177c2fb835 Mon Sep 17 00:00:00 2001 From: YeonGyu-Kim Date: Fri, 3 Apr 2026 19:38:26 +0900 Subject: [PATCH 2/3] refactor(shared): decompose model-capabilities into focused modules --- src/shared/model-capabilities.ts | 462 ------------------ .../model-capabilities/bundled-snapshot.ts | 15 + .../get-model-capabilities.ts | 140 ++++++ src/shared/model-capabilities/index.ts | 9 + .../runtime-model-readers.ts | 190 +++++++ src/shared/model-capabilities/types.ts | 80 +++ 6 files changed, 434 insertions(+), 462 deletions(-) delete mode 100644 src/shared/model-capabilities.ts create mode 100644 src/shared/model-capabilities/bundled-snapshot.ts create mode 100644 src/shared/model-capabilities/get-model-capabilities.ts create mode 100644 src/shared/model-capabilities/index.ts create mode 100644 src/shared/model-capabilities/runtime-model-readers.ts create mode 100644 src/shared/model-capabilities/types.ts diff --git a/src/shared/model-capabilities.ts b/src/shared/model-capabilities.ts deleted file mode 100644 index 0a9749243..000000000 --- a/src/shared/model-capabilities.ts +++ /dev/null @@ -1,462 +0,0 @@ -import bundledModelCapabilitiesSnapshotJson from "../generated/model-capabilities.generated.json" -import { findProviderModelMetadata, type ModelMetadata } from "./connected-providers-cache" -import { resolveModelIDAlias } from "./model-capability-aliases" -import { detectHeuristicModelFamily } from "./model-capability-heuristics" - -export type ModelCapabilitiesSnapshotEntry = { - id: string - family?: string - reasoning?: boolean - temperature?: boolean - toolCall?: boolean - modalities?: { - input?: string[] - output?: string[] - } - limit?: { - context?: number - input?: number - output?: number - } -} - -export type ModelCapabilitiesSnapshot = { - generatedAt: string - sourceUrl: string - models: Record -} - -export type ModelCapabilities = { - requestedModelID: string - canonicalModelID: string - family?: string - variants?: string[] - reasoningEfforts?: string[] - reasoning?: boolean - supportsThinking?: boolean - supportsTemperature?: boolean - supportsTopP?: boolean - maxOutputTokens?: number - toolCall?: boolean - modalities?: { - input?: string[] - output?: string[] - } - diagnostics: ModelCapabilitiesDiagnostics -} - -type GetModelCapabilitiesInput = { - providerID: string - modelID: string - runtimeModel?: ModelMetadata | Record - runtimeSnapshot?: ModelCapabilitiesSnapshot - bundledSnapshot?: ModelCapabilitiesSnapshot -} - -type ModelCapabilityOverride = { - variants?: string[] - reasoningEfforts?: string[] - supportsThinking?: boolean - supportsTemperature?: boolean - supportsTopP?: boolean -} - -type DiagnosticSource = - | "none" - | "runtime" - | "runtime-snapshot" - | "bundled-snapshot" - | "override" - | "heuristic" - | "canonical" - | "exact-alias" - | "pattern-alias" - -export type ModelCapabilitiesDiagnostics = { - resolutionMode: "snapshot-backed" | "alias-backed" | "heuristic-backed" | "unknown" - canonicalization: { - source: "canonical" | "exact-alias" | "pattern-alias" - ruleID?: string - } - snapshot: { - source: "runtime-snapshot" | "bundled-snapshot" | "none" - } - family: { source: "snapshot" | "heuristic" | "none" } - variants: { source: Exclude } - reasoningEfforts: { source: Exclude } - reasoning: { source: "runtime" | "runtime-snapshot" | "bundled-snapshot" | "none" } - supportsThinking: { source: "runtime" | "override" | "heuristic" | "runtime-snapshot" | "bundled-snapshot" | "none" } - supportsTemperature: { source: "runtime" | "override" | "runtime-snapshot" | "bundled-snapshot" | "none" } - supportsTopP: { source: "runtime" | "override" | "none" } - maxOutputTokens: { source: "runtime" | "runtime-snapshot" | "bundled-snapshot" | "none" } - toolCall: { source: "runtime" | "runtime-snapshot" | "bundled-snapshot" | "none" } - modalities: { source: "runtime" | "runtime-snapshot" | "bundled-snapshot" | "none" } -} - -const MODEL_ID_OVERRIDES: Record = {} - -function isRecord(value: unknown): value is Record { - return typeof value === "object" && value !== null && !Array.isArray(value) -} - -function normalizeLookupModelID(modelID: string): string { - return modelID.trim().toLowerCase() -} - -function readBoolean(value: unknown): boolean | undefined { - return typeof value === "boolean" ? value : undefined -} - -function readNumber(value: unknown): number | undefined { - return typeof value === "number" ? value : undefined -} - -function readStringArray(value: unknown): string[] | undefined { - if (!Array.isArray(value)) { - return undefined - } - - const strings = value.filter((item): item is string => typeof item === "string") - return strings.length > 0 ? strings : undefined -} - -function normalizeVariantKeys(value: unknown): string[] | undefined { - const arrayVariants = readStringArray(value) - if (arrayVariants) { - return arrayVariants.map((variant) => variant.toLowerCase()) - } - - if (!isRecord(value)) { - return undefined - } - - const variants = Object.keys(value).map((variant) => variant.toLowerCase()) - return variants.length > 0 ? variants : undefined -} - -function readModalityKeys(value: unknown): string[] | undefined { - const stringArray = readStringArray(value) - if (stringArray) { - return stringArray.map((entry) => entry.toLowerCase()) - } - - if (!isRecord(value)) { - return undefined - } - - const enabled = Object.entries(value) - .filter(([, supported]) => supported === true) - .map(([modality]) => modality.toLowerCase()) - - return enabled.length > 0 ? enabled : undefined -} - -function normalizeModalities(value: unknown): ModelCapabilities["modalities"] | undefined { - if (!isRecord(value)) { - return undefined - } - - const input = readModalityKeys(value.input) - const output = readModalityKeys(value.output) - - if (!input && !output) { - return undefined - } - - return { - ...(input ? { input } : {}), - ...(output ? { output } : {}), - } -} - -function normalizeSnapshot(snapshot: ModelCapabilitiesSnapshot | typeof bundledModelCapabilitiesSnapshotJson): ModelCapabilitiesSnapshot { - return snapshot as ModelCapabilitiesSnapshot -} - -function getOverride(modelID: string): ModelCapabilityOverride | undefined { - return MODEL_ID_OVERRIDES[normalizeLookupModelID(modelID)] -} - -function readRuntimeModelCapabilities(runtimeModel: Record | undefined): Record | undefined { - return isRecord(runtimeModel?.capabilities) ? runtimeModel.capabilities : undefined -} - -function readRuntimeModelLimitOutput(runtimeModel: Record | undefined): number | undefined { - if (!runtimeModel) { - return undefined - } - - const limit = isRecord(runtimeModel.limit) - ? runtimeModel.limit - : readRuntimeModelCapabilities(runtimeModel)?.limit - if (!isRecord(limit)) { - return undefined - } - - return readNumber(limit.output) -} - -function readRuntimeModelBoolean(runtimeModel: Record | undefined, keys: string[]): boolean | undefined { - if (!runtimeModel) { - return undefined - } - - const runtimeCapabilities = readRuntimeModelCapabilities(runtimeModel) - - for (const key of keys) { - const value = runtimeModel[key] - if (typeof value === "boolean") { - return value - } - - const capabilityValue = runtimeCapabilities?.[key] - if (typeof capabilityValue === "boolean") { - return capabilityValue - } - } - - return undefined -} - -function readRuntimeModelModalities(runtimeModel: Record | undefined): ModelCapabilities["modalities"] | undefined { - if (!runtimeModel) { - return undefined - } - - const rootModalities = normalizeModalities(runtimeModel.modalities) - if (rootModalities) { - return rootModalities - } - - const runtimeCapabilities = readRuntimeModelCapabilities(runtimeModel) - if (!runtimeCapabilities) { - return undefined - } - - const nestedModalities = normalizeModalities(runtimeCapabilities.modalities) - if (nestedModalities) { - return nestedModalities - } - - const capabilityModalities = normalizeModalities(runtimeCapabilities) - if (capabilityModalities) { - return capabilityModalities - } - - return undefined -} - -function readRuntimeModelVariants(runtimeModel: Record | undefined): string[] | undefined { - if (!runtimeModel) { - return undefined - } - - const rootVariants = normalizeVariantKeys(runtimeModel.variants) - if (rootVariants) { - return rootVariants - } - - const runtimeCapabilities = readRuntimeModelCapabilities(runtimeModel) - if (!runtimeCapabilities) { - return undefined - } - - return normalizeVariantKeys(runtimeCapabilities.variants) -} - -function readRuntimeModelTopPSupport(runtimeModel: Record | undefined): boolean | undefined { - return readRuntimeModelBoolean(runtimeModel, ["topP", "top_p"]) -} - -function readRuntimeModelToolCallSupport(runtimeModel: Record | undefined): boolean | undefined { - return readRuntimeModelBoolean(runtimeModel, ["toolCall", "tool_call", "toolcall"]) -} - -function readRuntimeModelReasoningSupport(runtimeModel: Record | undefined): boolean | undefined { - return readRuntimeModelBoolean(runtimeModel, ["reasoning"]) -} - -function readRuntimeModelTemperatureSupport(runtimeModel: Record | undefined): boolean | undefined { - return readRuntimeModelBoolean(runtimeModel, ["temperature"]) -} - -function readRuntimeModelThinkingSupport(runtimeModel: Record | undefined): boolean | undefined { - const capabilityValue = readRuntimeModelReasoningSupport(runtimeModel) - if (capabilityValue !== undefined) { - return capabilityValue - } - - const rootThinkingSupport = readRuntimeModelBoolean(runtimeModel, ["thinking", "supportsThinking"]) - if (rootThinkingSupport !== undefined) { - return rootThinkingSupport - } - - const runtimeCapabilities = readRuntimeModelCapabilities(runtimeModel) - if (!runtimeCapabilities) { - return undefined - } - - for (const key of ["thinking", "supportsThinking"] as const) { - const value = runtimeCapabilities[key] - if (typeof value === "boolean") { - return value - } - } - - return undefined -} - -function readRuntimeModel(runtimeModel: ModelMetadata | Record | undefined): Record | undefined { - return isRecord(runtimeModel) ? runtimeModel : undefined -} - -const bundledModelCapabilitiesSnapshot = normalizeSnapshot(bundledModelCapabilitiesSnapshotJson) - -export function getBundledModelCapabilitiesSnapshot(): ModelCapabilitiesSnapshot { - return bundledModelCapabilitiesSnapshot -} - -export function getModelCapabilities(input: GetModelCapabilitiesInput): ModelCapabilities { - const canonicalization = resolveModelIDAlias(input.modelID) - const requestedModelID = canonicalization.requestedModelID - const canonicalModelID = canonicalization.canonicalModelID - const override = getOverride(input.modelID) - const runtimeModel = readRuntimeModel( - input.runtimeModel ?? findProviderModelMetadata(input.providerID, input.modelID), - ) - const runtimeSnapshot = input.runtimeSnapshot - const bundledSnapshot = input.bundledSnapshot ?? bundledModelCapabilitiesSnapshot - const snapshotEntry = runtimeSnapshot?.models?.[canonicalModelID] ?? bundledSnapshot.models[canonicalModelID] - const heuristicFamily = detectHeuristicModelFamily(canonicalModelID) - const runtimeVariants = readRuntimeModelVariants(runtimeModel) - const snapshotSource: ModelCapabilitiesDiagnostics["snapshot"]["source"] = - runtimeSnapshot?.models?.[canonicalModelID] - ? "runtime-snapshot" - : bundledSnapshot.models[canonicalModelID] - ? "bundled-snapshot" - : "none" - const familySource: ModelCapabilitiesDiagnostics["family"]["source"] = - snapshotEntry?.family - ? "snapshot" - : heuristicFamily?.family - ? "heuristic" - : "none" - const variantsSource: ModelCapabilitiesDiagnostics["variants"]["source"] = - runtimeVariants - ? "runtime" - : override?.variants - ? "override" - : heuristicFamily?.variants - ? "heuristic" - : "none" - const reasoningEffortsSource: ModelCapabilitiesDiagnostics["reasoningEfforts"]["source"] = - override?.reasoningEfforts - ? "override" - : heuristicFamily?.reasoningEfforts - ? "heuristic" - : "none" - const reasoningSource: ModelCapabilitiesDiagnostics["reasoning"]["source"] = - readRuntimeModelReasoningSupport(runtimeModel) !== undefined - ? "runtime" - : snapshotEntry?.reasoning !== undefined - ? snapshotSource - : "none" - const supportsThinkingSource: ModelCapabilitiesDiagnostics["supportsThinking"]["source"] = - override?.supportsThinking !== undefined - ? "override" - : heuristicFamily?.supportsThinking !== undefined - ? "heuristic" - : readRuntimeModelThinkingSupport(runtimeModel) !== undefined - ? "runtime" - : snapshotEntry?.reasoning !== undefined - ? snapshotSource - : "none" - const supportsTemperatureSource: ModelCapabilitiesDiagnostics["supportsTemperature"]["source"] = - readRuntimeModelTemperatureSupport(runtimeModel) !== undefined - ? "runtime" - : override?.supportsTemperature !== undefined - ? "override" - : snapshotEntry?.temperature !== undefined - ? snapshotSource - : "none" - const supportsTopPSource: ModelCapabilitiesDiagnostics["supportsTopP"]["source"] = - readRuntimeModelTopPSupport(runtimeModel) !== undefined - ? "runtime" - : override?.supportsTopP !== undefined - ? "override" - : "none" - const maxOutputTokensSource: ModelCapabilitiesDiagnostics["maxOutputTokens"]["source"] = - readRuntimeModelLimitOutput(runtimeModel) !== undefined - ? "runtime" - : snapshotEntry?.limit?.output !== undefined - ? snapshotSource - : "none" - const toolCallSource: ModelCapabilitiesDiagnostics["toolCall"]["source"] = - readRuntimeModelToolCallSupport(runtimeModel) !== undefined - ? "runtime" - : snapshotEntry?.toolCall !== undefined - ? snapshotSource - : "none" - const modalitiesSource: ModelCapabilitiesDiagnostics["modalities"]["source"] = - readRuntimeModelModalities(runtimeModel) !== undefined - ? "runtime" - : snapshotEntry?.modalities !== undefined - ? snapshotSource - : "none" - const resolutionMode: ModelCapabilitiesDiagnostics["resolutionMode"] = - snapshotSource !== "none" && canonicalization.source === "canonical" - ? "snapshot-backed" - : snapshotSource !== "none" - ? "alias-backed" - : familySource === "heuristic" || variantsSource === "heuristic" || reasoningEffortsSource === "heuristic" - ? "heuristic-backed" - : "unknown" - - return { - requestedModelID, - canonicalModelID, - family: snapshotEntry?.family ?? heuristicFamily?.family, - variants: runtimeVariants ?? override?.variants ?? heuristicFamily?.variants, - reasoningEfforts: override?.reasoningEfforts ?? heuristicFamily?.reasoningEfforts, - reasoning: readRuntimeModelReasoningSupport(runtimeModel) ?? snapshotEntry?.reasoning, - supportsThinking: - override?.supportsThinking - ?? heuristicFamily?.supportsThinking - ?? readRuntimeModelThinkingSupport(runtimeModel) - ?? snapshotEntry?.reasoning, - supportsTemperature: - readRuntimeModelTemperatureSupport(runtimeModel) - ?? override?.supportsTemperature - ?? snapshotEntry?.temperature, - supportsTopP: - readRuntimeModelTopPSupport(runtimeModel) - ?? override?.supportsTopP, - maxOutputTokens: - readRuntimeModelLimitOutput(runtimeModel) - ?? snapshotEntry?.limit?.output, - toolCall: - readRuntimeModelToolCallSupport(runtimeModel) - ?? snapshotEntry?.toolCall, - modalities: - readRuntimeModelModalities(runtimeModel) - ?? snapshotEntry?.modalities, - diagnostics: { - resolutionMode, - canonicalization: { - source: canonicalization.source, - ...(canonicalization.ruleID ? { ruleID: canonicalization.ruleID } : {}), - }, - snapshot: { source: snapshotSource }, - family: { source: familySource }, - variants: { source: variantsSource }, - reasoningEfforts: { source: reasoningEffortsSource }, - reasoning: { source: reasoningSource }, - supportsThinking: { source: supportsThinkingSource }, - supportsTemperature: { source: supportsTemperatureSource }, - supportsTopP: { source: supportsTopPSource }, - maxOutputTokens: { source: maxOutputTokensSource }, - toolCall: { source: toolCallSource }, - modalities: { source: modalitiesSource }, - }, - } -} diff --git a/src/shared/model-capabilities/bundled-snapshot.ts b/src/shared/model-capabilities/bundled-snapshot.ts new file mode 100644 index 000000000..65644a8cf --- /dev/null +++ b/src/shared/model-capabilities/bundled-snapshot.ts @@ -0,0 +1,15 @@ +import bundledModelCapabilitiesSnapshotJson from "../../generated/model-capabilities.generated.json" + +import type { ModelCapabilitiesSnapshot } from "./types" + +function normalizeSnapshot( + snapshot: ModelCapabilitiesSnapshot | typeof bundledModelCapabilitiesSnapshotJson, +): ModelCapabilitiesSnapshot { + return snapshot as ModelCapabilitiesSnapshot +} + +const bundledModelCapabilitiesSnapshot = normalizeSnapshot(bundledModelCapabilitiesSnapshotJson) + +export function getBundledModelCapabilitiesSnapshot(): ModelCapabilitiesSnapshot { + return bundledModelCapabilitiesSnapshot +} diff --git a/src/shared/model-capabilities/get-model-capabilities.ts b/src/shared/model-capabilities/get-model-capabilities.ts new file mode 100644 index 000000000..fa27f1e86 --- /dev/null +++ b/src/shared/model-capabilities/get-model-capabilities.ts @@ -0,0 +1,140 @@ +import { findProviderModelMetadata } from "../connected-providers-cache" +import { resolveModelIDAlias } from "../model-capability-aliases" +import { detectHeuristicModelFamily } from "../model-capability-heuristics" + +import { getBundledModelCapabilitiesSnapshot } from "./bundled-snapshot" +import { + readRuntimeModel, + readRuntimeModelLimitOutput, + readRuntimeModelModalities, + readRuntimeModelReasoningSupport, + readRuntimeModelTemperatureSupport, + readRuntimeModelThinkingSupport, + readRuntimeModelToolCallSupport, + readRuntimeModelTopPSupport, + readRuntimeModelVariants, +} from "./runtime-model-readers" +import type { + GetModelCapabilitiesInput, + ModelCapabilities, + ModelCapabilitiesDiagnostics, + ModelCapabilityOverride, +} from "./types" + +const MODEL_ID_OVERRIDES: Record = {} + +function normalizeLookupModelID(modelID: string): string { + return modelID.trim().toLowerCase() +} + +function getOverride(modelID: string): ModelCapabilityOverride | undefined { + return MODEL_ID_OVERRIDES[normalizeLookupModelID(modelID)] +} + +export function getModelCapabilities(input: GetModelCapabilitiesInput): ModelCapabilities { + const canonicalization = resolveModelIDAlias(input.modelID) + const override = getOverride(input.modelID) + const runtimeModel = readRuntimeModel( + input.runtimeModel ?? findProviderModelMetadata(input.providerID, input.modelID), + ) + const runtimeSnapshot = input.runtimeSnapshot + const bundledSnapshot = input.bundledSnapshot ?? getBundledModelCapabilitiesSnapshot() + const snapshotEntry = runtimeSnapshot?.models?.[canonicalization.canonicalModelID] + ?? bundledSnapshot.models[canonicalization.canonicalModelID] + const heuristicFamily = detectHeuristicModelFamily(canonicalization.canonicalModelID) + + const runtimeVariants = readRuntimeModelVariants(runtimeModel) + const runtimeReasoning = readRuntimeModelReasoningSupport(runtimeModel) + const runtimeThinking = readRuntimeModelThinkingSupport(runtimeModel) + const runtimeTemperature = readRuntimeModelTemperatureSupport(runtimeModel) + const runtimeTopP = readRuntimeModelTopPSupport(runtimeModel) + const runtimeMaxOutputTokens = readRuntimeModelLimitOutput(runtimeModel) + const runtimeToolCall = readRuntimeModelToolCallSupport(runtimeModel) + const runtimeModalities = readRuntimeModelModalities(runtimeModel) + + const snapshotSource: ModelCapabilitiesDiagnostics["snapshot"]["source"] = + runtimeSnapshot?.models?.[canonicalization.canonicalModelID] + ? "runtime-snapshot" + : bundledSnapshot.models[canonicalization.canonicalModelID] + ? "bundled-snapshot" + : "none" + const familySource: ModelCapabilitiesDiagnostics["family"]["source"] = + snapshotEntry?.family ? "snapshot" : heuristicFamily?.family ? "heuristic" : "none" + const variantsSource: ModelCapabilitiesDiagnostics["variants"]["source"] = + runtimeVariants ? "runtime" : override?.variants ? "override" : heuristicFamily?.variants ? "heuristic" : "none" + const reasoningEffortsSource: ModelCapabilitiesDiagnostics["reasoningEfforts"]["source"] = + override?.reasoningEfforts ? "override" : heuristicFamily?.reasoningEfforts ? "heuristic" : "none" + const reasoningSource: ModelCapabilitiesDiagnostics["reasoning"]["source"] = + runtimeReasoning === undefined ? snapshotEntry?.reasoning === undefined ? "none" : snapshotSource : "runtime" + const supportsThinkingSource: ModelCapabilitiesDiagnostics["supportsThinking"]["source"] = + override?.supportsThinking !== undefined + ? "override" + : heuristicFamily?.supportsThinking !== undefined + ? "heuristic" + : runtimeThinking !== undefined + ? "runtime" + : snapshotEntry?.reasoning !== undefined + ? snapshotSource + : "none" + const supportsTemperatureSource: ModelCapabilitiesDiagnostics["supportsTemperature"]["source"] = + runtimeTemperature !== undefined + ? "runtime" + : override?.supportsTemperature !== undefined + ? "override" + : snapshotEntry?.temperature !== undefined + ? snapshotSource + : "none" + const supportsTopPSource: ModelCapabilitiesDiagnostics["supportsTopP"]["source"] = + runtimeTopP !== undefined ? "runtime" : override?.supportsTopP !== undefined ? "override" : "none" + const maxOutputTokensSource: ModelCapabilitiesDiagnostics["maxOutputTokens"]["source"] = + runtimeMaxOutputTokens !== undefined + ? "runtime" + : snapshotEntry?.limit?.output !== undefined + ? snapshotSource + : "none" + const toolCallSource: ModelCapabilitiesDiagnostics["toolCall"]["source"] = + runtimeToolCall !== undefined ? "runtime" : snapshotEntry?.toolCall !== undefined ? snapshotSource : "none" + const modalitiesSource: ModelCapabilitiesDiagnostics["modalities"]["source"] = + runtimeModalities !== undefined ? "runtime" : snapshotEntry?.modalities !== undefined ? snapshotSource : "none" + const resolutionMode: ModelCapabilitiesDiagnostics["resolutionMode"] = + snapshotSource !== "none" && canonicalization.source === "canonical" + ? "snapshot-backed" + : snapshotSource !== "none" + ? "alias-backed" + : familySource === "heuristic" || variantsSource === "heuristic" || reasoningEffortsSource === "heuristic" + ? "heuristic-backed" + : "unknown" + + return { + requestedModelID: canonicalization.requestedModelID, + canonicalModelID: canonicalization.canonicalModelID, + family: snapshotEntry?.family ?? heuristicFamily?.family, + variants: runtimeVariants ?? override?.variants ?? heuristicFamily?.variants, + reasoningEfforts: override?.reasoningEfforts ?? heuristicFamily?.reasoningEfforts, + reasoning: runtimeReasoning ?? snapshotEntry?.reasoning, + supportsThinking: override?.supportsThinking ?? heuristicFamily?.supportsThinking ?? runtimeThinking ?? snapshotEntry?.reasoning, + supportsTemperature: runtimeTemperature ?? override?.supportsTemperature ?? snapshotEntry?.temperature, + supportsTopP: runtimeTopP ?? override?.supportsTopP, + maxOutputTokens: runtimeMaxOutputTokens ?? snapshotEntry?.limit?.output, + toolCall: runtimeToolCall ?? snapshotEntry?.toolCall, + modalities: runtimeModalities ?? snapshotEntry?.modalities, + diagnostics: { + resolutionMode, + canonicalization: { + source: canonicalization.source, + ...(canonicalization.ruleID ? { ruleID: canonicalization.ruleID } : {}), + }, + snapshot: { source: snapshotSource }, + family: { source: familySource }, + variants: { source: variantsSource }, + reasoningEfforts: { source: reasoningEffortsSource }, + reasoning: { source: reasoningSource }, + supportsThinking: { source: supportsThinkingSource }, + supportsTemperature: { source: supportsTemperatureSource }, + supportsTopP: { source: supportsTopPSource }, + maxOutputTokens: { source: maxOutputTokensSource }, + toolCall: { source: toolCallSource }, + modalities: { source: modalitiesSource }, + }, + } +} diff --git a/src/shared/model-capabilities/index.ts b/src/shared/model-capabilities/index.ts new file mode 100644 index 000000000..99549195a --- /dev/null +++ b/src/shared/model-capabilities/index.ts @@ -0,0 +1,9 @@ +export { getBundledModelCapabilitiesSnapshot } from "./bundled-snapshot" +export { getModelCapabilities } from "./get-model-capabilities" +export type { + GetModelCapabilitiesInput, + ModelCapabilities, + ModelCapabilitiesDiagnostics, + ModelCapabilitiesSnapshot, + ModelCapabilitiesSnapshotEntry, +} from "./types" diff --git a/src/shared/model-capabilities/runtime-model-readers.ts b/src/shared/model-capabilities/runtime-model-readers.ts new file mode 100644 index 000000000..a7b740f32 --- /dev/null +++ b/src/shared/model-capabilities/runtime-model-readers.ts @@ -0,0 +1,190 @@ +import type { ModelMetadata } from "../connected-providers-cache" + +import type { ModelCapabilities } from "./types" + +function isRecord(value: unknown): value is Record { + return typeof value === "object" && value !== null && !Array.isArray(value) +} + +function readNumber(value: unknown): number | undefined { + return typeof value === "number" ? value : undefined +} + +function readStringArray(value: unknown): string[] | undefined { + if (!Array.isArray(value)) { + return undefined + } + + const strings = value.filter((item): item is string => typeof item === "string") + return strings.length > 0 ? strings : undefined +} + +function normalizeVariantKeys(value: unknown): string[] | undefined { + const arrayVariants = readStringArray(value) + if (arrayVariants) { + return arrayVariants.map((variant) => variant.toLowerCase()) + } + + if (!isRecord(value)) { + return undefined + } + + const variants = Object.keys(value).map((variant) => variant.toLowerCase()) + return variants.length > 0 ? variants : undefined +} + +function readModalityKeys(value: unknown): string[] | undefined { + const stringArray = readStringArray(value) + if (stringArray) { + return stringArray.map((entry) => entry.toLowerCase()) + } + + if (!isRecord(value)) { + return undefined + } + + const enabled = Object.entries(value) + .filter(([, supported]) => supported === true) + .map(([modality]) => modality.toLowerCase()) + + return enabled.length > 0 ? enabled : undefined +} + +function normalizeModalities(value: unknown): ModelCapabilities["modalities"] | undefined { + if (!isRecord(value)) { + return undefined + } + + const input = readModalityKeys(value.input) + const output = readModalityKeys(value.output) + + if (!input && !output) { + return undefined + } + + return { + ...(input ? { input } : {}), + ...(output ? { output } : {}), + } +} + +function readRuntimeModelCapabilities( + runtimeModel: Record | undefined, +): Record | undefined { + return isRecord(runtimeModel?.capabilities) ? runtimeModel.capabilities : undefined +} + +function readRuntimeModelBoolean( + runtimeModel: Record | undefined, + keys: string[], +): boolean | undefined { + const runtimeCapabilities = readRuntimeModelCapabilities(runtimeModel) + + for (const key of keys) { + const value = runtimeModel?.[key] + if (typeof value === "boolean") { + return value + } + + const capabilityValue = runtimeCapabilities?.[key] + if (typeof capabilityValue === "boolean") { + return capabilityValue + } + } + + return undefined +} + +export function readRuntimeModel( + runtimeModel: ModelMetadata | Record | undefined, +): Record | undefined { + return isRecord(runtimeModel) ? runtimeModel : undefined +} + +export function readRuntimeModelVariants( + runtimeModel: Record | undefined, +): string[] | undefined { + const rootVariants = normalizeVariantKeys(runtimeModel?.variants) + if (rootVariants) { + return rootVariants + } + + return normalizeVariantKeys(readRuntimeModelCapabilities(runtimeModel)?.variants) +} + +export function readRuntimeModelModalities( + runtimeModel: Record | undefined, +): ModelCapabilities["modalities"] | undefined { + const rootModalities = normalizeModalities(runtimeModel?.modalities) + if (rootModalities) { + return rootModalities + } + + const runtimeCapabilities = readRuntimeModelCapabilities(runtimeModel) + return ( + normalizeModalities(runtimeCapabilities?.modalities) + ?? normalizeModalities(runtimeCapabilities) + ) +} + +export function readRuntimeModelReasoningSupport( + runtimeModel: Record | undefined, +): boolean | undefined { + return readRuntimeModelBoolean(runtimeModel, ["reasoning"]) +} + +export function readRuntimeModelThinkingSupport( + runtimeModel: Record | undefined, +): boolean | undefined { + const capabilityValue = readRuntimeModelReasoningSupport(runtimeModel) + if (capabilityValue !== undefined) { + return capabilityValue + } + + const thinkingSupport = readRuntimeModelBoolean(runtimeModel, ["thinking", "supportsThinking"]) + if (thinkingSupport !== undefined) { + return thinkingSupport + } + + const runtimeCapabilities = readRuntimeModelCapabilities(runtimeModel) + for (const key of ["thinking", "supportsThinking"] as const) { + const value = runtimeCapabilities?.[key] + if (typeof value === "boolean") { + return value + } + } + + return undefined +} + +export function readRuntimeModelTemperatureSupport( + runtimeModel: Record | undefined, +): boolean | undefined { + return readRuntimeModelBoolean(runtimeModel, ["temperature"]) +} + +export function readRuntimeModelTopPSupport( + runtimeModel: Record | undefined, +): boolean | undefined { + return readRuntimeModelBoolean(runtimeModel, ["topP", "top_p"]) +} + +export function readRuntimeModelToolCallSupport( + runtimeModel: Record | undefined, +): boolean | undefined { + return readRuntimeModelBoolean(runtimeModel, ["toolCall", "tool_call", "toolcall"]) +} + +export function readRuntimeModelLimitOutput( + runtimeModel: Record | undefined, +): number | undefined { + const limit = isRecord(runtimeModel?.limit) + ? runtimeModel.limit + : readRuntimeModelCapabilities(runtimeModel)?.limit + + if (!isRecord(limit)) { + return undefined + } + + return readNumber(limit.output) +} diff --git a/src/shared/model-capabilities/types.ts b/src/shared/model-capabilities/types.ts new file mode 100644 index 000000000..74881c72e --- /dev/null +++ b/src/shared/model-capabilities/types.ts @@ -0,0 +1,80 @@ +import type { ModelMetadata } from "../connected-providers-cache" + +export type ModelCapabilitiesSnapshotEntry = { + id: string + family?: string + reasoning?: boolean + temperature?: boolean + toolCall?: boolean + modalities?: { + input?: string[] + output?: string[] + } + limit?: { + context?: number + input?: number + output?: number + } +} + +export type ModelCapabilitiesSnapshot = { + generatedAt: string + sourceUrl: string + models: Record +} + +export type ModelCapabilitiesDiagnostics = { + resolutionMode: "snapshot-backed" | "alias-backed" | "heuristic-backed" | "unknown" + canonicalization: { + source: "canonical" | "exact-alias" | "pattern-alias" + ruleID?: string + } + snapshot: { + source: "runtime-snapshot" | "bundled-snapshot" | "none" + } + family: { source: "snapshot" | "heuristic" | "none" } + variants: { source: "none" | "runtime" | "override" | "heuristic" | "canonical" } + reasoningEfforts: { source: "none" | "override" | "heuristic" } + reasoning: { source: "runtime" | "runtime-snapshot" | "bundled-snapshot" | "none" } + supportsThinking: { source: "runtime" | "override" | "heuristic" | "runtime-snapshot" | "bundled-snapshot" | "none" } + supportsTemperature: { source: "runtime" | "override" | "runtime-snapshot" | "bundled-snapshot" | "none" } + supportsTopP: { source: "runtime" | "override" | "none" } + maxOutputTokens: { source: "runtime" | "runtime-snapshot" | "bundled-snapshot" | "none" } + toolCall: { source: "runtime" | "runtime-snapshot" | "bundled-snapshot" | "none" } + modalities: { source: "runtime" | "runtime-snapshot" | "bundled-snapshot" | "none" } +} + +export type ModelCapabilities = { + requestedModelID: string + canonicalModelID: string + family?: string + variants?: string[] + reasoningEfforts?: string[] + reasoning?: boolean + supportsThinking?: boolean + supportsTemperature?: boolean + supportsTopP?: boolean + maxOutputTokens?: number + toolCall?: boolean + modalities?: { + input?: string[] + output?: string[] + } + diagnostics: ModelCapabilitiesDiagnostics +} + +export type GetModelCapabilitiesInput = { + providerID: string + modelID: string + runtimeModel?: ModelMetadata | Record + runtimeSnapshot?: ModelCapabilitiesSnapshot + bundledSnapshot?: ModelCapabilitiesSnapshot +} + +export type ModelCapabilityOverride = { + variants?: string[] + reasoningEfforts?: string[] + supportsThinking?: boolean + supportsTemperature?: boolean + supportsTopP?: boolean +} From e0feb16dabd6296072d112451dc164696e9d1344 Mon Sep 17 00:00:00 2001 From: YeonGyu-Kim Date: Fri, 3 Apr 2026 19:39:55 +0900 Subject: [PATCH 3/3] refactor(shared,config): remove redundant null checks and AI slop from code comments --- src/config/schema/experimental.ts | 1 - src/shared/context-limit-resolver.test.ts | 2 -- src/shared/legacy-plugin-warning.test.ts | 2 -- src/shared/migration/config-migration.ts | 2 +- src/shared/model-resolver.ts | 3 +-- src/shared/model-settings-compatibility.test.ts | 10 ---------- src/shared/model-settings-compatibility.ts | 14 +------------- src/shared/model-suggestion-retry.ts | 10 ---------- src/shared/opencode-storage-detection.test.ts | 14 +++++++------- 9 files changed, 10 insertions(+), 48 deletions(-) diff --git a/src/config/schema/experimental.ts b/src/config/schema/experimental.ts index fbcefb3b1..1805dda9f 100644 --- a/src/config/schema/experimental.ts +++ b/src/config/schema/experimental.ts @@ -5,7 +5,6 @@ export const ExperimentalConfigSchema = z.object({ aggressive_truncation: z.boolean().optional(), auto_resume: z.boolean().optional(), preemptive_compaction: z.boolean().optional(), - /** Truncate all tool outputs, not just whitelisted tools (default: false). Tool output truncator is enabled by default - disable via disabled_hooks. */ truncate_all_tool_outputs: z.boolean().optional(), /** Dynamic context pruning configuration */ dynamic_context_pruning: DynamicContextPruningConfigSchema.optional(), diff --git a/src/shared/context-limit-resolver.test.ts b/src/shared/context-limit-resolver.test.ts index b6a8f6d9a..a4346a6aa 100644 --- a/src/shared/context-limit-resolver.test.ts +++ b/src/shared/context-limit-resolver.test.ts @@ -41,7 +41,6 @@ describe("resolveActualContextLimit", () => { modelContextLimitsCache, }) - // then — models.dev reports 1M for GA models, resolver should respect it expect(actualLimit).toBe(1_000_000) }) @@ -89,7 +88,6 @@ describe("resolveActualContextLimit", () => { modelContextLimitsCache, }) - // then — explicit 1M flag overrides cached 200K expect(actualLimit).toBe(1_000_000) }) diff --git a/src/shared/legacy-plugin-warning.test.ts b/src/shared/legacy-plugin-warning.test.ts index 9d114f9db..47e8a39a9 100644 --- a/src/shared/legacy-plugin-warning.test.ts +++ b/src/shared/legacy-plugin-warning.test.ts @@ -69,8 +69,6 @@ describe("checkForLegacyPluginEntry", () => { }) it("returns no warning data when config is missing", () => { - // given — empty dir, no config files - // when const result = checkForLegacyPluginEntry(testConfigDir) diff --git a/src/shared/migration/config-migration.ts b/src/shared/migration/config-migration.ts index aae937244..abb90bccd 100644 --- a/src/shared/migration/config-migration.ts +++ b/src/shared/migration/config-migration.ts @@ -118,7 +118,7 @@ export function migrateConfigFile( fs.copyFileSync(configPath, backupPath) backupSucceeded = true } catch { - // Original file may not exist yet — skip backup + backupSucceeded = false } let writeSucceeded = false diff --git a/src/shared/model-resolver.ts b/src/shared/model-resolver.ts index 8b6a33d03..7b4ac32d1 100644 --- a/src/shared/model-resolver.ts +++ b/src/shared/model-resolver.ts @@ -92,7 +92,7 @@ export function flattenToFallbackModelStrings( // invalid strings like "provider/model high(low)". const model = entry.model .replace(/\([^()]+\)\s*$/, "") - .replace(/\s+([a-z][a-z0-9_-]*)\s*$/i, (match, suffix) => { + .replace(/\s+([a-z][a-z0-9_-]*)\s*$/i, (match: string, suffix: string) => { const normalized = String(suffix).toLowerCase() return KNOWN_VARIANTS.has(normalized) ? "" @@ -101,7 +101,6 @@ export function flattenToFallbackModelStrings( .trim() return `${model}(${variant})` } - // No explicit variant — preserve model string as-is (including any inline variant) return entry.model }) } diff --git a/src/shared/model-settings-compatibility.test.ts b/src/shared/model-settings-compatibility.test.ts index ca31d9f1e..d9b8a455d 100644 --- a/src/shared/model-settings-compatibility.test.ts +++ b/src/shared/model-settings-compatibility.test.ts @@ -244,10 +244,6 @@ describe("resolveCompatibleModelSettings", () => { expect(result.changes).toEqual([]) }) - // ----------------------------------------------------------------------- - // Registry coverage — every model family from FAMILY_CAPABILITIES - // ----------------------------------------------------------------------- - describe("model family registry coverage", () => { const familyCases: Array<{ name: string @@ -309,7 +305,6 @@ describe("resolveCompatibleModelSettings", () => { } }) - // GPT-5 specific: supports xhigh variant and xhigh reasoningEffort test("GPT-5 keeps xhigh variant and reasoningEffort", () => { const result = resolveCompatibleModelSettings({ providerID: "openai", @@ -345,7 +340,6 @@ describe("resolveCompatibleModelSettings", () => { }) }) - // Reasoning effort: "none" and "minimal" are valid per Vercel AI SDK test("GPT-5 keeps none reasoningEffort", () => { const result = resolveCompatibleModelSettings({ providerID: "openai", @@ -388,7 +382,6 @@ describe("resolveCompatibleModelSettings", () => { }) }) - // Reasoning effort downgrade within families that support it test("o-series downgrades xhigh reasoningEffort to high", () => { const result = resolveCompatibleModelSettings({ providerID: "openai", @@ -408,9 +401,6 @@ describe("resolveCompatibleModelSettings", () => { }) test("GPT-5 keeps xhigh but would downgrade a hypothetical beyond-max level", () => { - // GPT-5 supports up to "xhigh" — verify the ladder works by requesting - // a value that IS in the ladder but NOT in the family's allowed list. - // Since "xhigh" is the max for GPT-5 reasoningEffort, we verify it stays. const result = resolveCompatibleModelSettings({ providerID: "openai", modelID: "gpt-5.4", diff --git a/src/shared/model-settings-compatibility.ts b/src/shared/model-settings-compatibility.ts index 89661c2b2..f39875f43 100644 --- a/src/shared/model-settings-compatibility.ts +++ b/src/shared/model-settings-compatibility.ts @@ -51,10 +51,6 @@ export type ModelSettingsCompatibilityResult = { const VARIANT_LADDER = ["low", "medium", "high", "xhigh", "max"] const REASONING_LADDER = ["none", "minimal", "low", "medium", "high", "xhigh"] -// --------------------------------------------------------------------------- -// Generic resolution — one function for both fields -// --------------------------------------------------------------------------- - function downgradeWithinLadder(value: string, allowed: string[], ladder: string[]): string | undefined { const requestedIndex = ladder.indexOf(value) if (requestedIndex === -1) return undefined @@ -91,7 +87,6 @@ function resolveField( familyKnown: boolean, metadataOverride?: string[], ): FieldResolution { - // Priority 1: runtime metadata from provider if (metadataOverride) { if (metadataOverride.includes(normalized)) return { value: normalized } return { @@ -100,7 +95,6 @@ function resolveField( } } - // Priority 2: family heuristic from registry if (familyCaps) { if (familyCaps.includes(normalized)) return { value: normalized } return { @@ -109,24 +103,18 @@ function resolveField( } } - // Known family but field not in registry (e.g. Claude + reasoningEffort) if (familyKnown) { return { value: undefined, reason: "unsupported-by-model-family" } } - // Unknown family — drop the value return { value: undefined, reason: "unknown-model-family" } } -// --------------------------------------------------------------------------- -// Public API -// --------------------------------------------------------------------------- - export function resolveCompatibleModelSettings( input: ModelSettingsCompatibilityInput, ): ModelSettingsCompatibilityResult { const family = detectHeuristicModelFamily(input.modelID) - const familyKnown = family !== undefined + const familyKnown = Boolean(family) const changes: ModelSettingsCompatibilityChange[] = [] const metadataVariants = normalizeCapabilitiesVariants(input.capabilities) const metadataReasoningEfforts = normalizeCapabilitiesReasoningEfforts(input.capabilities) diff --git a/src/shared/model-suggestion-retry.ts b/src/shared/model-suggestion-retry.ts index 0ff9ca86e..7047b8bb5 100644 --- a/src/shared/model-suggestion-retry.ts +++ b/src/shared/model-suggestion-retry.ts @@ -93,7 +93,6 @@ export async function promptWithModelSuggestionRetry( ): Promise { const timeoutMs = options.timeoutMs ?? PROMPT_TIMEOUT_MS const timeoutContext = createPromptTimeoutContext(args, timeoutMs) - // 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, @@ -115,15 +114,6 @@ export async function promptWithModelSuggestionRetry( } } -/** - * 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, diff --git a/src/shared/opencode-storage-detection.test.ts b/src/shared/opencode-storage-detection.test.ts index 12238e508..620a7652a 100644 --- a/src/shared/opencode-storage-detection.test.ts +++ b/src/shared/opencode-storage-detection.test.ts @@ -108,21 +108,21 @@ describe("isSqliteBackend", () => { //#given versionReturnValue = true - //#when: first call — DB does not exist + //#when: first call, DB does not exist const first = isSqliteBackend() //#then expect(first).toBe(false) expect(versionCheckCalls.length).toBe(1) - //#when: second call — DB still does not exist (retry) + //#when: second call, DB still does not exist (retry) const second = isSqliteBackend() //#then: retried once expect(second).toBe(false) expect(versionCheckCalls.length).toBe(2) - //#when: third call — no more retries + //#when: third call, no more retries const third = isSqliteBackend() //#then: no further checks @@ -134,7 +134,7 @@ describe("isSqliteBackend", () => { //#given versionReturnValue = true - //#when: first call — DB does not exist + //#when: first call, DB does not exist const first = isSqliteBackend() //#then @@ -144,18 +144,18 @@ describe("isSqliteBackend", () => { mkdirSync(join(TEST_DATA_DIR, "opencode"), { recursive: true }) writeFileSync(DB_PATH, "") - //#when: second call — retry finds DB + //#when: second call, retry finds DB const second = isSqliteBackend() //#then: recovers to true and caches permanently expect(second).toBe(true) expect(versionCheckCalls.length).toBe(2) - //#when: third call — cached true + //#when: third call, cached true const third = isSqliteBackend() //#then: no further checks expect(third).toBe(true) expect(versionCheckCalls.length).toBe(2) }) -}) \ No newline at end of file +})