/** * db/models.js — Model aliases, MITM aliases, and custom models. */ import { getDbInstance } from "./core"; import { backupDbFile } from "./backup"; import { MODEL_COMPAT_PROTOCOL_KEYS, type ModelCompatProtocolKey, } from "@/shared/constants/modelCompat"; import { isForbiddenUpstreamHeaderName } from "@/shared/constants/upstreamHeaders"; type JsonRecord = Record; /** Built-in / alias models: tool-call + developer-role flags without a full custom row */ const MODEL_COMPAT_NAMESPACE = "modelCompatOverrides"; export { MODEL_COMPAT_PROTOCOL_KEYS, type ModelCompatProtocolKey }; export type ModelCompatPerProtocol = { normalizeToolCallId?: boolean; preserveOpenAIDeveloperRole?: boolean; /** Merged into upstream HTTP requests for this model (after default auth headers). */ upstreamHeaders?: Record; }; type CompatByProtocolMap = Partial>; function isCompatProtocolKey(p: string): p is ModelCompatProtocolKey { return (MODEL_COMPAT_PROTOCOL_KEYS as readonly string[]).includes(p); } const UPSTREAM_HEADERS_MAX = 16; const UPSTREAM_HEADER_NAME_MAX = 128; const UPSTREAM_HEADER_VALUE_MAX = 4096; function isValidUpstreamHeaderName(k: string): boolean { if (!k || k.length > UPSTREAM_HEADER_NAME_MAX) return false; if (isForbiddenUpstreamHeaderName(k)) return false; if (/[\r\n\0]/.test(k)) return false; if (/\s/.test(k)) return false; if (k.includes(":")) return false; return true; } /** Sanitize user-provided upstream header map (used when persisting and when reading for requests). */ export function sanitizeUpstreamHeadersMap( raw: Record | null | undefined ): Record { const out: Record = {}; if (!raw || typeof raw !== "object") return out; for (const [k0, v0] of Object.entries(raw)) { const k = String(k0).trim(); if (!k || !isValidUpstreamHeaderName(k)) { continue; } const v = typeof v0 === "string" ? v0.trim().slice(0, UPSTREAM_HEADER_VALUE_MAX) : String(v0 ?? "") .trim() .slice(0, UPSTREAM_HEADER_VALUE_MAX); if (v.includes("\r") || v.includes("\n")) continue; out[k] = v; if (Object.keys(out).length >= UPSTREAM_HEADERS_MAX) break; } return out; } function deepMergeCompatByProtocol( prev: CompatByProtocolMap | undefined, patch: Partial>> ): CompatByProtocolMap { const out: CompatByProtocolMap = { ...(prev || {}) }; for (const key of Object.keys(patch) as ModelCompatProtocolKey[]) { if (!isCompatProtocolKey(key)) continue; const deltas = patch[key]; if (!deltas || typeof deltas !== "object") continue; const hasDelta = Object.prototype.hasOwnProperty.call(deltas, "normalizeToolCallId") || Object.prototype.hasOwnProperty.call(deltas, "preserveOpenAIDeveloperRole") || Object.prototype.hasOwnProperty.call(deltas, "upstreamHeaders"); if (!hasDelta) continue; const cur: ModelCompatPerProtocol = { ...(out[key] || {}) }; if ("normalizeToolCallId" in deltas) { cur.normalizeToolCallId = Boolean(deltas.normalizeToolCallId); } if ("preserveOpenAIDeveloperRole" in deltas) { cur.preserveOpenAIDeveloperRole = Boolean(deltas.preserveOpenAIDeveloperRole); } if ("upstreamHeaders" in deltas) { const uh = deltas.upstreamHeaders; if (uh === undefined) { /* skip */ } else { const s = sanitizeUpstreamHeadersMap(uh as Record); if (Object.keys(s).length === 0) delete cur.upstreamHeaders; else cur.upstreamHeaders = s; } } if (Object.keys(cur).length === 0) delete out[key]; else out[key] = cur; } return out; } export type ModelCompatOverride = { id: string; normalizeToolCallId?: boolean; preserveOpenAIDeveloperRole?: boolean; compatByProtocol?: CompatByProtocolMap; upstreamHeaders?: Record; isHidden?: boolean; }; function readCompatList(providerId: string): ModelCompatOverride[] { const db = getDbInstance(); const row = db .prepare("SELECT value FROM key_value WHERE namespace = ? AND key = ?") .get(MODEL_COMPAT_NAMESPACE, providerId); const value = getKeyValue(row).value; if (!value) return []; try { const parsed = JSON.parse(value); return Array.isArray(parsed) ? parsed : []; } catch { return []; } } function writeCompatList(providerId: string, list: ModelCompatOverride[]) { const db = getDbInstance(); if (list.length === 0) { db.prepare("DELETE FROM key_value WHERE namespace = ? AND key = ?").run( MODEL_COMPAT_NAMESPACE, providerId ); } else { db.prepare("INSERT OR REPLACE INTO key_value (namespace, key, value) VALUES (?, ?, ?)").run( MODEL_COMPAT_NAMESPACE, providerId, JSON.stringify(list) ); } backupDbFile("pre-write"); } export function getModelCompatOverrides(providerId: string): ModelCompatOverride[] { return readCompatList(providerId); } export type ModelCompatPatch = { normalizeToolCallId?: boolean; preserveOpenAIDeveloperRole?: boolean | null; compatByProtocol?: CompatByProtocolMap; /** Replace top-level extra headers for override-only rows; omit to leave unchanged. */ upstreamHeaders?: Record | null; isHidden?: boolean | null; }; function compatByProtocolHasEntries(map: CompatByProtocolMap | undefined): boolean { if (!map || typeof map !== "object") return false; return Object.keys(map).some((k) => { const v = map[k as ModelCompatProtocolKey]; return v && typeof v === "object" && Object.keys(v).length > 0; }); } export function mergeModelCompatOverride( providerId: string, modelId: string, patch: ModelCompatPatch ) { const list = readCompatList(providerId); const idx = list.findIndex((e) => e.id === modelId); const prev = idx >= 0 ? { ...list[idx] } : { id: modelId }; const next: ModelCompatOverride = { ...prev, id: modelId }; if ("normalizeToolCallId" in patch) { if (patch.normalizeToolCallId) next.normalizeToolCallId = true; else delete next.normalizeToolCallId; } if ("preserveOpenAIDeveloperRole" in patch) { if (patch.preserveOpenAIDeveloperRole === null) { delete next.preserveOpenAIDeveloperRole; // unset: revert to default (undefined at read time) } else { next.preserveOpenAIDeveloperRole = Boolean(patch.preserveOpenAIDeveloperRole); } } if (patch.compatByProtocol && Object.keys(patch.compatByProtocol).length > 0) { const merged = deepMergeCompatByProtocol(next.compatByProtocol, patch.compatByProtocol); if (compatByProtocolHasEntries(merged)) next.compatByProtocol = merged; else delete next.compatByProtocol; } if ("upstreamHeaders" in patch) { if (patch.upstreamHeaders === null) { delete next.upstreamHeaders; } else if (patch.upstreamHeaders && typeof patch.upstreamHeaders === "object") { const s = sanitizeUpstreamHeadersMap(patch.upstreamHeaders as Record); if (Object.keys(s).length === 0) delete next.upstreamHeaders; else next.upstreamHeaders = s; } } const filtered = list.filter((e) => e.id !== modelId); const hasPreserveFlag = Object.prototype.hasOwnProperty.call(next, "preserveOpenAIDeveloperRole"); const hasTopUpstream = next.upstreamHeaders && Object.keys(next.upstreamHeaders).length > 0; if ("isHidden" in patch) { if (patch.isHidden === null) { delete next.isHidden; } else { next.isHidden = Boolean(patch.isHidden); } } const hasHiddenFlag = Object.prototype.hasOwnProperty.call(next, "isHidden"); if ( next.normalizeToolCallId || hasPreserveFlag || hasHiddenFlag || compatByProtocolHasEntries(next.compatByProtocol) || hasTopUpstream ) { filtered.push(next); } writeCompatList(providerId, filtered); } export function removeModelCompatOverride(providerId: string, modelId: string) { const list = readCompatList(providerId); const filtered = list.filter((e) => e.id !== modelId); if (filtered.length === list.length) return; writeCompatList(providerId, filtered); } function asRecord(value: unknown): JsonRecord { return value && typeof value === "object" && !Array.isArray(value) ? (value as JsonRecord) : {}; } function toNonEmptyString(value: unknown): string | null { return typeof value === "string" && value.trim().length > 0 ? value.trim() : null; } function getKeyValue(row: unknown): { key: string | null; value: string | null } { const record = asRecord(row); return { key: typeof record.key === "string" ? record.key : null, value: typeof record.value === "string" ? record.value : null, }; } // ──────────────── Model Aliases ──────────────── export async function getModelAliases() { const db = getDbInstance(); const rows = db .prepare("SELECT key, value FROM key_value WHERE namespace = 'modelAliases'") .all(); const result: Record = {}; for (const row of rows) { const { key, value } = getKeyValue(row); if (!key || value === null) continue; result[key] = JSON.parse(value); } return result; } export async function setModelAlias(alias: string, model: unknown) { const db = getDbInstance(); db.prepare( "INSERT OR REPLACE INTO key_value (namespace, key, value) VALUES ('modelAliases', ?, ?)" ).run(alias, JSON.stringify(model)); backupDbFile("pre-write"); } export async function deleteModelAlias(alias: string) { const db = getDbInstance(); db.prepare("DELETE FROM key_value WHERE namespace = 'modelAliases' AND key = ?").run(alias); backupDbFile("pre-write"); } // ──────────────── MITM Alias ──────────────── export async function getMitmAlias(toolName?: string) { const db = getDbInstance(); if (toolName) { const row = db .prepare("SELECT value FROM key_value WHERE namespace = 'mitmAlias' AND key = ?") .get(toolName); const value = getKeyValue(row).value; return value ? JSON.parse(value) : {}; } const rows = db.prepare("SELECT key, value FROM key_value WHERE namespace = 'mitmAlias'").all(); const result: Record = {}; for (const row of rows) { const { key, value } = getKeyValue(row); if (!key || value === null) continue; result[key] = JSON.parse(value); } return result; } export async function setMitmAliasAll(toolName: string, mappings: unknown) { const db = getDbInstance(); db.prepare( "INSERT OR REPLACE INTO key_value (namespace, key, value) VALUES ('mitmAlias', ?, ?)" ).run(toolName, JSON.stringify(mappings || {})); backupDbFile("pre-write"); } // ──────────────── Custom Models ──────────────── export async function getCustomModels(providerId?: string) { const db = getDbInstance(); if (providerId) { const row = db .prepare("SELECT value FROM key_value WHERE namespace = 'customModels' AND key = ?") .get(providerId); const value = getKeyValue(row).value; return value ? JSON.parse(value) : []; } const rows = db .prepare("SELECT key, value FROM key_value WHERE namespace = 'customModels'") .all(); const result: Record = {}; for (const row of rows) { const { key, value } = getKeyValue(row); if (!key || value === null) continue; result[key] = JSON.parse(value); } return result; } export async function getAllCustomModels() { const db = getDbInstance(); const rows = db .prepare("SELECT key, value FROM key_value WHERE namespace = 'customModels'") .all(); const result: Record = {}; for (const row of rows) { const { key, value } = getKeyValue(row); if (!key || value === null) continue; result[key] = JSON.parse(value); } return result; } export async function addCustomModel( providerId: string, modelId: string, modelName?: string, source = "manual", apiFormat: | "chat-completions" | "responses" | "embeddings" | "rerank" | "audio-transcriptions" | "audio-speech" | "images-generations" = "chat-completions", supportedEndpoints: string[] = ["chat"] ) { const db = getDbInstance(); const row = db .prepare("SELECT value FROM key_value WHERE namespace = 'customModels' AND key = ?") .get(providerId); const value = getKeyValue(row).value; const models = value ? JSON.parse(value) : []; const exists = models.find((m: JsonRecord) => m.id === modelId); if (exists) return exists; const model = { id: modelId, name: modelName || modelId, source, apiFormat, supportedEndpoints, }; models.push(model); db.prepare( "INSERT OR REPLACE INTO key_value (namespace, key, value) VALUES ('customModels', ?, ?)" ).run(providerId, JSON.stringify(models)); backupDbFile("pre-write"); return model; } /** * Replace the entire custom models list for a provider. * Preserves per-model compatibility overrides for models that still exist. */ export async function replaceCustomModels( providerId: string, models: Array<{ id: string; name?: string; source?: string; apiFormat?: string; supportedEndpoints?: string[]; inputTokenLimit?: number; outputTokenLimit?: number; description?: string; supportsThinking?: boolean; }>, { allowEmpty = false }: { allowEmpty?: boolean } = {} ) { // Guard: skip destructive clear when the caller hasn't explicitly opted in. // This prevents callers from wiping manually added models when the // upstream /models endpoint fails, times out, or returns an empty list. if (models.length === 0 && !allowEmpty) { const existing = await getCustomModels(providerId); return Array.isArray(existing) ? existing : []; } const db = getDbInstance(); const existing = await getCustomModels(providerId); const existingMap = new Map(); if (Array.isArray(existing)) { for (const m of existing) { if (m && typeof m === "object" && m.id) existingMap.set(m.id, m); } } // Merge: keep existing per-model compat flags if model still exists const merged = models.map((m) => { const prev = existingMap.get(m.id); return { id: m.id, name: m.name || m.id, source: m.source || "auto-sync", apiFormat: m.apiFormat || (prev as any)?.apiFormat || "chat-completions", supportedEndpoints: m.supportedEndpoints || (prev as any)?.supportedEndpoints || ["chat"], // Preserve metadata from provider API (or previous sync) ...(m.inputTokenLimit != null ? { inputTokenLimit: m.inputTokenLimit } : (prev as any)?.inputTokenLimit != null ? { inputTokenLimit: (prev as any).inputTokenLimit } : {}), ...(m.outputTokenLimit != null ? { outputTokenLimit: m.outputTokenLimit } : (prev as any)?.outputTokenLimit != null ? { outputTokenLimit: (prev as any).outputTokenLimit } : {}), ...(m.description != null ? { description: m.description } : (prev as any)?.description != null ? { description: (prev as any).description } : {}), ...(m.supportsThinking != null ? { supportsThinking: m.supportsThinking } : (prev as any)?.supportsThinking != null ? { supportsThinking: (prev as any).supportsThinking } : {}), // Preserve existing compat flags ...(prev && (prev as any).normalizeToolCallId !== undefined ? { normalizeToolCallId: (prev as any).normalizeToolCallId } : {}), ...(prev && (prev as any).preserveOpenAIDeveloperRole !== undefined ? { preserveOpenAIDeveloperRole: (prev as any).preserveOpenAIDeveloperRole } : {}), ...(prev && (prev as any).compatByProtocol ? { compatByProtocol: (prev as any).compatByProtocol } : {}), ...(prev && (prev as any).upstreamHeaders ? { upstreamHeaders: (prev as any).upstreamHeaders } : {}), }; }); if (merged.length === 0) { db.prepare("DELETE FROM key_value WHERE namespace = 'customModels' AND key = ?").run( providerId ); } else { db.prepare( "INSERT OR REPLACE INTO key_value (namespace, key, value) VALUES ('customModels', ?, ?)" ).run(providerId, JSON.stringify(merged)); } // Remove compat overrides for models that no longer exist const newIds = new Set(models.map((m) => m.id)); const compatList = readCompatList(providerId); const filteredCompat = compatList.filter((e) => newIds.has(e.id)); if (filteredCompat.length !== compatList.length) { writeCompatList(providerId, filteredCompat); } backupDbFile("pre-write"); return merged; } export async function removeCustomModel(providerId: string, modelId: string) { const db = getDbInstance(); const row = db .prepare("SELECT value FROM key_value WHERE namespace = 'customModels' AND key = ?") .get(providerId); if (!row) return false; const value = getKeyValue(row).value; if (!value) return false; const models = JSON.parse(value); const before = models.length; const filtered = models.filter((m: JsonRecord) => m.id !== modelId); if (filtered.length === before) return false; if (filtered.length === 0) { db.prepare("DELETE FROM key_value WHERE namespace = 'customModels' AND key = ?").run( providerId ); } else { db.prepare("UPDATE key_value SET value = ? WHERE namespace = 'customModels' AND key = ?").run( JSON.stringify(filtered), providerId ); } removeModelCompatOverride(providerId, modelId); backupDbFile("pre-write"); return true; } // ──────────────── Synced Available Models ──────────────── // Storage: namespace = 'syncedAvailableModels', key = ':' // Each connection stores its own model list. Reads union across all connections // for a provider. Deleting a connection removes only its models. export interface SyncedAvailableModel { id: string; name: string; source: "imported"; apiFormat?: string; supportedEndpoints?: string[]; inputTokenLimit?: number; outputTokenLimit?: number; description?: string; supportsThinking?: boolean; } type SyncedAvailableModelInput = Omit & { source?: string; }; function normalizeSyncedAvailableModel(model: unknown): SyncedAvailableModel | null { const record = asRecord(model); const id = toNonEmptyString(record.id) || toNonEmptyString(record.name) || toNonEmptyString(record.model); if (!id) return null; const name = toNonEmptyString(record.name) || toNonEmptyString(record.displayName) || toNonEmptyString(record.model) || id; const supportedEndpoints = Array.isArray(record.supportedEndpoints) ? Array.from( new Set( record.supportedEndpoints .map((endpoint) => toNonEmptyString(endpoint)) .filter((endpoint): endpoint is string => Boolean(endpoint)) ) ).sort() : undefined; return { id, name, source: "imported", ...(toNonEmptyString(record.apiFormat) ? { apiFormat: toNonEmptyString(record.apiFormat)! } : {}), ...(supportedEndpoints && supportedEndpoints.length > 0 ? { supportedEndpoints } : {}), ...(typeof record.inputTokenLimit === "number" ? { inputTokenLimit: record.inputTokenLimit } : {}), ...(typeof record.outputTokenLimit === "number" ? { outputTokenLimit: record.outputTokenLimit } : {}), ...(typeof record.description === "string" ? { description: record.description } : {}), ...(record.supportsThinking === true ? { supportsThinking: true } : {}), }; } function normalizeSyncedAvailableModels(models: unknown): SyncedAvailableModel[] { if (!Array.isArray(models)) return []; const deduped = new Map(); for (const model of models) { const normalized = normalizeSyncedAvailableModel(model); if (normalized) deduped.set(normalized.id, normalized); } return Array.from(deduped.values()); } /** * Get synced available models for a specific provider connection. */ export async function getSyncedAvailableModelsForConnection( providerId: string, connectionId: string ): Promise { const db = getDbInstance(); const key = `${providerId}:${connectionId}`; const row = db .prepare("SELECT value FROM key_value WHERE namespace = 'syncedAvailableModels' AND key = ?") .get(key); const value = getKeyValue(row).value; if (!value) return []; try { const models = JSON.parse(value); return normalizeSyncedAvailableModels(models); } catch { return []; } } /** * Get all synced available models for a provider, unioned across all connections. */ export async function getSyncedAvailableModels( providerId: string ): Promise { const db = getDbInstance(); const rows = db .prepare( "SELECT key, value FROM key_value WHERE namespace = 'syncedAvailableModels' AND key LIKE ?" ) .all(`${providerId}:%`); const map = new Map(); for (const row of rows) { const { key, value } = getKeyValue(row); if (!key || value === null) continue; const models = normalizeSyncedAvailableModels(JSON.parse(value)); for (const m of models) { if (m.id) map.set(m.id, m); } } return Array.from(map.values()); } /** * Get synced available models for a provider grouped by connection id. */ export async function getSyncedAvailableModelsByConnection( providerId: string ): Promise> { const db = getDbInstance(); const prefix = `${providerId}:`; const rows = db .prepare( "SELECT key, value FROM key_value WHERE namespace = 'syncedAvailableModels' AND key LIKE ?" ) .all(`${prefix}%`); const result: Record = {}; for (const row of rows) { const { key, value } = getKeyValue(row); if (!key || value === null || !key.startsWith(prefix)) continue; try { const connectionId = key.slice(prefix.length); result[connectionId] = normalizeSyncedAvailableModels(JSON.parse(value)); } catch { // Ignore malformed legacy entries. } } return result; } /** * Get all synced available models across all providers. */ export async function getAllSyncedAvailableModels(): Promise< Record > { const db = getDbInstance(); const rows = db .prepare("SELECT key, value FROM key_value WHERE namespace = 'syncedAvailableModels'") .all(); // Group by providerId (before the colon) const byProvider = new Map>(); for (const row of rows) { const { key, value } = getKeyValue(row); if (!key || value === null) continue; const providerId = key.split(":")[0]; if (!byProvider.has(providerId)) byProvider.set(providerId, new Map()); const models = normalizeSyncedAvailableModels(JSON.parse(value)); const map = byProvider.get(providerId)!; for (const m of models) { if (m.id) map.set(m.id, m); } } const result: Record = {}; for (const [providerId, map] of byProvider) { result[providerId] = Array.from(map.values()); } return result; } /** * Replace the model list for a specific connection. * Key format: ':' */ export async function replaceSyncedAvailableModelsForConnection( providerId: string, connectionId: string, models: SyncedAvailableModelInput[] ): Promise { const db = getDbInstance(); const key = `${providerId}:${connectionId}`; const normalizedModels = normalizeSyncedAvailableModels(models); if (normalizedModels.length === 0) { db.prepare("DELETE FROM key_value WHERE namespace = 'syncedAvailableModels' AND key = ?").run( key ); } else { db.prepare( "INSERT OR REPLACE INTO key_value (namespace, key, value) VALUES ('syncedAvailableModels', ?, ?)" ).run(key, JSON.stringify(normalizedModels)); } backupDbFile("pre-write"); // Return the full unioned list for the provider return getSyncedAvailableModels(providerId); } /** * Delete all synced models for a specific connection. * Returns the remaining unioned list for the provider. */ export async function deleteSyncedAvailableModelsForConnection( providerId: string, connectionId: string ): Promise { const db = getDbInstance(); const key = `${providerId}:${connectionId}`; db.prepare("DELETE FROM key_value WHERE namespace = 'syncedAvailableModels' AND key = ?").run( key ); backupDbFile("pre-write"); return getSyncedAvailableModels(providerId); } /** * Delete all synced models for every connection belonging to a provider. * Returns the number of connection-scoped synced model lists removed. */ export async function deleteSyncedAvailableModelsForProvider(providerId: string): Promise { const db = getDbInstance(); const keyPrefix = `${providerId}:`; const result = db .prepare( "DELETE FROM key_value WHERE namespace = 'syncedAvailableModels' AND substr(key, 1, ?) = ?" ) .run(keyPrefix.length, keyPrefix); backupDbFile("pre-write"); return Number(result.changes || 0); } /** * Prune stale synced available models for a provider, keeping only the specified allowed connection IDs. * Returns the number of keys deleted. */ export async function pruneStaleSyncedAvailableModelsForProvider( providerId: string, allowedConnectionIds: string[] ): Promise { const db = getDbInstance(); if (allowedConnectionIds.length === 0) { return deleteSyncedAvailableModelsForProvider(providerId); } const placeholders = allowedConnectionIds.map(() => "?").join(","); const keyPrefix = `${providerId}:`; const allowedKeys = allowedConnectionIds.map((id) => `${providerId}:${id}`); const result = db .prepare( `DELETE FROM key_value WHERE namespace = 'syncedAvailableModels' AND key LIKE ? AND key NOT IN (${placeholders})` ) .run(`${keyPrefix}%`, ...allowedKeys); backupDbFile("pre-write"); return Number(result.changes || 0); } export async function updateCustomModel( providerId: string, modelId: string, updates: Record = {} ) { const db = getDbInstance(); const row = db .prepare("SELECT value FROM key_value WHERE namespace = 'customModels' AND key = ?") .get(providerId); if (!row) return null; const value = getKeyValue(row).value; if (!value) return null; const models = JSON.parse(value); const index = models.findIndex((m: JsonRecord) => m.id === modelId); if (index === -1) return null; const current = models[index]; const currentCompat = (current as JsonRecord).compatByProtocol as CompatByProtocolMap | undefined; let mergedCompat: CompatByProtocolMap | undefined = currentCompat; if ( updates.compatByProtocol !== undefined && typeof updates.compatByProtocol === "object" && updates.compatByProtocol !== null && !Array.isArray(updates.compatByProtocol) ) { mergedCompat = deepMergeCompatByProtocol( currentCompat, updates.compatByProtocol as Partial< Record> > ); if (!compatByProtocolHasEntries(mergedCompat)) mergedCompat = undefined; } const next: JsonRecord = { ...current, ...(updates.modelName !== undefined ? { name: updates.modelName || current.name } : {}), ...(updates.apiFormat !== undefined ? { apiFormat: updates.apiFormat } : {}), ...(updates.supportedEndpoints !== undefined ? { supportedEndpoints: updates.supportedEndpoints } : {}), ...(updates.normalizeToolCallId !== undefined ? { normalizeToolCallId: Boolean(updates.normalizeToolCallId) } : {}), ...(updates.isHidden !== undefined ? { isHidden: Boolean(updates.isHidden) } : {}), }; if (Object.prototype.hasOwnProperty.call(updates, "preserveOpenAIDeveloperRole")) { if (updates.preserveOpenAIDeveloperRole === null) { delete next.preserveOpenAIDeveloperRole; } else { next.preserveOpenAIDeveloperRole = Boolean(updates.preserveOpenAIDeveloperRole); } } if (updates.compatByProtocol !== undefined) { if (mergedCompat && compatByProtocolHasEntries(mergedCompat)) { next.compatByProtocol = mergedCompat; } else { delete next.compatByProtocol; } } if (Object.prototype.hasOwnProperty.call(updates, "upstreamHeaders")) { const uh = updates.upstreamHeaders; if (uh === null || uh === undefined) { delete next.upstreamHeaders; } else if (typeof uh === "object" && !Array.isArray(uh)) { const s = sanitizeUpstreamHeadersMap(uh as Record); if (Object.keys(s).length === 0) delete next.upstreamHeaders; else next.upstreamHeaders = s; } } models[index] = next; db.prepare("UPDATE key_value SET value = ? WHERE namespace = 'customModels' AND key = ?").run( JSON.stringify(models), providerId ); backupDbFile("pre-write"); return next; } /** Single custom model row from key_value customModels, or null */ function getCustomModelRow(providerId: string, modelId: string): JsonRecord | null { const db = getDbInstance(); const row = db .prepare("SELECT value FROM key_value WHERE namespace = 'customModels' AND key = ?") .get(providerId); const value = getKeyValue(row).value; if (!value) return null; try { const models = JSON.parse(value) as unknown; if (!Array.isArray(models)) return null; const m = models.find((x: unknown) => { if (!x || typeof x !== "object" || Array.isArray(x)) return false; return (x as { id?: string }).id === modelId; }) as JsonRecord | undefined; return m ?? null; } catch { return null; } } /** * Whether the given provider/model has "normalize tool call id" (9-char Mistral-style) enabled. * Custom model row wins; otherwise {@link getModelCompatOverrides}. * When `sourceFormat` is one of `openai` | `openai-responses` | `claude`, per-protocol * `compatByProtocol[sourceFormat].normalizeToolCallId` overrides the legacy top-level flag. */ export function getModelNormalizeToolCallId( providerId: string, modelId: string, sourceFormat?: string | null ): boolean { const m = getCustomModelRow(providerId, modelId); const protocol = sourceFormat && isCompatProtocolKey(sourceFormat) ? sourceFormat : null; if (m) { if (protocol) { const pc = (m.compatByProtocol as CompatByProtocolMap | undefined)?.[protocol]; if (pc && Object.prototype.hasOwnProperty.call(pc, "normalizeToolCallId")) { return Boolean(pc.normalizeToolCallId); } } return Boolean(m.normalizeToolCallId); } const co = readCompatList(providerId).find((e) => e.id === modelId); if (protocol && co?.compatByProtocol?.[protocol]) { const pc = co.compatByProtocol[protocol]!; if (Object.prototype.hasOwnProperty.call(pc, "normalizeToolCallId")) { return Boolean(pc.normalizeToolCallId); } } return Boolean(co?.normalizeToolCallId); } /** * Explicit preserve-openai-developer preference for this provider/model. * `undefined` = unset → routing keeps legacy default (preserve developer for OpenAI format). * `false` = map developer → system (e.g. MiniMax). `true` = keep developer. * Per-protocol overrides live under `compatByProtocol[sourceFormat]` when `sourceFormat` matches. */ export function getModelPreserveOpenAIDeveloperRole( providerId: string, modelId: string, sourceFormat?: string | null ): boolean | undefined { const m = getCustomModelRow(providerId, modelId); const protocol = sourceFormat && isCompatProtocolKey(sourceFormat) ? sourceFormat : null; if (m) { if (protocol) { const pc = (m.compatByProtocol as CompatByProtocolMap | undefined)?.[protocol]; if (pc && Object.prototype.hasOwnProperty.call(pc, "preserveOpenAIDeveloperRole")) { return Boolean(pc.preserveOpenAIDeveloperRole); } } if (Object.prototype.hasOwnProperty.call(m, "preserveOpenAIDeveloperRole")) { return Boolean(m.preserveOpenAIDeveloperRole); } return undefined; } const co = readCompatList(providerId).find((e) => e.id === modelId); if (protocol && co?.compatByProtocol?.[protocol]) { const pc = co.compatByProtocol[protocol]!; if (Object.prototype.hasOwnProperty.call(pc, "preserveOpenAIDeveloperRole")) { return Boolean(pc.preserveOpenAIDeveloperRole); } } if (co && Object.prototype.hasOwnProperty.call(co, "preserveOpenAIDeveloperRole")) { return Boolean(co.preserveOpenAIDeveloperRole); } return undefined; } /** * Check if the model is flagged as hidden from the public catalog. */ export function getModelIsHidden(providerId: string, modelId: string): boolean { const m = getCustomModelRow(providerId, modelId); if (m && Object.prototype.hasOwnProperty.call(m, "isHidden")) { return Boolean(m.isHidden); } const co = readCompatList(providerId).find((e) => e.id === modelId); return Boolean(co?.isHidden); } function readUpstreamFromJsonRecord( row: JsonRecord | null | undefined, key: "upstreamHeaders" ): Record | undefined { if (!row) return undefined; const raw = row[key]; if (!raw || typeof raw !== "object" || Array.isArray(raw)) return undefined; const s = sanitizeUpstreamHeadersMap(raw as Record); return Object.keys(s).length > 0 ? s : undefined; } /** * Extra HTTP headers to send to the upstream provider for this model (after executor auth headers). * Order: top-level `upstreamHeaders` on the custom model row (override list merged under custom), * then per-protocol `compatByProtocol[sourceFormat].upstreamHeaders` (wins on key conflict). * Use for gateways that expect `Authentication`, `X-API-Key`, etc. alongside Bearer. * * `modelId` should be the **canonical** model id when known. Callers that accept client aliases * (e.g. chat proxy) should merge results for both alias and `resolveModelAlias(alias)` so UI * config on the resolved id still applies — see `chatCore` merge. */ export function getModelUpstreamExtraHeaders( providerId: string, modelId: string, sourceFormat?: string | null ): Record { const protocol = sourceFormat && isCompatProtocolKey(sourceFormat) ? sourceFormat : null; const m = getCustomModelRow(providerId, modelId); const base: Record = {}; if (m) { const fromModel = readUpstreamFromJsonRecord(m, "upstreamHeaders"); if (fromModel) Object.assign(base, fromModel); if (protocol) { const pc = (m.compatByProtocol as CompatByProtocolMap | undefined)?.[protocol]; const fromProto = pc?.upstreamHeaders; if (fromProto && typeof fromProto === "object") { Object.assign(base, sanitizeUpstreamHeadersMap(fromProto as Record)); } } return base; } const co = readCompatList(providerId).find((e) => e.id === modelId); if (co?.upstreamHeaders) { Object.assign(base, sanitizeUpstreamHeadersMap(co.upstreamHeaders as Record)); } if (protocol && co?.compatByProtocol?.[protocol]?.upstreamHeaders) { Object.assign( base, sanitizeUpstreamHeadersMap( co.compatByProtocol[protocol]!.upstreamHeaders as Record ) ); } return base; }