import YAML from "yaml"; import type { ModelConfig, ProviderAccount, ProviderType, } from "../../../src/core/types.js"; import { modelGuidToDisplay, resolveEntryId } from "./yaml-shared.js"; const tokenFormatter = new Intl.NumberFormat("en-US", { notation: "compact", maximumFractionDigits: 1 }); const formatTokens = (n: number) => tokenFormatter.format(n); function validateTemperatureDisabled(value: unknown, modelName: string): boolean | undefined { if (value === undefined || value === null) return undefined; if (typeof value === "boolean") return value; throw new Error( `temperature_disabled for model "${modelName}" must be true, false, or omitted (got: ${JSON.stringify(value)}). ` + "Non-boolean values are silently ambiguous (e.g. the string \"false\" is truthy) and are rejected instead of guessed." ); } interface EditableModelData { name: string; id?: string; model_id?: string; token_limit?: number; max_output_tokens?: number; thinking_budget?: number; temperature_disabled?: boolean | null; _delete?: boolean; } interface EditableProviderData { name: string; type: "llm" | "storage"; url: string; api_key?: string; default_model?: string; token_limit?: number | null; extra_headers?: Record; enabled?: boolean; models?: EditableModelData[]; _delete?: boolean; } export interface ProviderYAMLResult { account: ProviderAccount; _delete: boolean; } const PLACEHOLDER_PROVIDER_NAME = "My Provider"; const PLACEHOLDER_PROVIDER_URL = "https://api.example.com/v1"; const PLACEHOLDER_PROVIDER_API_KEY = "your-api-key-or-$ENVAR"; const PLACEHOLDER_PROVIDER_DEFAULT_MODEL = "model-name"; function parseModels(editableModels: EditableModelData[]): ModelConfig[] { const result: ModelConfig[] = []; for (const m of editableModels) { if (m._delete) continue; const modelId = m.model_id ?? undefined; result.push({ id: crypto.randomUUID(), name: m.name, model_id: (modelId === null || modelId === m.name) ? undefined : modelId, token_limit: m.token_limit ?? undefined, max_output_tokens: m.max_output_tokens ?? undefined, thinking_budget: m.thinking_budget ?? undefined, temperature_disabled: validateTemperatureDisabled(m.temperature_disabled, m.name), }); } return result; } export function newProviderToYAML(name?: string): string { const placeholderData = { name: name ?? PLACEHOLDER_PROVIDER_NAME, type: "llm", url: PLACEHOLDER_PROVIDER_URL, api_key: PLACEHOLDER_PROVIDER_API_KEY, default_model: PLACEHOLDER_PROVIDER_DEFAULT_MODEL, token_limit: null, extra_headers: {}, enabled: true, }; const modelsYAML = [ "models:", " - name: default", " model_id: default", " token_limit: null", " max_output_tokens: null", " thinking_budget: null", " temperature_disabled: null", " # _delete: true", "# _delete: true # Delete this entire provider", ].join("\n"); return YAML.stringify(placeholderData, { lineWidth: 0 }).trimEnd() + "\n" + modelsYAML + "\n"; } export function newProviderFromYAML(yamlContent: string): ProviderAccount { const cleaned = yamlContent .split('\n') .filter(line => !/^\s*#/.test(line)) .join('\n'); const data = YAML.parse(cleaned) as EditableProviderData; if (!data.name || data.name === PLACEHOLDER_PROVIDER_NAME) { throw new Error("Provider name is required"); } if (!data.url || data.url === PLACEHOLDER_PROVIDER_URL) { throw new Error("Provider URL is required"); } if (data.api_key === PLACEHOLDER_PROVIDER_API_KEY) { data.api_key = undefined; } if (data.default_model === PLACEHOLDER_PROVIDER_DEFAULT_MODEL) { data.default_model = undefined; } if (data.token_limit !== undefined && data.token_limit !== null && (typeof data.token_limit !== "number" || isNaN(data.token_limit))) { throw new Error(`token_limit must be a number (got: ${JSON.stringify(data.token_limit)}). Note: underscore separators (100_000) are not valid in YAML.`); } const models = parseModels(data.models ?? []); return { id: crypto.randomUUID(), name: data.name, type: (data.type === "storage" ? "storage" : "llm") as ProviderType, url: data.url, api_key: data.api_key, default_model: resolveProviderDefaultModel(data.default_model, [], models), token_limit: data.token_limit ?? undefined, extra_headers: data.extra_headers && Object.keys(data.extra_headers).length > 0 ? data.extra_headers : undefined, enabled: data.enabled ?? true, models: models.length > 0 ? models : undefined, created_at: new Date().toISOString(), }; } export function providerToYAML(account: ProviderAccount): string { const defaultModelDisplay = account.default_model ? modelGuidToDisplay(account.default_model, [account]) : undefined; const topData = { name: account.name, type: account.type as "llm" | "storage", url: account.url, api_key: account.api_key, default_model: defaultModelDisplay, token_limit: account.token_limit ?? null, extra_headers: account.extra_headers, enabled: account.enabled ?? true, }; const topYAML = YAML.stringify(topData, { lineWidth: 0 }).trimEnd(); const modelLines: string[] = ["models:"]; const modelList = account.models ?? []; if (modelList.length > 0) { for (const m of modelList) { modelLines.push(` - name: ${m.name}`); modelLines.push(` id: ${m.id}`); modelLines.push(` model_id: ${m.model_id ?? m.name}`); modelLines.push(` token_limit: ${m.token_limit ?? null}`); modelLines.push(` max_output_tokens: ${m.max_output_tokens ?? null}`); modelLines.push(` thinking_budget: ${m.thinking_budget ?? null}`); modelLines.push(` temperature_disabled: ${m.temperature_disabled ?? null}`); if (m.total_calls !== undefined || m.total_tokens_in !== undefined) { const tokensIn = m.total_tokens_in ?? 0; const tokensOut = m.total_tokens_out ?? 0; modelLines.push(` # stats: ${formatTokens(m.total_calls ?? 0)} calls ยท ${formatTokens(tokensIn)} in / ${formatTokens(tokensOut)} out`); if (m.last_used) { modelLines.push(` # used: ${m.last_used}`); } } modelLines.push(` _delete: false`); } } else { modelLines.push(" - name: default"); modelLines.push(` model_id: default`); modelLines.push(` token_limit: null`); modelLines.push(` max_output_tokens: null`); modelLines.push(` thinking_budget: null`); modelLines.push(" temperature_disabled: null"); modelLines.push(" _delete: false"); } modelLines.push("_delete: false # Set to true to delete this entire provider"); return topYAML + "\n" + modelLines.join("\n") + "\n"; } function resolveProviderDefaultModel( raw: string | undefined, existingModels: ModelConfig[], parsedModels: ModelConfig[] ): string | undefined { if (!raw) return undefined; const colonIdx = raw.indexOf(':'); const namePart = colonIdx >= 0 ? raw.substring(colonIdx + 1) : raw; const resolvedId = existingModels.find(m => m.id === raw)?.id ?? existingModels.find(m => m.name === namePart)?.id ?? parsedModels.find(m => m.id === raw)?.id ?? parsedModels.find(m => m.name === namePart)?.id; if (!resolvedId) return undefined; // Model kept its id across a rename -> still valid. Model actually // removed (delete or dropped from the YAML) -> no longer valid. return parsedModels.some(m => m.id === resolvedId) ? resolvedId : undefined; } export function providerFromYAML(yamlContent: string, original: ProviderAccount): ProviderYAMLResult { const cleaned = yamlContent .split('\n') .filter(line => !/^\s*#/.test(line)) .join('\n'); const data = YAML.parse(cleaned) as EditableProviderData; if (!data.name) { throw new Error("Provider name is required"); } if (!data.url) { throw new Error("Provider URL is required"); } if (data.token_limit !== undefined && data.token_limit !== null && (typeof data.token_limit !== "number" || isNaN(data.token_limit))) { throw new Error(`token_limit must be a number (got: ${JSON.stringify(data.token_limit)}). Note: underscore separators (100_000) are not valid in YAML.`); } if (data._delete) { return { account: original, _delete: true }; } const existingModels = original.models ?? []; const seenIds = new Set(); const parsedModels: ModelConfig[] = []; for (const m of data.models ?? []) { const id = resolveEntryId({ id: m.id, name: m.name }, existingModels, seenIds, "Model"); if (m._delete) continue; const existing = existingModels.find(em => em.id === id); const modelId = m.model_id ?? undefined; parsedModels.push({ id, name: m.name, model_id: (modelId === null || modelId === m.name) ? undefined : modelId, token_limit: m.token_limit ?? undefined, max_output_tokens: m.max_output_tokens ?? undefined, thinking_budget: m.thinking_budget ?? undefined, temperature_disabled: validateTemperatureDisabled(m.temperature_disabled, m.name), total_calls: existing?.total_calls, total_tokens_in: existing?.total_tokens_in, total_tokens_out: existing?.total_tokens_out, last_used: existing?.last_used, }); } const account: ProviderAccount = { id: original.id, name: data.name, type: (data.type === "storage" ? "storage" : "llm") as ProviderType, url: data.url, api_key: data.api_key, default_model: resolveProviderDefaultModel(data.default_model, existingModels, parsedModels), token_limit: data.token_limit ?? undefined, extra_headers: data.extra_headers && Object.keys(data.extra_headers).length > 0 ? data.extra_headers : undefined, enabled: data.enabled ?? true, models: parsedModels.length > 0 ? parsedModels : undefined, created_at: original.created_at, }; return { account, _delete: false }; }