Files
UHD_Media_Tool/packages/agent/src/pi/model.ts
T
rootandClaude Opus 5.5 743fe2486d feat(agent): per-upstream pi Models with isolated credential stores
- PiUpstreamClient: one Models per upstream holding only its provider; credential
  store adapter maps to that upstream's secret, so several accounts of one provider
  coexist; host env is only read for authKind none (ambient cloud creds)
- provider catalog from builtinProviders() plus static oauth modes, env fields and
  subscription terms notes; openai-compatible custom endpoints
- fake upstream provider with mock OAuth (browser paste / device approval)
- PiAgentRuntime accepts an upstream; thinking level clamped to the model

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-09-28 02:23:09 +08:00

74 lines
3.5 KiB
TypeScript

import type { CompactionSettings } from "@earendil-works/pi-agent-core";
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 { ResolvedAgentSettings } from "../settings.ts";
/** Pi 侧需要的模型装配结果(仅在 src/pi/* 内部流转) */
export interface ResolvedPiModel {
models: MutableModels;
model: Model<Api>;
/** 夹到模型支持范围后的思考档位;缺省用设置值 */
thinkingLevel?: ResolvedAgentSettings["thinkingLevel"];
}
export type PiModelResolver = (settings: ResolvedAgentSettings) => ResolvedPiModel | Promise<ResolvedPiModel>;
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<Api>;
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() */
export function compactionFromSettings(settings: ResolvedAgentSettings, contextWindow: number): CompactionSettings {
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)),
};
}