import { z } from "zod"; import { Id, Timestamp } from "./common.ts"; // Pi 模型上游(W7-A):一个上游 = 一个 pi-ai provider 的一份凭据(API Key / 订阅登录 token / 环境凭据)。 // 同一 provider 可以建多个上游(例如两个 Codex 账号);Agent 运行按上游 id 取凭据。 // 研究结论与映射见 docs/design/pi-providers.md;provider 列表锁定 pi-ai 0.99.2 的 builtinProviders()。 /** pi-ai 0.99.2 内置的 42 个 provider id(含 TypeSafe 分类服务) */ export const PI_BUILTIN_PROVIDER_IDS = [ "amazon-bedrock", "ant-ling", "anthropic", "azure-openai-responses", "baseten", "cerebras", "cloudflare-ai-gateway", "cloudflare-workers-ai", "deepseek", "fireworks", "github-copilot", "google", "google-vertex", "groq", "huggingface", "kimi-coding", "meta", "minimax", "minimax-cn", "mistral", "moonshotai", "moonshotai-cn", "nvidia", "openai", "openai-codex", "opencode", "opencode-go", "openrouter", "qwen-token-plan", "qwen-token-plan-cn", "qwen-token-plan-individual", "radius", "together", "typesafe", "vercel-ai-gateway", "xai", "xiaomi", "xiaomi-token-plan-ams", "xiaomi-token-plan-cn", "xiaomi-token-plan-sgp", "zai", "zai-coding-cn", ] as const; /** * 上游可选的 provider:42 个内置 + openai-compatible(自定义 OpenAI 兼容端点,必须填 baseUrl 与模型) * + fake(脚本化假模型,不联网,演示与测试用)。 */ export const PiProviderId = z.enum([...PI_BUILTIN_PROVIDER_IDS, "openai-compatible", "fake"]); export type PiProviderId = z.infer; /** api_key:存一把 key(可附带 env);oauth:订阅登录 token 包;none:不存密钥(fake、无鉴权本地端点、云环境凭据链) */ export const PiAuthKind = z.enum(["api_key", "oauth", "none"]); export type PiAuthKind = z.infer; /** * 上游状态(服务端维护,只读): * ok = 凭据就绪;auth_required = 需要填 key 或登录;expired = OAuth 刷新失败,需要重新登录;error = 最近一次测试/调用失败 */ export const PiUpstreamStatus = z.enum(["ok", "auth_required", "expired", "error"]); export type PiUpstreamStatus = z.infer; /** pi 的思考档位(比 settings.ThinkingLevel 多一个 max) */ export const PiThinkingLevel = z.enum(["off", "minimal", "low", "medium", "high", "xhigh", "max"]); export type PiThinkingLevel = z.infer; /** 订阅登录方式:browser = 打开授权链接后把地址栏 URL 粘回来;device = 在任意设备输入验证码,服务端轮询 */ export const PiOAuthMode = z.enum(["browser", "device"]); export type PiOAuthMode = z.infer; /** 自定义端点可选的 API 形状 */ export const PiCustomApi = z.enum([ "openai-completions", "openai-responses", "anthropic-messages", "google-generative-ai", ]); export type PiCustomApi = z.infer; /** provider 读取的配置名(AZURE_OPENAI_BASE_URL 这类大写名;其它名字 provider 不会读) */ const EnvName = z.string().min(1).max(64); /** * provider 专属的非密字段: * env = provider 需要的配置值(Cloudflare ACCOUNT_ID/GATEWAY_ID、Vertex project/location、Azure resource 等,见能力表 envFields); * api/compat/headers 只对 openai-compatible 生效。 */ export const PiUpstreamExtra = z .object({ env: z.record(EnvName, z.string().max(2048)).optional(), api: PiCustomApi.optional(), compat: z.record(z.string(), z.unknown()).optional(), headers: z.record(z.string().min(1).max(128), z.string().max(2048)).optional(), /** 自定义模型的上下文窗口(openai-compatible) */ contextWindow: z.number().int().min(1000).max(10_000_000).optional(), note: z.string().max(500).optional(), }) .strict(); export type PiUpstreamExtra = z.infer; // ── 能力表(GET /pi/providers) ── export const PiModelInfo = z.object({ id: z.string(), name: z.string(), api: z.string(), reasoning: z.boolean(), /** getSupportedThinkingLevels(model):该模型能用的思考档位 */ thinkingLevels: z.array(PiThinkingLevel), contextWindow: z.number().int().nonnegative(), maxTokens: z.number().int().nonnegative(), input: z.array(z.enum(["text", "image"])), /** static = 随包目录;dynamic = 上游拉取(仅 Radius) */ source: z.enum(["static", "dynamic", "custom"]), }); export type PiModelInfo = z.infer; export const PiEnvField = z.object({ name: EnvName, label: z.string(), required: z.boolean(), hint: z.string().nullable(), }); export type PiEnvField = z.infer; export const PiProviderGroup = z.enum(["subscription", "api_key", "cloud", "custom", "test"]); export type PiProviderGroup = z.infer; export const PiProviderInfo = z.object({ id: PiProviderId, name: z.string(), /** UI 分组:订阅登录 / API Key / 云凭据 / 自定义兼容端点 / 测试 */ group: PiProviderGroup, /** 可选的认证方式;第一个是推荐项 */ authKinds: z.array(PiAuthKind).min(1), /** 订阅登录;null = 不支持 */ oauth: z .object({ name: z.string(), /** 是否借用个人订阅(Claude Pro/Max、ChatGPT Plus/Pro 等) */ isSubscription: z.boolean(), /** 可用的登录方式,第一个是推荐项(有 device 的优先 device) */ modes: z.array(PiOAuthMode).min(1), /** 条款风险提示(一句事实);非订阅型为 null */ termsNote: z.string().nullable(), }) .nullable(), /** 订阅登录 provider 的条款风险提示(同 oauth.termsNote,便于列表直接展示) */ termsNote: z.string().nullable(), /** 是否允许覆盖 baseUrl(OAuth 型与按 token 计算域名的不允许) */ baseUrlOverridable: z.boolean(), defaultBaseUrl: z.string().nullable(), /** 需要额外填写的 env 字段 */ envFields: z.array(PiEnvField), apis: z.array(z.string()), /** 任一模型支持思考 */ supportsThinking: z.boolean(), /** 模型表可动态刷新(Radius) */ dynamicModels: z.boolean(), modelCount: z.number().int().nonnegative(), /** 静态模型表;GET /pi/providers?models=false 时为空数组 */ models: z.array(PiModelInfo), note: z.string().nullable(), }); export type PiProviderInfo = z.infer; export const PiProvidersQuery = z.object({ /** false = 不带静态模型表(只要能力与计数) */ models: z.enum(["true", "false"]).optional(), }); export const PiProvidersResponse = z.object({ /** pi-ai 版本与随包模型目录生成时间 */ piVersion: z.string(), catalogGeneratedAt: Timestamp.nullable(), providers: z.array(PiProviderInfo), }); export type PiProvidersResponse = z.infer; // ── 上游 ── /** pi_upstream 表行(secret_ref 不出 API) */ export const PiUpstreamRow = z.object({ id: Id, name: z.string().min(1).max(64), provider: PiProviderId, authKind: PiAuthKind, baseUrl: z.url().nullable(), defaultModel: z.string().min(1).max(200).nullable(), thinkingLevel: PiThinkingLevel, extra: PiUpstreamExtra, enabled: z.boolean(), status: PiUpstreamStatus, lastError: z.string().nullable(), lastVerifiedAt: Timestamp.nullable(), /** OAuth access token 过期时间(刷新任务据此挑选;永久 key 为 null) */ oauthExpiresAt: Timestamp.nullable(), createdAt: Timestamp, updatedAt: Timestamp, }); export type PiUpstreamRow = z.infer; /** API 视图:不含任何密钥,只给是否已配置与掩码 */ export const PiUpstream = PiUpstreamRow.extend({ /** 已存 key / 已登录 */ hasCredential: z.boolean(), /** API Key 掩码(前 4 + 后 4);OAuth / none 为 null */ credentialMask: z.string().nullable(), /** OAuth 账号信息(Codex accountId、Copilot 域名等,非密);无则 null */ account: z.string().nullable(), /** 是否为 settings.pi.defaultUpstreamId */ isDefault: z.boolean(), }); export type PiUpstream = z.infer; export const PiUpstreamList = z.object({ items: z.array(PiUpstream), defaultUpstreamId: Id.nullable(), }); export type PiUpstreamList = z.infer; /** 只写;空串 = 不设置 / 不修改 */ const ApiKeyInput = z.string().max(8192); /** 新建上游。apiKey 只写(存 secret 表);oauth 型建好后走 /oauth/start 登录 */ export const PiUpstreamCreate = z .object({ name: z.string().trim().min(1).max(64), provider: PiProviderId, authKind: PiAuthKind, apiKey: ApiKeyInput.optional(), baseUrl: z.url().nullable().optional(), defaultModel: z.string().trim().min(1).max(200).nullable().optional(), thinkingLevel: PiThinkingLevel.optional(), extra: PiUpstreamExtra.optional(), enabled: z.boolean().optional(), /** 同时设为默认上游(settings.pi.defaultUpstreamId);没有默认上游时自动设为默认 */ makeDefault: z.boolean().optional(), }) .superRefine((v, ctx) => { if (v.authKind !== "api_key" && v.apiKey?.trim()) ctx.addIssue({ code: "custom", path: ["apiKey"], message: "只有 api_key 型模型服务可以填 apiKey" }); if (v.authKind === "oauth" && v.baseUrl) ctx.addIssue({ code: "custom", path: ["baseUrl"], message: "订阅登录型模型服务不能覆盖 baseUrl" }); if (v.provider === "openai-compatible") { if (!v.baseUrl) ctx.addIssue({ code: "custom", path: ["baseUrl"], message: "自定义兼容端点必须填 baseUrl" }); if (!v.defaultModel) ctx.addIssue({ code: "custom", path: ["defaultModel"], message: "自定义兼容端点必须填模型 id" }); } }); export type PiUpstreamCreate = z.infer; /** 修改上游:provider 不可改;apiKey 传 null 清除;authKind 变化时旧凭据一并删除 */ export const PiUpstreamUpdate = z.object({ name: z.string().trim().min(1).max(64).optional(), authKind: PiAuthKind.optional(), apiKey: ApiKeyInput.nullable().optional(), baseUrl: z.url().nullable().optional(), defaultModel: z.string().trim().min(1).max(200).nullable().optional(), thinkingLevel: PiThinkingLevel.optional(), extra: PiUpstreamExtra.optional(), enabled: z.boolean().optional(), /** true = 设为默认上游;false = 若它是默认则取消默认 */ makeDefault: z.boolean().optional(), }); export type PiUpstreamUpdate = z.infer; // ── 测试 / 模型 ── export const PiUpstreamTestRequest = z.object({ /** * auth(默认)= 只解析凭据(OAuth 必要时刷新)+ 校验模型在目录中,不产生费用; * completion = 额外发一次最小补全(1 个输出 token,会产生极少费用) */ mode: z.enum(["auth", "completion"]).optional(), model: z.string().min(1).max(200).optional(), }); export type PiUpstreamTestRequest = z.infer; export const PiTestErrorKind = z.enum(["auth", "oauth", "model", "network", "config", "other"]); export type PiTestErrorKind = z.infer; export const PiUpstreamTestResult = z.object({ ok: z.boolean(), mode: z.enum(["auth", "completion"]), latencyMs: z.number().int().nonnegative(), /** 凭据来源说明(stored credential / OAuth / AWS_PROFILE …) */ source: z.string().nullable(), model: z.string().nullable(), errorKind: PiTestErrorKind.nullable(), message: z.string().nullable(), upstream: PiUpstream, }); export type PiUpstreamTestResult = z.infer; /** 模型列表来源:remote = 从上游的模型列表接口拉取(已存库);static = 随包静态目录(没拉过、拉取失败或 provider 没有列表接口) */ export const PiModelListSource = z.enum(["remote", "static"]); export type PiModelListSource = z.infer; /** 列表里的一个模型(字段按上游接口能给的归一化;拿不到为 null) */ export const PiListedModel = z.object({ id: z.string().min(1).max(300), displayName: z.string().max(300).nullable(), contextWindow: z.number().int().nonnegative().nullable(), /** 支持图片输入 */ supportsVision: z.boolean().nullable(), /** 支持工具调用 */ supportsTools: z.boolean().nullable(), /** 价格(美元 / 百万 token) */ pricing: z.object({ input: z.number().nullable(), output: z.number().nullable() }).nullable(), /** 这一条来自哪里:remote = 上游列表;static = 只在静态目录里 */ origin: PiModelListSource, }); export type PiListedModel = z.infer; /** 获取失败的分类:unsupported = provider 没有模型列表接口 */ export const PiModelFetchErrorKind = z.enum(["auth", "network", "unsupported", "config", "other"]); export type PiModelFetchErrorKind = z.infer; /** * GET /pi/upstreams/{id}/models、POST …/models/refresh、POST /pi/models/probe 的响应。 * models = 远程列表与静态目录合并去重(同 id 远程优先,远程缺的字段用静态目录补),远程在前。 */ export const PiUpstreamModels = z.object({ source: PiModelListSource, /** 远程列表拉取时间;static 为 null */ fetchedAt: Timestamp.nullable(), models: z.array(PiListedModel), /** 该 provider 有模型列表接口(没有时「获取模型列表」直接回静态表) */ remoteSupported: z.boolean(), /** 本次获取失败的原因(一句话,已脱敏);成功或只是读取时为 null */ error: z.string().nullable(), errorKind: PiModelFetchErrorKind.nullable(), }); export type PiUpstreamModels = z.infer; /** 新建上游前试拉模型列表(不落库;凭证只用于这一次请求) */ export const PiModelsProbeRequest = z.object({ provider: PiProviderId, authKind: PiAuthKind, apiKey: ApiKeyInput.optional(), baseUrl: z.url().nullable().optional(), extra: PiUpstreamExtra.optional(), }); export type PiModelsProbeRequest = z.infer; // ── 订阅登录状态机 ── export const PiOAuthStartRequest = z.object({ /** 缺省取能力表 oauth.modes[0] */ mode: PiOAuthMode.optional(), /** GitHub Copilot:GitHub Enterprise 域名(空 = github.com) */ enterpriseDomain: z.string().max(253).optional(), }); export type PiOAuthStartRequest = z.infer; /** * 登录会话状态: * pending = 等用户去授权(browser 等粘贴,device 服务端轮询中);succeeded = 已写入凭据; * failed = 流程报错(message 可读);expired = 会话超时;cancelled = 被新会话/登出取消 */ export const PiOAuthState = z.enum(["pending", "succeeded", "failed", "expired", "cancelled"]); export type PiOAuthState = z.infer; export const PiOAuthStartResult = z.object({ sessionId: z.string(), mode: PiOAuthMode, state: PiOAuthState, /** browser:在自己的浏览器打开;授权后跳到 localhost 打不开是预期的,复制地址栏完整 URL 粘回 */ authorizeUrl: z.string().nullable(), /** device:在 verificationUrl 输入 userCode */ userCode: z.string().nullable(), verificationUrl: z.string().nullable(), /** 给用户看的提示(provider 原话) */ instructions: z.string().nullable(), expiresAt: Timestamp, message: z.string().nullable(), }); export type PiOAuthStartResult = z.infer; export const PiOAuthCompleteRequest = z.object({ sessionId: z.string().min(1).max(64), /** browser:地址栏完整 URL(或 code#state / 裸 code);device 不填 */ pastedUrlOrCode: z.string().trim().min(1).max(8192).optional(), /** 本次请求最多等待多久(毫秒,默认 25s,上限 60s);超时返回 state=pending,前端再调一次 */ waitMs: z.number().int().min(0).max(60_000).optional(), }); export type PiOAuthCompleteRequest = z.infer; export const PiOAuthCompleteResult = z.object({ sessionId: z.string(), state: PiOAuthState, message: z.string().nullable(), /** 最近的进度提示(Exchanging authorization code… 等) */ progress: z.array(z.string()), upstream: PiUpstream, }); export type PiOAuthCompleteResult = z.infer;