import { type Api, createModels, createProvider, type Model, type MutableModels } from "@earendil-works/pi-ai"; import { anthropicMessagesApi } from "@earendil-works/pi-ai/api/anthropic-messages.lazy"; import { openAICompletionsApi } from "@earendil-works/pi-ai/api/openai-completions.lazy"; import type { CompactionPolicy } from "@earendil-works/pi-durable"; import type { ResolvedAgentSettings } from "../settings.ts"; /** Pi 侧需要的模型装配结果(仅在 src/pi/* 内部流转) */ export interface ResolvedPiModel { models: MutableModels; model: Model; /** 夹到模型支持范围后的思考档位;缺省用设置值 */ thinkingLevel?: ResolvedAgentSettings["thinkingLevel"]; } export type PiModelResolver = (settings: ResolvedAgentSettings) => ResolvedPiModel | Promise; const ANTHROPIC_DEFAULT_BASE = "https://api.anthropic.com"; const PROVIDER_ID = { anthropic: "umt-anthropic", "openai-compatible": "umt-openai-compatible" } as const; /** * 设置 → pi-ai createProvider。apiKey 只在请求时经 auth.resolve 交给 SDK,不读环境变量。 * Anthropic baseUrl 不带 /v1;OpenAI 兼容 baseUrl 带 /v1(与 spike 抓包结论一致)。 */ export function resolveModelFromSettings(settings: ResolvedAgentSettings): ResolvedPiModel { if (settings.provider !== "anthropic" && settings.provider !== "openai-compatible") throw new Error(`provider ${settings.provider} 需要按模型服务装配(PiAgentRuntimeOptions.upstream)`); if (!settings.apiKey) throw new Error("未配置模型服务密钥"); const providerId = PROVIDER_ID[settings.provider]; const common = { id: settings.model, name: settings.model, provider: providerId, reasoning: settings.thinkingLevel !== "off", input: ["text"] as ("text" | "image")[], cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, contextWindow: settings.contextBudgetTokens, maxTokens: Math.min(settings.maxOutputTokens, Math.floor(settings.contextBudgetTokens / 2)), }; const apiKey = settings.apiKey; const auth = { apiKey: { name: "umt-secret", resolve: async () => ({ auth: { apiKey } }) } }; const models = createModels(); let model: Model; if (settings.provider === "anthropic") { const m: Model<"anthropic-messages"> = { ...common, api: "anthropic-messages", baseUrl: (settings.baseUrl ?? ANTHROPIC_DEFAULT_BASE).replace(/\/+$/, "").replace(/\/v1$/, ""), }; models.setProvider(createProvider({ id: providerId, auth, models: [m], api: anthropicMessagesApi() })); model = m; } else { if (!settings.baseUrl) throw new Error("openai-compatible 模型服务必须配置 baseUrl(带 /v1)"); const m: Model<"openai-completions"> = { ...common, api: "openai-completions", baseUrl: settings.baseUrl.replace(/\/+$/, ""), compat: { supportsDeveloperRole: false }, }; models.setProvider(createProvider({ id: providerId, auth, models: [m], api: openAICompletionsApi() })); model = m; } return { models, model }; } /** * 压缩设置:threshold 模式下占用达到 threshold 比例时阻塞压缩;manual 只允许手动 compact()。 * backgroundTokens=0:不做 pi-durable 的后台提前压缩,行为与 pi-agent-core 0.99 的阈值压缩一致。 */ export function compactionFromSettings(settings: ResolvedAgentSettings, contextWindow: number): CompactionPolicy { const reserveTokens = Math.max(256, Math.round(contextWindow * (1 - settings.compaction.threshold))); return { enabled: settings.compaction.mode === "threshold", reserveTokens, keepRecentTokens: Math.max(256, Math.round(contextWindow * 0.25)), backgroundTokens: 0, }; }