3c9b65d982
- Nuxt 4 + Nuxt UI 4 + AI SDK 7 + Drizzle/SQLite - 主编规划大纲与章节简报、选派作家;作家流式写入章节文件并实时推送前端 - 逐章确认模式(默认)与自动模式;摘要与故事记忆 - 酒馆角色卡(PNG/JSON,V1-V3)与世界书导入 - 自定义最高优先级系统提示词、审稿/修改/多版本、任务日志与提示词检视 - 模拟 LLM 服务与端到端测试脚本 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
98 lines
4.1 KiB
TypeScript
98 lines
4.1 KiB
TypeScript
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<ProviderType, string> = {
|
|
'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<string[]> {
|
|
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
|
|
}
|