import { assertNever, hostFromHttpsUrl, secretNameFor, PROVIDER_IDS, type ProviderId, type SecretPrefix, } from './validation.ts' export { PROVIDER_IDS } export type ProviderAuth = 'bearer' | 'anthropic' export type ProviderCatalogEntry = { id: ProviderId label: string secretPrefix: SecretPrefix hosts: Array defaultModel: string auth: ProviderAuth docs: string console: string } export const PROVIDERS: Record = { openai: { id: 'openai', label: 'OpenAI', secretPrefix: 'openaiApiKey', hosts: ['api.openai.com'], defaultModel: 'gpt-4o-mini', auth: 'bearer', docs: 'https://platform.openai.com/docs/api-reference/chat', console: 'https://platform.openai.com/api-keys', }, anthropic: { id: 'anthropic', label: 'Anthropic', secretPrefix: 'anthropicApiKey', hosts: ['api.anthropic.com'], defaultModel: 'claude-sonnet-4-0', auth: 'anthropic', docs: 'https://docs.anthropic.com/en/api/messages', console: 'https://console.anthropic.com/settings/keys', }, groq: { id: 'groq', label: 'Groq', secretPrefix: 'groqApiKey', hosts: ['api.groq.com'], defaultModel: 'llama-3.1-8b-instant', auth: 'bearer', docs: 'https://console.groq.com/docs/openai', console: 'https://console.groq.com/keys', }, cloudflare: { id: 'cloudflare', label: 'Cloudflare Workers AI', secretPrefix: 'cloudflareApiToken', hosts: ['api.cloudflare.com', 'gateway.ai.cloudflare.com'], defaultModel: '@cf/meta/llama-3.1-8b-instruct', auth: 'bearer', docs: 'https://developers.cloudflare.com/workers-ai/configuration/open-ai-compatibility/', console: 'https://dash.cloudflare.com/profile/api-tokens', }, 'openai-compatible': { id: 'openai-compatible', label: 'OpenAI-compatible', secretPrefix: 'openaiCompatibleApiKey', hosts: [], defaultModel: '', auth: 'bearer', docs: 'https://platform.openai.com/docs/api-reference/chat', console: '', }, } export function providerCatalog(id: ProviderId): ProviderCatalogEntry { const entry = PROVIDERS[id] if (!entry) return assertNever(id, 'provider') return entry } export function defaultSecretName( provider: ProviderId, account?: string, override?: string, ): string { return secretNameFor(providerCatalog(provider).secretPrefix, account, override) } export function secretSetupUrl( secretName: string, hosts: Array, description: string, ): string { const params = new URLSearchParams({ name: secretName, description, scope: 'user', }) if (hosts.length > 0) params.set('allowedHosts', hosts.join(',')) return 'https://kody.codes/account/secrets/new?' + params.toString() } export function providerSecretUrl( provider: ProviderId, secretName?: string, compatibleHost?: string, ): string { const entry = providerCatalog(provider) const name = secretName ?? defaultSecretName(provider) const hosts = provider === 'openai-compatible' && compatibleHost ? [compatibleHost] : entry.hosts return secretSetupUrl( name, hosts, entry.label + ' API key for Kody AI helpers', ) } export function chatUrl(input: { provider: ProviderId baseUrl?: string cloudflareAccountId?: string cloudflareAiGatewayId?: string }): { href: string; host: string } { switch (input.provider) { case 'openai': return { href: 'https://api.openai.com/v1/chat/completions', host: 'api.openai.com', } case 'anthropic': return { href: 'https://api.anthropic.com/v1/messages', host: 'api.anthropic.com', } case 'groq': return { href: 'https://api.groq.com/openai/v1/chat/completions', host: 'api.groq.com', } case 'cloudflare': { const accountId = input.cloudflareAccountId if (!accountId) { throw new Error( 'cloudflareAccountId is required for Cloudflare Workers AI. Fork this package and save it with ./settings (confirm: true). This package never bakes in an account id.', ) } if (input.cloudflareAiGatewayId) { return { href: 'https://gateway.ai.cloudflare.com/v1/' + accountId + '/' + input.cloudflareAiGatewayId + '/workers-ai/v1/chat/completions', host: 'gateway.ai.cloudflare.com', } } return { href: 'https://api.cloudflare.com/client/v4/accounts/' + accountId + '/ai/v1/chat/completions', host: 'api.cloudflare.com', } } case 'openai-compatible': { if (!input.baseUrl) { throw new Error( 'baseUrl is required for openai-compatible. Pass an https origin+path (no credentials) or store it in ./settings on your fork.', ) } const href = input.baseUrl.endsWith('/chat/completions') ? input.baseUrl : input.baseUrl.replace(/\/$/, '') + '/chat/completions' return { href, host: hostFromHttpsUrl(href) } } default: return assertNever(input.provider, 'provider') } } export function modelsUrl(input: { provider: ProviderId baseUrl?: string cloudflareAccountId?: string }): { href: string; host: string } | null { switch (input.provider) { case 'openai': return { href: 'https://api.openai.com/v1/models', host: 'api.openai.com' } case 'anthropic': return { href: 'https://api.anthropic.com/v1/models', host: 'api.anthropic.com' } case 'groq': return { href: 'https://api.groq.com/openai/v1/models', host: 'api.groq.com' } case 'cloudflare': { if (!input.cloudflareAccountId) return null return { href: 'https://api.cloudflare.com/client/v4/accounts/' + input.cloudflareAccountId + '/ai/models/search', host: 'api.cloudflare.com', } } case 'openai-compatible': { if (!input.baseUrl) return null const root = input.baseUrl.replace(/\/chat\/completions$/, '').replace(/\/$/, '') const href = root.endsWith('/models') ? root : root + '/models' return { href, host: hostFromHttpsUrl(href) } } default: return assertNever(input.provider, 'provider') } } export function requestHeaders( provider: ProviderId, secretName: string, ): Record { const placeholder = '{{secret:' + secretName + '|scope=user}}' const headers: Record = { Accept: 'application/json', 'Content-Type': 'application/json', } switch (provider) { case 'openai': case 'groq': case 'cloudflare': case 'openai-compatible': headers.Authorization = 'Bearer ' + placeholder return headers case 'anthropic': headers['x-api-key'] = placeholder headers['anthropic-version'] = '2023-06-01' return headers default: return assertNever(provider, 'provider') } }