Merge pull request #3071 from code-yeongyu/refactor/deslop-shared-config
refactor(shared): remove AI slop from shared and config modules
This commit is contained in:
@@ -5,7 +5,6 @@ export const ExperimentalConfigSchema = z.object({
|
|||||||
aggressive_truncation: z.boolean().optional(),
|
aggressive_truncation: z.boolean().optional(),
|
||||||
auto_resume: z.boolean().optional(),
|
auto_resume: z.boolean().optional(),
|
||||||
preemptive_compaction: 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(),
|
truncate_all_tool_outputs: z.boolean().optional(),
|
||||||
/** Dynamic context pruning configuration */
|
/** Dynamic context pruning configuration */
|
||||||
dynamic_context_pruning: DynamicContextPruningConfigSchema.optional(),
|
dynamic_context_pruning: DynamicContextPruningConfigSchema.optional(),
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import { existsSync, readFileSync, writeFileSync, mkdirSync } from "fs"
|
|
||||||
import { join } from "path"
|
|
||||||
import { log } from "./logger"
|
import { log } from "./logger"
|
||||||
import * as dataPath from "./data-path"
|
import * as dataPath from "./data-path"
|
||||||
|
import { createJsonFileCacheStore } from "./json-file-cache-store"
|
||||||
|
|
||||||
const CONNECTED_PROVIDERS_CACHE_FILE = "connected-providers.json"
|
const CONNECTED_PROVIDERS_CACHE_FILE = "connected-providers.json"
|
||||||
const PROVIDER_MODELS_CACHE_FILE = "provider-models.json"
|
const PROVIDER_MODELS_CACHE_FILE = "provider-models.json"
|
||||||
@@ -47,115 +46,52 @@ function isRecord(value: unknown): value is Record<string, unknown> {
|
|||||||
export function createConnectedProvidersCacheStore(
|
export function createConnectedProvidersCacheStore(
|
||||||
getCacheDir: () => string = dataPath.getOmoOpenCodeCacheDir
|
getCacheDir: () => string = dataPath.getOmoOpenCodeCacheDir
|
||||||
) {
|
) {
|
||||||
function getCacheFilePath(filename: string): string {
|
const connectedProvidersCacheStore = createJsonFileCacheStore<ConnectedProvidersCache>({
|
||||||
return join(getCacheDir(), filename)
|
getCacheDir,
|
||||||
}
|
filename: CONNECTED_PROVIDERS_CACHE_FILE,
|
||||||
|
logPrefix: "connected-providers-cache",
|
||||||
let memConnected: string[] | null | undefined
|
cacheLabel: "Cache",
|
||||||
let memProviderModels: ProviderModelsCache | null | undefined
|
describe: (value) => ({ count: value.connected.length, updatedAt: value.updatedAt }),
|
||||||
|
})
|
||||||
function ensureCacheDir(): void {
|
const providerModelsCacheStore = createJsonFileCacheStore<ProviderModelsCache>({
|
||||||
const cacheDir = getCacheDir()
|
getCacheDir,
|
||||||
if (!existsSync(cacheDir)) {
|
filename: PROVIDER_MODELS_CACHE_FILE,
|
||||||
mkdirSync(cacheDir, { recursive: true })
|
logPrefix: "connected-providers-cache",
|
||||||
}
|
cacheLabel: "Provider-models cache",
|
||||||
}
|
describe: (value) => ({
|
||||||
|
providerCount: Object.keys(value.models).length,
|
||||||
|
updatedAt: value.updatedAt,
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
|
||||||
function readConnectedProvidersCache(): string[] | null {
|
function readConnectedProvidersCache(): string[] | null {
|
||||||
if (memConnected !== undefined) return memConnected
|
return connectedProvidersCacheStore.read()?.connected ?? null
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function hasConnectedProvidersCache(): boolean {
|
function hasConnectedProvidersCache(): boolean {
|
||||||
const cacheFile = getCacheFilePath(CONNECTED_PROVIDERS_CACHE_FILE)
|
return connectedProvidersCacheStore.has()
|
||||||
return existsSync(cacheFile)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function writeConnectedProvidersCache(connected: string[]): void {
|
function writeConnectedProvidersCache(connected: string[]): void {
|
||||||
ensureCacheDir()
|
connectedProvidersCacheStore.write({
|
||||||
const cacheFile = getCacheFilePath(CONNECTED_PROVIDERS_CACHE_FILE)
|
|
||||||
|
|
||||||
const data: ConnectedProvidersCache = {
|
|
||||||
connected,
|
connected,
|
||||||
updatedAt: new Date().toISOString(),
|
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 {
|
function readProviderModelsCache(): ProviderModelsCache | null {
|
||||||
if (memProviderModels !== undefined) return memProviderModels
|
return providerModelsCacheStore.read()
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function hasProviderModelsCache(): boolean {
|
function hasProviderModelsCache(): boolean {
|
||||||
const cacheFile = getCacheFilePath(PROVIDER_MODELS_CACHE_FILE)
|
return providerModelsCacheStore.has()
|
||||||
return existsSync(cacheFile)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function writeProviderModelsCache(data: { models: Record<string, string[] | ModelMetadata[]>; connected: string[] }): void {
|
function writeProviderModelsCache(data: { models: Record<string, string[] | ModelMetadata[]>; connected: string[] }): void {
|
||||||
ensureCacheDir()
|
providerModelsCacheStore.write({
|
||||||
const cacheFile = getCacheFilePath(PROVIDER_MODELS_CACHE_FILE)
|
|
||||||
|
|
||||||
const cacheData: ProviderModelsCache = {
|
|
||||||
...data,
|
...data,
|
||||||
updatedAt: new Date().toISOString(),
|
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: {
|
async function updateConnectedProvidersCache(client: {
|
||||||
@@ -223,8 +159,8 @@ export function createConnectedProvidersCacheStore(
|
|||||||
}
|
}
|
||||||
|
|
||||||
function _resetMemCacheForTesting(): void {
|
function _resetMemCacheForTesting(): void {
|
||||||
memConnected = undefined
|
connectedProvidersCacheStore.resetMemory()
|
||||||
memProviderModels = undefined
|
providerModelsCacheStore.resetMemory()
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -256,7 +192,7 @@ export function findProviderModelMetadata(
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
if (entry?.id === modelID) {
|
if (entry.id === modelID) {
|
||||||
return entry
|
return entry
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -41,7 +41,6 @@ describe("resolveActualContextLimit", () => {
|
|||||||
modelContextLimitsCache,
|
modelContextLimitsCache,
|
||||||
})
|
})
|
||||||
|
|
||||||
// then — models.dev reports 1M for GA models, resolver should respect it
|
|
||||||
expect(actualLimit).toBe(1_000_000)
|
expect(actualLimit).toBe(1_000_000)
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -89,7 +88,6 @@ describe("resolveActualContextLimit", () => {
|
|||||||
modelContextLimitsCache,
|
modelContextLimitsCache,
|
||||||
})
|
})
|
||||||
|
|
||||||
// then — explicit 1M flag overrides cached 200K
|
|
||||||
expect(actualLimit).toBe(1_000_000)
|
expect(actualLimit).toBe(1_000_000)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,98 @@
|
|||||||
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs"
|
||||||
|
import { join } from "node:path"
|
||||||
|
|
||||||
|
import { log } from "./logger"
|
||||||
|
|
||||||
|
type JsonFileCacheStoreOptions<TValue> = {
|
||||||
|
getCacheDir: () => string
|
||||||
|
filename: string
|
||||||
|
logPrefix: string
|
||||||
|
cacheLabel: string
|
||||||
|
describe: (value: TValue) => Record<string, unknown>
|
||||||
|
serialize?: (value: TValue) => string
|
||||||
|
}
|
||||||
|
|
||||||
|
type JsonFileCacheStore<TValue> = {
|
||||||
|
read: () => TValue | null
|
||||||
|
has: () => boolean
|
||||||
|
write: (value: TValue) => void
|
||||||
|
resetMemory: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
function toLogLabel(cacheLabel: string): string {
|
||||||
|
return cacheLabel.toLowerCase()
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createJsonFileCacheStore<TValue>(
|
||||||
|
options: JsonFileCacheStoreOptions<TValue>,
|
||||||
|
): JsonFileCacheStore<TValue> {
|
||||||
|
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,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -69,8 +69,6 @@ describe("checkForLegacyPluginEntry", () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it("returns no warning data when config is missing", () => {
|
it("returns no warning data when config is missing", () => {
|
||||||
// given — empty dir, no config files
|
|
||||||
|
|
||||||
// when
|
// when
|
||||||
const result = checkForLegacyPluginEntry(testConfigDir)
|
const result = checkForLegacyPluginEntry(testConfigDir)
|
||||||
|
|
||||||
|
|||||||
@@ -118,7 +118,7 @@ export function migrateConfigFile(
|
|||||||
fs.copyFileSync(configPath, backupPath)
|
fs.copyFileSync(configPath, backupPath)
|
||||||
backupSucceeded = true
|
backupSucceeded = true
|
||||||
} catch {
|
} catch {
|
||||||
// Original file may not exist yet — skip backup
|
backupSucceeded = false
|
||||||
}
|
}
|
||||||
|
|
||||||
let writeSucceeded = false
|
let writeSucceeded = false
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs"
|
|
||||||
import { join } from "path"
|
|
||||||
import * as dataPath from "./data-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"
|
import type { ModelCapabilitiesSnapshot, ModelCapabilitiesSnapshotEntry } from "./model-capabilities"
|
||||||
|
|
||||||
export const MODELS_DEV_SOURCE_URL = "https://models.dev/api.json"
|
export const MODELS_DEV_SOURCE_URL = "https://models.dev/api.json"
|
||||||
@@ -162,61 +160,28 @@ export async function fetchModelCapabilitiesSnapshot(args: {
|
|||||||
export function createModelCapabilitiesCacheStore(
|
export function createModelCapabilitiesCacheStore(
|
||||||
getCacheDir: () => string = dataPath.getOmoOpenCodeCacheDir,
|
getCacheDir: () => string = dataPath.getOmoOpenCodeCacheDir,
|
||||||
) {
|
) {
|
||||||
let memSnapshot: ModelCapabilitiesSnapshot | null | undefined
|
const snapshotCacheStore = createJsonFileCacheStore<ModelCapabilitiesSnapshot>({
|
||||||
|
getCacheDir,
|
||||||
function getCacheFilePath(): string {
|
filename: MODEL_CAPABILITIES_CACHE_FILE,
|
||||||
return join(getCacheDir(), MODEL_CAPABILITIES_CACHE_FILE)
|
logPrefix: "model-capabilities-cache",
|
||||||
}
|
cacheLabel: "Cache",
|
||||||
|
describe: (snapshot) => ({
|
||||||
function ensureCacheDir(): void {
|
modelCount: Object.keys(snapshot.models).length,
|
||||||
const cacheDir = getCacheDir()
|
generatedAt: snapshot.generatedAt,
|
||||||
if (!existsSync(cacheDir)) {
|
}),
|
||||||
mkdirSync(cacheDir, { recursive: true })
|
serialize: (snapshot) => `${JSON.stringify(snapshot, null, 2)}\n`,
|
||||||
}
|
})
|
||||||
}
|
|
||||||
|
|
||||||
function readModelCapabilitiesCache(): ModelCapabilitiesSnapshot | null {
|
function readModelCapabilitiesCache(): ModelCapabilitiesSnapshot | null {
|
||||||
if (memSnapshot !== undefined) {
|
return snapshotCacheStore.read()
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function hasModelCapabilitiesCache(): boolean {
|
function hasModelCapabilitiesCache(): boolean {
|
||||||
return existsSync(getCacheFilePath())
|
return snapshotCacheStore.has()
|
||||||
}
|
}
|
||||||
|
|
||||||
function writeModelCapabilitiesCache(snapshot: ModelCapabilitiesSnapshot): void {
|
function writeModelCapabilitiesCache(snapshot: ModelCapabilitiesSnapshot): void {
|
||||||
ensureCacheDir()
|
snapshotCacheStore.write(snapshot)
|
||||||
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,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function refreshModelCapabilitiesCache(args: {
|
async function refreshModelCapabilitiesCache(args: {
|
||||||
|
|||||||
@@ -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<string, ModelCapabilitiesSnapshotEntry>
|
|
||||||
}
|
|
||||||
|
|
||||||
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<string, unknown>
|
|
||||||
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<DiagnosticSource, "runtime-snapshot" | "bundled-snapshot" | "exact-alias" | "pattern-alias"> }
|
|
||||||
reasoningEfforts: { source: Exclude<DiagnosticSource, "runtime-snapshot" | "bundled-snapshot" | "canonical" | "exact-alias" | "pattern-alias" | "runtime"> }
|
|
||||||
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<string, ModelCapabilityOverride> = {}
|
|
||||||
|
|
||||||
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
||||||
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<string, unknown> | undefined): Record<string, unknown> | undefined {
|
|
||||||
return isRecord(runtimeModel?.capabilities) ? runtimeModel.capabilities : undefined
|
|
||||||
}
|
|
||||||
|
|
||||||
function readRuntimeModelLimitOutput(runtimeModel: Record<string, unknown> | 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<string, unknown> | 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<string, unknown> | 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<string, unknown> | 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<string, unknown> | undefined): boolean | undefined {
|
|
||||||
return readRuntimeModelBoolean(runtimeModel, ["topP", "top_p"])
|
|
||||||
}
|
|
||||||
|
|
||||||
function readRuntimeModelToolCallSupport(runtimeModel: Record<string, unknown> | undefined): boolean | undefined {
|
|
||||||
return readRuntimeModelBoolean(runtimeModel, ["toolCall", "tool_call", "toolcall"])
|
|
||||||
}
|
|
||||||
|
|
||||||
function readRuntimeModelReasoningSupport(runtimeModel: Record<string, unknown> | undefined): boolean | undefined {
|
|
||||||
return readRuntimeModelBoolean(runtimeModel, ["reasoning"])
|
|
||||||
}
|
|
||||||
|
|
||||||
function readRuntimeModelTemperatureSupport(runtimeModel: Record<string, unknown> | undefined): boolean | undefined {
|
|
||||||
return readRuntimeModelBoolean(runtimeModel, ["temperature"])
|
|
||||||
}
|
|
||||||
|
|
||||||
function readRuntimeModelThinkingSupport(runtimeModel: Record<string, unknown> | 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<string, unknown> | undefined): Record<string, unknown> | 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 },
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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<string, ModelCapabilityOverride> = {}
|
||||||
|
|
||||||
|
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 },
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
export { getBundledModelCapabilitiesSnapshot } from "./bundled-snapshot"
|
||||||
|
export { getModelCapabilities } from "./get-model-capabilities"
|
||||||
|
export type {
|
||||||
|
GetModelCapabilitiesInput,
|
||||||
|
ModelCapabilities,
|
||||||
|
ModelCapabilitiesDiagnostics,
|
||||||
|
ModelCapabilitiesSnapshot,
|
||||||
|
ModelCapabilitiesSnapshotEntry,
|
||||||
|
} from "./types"
|
||||||
@@ -0,0 +1,190 @@
|
|||||||
|
import type { ModelMetadata } from "../connected-providers-cache"
|
||||||
|
|
||||||
|
import type { ModelCapabilities } from "./types"
|
||||||
|
|
||||||
|
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||||
|
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<string, unknown> | undefined,
|
||||||
|
): Record<string, unknown> | undefined {
|
||||||
|
return isRecord(runtimeModel?.capabilities) ? runtimeModel.capabilities : undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
function readRuntimeModelBoolean(
|
||||||
|
runtimeModel: Record<string, unknown> | 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<string, unknown> | undefined,
|
||||||
|
): Record<string, unknown> | undefined {
|
||||||
|
return isRecord(runtimeModel) ? runtimeModel : undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
export function readRuntimeModelVariants(
|
||||||
|
runtimeModel: Record<string, unknown> | undefined,
|
||||||
|
): string[] | undefined {
|
||||||
|
const rootVariants = normalizeVariantKeys(runtimeModel?.variants)
|
||||||
|
if (rootVariants) {
|
||||||
|
return rootVariants
|
||||||
|
}
|
||||||
|
|
||||||
|
return normalizeVariantKeys(readRuntimeModelCapabilities(runtimeModel)?.variants)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function readRuntimeModelModalities(
|
||||||
|
runtimeModel: Record<string, unknown> | 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<string, unknown> | undefined,
|
||||||
|
): boolean | undefined {
|
||||||
|
return readRuntimeModelBoolean(runtimeModel, ["reasoning"])
|
||||||
|
}
|
||||||
|
|
||||||
|
export function readRuntimeModelThinkingSupport(
|
||||||
|
runtimeModel: Record<string, unknown> | 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<string, unknown> | undefined,
|
||||||
|
): boolean | undefined {
|
||||||
|
return readRuntimeModelBoolean(runtimeModel, ["temperature"])
|
||||||
|
}
|
||||||
|
|
||||||
|
export function readRuntimeModelTopPSupport(
|
||||||
|
runtimeModel: Record<string, unknown> | undefined,
|
||||||
|
): boolean | undefined {
|
||||||
|
return readRuntimeModelBoolean(runtimeModel, ["topP", "top_p"])
|
||||||
|
}
|
||||||
|
|
||||||
|
export function readRuntimeModelToolCallSupport(
|
||||||
|
runtimeModel: Record<string, unknown> | undefined,
|
||||||
|
): boolean | undefined {
|
||||||
|
return readRuntimeModelBoolean(runtimeModel, ["toolCall", "tool_call", "toolcall"])
|
||||||
|
}
|
||||||
|
|
||||||
|
export function readRuntimeModelLimitOutput(
|
||||||
|
runtimeModel: Record<string, unknown> | undefined,
|
||||||
|
): number | undefined {
|
||||||
|
const limit = isRecord(runtimeModel?.limit)
|
||||||
|
? runtimeModel.limit
|
||||||
|
: readRuntimeModelCapabilities(runtimeModel)?.limit
|
||||||
|
|
||||||
|
if (!isRecord(limit)) {
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
return readNumber(limit.output)
|
||||||
|
}
|
||||||
@@ -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<string, ModelCapabilitiesSnapshotEntry>
|
||||||
|
}
|
||||||
|
|
||||||
|
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<string, unknown>
|
||||||
|
runtimeSnapshot?: ModelCapabilitiesSnapshot
|
||||||
|
bundledSnapshot?: ModelCapabilitiesSnapshot
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ModelCapabilityOverride = {
|
||||||
|
variants?: string[]
|
||||||
|
reasoningEfforts?: string[]
|
||||||
|
supportsThinking?: boolean
|
||||||
|
supportsTemperature?: boolean
|
||||||
|
supportsTopP?: boolean
|
||||||
|
}
|
||||||
@@ -92,7 +92,7 @@ export function flattenToFallbackModelStrings(
|
|||||||
// invalid strings like "provider/model high(low)".
|
// invalid strings like "provider/model high(low)".
|
||||||
const model = entry.model
|
const model = entry.model
|
||||||
.replace(/\([^()]+\)\s*$/, "")
|
.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()
|
const normalized = String(suffix).toLowerCase()
|
||||||
return KNOWN_VARIANTS.has(normalized)
|
return KNOWN_VARIANTS.has(normalized)
|
||||||
? ""
|
? ""
|
||||||
@@ -101,7 +101,6 @@ export function flattenToFallbackModelStrings(
|
|||||||
.trim()
|
.trim()
|
||||||
return `${model}(${variant})`
|
return `${model}(${variant})`
|
||||||
}
|
}
|
||||||
// No explicit variant — preserve model string as-is (including any inline variant)
|
|
||||||
return entry.model
|
return entry.model
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -244,10 +244,6 @@ describe("resolveCompatibleModelSettings", () => {
|
|||||||
expect(result.changes).toEqual([])
|
expect(result.changes).toEqual([])
|
||||||
})
|
})
|
||||||
|
|
||||||
// -----------------------------------------------------------------------
|
|
||||||
// Registry coverage — every model family from FAMILY_CAPABILITIES
|
|
||||||
// -----------------------------------------------------------------------
|
|
||||||
|
|
||||||
describe("model family registry coverage", () => {
|
describe("model family registry coverage", () => {
|
||||||
const familyCases: Array<{
|
const familyCases: Array<{
|
||||||
name: string
|
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", () => {
|
test("GPT-5 keeps xhigh variant and reasoningEffort", () => {
|
||||||
const result = resolveCompatibleModelSettings({
|
const result = resolveCompatibleModelSettings({
|
||||||
providerID: "openai",
|
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", () => {
|
test("GPT-5 keeps none reasoningEffort", () => {
|
||||||
const result = resolveCompatibleModelSettings({
|
const result = resolveCompatibleModelSettings({
|
||||||
providerID: "openai",
|
providerID: "openai",
|
||||||
@@ -388,7 +382,6 @@ describe("resolveCompatibleModelSettings", () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
// Reasoning effort downgrade within families that support it
|
|
||||||
test("o-series downgrades xhigh reasoningEffort to high", () => {
|
test("o-series downgrades xhigh reasoningEffort to high", () => {
|
||||||
const result = resolveCompatibleModelSettings({
|
const result = resolveCompatibleModelSettings({
|
||||||
providerID: "openai",
|
providerID: "openai",
|
||||||
@@ -408,9 +401,6 @@ describe("resolveCompatibleModelSettings", () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
test("GPT-5 keeps xhigh but would downgrade a hypothetical beyond-max level", () => {
|
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({
|
const result = resolveCompatibleModelSettings({
|
||||||
providerID: "openai",
|
providerID: "openai",
|
||||||
modelID: "gpt-5.4",
|
modelID: "gpt-5.4",
|
||||||
|
|||||||
@@ -51,10 +51,6 @@ export type ModelSettingsCompatibilityResult = {
|
|||||||
const VARIANT_LADDER = ["low", "medium", "high", "xhigh", "max"]
|
const VARIANT_LADDER = ["low", "medium", "high", "xhigh", "max"]
|
||||||
const REASONING_LADDER = ["none", "minimal", "low", "medium", "high", "xhigh"]
|
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 {
|
function downgradeWithinLadder(value: string, allowed: string[], ladder: string[]): string | undefined {
|
||||||
const requestedIndex = ladder.indexOf(value)
|
const requestedIndex = ladder.indexOf(value)
|
||||||
if (requestedIndex === -1) return undefined
|
if (requestedIndex === -1) return undefined
|
||||||
@@ -91,7 +87,6 @@ function resolveField(
|
|||||||
familyKnown: boolean,
|
familyKnown: boolean,
|
||||||
metadataOverride?: string[],
|
metadataOverride?: string[],
|
||||||
): FieldResolution {
|
): FieldResolution {
|
||||||
// Priority 1: runtime metadata from provider
|
|
||||||
if (metadataOverride) {
|
if (metadataOverride) {
|
||||||
if (metadataOverride.includes(normalized)) return { value: normalized }
|
if (metadataOverride.includes(normalized)) return { value: normalized }
|
||||||
return {
|
return {
|
||||||
@@ -100,7 +95,6 @@ function resolveField(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Priority 2: family heuristic from registry
|
|
||||||
if (familyCaps) {
|
if (familyCaps) {
|
||||||
if (familyCaps.includes(normalized)) return { value: normalized }
|
if (familyCaps.includes(normalized)) return { value: normalized }
|
||||||
return {
|
return {
|
||||||
@@ -109,24 +103,18 @@ function resolveField(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Known family but field not in registry (e.g. Claude + reasoningEffort)
|
|
||||||
if (familyKnown) {
|
if (familyKnown) {
|
||||||
return { value: undefined, reason: "unsupported-by-model-family" }
|
return { value: undefined, reason: "unsupported-by-model-family" }
|
||||||
}
|
}
|
||||||
|
|
||||||
// Unknown family — drop the value
|
|
||||||
return { value: undefined, reason: "unknown-model-family" }
|
return { value: undefined, reason: "unknown-model-family" }
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// Public API
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
export function resolveCompatibleModelSettings(
|
export function resolveCompatibleModelSettings(
|
||||||
input: ModelSettingsCompatibilityInput,
|
input: ModelSettingsCompatibilityInput,
|
||||||
): ModelSettingsCompatibilityResult {
|
): ModelSettingsCompatibilityResult {
|
||||||
const family = detectHeuristicModelFamily(input.modelID)
|
const family = detectHeuristicModelFamily(input.modelID)
|
||||||
const familyKnown = family !== undefined
|
const familyKnown = Boolean(family)
|
||||||
const changes: ModelSettingsCompatibilityChange[] = []
|
const changes: ModelSettingsCompatibilityChange[] = []
|
||||||
const metadataVariants = normalizeCapabilitiesVariants(input.capabilities)
|
const metadataVariants = normalizeCapabilitiesVariants(input.capabilities)
|
||||||
const metadataReasoningEfforts = normalizeCapabilitiesReasoningEfforts(input.capabilities)
|
const metadataReasoningEfforts = normalizeCapabilitiesReasoningEfforts(input.capabilities)
|
||||||
|
|||||||
@@ -93,7 +93,6 @@ export async function promptWithModelSuggestionRetry(
|
|||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const timeoutMs = options.timeoutMs ?? PROMPT_TIMEOUT_MS
|
const timeoutMs = options.timeoutMs ?? PROMPT_TIMEOUT_MS
|
||||||
const timeoutContext = createPromptTimeoutContext(args, timeoutMs)
|
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
|
// model errors happen asynchronously server-side and cannot be caught here
|
||||||
const promptPromise = client.session.promptAsync({
|
const promptPromise = client.session.promptAsync({
|
||||||
...args,
|
...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(
|
export async function promptSyncWithModelSuggestionRetry(
|
||||||
client: Client,
|
client: Client,
|
||||||
args: PromptArgs,
|
args: PromptArgs,
|
||||||
|
|||||||
@@ -108,21 +108,21 @@ describe("isSqliteBackend", () => {
|
|||||||
//#given
|
//#given
|
||||||
versionReturnValue = true
|
versionReturnValue = true
|
||||||
|
|
||||||
//#when: first call — DB does not exist
|
//#when: first call, DB does not exist
|
||||||
const first = isSqliteBackend()
|
const first = isSqliteBackend()
|
||||||
|
|
||||||
//#then
|
//#then
|
||||||
expect(first).toBe(false)
|
expect(first).toBe(false)
|
||||||
expect(versionCheckCalls.length).toBe(1)
|
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()
|
const second = isSqliteBackend()
|
||||||
|
|
||||||
//#then: retried once
|
//#then: retried once
|
||||||
expect(second).toBe(false)
|
expect(second).toBe(false)
|
||||||
expect(versionCheckCalls.length).toBe(2)
|
expect(versionCheckCalls.length).toBe(2)
|
||||||
|
|
||||||
//#when: third call — no more retries
|
//#when: third call, no more retries
|
||||||
const third = isSqliteBackend()
|
const third = isSqliteBackend()
|
||||||
|
|
||||||
//#then: no further checks
|
//#then: no further checks
|
||||||
@@ -134,7 +134,7 @@ describe("isSqliteBackend", () => {
|
|||||||
//#given
|
//#given
|
||||||
versionReturnValue = true
|
versionReturnValue = true
|
||||||
|
|
||||||
//#when: first call — DB does not exist
|
//#when: first call, DB does not exist
|
||||||
const first = isSqliteBackend()
|
const first = isSqliteBackend()
|
||||||
|
|
||||||
//#then
|
//#then
|
||||||
@@ -144,18 +144,18 @@ describe("isSqliteBackend", () => {
|
|||||||
mkdirSync(join(TEST_DATA_DIR, "opencode"), { recursive: true })
|
mkdirSync(join(TEST_DATA_DIR, "opencode"), { recursive: true })
|
||||||
writeFileSync(DB_PATH, "")
|
writeFileSync(DB_PATH, "")
|
||||||
|
|
||||||
//#when: second call — retry finds DB
|
//#when: second call, retry finds DB
|
||||||
const second = isSqliteBackend()
|
const second = isSqliteBackend()
|
||||||
|
|
||||||
//#then: recovers to true and caches permanently
|
//#then: recovers to true and caches permanently
|
||||||
expect(second).toBe(true)
|
expect(second).toBe(true)
|
||||||
expect(versionCheckCalls.length).toBe(2)
|
expect(versionCheckCalls.length).toBe(2)
|
||||||
|
|
||||||
//#when: third call — cached true
|
//#when: third call, cached true
|
||||||
const third = isSqliteBackend()
|
const third = isSqliteBackend()
|
||||||
|
|
||||||
//#then: no further checks
|
//#then: no further checks
|
||||||
expect(third).toBe(true)
|
expect(third).toBe(true)
|
||||||
expect(versionCheckCalls.length).toBe(2)
|
expect(versionCheckCalls.length).toBe(2)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user