import { createAnthropic } from '@ai-sdk/anthropic' import { createGoogle } from '@ai-sdk/google' import { createOpenAICompatible } from '@ai-sdk/openai-compatible' import type { LanguageModel } from 'ai' import { eq } from 'drizzle-orm' import type { ProviderType } from '#shared/types' type ProviderRow = typeof tables.providers.$inferSelect type ModelRow = typeof tables.models.$inferSelect export const DEFAULT_BASE_URL: Record = { 'openai-compatible': 'https://api.openai.com/v1', 'anthropic': 'https://api.anthropic.com/v1', 'google': 'https://generativelanguage.googleapis.com/v1beta' } function trimUrl(url: string) { return url.trim().replace(/\/+$/, '') } export function createLanguageModel(provider: ProviderRow, modelId: string): LanguageModel { const apiKey = decryptSecret(provider.apiKeyEnc) const baseURL = trimUrl(provider.baseUrl) || DEFAULT_BASE_URL[provider.type] switch (provider.type) { case 'anthropic': return createAnthropic({ apiKey, baseURL })(modelId) case 'google': return createGoogle({ apiKey, baseURL })(modelId) default: return createOpenAICompatible({ name: 'openai-compatible', apiKey, baseURL, includeUsage: true })(modelId) } } export interface ResolvedModel { model: LanguageModel label: string settings: { temperature?: number, topP?: number, maxOutputTokens?: number } } export function resolveModel(modelRowId: string | null | undefined): ResolvedModel { if (!modelRowId) throw createError({ statusCode: 400, message: '该成员尚未指定模型,请先在「AI 团队」里选择模型' }) const db = useDb() const row = db.select().from(tables.models).where(eq(tables.models.id, modelRowId)).get() if (!row) throw createError({ statusCode: 400, message: '指定的模型不存在,可能已被删除' }) const provider = db.select().from(tables.providers).where(eq(tables.providers.id, row.providerId)).get() if (!provider) throw createError({ statusCode: 400, message: '模型所属的 API 提供方不存在' }) return { model: createLanguageModel(provider, row.modelId), label: `${provider.name} / ${row.label || row.modelId}`, settings: modelSettings(row) } } export function modelSettings(row: ModelRow) { const s: ResolvedModel['settings'] = {} if (row.temperature != null) s.temperature = row.temperature if (row.topP != null) s.topP = row.topP if (row.maxOutputTokens != null) s.maxOutputTokens = row.maxOutputTokens return s } /** 从提供方拉取可用模型列表 */ export async function listRemoteModels(type: ProviderType, baseUrl: string, apiKey: string): Promise { const base = trimUrl(baseUrl) || DEFAULT_BASE_URL[type] if (type === 'anthropic') { const res = await $fetch<{ data: { id: string }[] }>(`${base}/models`, { headers: { 'x-api-key': apiKey, 'anthropic-version': '2023-06-01' }, query: { limit: 1000 } }) return res.data.map(m => m.id) } if (type === 'google') { const res = await $fetch<{ models: { name: string, supportedGenerationMethods?: string[] }[] }>(`${base}/models`, { headers: { 'x-goog-api-key': apiKey }, query: { pageSize: 1000 } }) return res.models .filter(m => !m.supportedGenerationMethods || m.supportedGenerationMethods.includes('generateContent')) .map(m => m.name.replace(/^models\//, '')) } const res = await $fetch<{ data: { id: string }[] }>(`${base}/models`, { headers: { Authorization: `Bearer ${apiKey}` } }) return res.data.map(m => m.id).sort() } /** 把 AI SDK / HTTP 错误整理成用户能看懂的信息 */ export function describeError(err: unknown): string { if (!err) return '未知错误' const e = err as { name?: string, message?: string, statusCode?: number, responseBody?: string, data?: unknown, cause?: unknown } if (e.name === 'AbortError') return '已取消' let msg = e.message || String(err) if (e.statusCode) msg = `[HTTP ${e.statusCode}] ${msg}` if (e.responseBody) msg += `\n${String(e.responseBody).slice(0, 800)}` else if (e.data) msg += `\n${JSON.stringify(e.data).slice(0, 800)}` return msg }