import { Hotkey } from "./components/kb-select/kb-shortcut-select.tsx"; import { ImageInfo } from "./utils/image-utils.ts"; export type ImageModalityConfig = { enabled: boolean; maxSizeMB: number; acceptedMimeTypes: string[]; }; export type MultimodalConfig = { image?: ImageModalityConfig; }; export type ProviderConfig = { shortcut: Hotkey; type?: "standard" | "openai-responses" | "anthropic" | "codex"; name: string; description?: string; envVar: string; baseUrl: string; models: Array<{ model: string; nickname: string; context: number; reasoning?: "low" | "medium" | "high" | "xhigh"; modalities?: MultimodalConfig; }>; testModel: string; }; export const PROVIDERS = { synthetic: { shortcut: "s" as const, name: "Synthetic", description: "Synthetic offers syn: aliases that auto-route to the latest recommended models, so these never go stale. \n\nFor more details, see https://dev.synthetic.new/docs/api/overview", envVar: "SYNTHETIC_API_KEY", baseUrl: "https://api.synthetic.new/openai/v1", models: [ { model: "syn:large:text", nickname: "syn:large:text", context: 512 * 1024, }, { model: "syn:small:text", nickname: "syn:small:text", context: 192 * 1024, }, { model: "syn:large:vision", nickname: "syn:large:vision", context: 512 * 1024, modalities: { image: { enabled: true, maxSizeMB: 10, acceptedMimeTypes: ["image/jpeg", "image/png", "image/webp", "image/gif"], }, }, }, { model: "syn:small:vision", nickname: "syn:small:vision", context: 256 * 1024, modalities: { image: { enabled: true, maxSizeMB: 10, acceptedMimeTypes: ["image/jpeg", "image/png", "image/webp", "image/gif"], }, }, }, ], testModel: "syn:small:text", } satisfies ProviderConfig, codex: { shortcut: "d" as const, type: "codex", name: "OpenAI Codex Subscription", envVar: "OPENAI_CODEX_ACCESS_TOKEN", baseUrl: "https://chatgpt.com/backend-api/codex", models: [ { model: "gpt-5.6-sol", nickname: "GPT-5.6 Sol", context: 384 * 1024, reasoning: "xhigh", modalities: { image: { enabled: true, maxSizeMB: 20, acceptedMimeTypes: ["image/jpeg", "image/png", "image/webp", "image/gif"], }, }, }, ], testModel: "gpt-5.6-sol", } satisfies ProviderConfig, openai: { shortcut: "o" as const, type: "openai-responses", name: "OpenAI API", envVar: "OPENAI_API_KEY", baseUrl: "https://api.openai.com/v1", models: [ { model: "gpt-5.6", nickname: "GPT-5.6 Sol", context: 384 * 1024, reasoning: "xhigh", modalities: { image: { enabled: true, maxSizeMB: 20, acceptedMimeTypes: ["image/jpeg", "image/png", "image/webp", "image/gif"], }, }, }, { model: "gpt-5.6-luna", nickname: "GPT-5.6 Luna", context: 200 * 1024, reasoning: "medium", modalities: { image: { enabled: true, maxSizeMB: 20, acceptedMimeTypes: ["image/jpeg", "image/png", "image/webp", "image/gif"], }, }, }, ], testModel: "gpt-5.6-luna", } satisfies ProviderConfig, anthropic: { shortcut: "a" as const, type: "anthropic", name: "Anthropic", envVar: "ANTHROPIC_API_KEY", baseUrl: "https://api.anthropic.com", models: [ { model: "claude-fable-5", nickname: "Claude Fable 5", context: 200 * 1000, reasoning: "medium", modalities: { image: { enabled: true, maxSizeMB: 30, acceptedMimeTypes: ["image/jpeg", "image/png", "image/webp", "image/gif"], }, }, }, { model: "claude-opus-5", nickname: "Claude Opus 5", context: 200 * 1000, reasoning: "medium", modalities: { image: { enabled: true, maxSizeMB: 30, acceptedMimeTypes: ["image/jpeg", "image/png", "image/webp", "image/gif"], }, }, }, { model: "claude-haiku-4-5-20251001", nickname: "Claude 4.5 Haiku", context: 200 * 1000, reasoning: "medium", modalities: { image: { enabled: true, maxSizeMB: 30, acceptedMimeTypes: ["image/jpeg", "image/png", "image/webp", "image/gif"], }, }, }, ], testModel: "claude-haiku-4-5-20251001", } satisfies ProviderConfig, grok: { shortcut: "x" as const, name: "xAI", envVar: "XAI_API_KEY", baseUrl: "https://api.x.ai/v1", models: [ { model: "grok-4-latest", nickname: "Grok 4", context: 64 * 1024, modalities: { image: { enabled: true, maxSizeMB: 20, acceptedMimeTypes: ["image/jpeg", "image/png"], }, }, }, ], testModel: "grok-4-latest", } satisfies ProviderConfig, }; export const DEFAULT_MULTIMODAL_IMAGE_MODEL_EXAMPLE = PROVIDERS.synthetic.models.find(m => m.modalities?.image.enabled)?.nickname ?? "Syn Large Vision"; export type CanDisplayImageResult = { ok: true } | { ok: false; reason: string }; export function canDisplayImage( modalities: MultimodalConfig | undefined, image: ImageInfo, ): CanDisplayImageResult { if (!modalities?.image?.enabled) { return { ok: false, reason: "Your model does not support image viewing." }; } if (!modalities.image.acceptedMimeTypes.includes(image.mimeType)) { return { ok: false, reason: `Your model does not support ${image.mimeType} images. Supported formats: ${modalities.image.acceptedMimeTypes.join(", ")}.`, }; } if (image.sizeBytes != null && image.sizeBytes > modalities.image.maxSizeMB * 1024 * 1024) { const sizeMB = (image.sizeBytes / (1024 * 1024)).toFixed(1); return { ok: false, reason: `Image file is too large (${sizeMB} MB). Maximum supported size is ${modalities.image.maxSizeMB} MB.`, }; } return { ok: true }; } export type ProviderKey = keyof typeof PROVIDERS; export function recommendedModel(provider: ProviderKey): ProviderConfig["models"][number] { return PROVIDERS[provider].models[0]; } export const SYNTHETIC_PROVIDER = PROVIDERS.synthetic; export const CODEX_PROVIDER = PROVIDERS.codex; export function keyFromName(name: string): keyof typeof PROVIDERS { for (const [key, value] of Object.entries(PROVIDERS)) { if (value.name === name) return key as keyof typeof PROVIDERS; } throw new Error(`No provider named ${name} found`); } export function providerForBaseUrl(baseUrl: string): ProviderConfig | null { const provider = Object.values(PROVIDERS).find(p => p.baseUrl === baseUrl); if (provider == null) return null; return provider; }