Skip to content

Built for people who want to own their automations. Join the waitlist for an invite.

Package listing

@kody/ai

src/providers.ts

244 lines · 6.4 KB · TypeScript
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<string>
	defaultModel: string
	auth: ProviderAuth
	docs: string
	console: string
}

export const PROVIDERS: Record<ProviderId, ProviderCatalogEntry> = {
	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<string>,
	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<string, string> {
	const placeholder = '{{secret:' + secretName + '|scope=user}}'
	const headers: Record<string, string> = {
		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')
	}
}