import { AI_AGENT_DEFINITIONS, DEFAULT_AI_AGENT, getAiAgentAvailability, normalizeStoredAiAgent, type AiAgentId, type AiAgentsStatus, } from './aiAgents' import providerCatalog from '../shared/aiModelProviderCatalog.json' with { type: 'json' } import type { Settings } from '../types' import type { TranslationKey } from './i18n' export type AiModelProviderKind = 'open_ai' | 'anthropic' | 'open_ai_compatible' | 'ollama' | 'lm_studio' | 'open_router' | 'gemini' export type AiTargetKind = 'agent' | 'api_model' export type AiModelApiKeyStorage = 'none' | 'env' | 'local_file' export interface AiModelCapabilities { streaming: boolean tools: boolean vision: boolean json_mode: boolean reasoning: boolean } export interface AiModelDefinition { id: string display_name?: string | null context_window?: number | null max_output_tokens?: number | null capabilities: AiModelCapabilities } export interface AiModelProvider { id: string name: string kind: AiModelProviderKind base_url?: string | null api_key_storage?: AiModelApiKeyStorage | null api_key_env_var?: string | null headers?: Record | null models: AiModelDefinition[] } export interface AiModelProviderCatalogEntry { kind: AiModelProviderKind name: string label_key: TranslationKey base_url: string runtime_base_url: string | null default_model_id: string api_key_storage: AiModelApiKeyStorage api_key_env_var: string | null local: boolean } export type AiTarget = | { kind: 'agent'; agent: AiAgentId; id: string; label: string; shortLabel: string } | { kind: 'api_model'; provider: AiModelProvider; model: AiModelDefinition; id: string; label: string; shortLabel: string } export type AiModelTarget = Extract export const AI_TARGET_PREFIX_AGENT = 'agent:' export const AI_TARGET_PREFIX_MODEL = 'model:' const AI_MODEL_PROVIDER_CATALOG = providerCatalog as readonly AiModelProviderCatalogEntry[] const AI_MODEL_PROVIDER_CATALOG_BY_KIND = new Map( AI_MODEL_PROVIDER_CATALOG.map((entry) => [entry.kind, entry]), ) export const LOCAL_AI_PROVIDER_KINDS: readonly AiModelProviderKind[] = AI_MODEL_PROVIDER_CATALOG .filter((entry) => entry.local) .map((entry) => entry.kind) export const DEFAULT_MODEL_CAPABILITIES: AiModelCapabilities = { streaming: false, tools: false, vision: false, json_mode: false, reasoning: false, } export function aiModelProviderCatalog(): readonly AiModelProviderCatalogEntry[] { return AI_MODEL_PROVIDER_CATALOG } export function aiModelProviderCatalogEntry(kind: AiModelProviderKind): AiModelProviderCatalogEntry { const entry = AI_MODEL_PROVIDER_CATALOG_BY_KIND.get(kind) if (!entry) throw new Error(`Unknown AI model provider kind: ${kind}`) return entry } export function agentTargetId(agent: AiAgentId): string { return `${AI_TARGET_PREFIX_AGENT}${agent}` } export function modelTargetId(providerId: string, modelId: string): string { return `${AI_TARGET_PREFIX_MODEL}${providerId}/${modelId}` } export function configuredModelTargets(providers: AiModelProvider[] | null | undefined): AiModelTarget[] { return (providers ?? []).flatMap((provider) => provider.models.map((model) => { const displayName = model.display_name || model.id return { kind: 'api_model' as const, provider, model, id: modelTargetId(provider.id, model.id), label: `${provider.name} ยท ${displayName}`, shortLabel: displayName, } })) } export function agentTargets(): AiTarget[] { return AI_AGENT_DEFINITIONS.map((definition) => { return { kind: 'agent' as const, agent: definition.id, id: agentTargetId(definition.id), label: definition.label, shortLabel: definition.shortLabel, } }) } export function resolveAiTarget(settings: Settings): AiTarget { const providers = normalizeAiModelProviders(settings.ai_model_providers) const agents = agentTargets() const targets = [...agents, ...configuredModelTargets(providers)] const storedLegacyAgent = normalizeStoredAiAgent(settings.default_ai_agent) const legacyAgent = storedLegacyAgent ?? DEFAULT_AI_AGENT const target = resolveStoredAiTarget(settings.default_ai_target, targets) if (target) { if (shouldPreferLegacyAgent(target, storedLegacyAgent)) return agentTargetFor(agents, legacyAgent) ?? target return target } return agentTargetFor(agents, legacyAgent) ?? agents[0] } export function targetAgent(target: AiTarget): AiAgentId { return target.kind === 'agent' ? target.agent : DEFAULT_AI_AGENT } function shouldPreferLegacyAgent(target: AiTarget, storedLegacyAgent: AiAgentId | null): boolean { if (target.kind !== 'agent') return false if (!storedLegacyAgent) return false if (target.agent !== DEFAULT_AI_AGENT) return false return target.agent !== storedLegacyAgent } function agentTargetFor(agents: AiTarget[], agent: AiAgentId): AiTarget | undefined { return agents.find((candidate) => candidate.kind === 'agent' && candidate.agent === agent) } function resolveStoredAiTarget(storedTarget: string | null | undefined, targets: AiTarget[]): AiTarget | null { const target = storedTarget?.trim() if (!target) return null const exactTarget = targets.find((candidate) => candidate.id === target) if (exactTarget) return exactTarget const legacyAgent = normalizeStoredAiAgent(target) if (!legacyAgent) return null return targets.find((candidate) => candidate.kind === 'agent' && candidate.agent === legacyAgent) ?? null } export function normalizeAiModelProviders(providers: AiModelProvider[] | null | undefined): AiModelProvider[] { return (providers ?? []).map(normalizeAiModelProvider).filter((provider): provider is AiModelProvider => provider !== null) } export function normalizeAiModelProvider(provider: AiModelProvider): AiModelProvider | null { const id = provider.id.trim().toLowerCase() const name = provider.name.trim() const models = provider.models.map(normalizeAiModelDefinition).filter((model): model is AiModelDefinition => model !== null) if (!id || !name || models.length === 0) return null return { ...provider, id, name, base_url: emptyToNull(provider.base_url), api_key_storage: normalizeApiKeyStorage(provider), api_key_env_var: emptyToNull(provider.api_key_env_var), models, } } function normalizeApiKeyStorage(provider: AiModelProvider): 'none' | 'env' | 'local_file' { if (provider.api_key_storage === 'local_file') return 'local_file' if (provider.api_key_storage === 'env' || emptyToNull(provider.api_key_env_var)) return 'env' return 'none' } function normalizeAiModelDefinition(model: AiModelDefinition): AiModelDefinition | null { const id = model.id.trim() if (!id) return null return { ...model, id, display_name: emptyToNull(model.display_name), capabilities: model.capabilities ?? DEFAULT_MODEL_CAPABILITIES, } } function emptyToNull(value: string | null | undefined): string | null { const trimmed = value?.trim() return trimmed ? trimmed : null } export function isLocalAiProvider(provider: AiModelProvider): boolean { return aiModelProviderCatalogEntry(provider.kind).local } export function aiTargetReady(target: AiTarget, statuses: AiAgentsStatus): boolean { if (target.kind === 'api_model') return true return getAiAgentAvailability(statuses, target.agent).status === 'installed' }