前言
学习自己用的agent的源码,可以让我更好的使用它,那么就学习一下pi吧,这里直接看代码了,就不参考什么文章或者是在线文档了
Pi的提示词
首先我们要定位到初始提示词位置/packages/coding-agent/src/core/system-prompt.ts
/** Build the ordered, independently replaceable sections of the structured system prompt. */export function buildSystemPromptSections(input: BuildSystemPromptOptions): SystemPromptSections { const options = normalizeBuildSystemPromptOptions(input); const { customPrompt, selectedTools, hiddenTools, toolSnippets, toolGuidelines, promptGuidelines, appendSystemPrompt, sections: customSections, cwd, contextFiles, skills, } = options;
for (const name of Object.keys(customSections)) { if (!SYSTEM_PROMPT_SECTION_NAME.test(name) || name === "preamble") { throw new Error(`Invalid system prompt section name: ${name}`); } }
const declaredTools = selectedTools.filter((name) => !hiddenTools.includes(name)); const promptSections: Record<string, string> = {}; if (customPrompt) { promptSections.preamble = customPrompt; } else { promptSections.preamble = "You are an expert coding assistant operating inside pi, a coding agent harness. You help users by reading files, executing commands, editing code, and writing new files."; const visibleTools = declaredTools.filter((name) => !!toolSnippets[name]); const tools = visibleTools.length > 0 ? visibleTools.map((name) => `- ${name}: ${toolSnippets[name]}`).join("\n") : "(none)"; promptSections.tools = `${tools}\n\nIn addition to the tools above, you may have access to other custom tools depending on the project.`; promptSections.rules = buildRules(declaredTools, toolGuidelines, promptGuidelines); promptSections.docs = `Pi documentation (read only when the user asks about pi itself, its SDK, extensions, themes, skills, or TUI):- Main documentation: ${getReadmePath()}- Additional docs: ${getDocsPath()}- Examples: ${getExamplesPath()} (extensions, custom tools, SDK)- When reading pi docs or examples, resolve docs/... under Additional docs and examples/... under Examples, not the current working directory- When asked about: extensions (docs/extensions.md, examples/extensions/), themes (docs/themes.md), skills (docs/skills.md), prompt templates (docs/prompt-templates.md), TUI components (docs/tui.md), keybindings (docs/keybindings.md), SDK integrations (docs/sdk.md), custom providers (docs/custom-provider.md), adding models (docs/models.md), pi packages (docs/packages.md), environment variables (docs/environment-variables.md), MCP servers (docs/mcp.md), codemode scripts and non-LLM models such as classifiers and image models (docs/codemode.md)- When working on pi topics, read the docs and examples, and follow .md cross-references before implementing- Always read pi .md files completely and follow links to related docs (e.g., tui.md for TUI API details)`; }
if (appendSystemPrompt) promptSections.addendum = appendSystemPrompt; if (contextFiles.length > 0) promptSections.project_context = renderProjectContext(contextFiles); // A hidden reader is still reachable through another tool, so skills stay but the hint names no tool. const readers = ["read", "bash"] as const; const skillFileReadTool = readers.find((tool) => declaredTools.includes(tool)) ?? (readers.some((tool) => selectedTools.includes(tool)) ? "indirect" : undefined); if (skillFileReadTool && skills.length > 0) { const skillsPrompt = formatSkillsForPrompt(skills, skillFileReadTool).trim(); if (skillsPrompt) promptSections.skills = skillsPrompt; } promptSections.cwd = cwd.replace(/\\/g, "/"); for (const [name, content] of Object.entries(customSections)) { if (content) promptSections[name] = content; }
const sections: SystemPromptSections = { preamble: promptSections.preamble }; for (const [name, content] of Object.entries(promptSections)) { if (name !== "preamble") sections[name] = `<${name}>\n${content}\n</${name}>`; } return sections;}一眼丁真看到
“You are an expert coding assistant operating inside pi, a coding agent harness. You help users by reading files, executing commands, editing code, and writing new files.”
经典设置场景,这里的提示词是动态拼接的,看看工具
const declaredTools = selectedTools.filter((name) => !hiddenTools.includes(name));
这里是直接把隐藏的工具给排除了,初始提示词不会有这些,注意
In addition to the tools above, you may have access to other custom tools depending on the project.
这里说注意场景不同可以调用其他的工具去帮助用户,其实是pi的工具调用机制和hook在发力
promptSections.rules = buildRules(declaredTools, toolGuidelines, promptGuidelines);
把已声明工具、工具使用指南、提示词指南组合成规则段落,这里就包括了一些插件定义的工具,比如ffgrep/fffind等
然后是docs部分,其实这里就是为什么很多人说pi可以自己改pi,因为他把自己的源码位置给agent让他自己知道了(),并不是天生的
然后下面是AGENT.md的内容,注意排位是~/.pi/agent/AGENTS.md > ~/<PWD>/.pi/AGENTS.md,里面写的东西全部都会被加载进去
再下面是你的skills加载内容,举例
<skill> <name>bun-dev</name> <description>This skill should be used when working with Bun runtime, bun:sqlite, Bun.serve, bun:test, or when "Bun", "bun:test", or Bun-specific patterns are mentioned.</description> <location>/Users/zsm/.pi/agent/skills/bun-dev/SKILL.md</location> </skill>这段描述如果自己写过skill的话应该很熟悉,就是SKILL.md里面写的
---name: bun-devdescription: This skill should be used when working with Bun runtime, bun:sqlite, Bun.serve, bun:test, or when "Bun", "bun:test", or Bun-specific patterns are mentioned.metadata: version: "1.0.0"---这样也就是为什么skill需要渐进式批露的原因之一,当然了因为pi里面有插件系统,一些插件的描述也会加进去,在下面就是mcp了
如果你在mcp.json里面配了简要描述,也会写进去
- mcp__obs (codemode): agentic-obs:本地录屏
这就是一个完整的初始提示词结构了,如果你什么都没有加,提示词应该长这样
You are an expert coding assistant operating inside pi, a coding agent harness. You help users by reading files, executing commands, editing code, and writing new files.
<tools>- read: Read file contents- bash: Execute bash commands (ls, grep, find, etc.)- edit: Make precise file edits with exact text replacement, including multiple disjoint edits in one call- write: Create or overwrite files
In addition to the tools above, you may have access to other custom tools depending on the project.</tools>
<rules>- Use bash for file operations like ls, rg, find- Use read to examine files instead of cat or sed.- You can inspect PI_* environment variables for current model and session details.- Use edit for precise changes (edits[].oldText must match exactly)- When changing multiple separate locations in one file, use one edit call with multiple entries in edits[] instead of multiple edit calls- Each edits[].oldText is matched against the original file, not after earlier edits are applied. Do not emit overlapping or nested edits. Merge nearby changes into one edit.- Keep edits[].oldText as small as possible while still being unique in the file. Do not pad with large unchanged regions.- Use write only for new files or complete rewrites.- Be concise in your responses- Show file paths clearly when working with files</rules>
<docs>Pi documentation (read only when the user asks about pi itself, its SDK, extensions, themes, skills, or TUI):- Main documentation:- Additional docs:- Examples: /pi/packages/coding-agent/examples (extensions, custom tools, SDK)- When reading pi docs or examples, resolve docs/... under Additional docs and examples/... under Examples, not the current working directory- When asked about: extensions (docs/extensions.md, examples/extensions/), themes (docs/themes.md), skills (docs/skills.md), prompt templates (docs/prompt-templates.md), TUI components (docs/tui.md), keybindings (docs/keybindings.md), SDK integrations (docs/sdk.md), custom providers (docs/custom-provider.md), adding models (docs/models.md), pi packages (docs/packages.md), environment variables (docs/environment-variables.md), MCP servers (docs/mcp.md), codemode scripts and non-LLM models such as classifiers and image models (docs/codemode.md)- When working on pi topics, read the docs and examples, and follow .md cross-references before implementing- Always read pi .md files completely and follow links to related docs (e.g., tui.md for TUI API details)</docs>
<cwd>
</cwd>那我们输入的提示词会被怎么拼接嘞
system└── preamble / tools / rules / docs / project_context / skills / cwd / 扩展段user:帮我看看目录下原生pi的默认提示词在那个文件夹assistant:思考 + toolCall(read/bash)toolResult:文件内容user:对目录下代码进行审计...假设skill变了,他的system提示词不是整段重新发送,而是做renderSystemMessageUpdate
/** * Render a later system message for APIs that accept system messages mid-conversation. * Section changes are framed by name so the model can relate them to the leading prompt. * This framing is request-time only and may change between versions. */export function renderSystemMessageUpdate(message: SystemMessage): string { const parts: string[] = []; const text = contentText(message.content); if (text.length > 0) parts.push(text); for (const [name, value] of Object.entries(message.sections ?? {})) { parts.push( value === null ? `Removed system prompt section "${name}".` : `Updated system prompt section "${name}":\n\n${value}`, ); } return parts.join("\n\n");}Pi工具
前面说到了工具的事情,那就看看他的工具是怎么定义的吧,顺着hiddenTools我们找到agent-session.ts文件,里面对隐藏的tools做了定义和处理,而在packages/coding-agent/src/core/extensions/types.ts中做了契约定义,而具体的判断在ackages/coding-agent/src/extensions/codemode/tool.ts
从头来看,在packages/coding-agent/src/core/tools/tool-definition-wrapper.ts中写明了tools的定义
/** Wrap a ToolDefinition into an AgentTool for the core runtime. */export function wrapToolDefinition<TDetails = unknown>( definition: ToolDefinition<any, TDetails>, ctxFactory?: ToolContextFactory,): AgentTool<any, TDetails> { return { name: definition.name, label: definition.label, description: definition.description, parameters: definition.parameters, outputSchema: definition.outputSchema, constrainedSampling: definition.constrainedSampling, prepareArguments: definition.prepareArguments, executionMode: definition.executionMode, execute: (toolCallId, params, signal, onUpdate, ctx?: ExtensionToolContext) => definition.execute( toolCallId, params, signal, onUpdate, ctx ?? (ctxFactory?.(toolCallId, signal) as ExtensionToolContext), ), };}
export function createToolDefinitionFromAgentTool(tool: AgentTool<any>): ToolDefinition<any, unknown> { return { name: tool.name, label: tool.label, description: tool.description, parameters: tool.parameters as any, outputSchema: tool.outputSchema, constrainedSampling: tool.constrainedSampling, prepareArguments: tool.prepareArguments, executionMode: tool.executionMode, execute: async (toolCallId, params, signal, onUpdate) => tool.execute(toolCallId, params, signal, onUpdate), };}定义了作者、AgentTool以及将什么东西发送过去,用适配器整个包起来,让SDK传入的裸AgentTool也能进同一个注册器
再看agent-session.ts文件中定义的激活和注册在_refreshToolRegistry函数,代码太长就不贴了
- 合并三个来源:内置 _baseToolDefinitions、扩展registerTool的、SDK _customTools。
- 按 _isAllowedTool(name) 过滤(—tools /—exclude-tools,agent-session.ts:1534;MCP 工具默认不被 allowlist 误杀)。
- 建两个平行 map:_toolDefinitions(定义 +sourceInfo,供宿主查询)和 _toolRegistry(包好的AgentTool,供执行)。两者必须同步。
- 决定 activeToolNames:默认激活 + defaultActive !==false + allowlist 命中 + _pendingToolNames。
这就是大概的流程,里面的exposure与hiddenDeclarations也不太一样,这个看下面的部分
那么我们要知道的是,tools如何发给模型的呢?pi中是system message的增量存在transcript里,可以在transcript.ts中看到
export function createInitialSystemMessage( systemPrompt: string | undefined, tools: Tool[] | undefined,): SystemMessage | undefined { const hasSystemPrompt = systemPrompt !== undefined && systemPrompt.length > 0; const hasTools = tools !== undefined && tools.length > 0; if (!hasSystemPrompt && !hasTools) return undefined; return { role: "system", content: systemPrompt ?? "", ...(hasTools ? { toolsAdded: tools } : {}), timestamp: 0, };}初始工具被写入到toolsAdded,再看agent-loop.ts里面写的
function declareToolChanges(context: AgentContext, pendingMessages: AgentMessage[]): AgentMessage[] { let systemIndex = -1; for (let i = pendingMessages.length - 1; i >= 0; i--) { if (pendingMessages[i].role === "system") { systemIndex = i; break; } } const pending = pendingMessages[systemIndex] as SystemMessage | undefined; const baseline = pending ? pendingMessages.map((message, index) => index === systemIndex ? withToolChanges(pending, NO_CHANGES) : message, ) : pendingMessages; const changes = getToolStateChanges( getCurrentTools([...context.messages, ...baseline]), (context.tools ?? []).map(toToolDeclaration), ); const unchanged = changes.toolsAdded.length === 0 && changes.toolsRemoved.length === 0;
if (pending) { // Keep the caller's message object when it already declares no tool changes. if (unchanged && !pending.toolsAdded?.length && !pending.toolsRemoved?.length) return pendingMessages; return baseline.map((message, index) => (index === systemIndex ? withToolChanges(pending, changes) : message)); } if (unchanged) return pendingMessages; const update = withToolChanges({ role: "system", content: "", timestamp: Date.now() }, changes); const insertIndex = pendingMessages.findIndex((message) => message.role !== "system"); const index = insertIndex === -1 ? pendingMessages.length : insertIndex; return [...pendingMessages.slice(0, index), update, ...pendingMessages.slice(index)];}在每次请求前,把context.tools与transcript已声明的差量写成新的system message的toolsAdded/toolsRemoved
不过好玩的是,anthropic-messages.ts中为了兼容claudecode模型,还做了映射
function convertTools( tools: Tool[], isOAuthToken: boolean, supportsEagerToolInputStreaming: boolean, supportsStrictTools: boolean, cacheControl?: CacheControlEphemeral,): BetaTool[] { if (!tools) return [];
return tools.map((tool, index) => { const strict = resolveJsonSchemaStrictSampling(tool, supportsStrictTools, isAnthropicStrictUnsupportedKeyword); const parameters = getJsonSchemaToolParameters(tool, strict); const schema = parameters as { properties?: unknown; required?: string[] }; const legacyInputSchema = { type: "object" as const, properties: schema.properties ?? {}, required: schema.required ?? [], }; const inputSchema = strict === true ? { ...(parameters as Record<string, unknown>), ...legacyInputSchema, } : legacyInputSchema;
return { name: isOAuthToken ? toClaudeCodeName(tool.name) : tool.name, description: tool.description, ...(supportsEagerToolInputStreaming ? { eager_input_streaming: true } : {}), ...(strict === true ? { strict: true } : {}), input_schema: inputSchema, ...(cacheControl && index === tools.length - 1 ? { cache_control: cacheControl } : {}), }; });}会话中途如果加减工具也是当作增量去写入,和上面的提示词一样的处理。工具调用怎么被解析出来呢
以A/为例
- content_block_start 且 type === “tool_use” → 建toolCall block,arguments先给{},另开partialJson: "" 暂存。
- content_block_delta 且 type === “input_json_delta” → partialJson += delta,立刻 parseStreamingJson()做增量容错解析,发 toolcall_delta。
- content_block_stop → 再解析一次定稿,删掉partialJson 临时字段,发 toolcall_end。
那么核心调用肯定是loop里面的过程了,先看看保护机制,假设你工具调用的时候token用的要压缩了,会怎么样呢
if (toolCalls.length > 0) { // A "length" stop means the output was cut off by the token limit, so // every tool call in the message may carry truncated arguments. Fail // them all instead of executing potentially borked calls. const executedToolBatch = message.stopReason === "length" ? await failToolCallsFromTruncatedMessage(toolCalls, emit) : await executeToolCalls(currentContext, message, config, signal, emit); toolResults.push(...executedToolBatch.messages); hasMoreToolCalls = !executedToolBatch.terminate;
for (const result of toolResults) { currentContext.messages.push(result); newMessages.push(result); } }这里会直接停掉,然后错误结果让模型重发。因为流式解析会让半截json也”验证通过”,执行它等于执行错的参数。
看看调用一次工具的流程prepareToolCall函数
const tool = tools.find((t) => t.name === toolCall.name);
首先在找这个tools在不在
function prepareToolCallArguments(tool: AgentTool<any>, toolCall: AgentToolCall): AgentToolCall { if (!tool.prepareArguments) { return toolCall; } const preparedArguments = tool.prepareArguments(toolCall.arguments); if (preparedArguments === toolCall.arguments) { return toolCall; } return { ...toolCall, arguments: preparedArguments as Record<string, any>, };}对模型的不规范输出做处理,再看看validateToolArguments
export function validateToolArguments(tool: Tool, toolCall: ToolCall): any { const args = structuredClone(toolCall.arguments); normalizeOptionalNulls(args, tool.parameters as JsonSchemaObject); Value.Convert(tool.parameters, args);
const validator = getValidator(tool.parameters); if (!Object.getOwnPropertySymbols(tool.parameters).includes(TYPEBOX_KIND)) { const coerced = coerceWithJsonSchema(args, tool.parameters as JsonSchemaObject); if (coerced !== args) { if (typeof args === "object" && args !== null && typeof coerced === "object" && coerced !== null) { for (const key of Object.keys(args)) { delete args[key]; } Object.assign(args, coerced); } else { return validator.Check(coerced) ? coerced : args; } } }
if (validator.Check(args)) { return args; }
const errors = validator .Errors(args) .map((error) => ` - ${formatValidationPath(error)}: ${error.message}`) .join("\n") || "Unknown validation error";
const errorMessage = `Validation failed for tool "${toolCall.name}":\n${errors}\n\nReceived arguments:\n${JSON.stringify(toolCall.arguments, null, 2)}`;
throw new Error(errorMessage);}做了转换处理,再看看executePreparedToolCall
async function executePreparedToolCall( prepared: PreparedToolCall, signal: AbortSignal | undefined, onUpdate: ToolUpdateSink,): Promise<ExecutedToolCallOutcome> { const updateEvents: Promise<void>[] = []; let acceptingUpdates = true; const startedAt = performance.now(); const elapsed = () => Math.round(performance.now() - startedAt);
try { const result = await prepared.tool.execute( prepared.toolCall.id, prepared.args as never, signal, (partialResult) => { if (!acceptingUpdates) return; updateEvents.push(Promise.resolve(onUpdate(partialResult))); }, ); const durationMs = elapsed(); acceptingUpdates = false; await Promise.all(updateEvents); return { result, isError: result.isError === true, durationMs }; } catch (error) { const durationMs = elapsed(); acceptingUpdates = false; await Promise.all(updateEvents); return { result: createErrorToolResult(error instanceof Error ? error.message : String(error)), isError: true, durationMs, }; } finally { acceptingUpdates = false; }}跑出的东西是边跑边推的,为了保证返回前关闭就加了个acceptingUpdates,避免工具返回后回调还在飞
function shouldTerminateToolBatch(finalizedCalls: FinalizedToolCallOutcome[]): boolean { return finalizedCalls.length > 0 && finalizedCalls.every((finalized) => finalized.result.terminate === true);}这里规定了全部结束才停,因为一些工具调用的时候会并发,比如你去curl一个网站的时候
再看codemode,这个是比较重量级的,你知道的,他是在调用其他的工具去执行的,不过这个走的是nested-tool-calls.ts中的NestedToolCallRunner这个链路有点小复杂
链路:ToolDefinition.execute(…, ctx) → ctx.executeTool()(runner.ts 用Object.defineProperties 挂上去,保证 getter 惰性、能做 stale-instance 校验)→AgentSession._executeNestedToolCall→ 复用 runToolCall()。
tool_search / codemode → 改变"能看到什么" exposure + hiddenDeclarations → 决定"声明什么" transcript system messages → 记录"声明过什么"(可回放) agent-loop executeToolCalls → 决定"怎么跑" runToolCall → 嵌套调用复用同一条管道 before/afterToolCall hooks → 拦截点(权限、结果改写)exposure
我们要知道pi的几种工具情况
| exposure | declared | callable | 注册即激活 | 语义 |
|---|---|---|---|---|
direct | active 时 | active 时 | 是 | 普通工具,模型和脚本都能用 |
model-only | active 时 | 永不 | 是 | 只给模型用的编排/交互工具 |
codemode | 仅显式激活时 | 始终(只要注册了) | 否 | 脚本工具,codemode 描述里会列出 |
deferred | 仅显式激活时 | 始终 | 否 | 同上,但 codemode 描述不列,靠 tool_search 发现 |
hidden | 永不 | 永不 | 否 | 注册但不可达 |
- declared? — 是否出现在发给模型的工具声明里
- callable? — 是否能被 ctx.executeTool() / codemode 脚本调用
这个设计就很巧妙,比如我有一个mcp服务,mcp下面本质是tools调用,如果我把一个tools关闭了,如果我把他永久删除在配置里面,但是上下文里面出现过,那么就会自相矛盾,llm去recall的时候可能会炸开,但是我设置横hidden就没有这种问题了
而前面说的hiddenDeclarations就不能这样,exposure 挂在定义上、激活前就确定了,而后者是在每次激活时由激活的那批工具的钩子算出来的,就像一些初始化处理,我不想每次都有那么多的tools或者是subagent定义去占上下文,可以写一个钩子脚本把这些工具都包进去,让agent只看这个钩子(但是好像会影响llm的思考速度,还在优化)
Pi的模型兼容
首先看这个可以看看packages/ai/README.md,官方写的接入供应商文档,还是很权威的
这个时候就会想,为什么我们需要对不同的厂家模型做适配呢?
我们的一个普通信息在pi中是这样的,前面提示词应该有说到
{ role: “user”, content: “帮我读一下 main.ts”, timestamp: 1748697600000 }
但是不同的模型长的样子不一样
claude:{ role: “user”, content: [{ type: “text”, text: “帮我读一下 main.ts” }] } gpt:{ role: “user”, content: “帮我读一下 main.ts” } google:{ role: “user”, parts: [{ text: “帮我读一下 main.ts” }] } aws:{ role: “user”, content: [{ text: “帮我读一下 main.ts” }] } …
那么也就是消息格式、流式传输、思考模式和缓存处理都不一样,那么处理起来还是比较麻烦的,不过好事是大部分模型厂家都选择了claude和openai的协议()
看看providers/anthropic.ts其实加入一个适配并不麻烦
export function anthropicProvider(): Provider<"anthropic-messages"> { return createProvider({ id: "anthropic", name: "Anthropic", baseUrl: "https://api.anthropic.com", auth: { apiKey: anthropicApiKeyAuth(), oauth: lazyOAuth({ name: "Anthropic (Claude Pro/Max)", isSubscription: true, load: loadAnthropicOAuth, }), }, models: Object.values(ANTHROPIC_MODELS), api: anthropicMessagesApi(), });}我感觉核心的点主要是compat 是数据,不是分支,比如openai-completions有gpt、ds、xai等等都在用,是怎么做选择的呢
function detectCompat(model: Model<"openai-completions">): ResolvedOpenAICompletionsCompat { const provider = model.provider; const baseUrl = model.baseUrl;
const isZai = provider === "zai" || provider === "zai-coding-cn" || baseUrl.includes("api.z.ai") || baseUrl.includes("open.bigmodel.cn"); const isTogether = provider === "together" || baseUrl.includes("api.together.ai") || baseUrl.includes("api.together.xyz"); const isMoonshot = provider === "moonshotai" || provider === "moonshotai-cn" || baseUrl.includes("api.moonshot."); const isOpenRouter = provider === "openrouter" || baseUrl.includes("openrouter.ai"); const isCloudflareWorkersAI = provider === "cloudflare-workers-ai" || baseUrl.includes("api.cloudflare.com"); const isCloudflareAiGateway = provider === "cloudflare-ai-gateway" || baseUrl.includes("gateway.ai.cloudflare.com"); const isNvidia = provider === "nvidia" || baseUrl.includes("integrate.api.nvidia.com"); const isAntLing = provider === "ant-ling" || baseUrl.includes("api.ant-ling.com"); const isCerebras = provider === "cerebras" || baseUrl.includes("cerebras.ai"); const isDeepSeek = provider === "deepseek" || baseUrl.toLowerCase().includes("deepseek.com");这里是直接通过url去判断,然后发送过去逐字节处理
function getCompat(model: Model<"openai-completions">): ResolvedOpenAICompletionsCompat { const detected = detectCompat(model); if (!model.compat) return detected;
return { supportsStore: model.compat.supportsStore ?? detected.supportsStore, supportsDeveloperRole: model.compat.supportsDeveloperRole ?? detected.supportsDeveloperRole, supportsReasoningEffort: model.compat.supportsReasoningEffort ?? detected.supportsReasoningEffort, supportsUsageInStreaming: model.compat.supportsUsageInStreaming ?? detected.supportsUsageInStreaming, supportsFinishReason: model.compat.supportsFinishReason ?? detected.supportsFinishReason,model.compat 部分设置时,未设置的字段回落到探测值,那么我们实际找的也就是一家的不同点了
那么thinking如何解决呢,这里方法其实我感觉是笨方法吧,就是每个都写一种情况,比如
if (compat.thinkingFormat === "zai" && model.reasoning) { const zaiParams = params as Omit<typeof params, "reasoning_effort"> & { thinking?: { type: "enabled" | "disabled"; clear_thinking?: boolean }; reasoning_effort?: string; }; zaiParams.thinking = options?.reasoningEffort ? { type: "enabled", clear_thinking: false } : { type: "disabled" }; if (options?.reasoningEffort && compat.supportsReasoningEffort) { const mappedEffort = model.thinkingLevelMap?.[options.reasoningEffort]; const effort = mappedEffort === undefined ? options.reasoningEffort : mappedEffort; if (typeof effort === "string") { zaiParams.reasoning_effort = effort; } } }那么就看看加载部分,在api/<id>.ts定义了四十多个厂家,全部加载进去包爆炸的,看看anthropic-messages.lazy.ts
export const anthropicMessagesApi = (): ProviderStreams => lazyApi(() => import(”./anthropic-messages.ts”));
懒加载是个好东西,但是mcp为什么没有懒加载,nmd
但这里有个类型层面的困难
lazy.ts里面写了
export function lazyStream( model: Model<Api>, setup: () => Promise<AsyncIterable<AssistantMessageEvent>>,): AssistantMessageEventStream { const startedAt = Date.now(); const outer = new AssistantMessageEventStream();
setup() .then((inner) => forwardStream(outer, inner)) .catch((error) => { const message = createSetupErrorMessage(model, error, startedAt); outer.push({ type: "error", reason: "error", error: message }); outer.end(message); });
return outer;}立刻返回一个流,内部转发生迟。setup失败也不是throw,而是转成协议内的error事件——所以调用方永远不需要try/catch.这样在一定层面解决了异步的问题
pi还有一个更大的特点,比如我用gpt工作感觉他笨蛋了想换个模型,可以直接/model切换成其他的,但是在供应商不同、思考参数不同的情况下,是如何解决的呢,在readme的Cross-Provider Handoffs中已有描述,如果协议一样,直接继续发送,如果不用,就把thinking的东西降级成<thinking>...</thinking> 标发送过去,user的输入和工具的结果都直接传,如果后来的模型不能看图片呢?就换成image omitted: model does not support images占位符就行了
而模型数据是从models.dev等权威机构获取的,并不是直接写入进去,太麻烦了
比较好的写法还有
export type Api = KnownApi | (string & {}); export type ImageApi = KnownImageApi | (string & {}); export type ClassifierApi = KnownClassifierApi | (string & {});(string & {}) 是 TS 的开放联合惯用法:保留 KnownApi 的字面量自动补全,同时允许任意自定义字符串。加一个新 API 不需要改 types.ts——只要写一个导出stream/streamSimple 的模块,传给 createProvider({ api }) 即可。
自定义Provider加载
其实过程都是差不多的,中转站的模型配置写在~/.pi/agent/models.json中,加载方法在model-config.ts文件中,加载后装配在provider-composer.ts文件中
比如我在中转站用cc,就会因为协议被路由到anthropic-messages.ts做处理,而gpt就会到openai-responses.ts,而你的参数、模型等就是写什么是什么,pi不会去主动获取验证
并且一些模型厂家,比如minimax的3.1flash在官网的model中获取不到,这种情况还得自己写进去,绷不住了()
其实看到这里,也就知道该如何写一个针对pi的ccswitch了吧,如果知道了pi的怎么写,其实其他的也都知道了()
最后
还是得多看看啊,可以给pi调优一点,要不然一些东西还是太难受了
部分信息可能已经过时