Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 16 additions & 9 deletions doc/pi_multiprovider_integration.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,13 @@ smoke 覆盖;34 来源与 Proxy 路由的最终全仓测试、lint、声明构

## 依赖版本与 ESM 加载

当前使用 `@earendil-works/pi-ai@1.0.0` 和
当前使用 `@earendil-works/pi-ai@1.3.0` 和
`@google/genai@2.21.0`。Pi 的间接依赖沿用其发布版本声明(包括 Anthropic SDK
0.124.0 和 Google SDK 2.21.0),不强行替换上游依赖。
0.129.0 和 Google SDK 2.21.0),不强行替换上游依赖。

Pi 1.3.0 目录包含 Google / OpenCode Zen 的 `gemini-4-argon`,以及 OpenCode Go 的
`gemini-4-argon-contributor`。Google 代理适配器复用上游的思考等级转换,发送
`thinkingLevel`;Argon 不支持关闭思考,关闭或最低等级请求会按目录约束回落到 `LOW`。

Pi 1.x 在 provider 边界将 `Context.systemPrompt` 和 `Context.tools` 规范化为 transcript
中的系统消息。Google 自定义传输适配器从该消息序列读取提示词和工具;直接调用 adapter 的测试也先调用
Expand Down Expand Up @@ -62,7 +66,7 @@ Codex。
| Google Gemini | `google-generative-ai` | `https://generativelanguage.googleapis.com/v1beta` |
| Groq | `openai-completions` | `https://api.groq.com/openai/v1` |
| Hugging Face | `openai-completions` | `https://router.huggingface.co/v1` |
| Kimi For Coding | `anthropic-messages` | `https://api.kimi.com/coding` |
| Kimi For Coding | `openai-completions` | `https://api.kimi.com/coding/v1` |
| MiniMax | `anthropic-messages` | `https://api.minimax.io/anthropic` |
| MiniMax(中国) | `anthropic-messages` | `https://api.minimaxi.com/anthropic` |
| Mistral | `mistral-conversations` | `https://api.mistral.ai` |
Expand All @@ -71,8 +75,8 @@ Codex。
| NVIDIA | `openai-completions` | `https://integrate.api.nvidia.com/v1` |
| OpenAI | `openai-responses`、`openai-completions` | `https://api.openai.com/v1` |
| OpenAI Codex | `openai-codex-responses` | `https://chatgpt.com/backend-api` |
| OpenCode Zen | `anthropic-messages`、`google-generative-ai`、`openai-completions`、`openai-responses` | Messages:`https://opencode.ai/zen`;其他 API:`https://opencode.ai/zen/v1` |
| OpenCode Go | `anthropic-messages`、`openai-completions`、`openai-responses` | Messages:`https://opencode.ai/zen/go`;其他 API:`https://opencode.ai/zen/go/v1` |
| OpenCode Zen | `anthropic-messages`、`google-generative-ai`、`openai-completions`、`openai-responses` | Messages:`https://opencode.ai/inference`;其他 API:`https://opencode.ai/inference/v1` |
| OpenCode Go | `anthropic-messages`、`google-generative-ai`、`openai-completions`、`openai-responses` | Messages:`https://opencode.ai/inference/go`;其他 API:`https://opencode.ai/inference/go/v1` |
| OpenRouter | `openai-completions`、`anthropic-messages` | Completions:`https://openrouter.ai/api/v1`;Messages:`https://openrouter.ai/api` |
| Qwen Token Plan | `openai-completions` | `https://token-plan.ap-southeast-1.maas.aliyuncs.com/compatible-mode/v1` |
| Qwen Token Plan(中国) | `openai-completions` | `https://token-plan.cn-beijing.maas.aliyuncs.com/compatible-mode/v1` |
Expand Down Expand Up @@ -443,6 +447,7 @@ SSE 在流结束后合并输出,中断或读取失败时保留已收到的正
可按场景 ID 子串筛选,多个子串用逗号分隔。密钥仅在实际请求的 fetch 边界注入,界面使用占位值;报告对错误信息脱敏,写入已忽略的
`coverage/pi-st-final/configured-cases.json`。本集合只通过上述显式命令启动,默认 `yarn test` /
`yarn test:coverage` 排除 `tests/live/`,不会执行它;凭据文件及测试报告不得提交。
单项复测保留该场景在完整矩阵中的预期变量值,便于对照失败结果。
- `update:pi:st-prompt-fixtures`
在隔离真实浏览器中同时捕获 Legacy 与 Pi 的当前预设、其他预设、内置破限三条 prompt 路径,并生成带版本/产物 provenance 的回归 fixtures。三路分别为 12/11/15 条 messages,逐路 JSON 完全一致且没有允许差异或 normalization;覆盖宏、prompt-only 正则、角色卡、世界书过滤与深度、历史裁剪、注入和
`filterPrompts`。这三条“聊天消息”fixtures 均未出现历史工具消息,因此首版不额外扩大该回归范围。
Expand Down Expand Up @@ -475,11 +480,13 @@ SSE 在流结束后合并输出,中断或读取失败时保留已收到的正
429 会立即停止后续请求。本次受控重试中 Responses、Anthropic Messages 和 Chat
Completions 三条均成功返回预期响应,且路径、signal、认证头和 CORS 检查全部通过。
- `yarn test:pi:st-server-cancel` 让真实 Firefox 中的 Pi
Responses 请求和主聊天请求同时连接到本机跨端口流式 HTTP 服务。停止 Pi 后,服务端观察到对应响应在
`finish` 前关闭,而主聊天流与 signal 仍保持活跃;只有再点击真实 `#mes_stop`
后主聊天流才关闭。该测试证明浏览器到可观测服务端的取消传播及两条 stop 路径隔离,不代表任何真实 Provider 已停止生成或计费。
Responses 请求和通过 `ChatCompletionService` 发起的独立主传输同时连接到本机跨端口流式 HTTP 服务。
按 generation ID 停止 Pi 后,服务端观察到对应响应在 `finish` 前关闭,而主传输仍保持活跃;
再中止主传输自己的 `AbortController`,确认其连接关闭。原生发送/停止按钮生命周期由 `test:pi:st-features`
验证。该测试证明浏览器到可观测服务端的取消传播及两条 stop 路径隔离,不代表任何真实 Provider 已停止生成或计费。
- `yarn test:pi:st-live` 使用临时 ST/Firefox
profile 和同一份内存凭据尝试真实浏览器请求,并在结束前清空页面内存、扫描临时数据后再删除目录。OpenAI
profile 和同一份内存凭据尝试真实浏览器请求,默认模型为 `openai/gpt-4.1-mini`,
可通过 `MVU_PI_ST_LIVE_MODEL` 覆盖;结束前清空页面内存、扫描临时数据后再删除目录。OpenAI
Responses 与 OpenAI Chat Completions 已分别通过:请求到达 `/api/v1/responses` 和
`/api/v1/chat/completions`,HTTP 200、CORS、Authorization、真实且未预先中止的
`AbortSignal`、最终响应 marker、变量更新标签和 analysis 清理均符合预期。OpenRouter-compatible
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@
],
"private": true,
"dependencies": {
"@earendil-works/pi-ai": "1.0.0",
"@earendil-works/pi-ai": "1.3.0",
"@google/genai": "2.21.0",
"compare-versions": "^6.1.1",
"corepack": "^0.32.0",
Expand Down
4 changes: 2 additions & 2 deletions src/function/update/model_list.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,8 +95,8 @@ const OPENAI_MODEL_DISCOVERY: Readonly<
'X-GitHub-Api-Version': '2026-06-01',
}),
}),
opencode: Object.freeze({ baseUrl: 'https://opencode.ai/zen/v1' }),
'opencode-go': Object.freeze({ baseUrl: 'https://opencode.ai/zen/go/v1' }),
opencode: Object.freeze({ baseUrl: 'https://opencode.ai/inference/v1' }),
'opencode-go': Object.freeze({ baseUrl: 'https://opencode.ai/inference/go/v1' }),
'vercel-ai-gateway': Object.freeze({ baseUrl: 'https://ai-gateway.vercel.sh/v1' }),
});

Expand Down
63 changes: 6 additions & 57 deletions src/function/update/pi/google_proxy_adapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,16 @@ import {
createAssistantMessageEventStream,
getCurrentSystemPrompt,
getCurrentTools,
getDisabledGoogleThinkingConfig,
isGoogleThinkingPart,
mapGoogleStopReason,
resolveGoogleFunctionCallingMode,
resolveGoogleThinkingLevel,
retainGoogleThoughtSignature,
retryGoogleRequest,
supportsGoogleStrictToolSampling,
toGoogleThinkingLevel,
usesGoogleThinkingLevel,
type Api,
type AssistantMessage,
type AssistantMessageEventStream,
Expand Down Expand Up @@ -279,7 +282,7 @@ function buildParams(
}
config.thinkingConfig = thinking_config;
} else if (model.reasoning && options.thinking && !options.thinking.enabled) {
config.thinkingConfig = getDisabledThinkingConfig(model);
config.thinkingConfig = getDisabledGoogleThinkingConfig(model);
}

if (options.signal) {
Expand Down Expand Up @@ -562,60 +565,6 @@ function streamWithInjectedFetch(
return stream;
}

/** 识别使用思考等级配置的 Gemma 4 模型名称。 */
function isGemma4Model(model: Model<'google-generative-ai'>): boolean {
return /gemma-?4/.test(model.id.toLowerCase());
}

/** 识别 Gemini 3 Pro 系列,以使用其支持的思考等级范围。 */
function isGemini3ProModel(model: Model<'google-generative-ai'>): boolean {
return /gemini-3(?:\.\d+)?-pro/.test(model.id.toLowerCase());
}

/** 识别 Gemini 3 Flash 系列及对应 latest 别名。 */
function isGemini3FlashModel(model: Model<'google-generative-ai'>): boolean {
const id = model.id.toLowerCase();
return (
/gemini-3(?:\.\d+)?-flash/.test(id) ||
id === 'gemini-flash-latest' ||
id === 'gemini-flash-lite-latest'
);
}

/** 为不同 Google 模型选择关闭或最低允许思考配置,避免发送不支持的零预算。 */
function getDisabledThinkingConfig(model: Model<'google-generative-ai'>): ThinkingConfig {
if (isGemini3ProModel(model)) {
return { thinkingLevel: 'LOW' as ThinkingConfig['thinkingLevel'] };
}
if (isGemini3FlashModel(model) || isGemma4Model(model)) {
return { thinkingLevel: 'MINIMAL' as ThinkingConfig['thinkingLevel'] };
}
return { thinkingBudget: 0 };
}

/** 把 Pi 思考等级映射到当前 Google 模型实际支持的等级。 */
function getThinkingLevel(
effort: ResolvedGoogleThinkingLevel,
model: Model<'google-generative-ai'>
): GoogleApiThinkingLevel {
if (isGemini3ProModel(model)) {
return effort === 'minimal' || effort === 'low' ? 'LOW' : 'HIGH';
}
if (isGemma4Model(model)) {
return effort === 'minimal' || effort === 'low' ? 'MINIMAL' : 'HIGH';
}
switch (effort) {
case 'minimal':
return 'MINIMAL';
case 'low':
return 'LOW';
case 'medium':
return 'MEDIUM';
case 'high':
return 'HIGH';
}
}

/** 优先使用自定义思考预算,否则采用相应模型的预算映射;未知模型交由服务端动态分配。 */
function getGoogleBudget(
model: Model<'google-generative-ai'>,
Expand Down Expand Up @@ -671,13 +620,13 @@ function streamSimpleWithInjectedFetch(
);
}
const resolved_level = resolveGoogleThinkingLevel(model, clamped_reasoning);
if (isGemini3ProModel(model) || isGemini3FlashModel(model) || isGemma4Model(model)) {
if (usesGoogleThinkingLevel(model)) {
return streamWithInjectedFetch(
model,
context,
{
...base,
thinking: { enabled: true, level: getThinkingLevel(resolved_level, model) },
thinking: { enabled: true, level: toGoogleThinkingLevel(resolved_level) },
},
fetch_impl
);
Expand Down
3 changes: 3 additions & 0 deletions src/function/update/pi/pi_gateway.ts
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,7 @@ export { openAICodexResponsesApi } from '@earendil-works/pi-ai/api/openai-codex-
export {
convertMessages as convertGoogleMessages,
convertTools as convertGoogleTools,
getDisabledGoogleThinkingConfig,
isThinkingPart as isGoogleThinkingPart,
// 本地 Google SDK 可能先于 Pi 固定的 SDK 版本增加枚举值,停止原因仍沿用 Pi 的映射。
mapStopReasonString as mapGoogleStopReason,
Expand All @@ -117,6 +118,8 @@ export {
retainThoughtSignature as retainGoogleThoughtSignature,
retryGoogleRequest,
supportsGoogleStrictToolSampling,
toGoogleThinkingLevel,
usesGoogleThinkingLevel,
} from '@earendil-works/pi-ai/api/google-shared';
export type {
GoogleApiThinkingLevel,
Expand Down
35 changes: 21 additions & 14 deletions src/function/update/pi/provider_target.ts
Original file line number Diff line number Diff line change
Expand Up @@ -215,12 +215,12 @@ export const PI_PROVIDER_TARGET_REGISTRY: Readonly<
'kimi-coding': definePiProviderTarget({
key: 'kimi-coding',
providerId: 'kimi-coding',
defaultApi: 'anthropic-messages',
allowedApis: Object.freeze(['anthropic-messages'] as const),
defaultApi: 'openai-completions',
allowedApis: Object.freeze(['openai-completions'] as const),
defaultAuthType: 'api_key',
allowedAuthTypes: Object.freeze(['api_key'] as const),
defaultBaseUrl: 'https://api.kimi.com/coding',
corsProxyRequiredApis: ['anthropic-messages'],
defaultBaseUrl: 'https://api.kimi.com/coding/v1',
corsProxyRequiredApis: ['openai-completions'],
allowCustomEndpoint: false,
}),
minimax: definePiProviderTarget({
Expand Down Expand Up @@ -318,12 +318,12 @@ export const PI_PROVIDER_TARGET_REGISTRY: Readonly<
] as const),
defaultAuthType: 'api_key',
allowedAuthTypes: Object.freeze(['api_key'] as const),
defaultBaseUrl: 'https://opencode.ai/zen',
defaultBaseUrl: 'https://opencode.ai/inference',
apiBaseUrls: Object.freeze({
'anthropic-messages': 'https://opencode.ai/zen',
'google-generative-ai': 'https://opencode.ai/zen/v1',
'openai-completions': 'https://opencode.ai/zen/v1',
'openai-responses': 'https://opencode.ai/zen/v1',
'anthropic-messages': 'https://opencode.ai/inference',
'google-generative-ai': 'https://opencode.ai/inference/v1',
'openai-completions': 'https://opencode.ai/inference/v1',
'openai-responses': 'https://opencode.ai/inference/v1',
}),
corsProxyRequiredApis: [
'anthropic-messages',
Expand All @@ -339,18 +339,25 @@ export const PI_PROVIDER_TARGET_REGISTRY: Readonly<
defaultApi: 'anthropic-messages',
allowedApis: Object.freeze([
'anthropic-messages',
'google-generative-ai',
'openai-completions',
'openai-responses',
] as const),
defaultAuthType: 'api_key',
allowedAuthTypes: Object.freeze(['api_key'] as const),
defaultBaseUrl: 'https://opencode.ai/zen/go',
defaultBaseUrl: 'https://opencode.ai/inference/go',
apiBaseUrls: Object.freeze({
'anthropic-messages': 'https://opencode.ai/zen/go',
'openai-completions': 'https://opencode.ai/zen/go/v1',
'openai-responses': 'https://opencode.ai/zen/go/v1',
'anthropic-messages': 'https://opencode.ai/inference/go',
'google-generative-ai': 'https://opencode.ai/inference/go/v1',
'openai-completions': 'https://opencode.ai/inference/go/v1',
'openai-responses': 'https://opencode.ai/inference/go/v1',
}),
corsProxyRequiredApis: ['anthropic-messages', 'openai-completions', 'openai-responses'],
corsProxyRequiredApis: [
'anthropic-messages',
'google-generative-ai',
'openai-completions',
'openai-responses',
],
allowCustomEndpoint: false,
}),
openrouter: definePiProviderTarget({
Expand Down
4 changes: 2 additions & 2 deletions src/panel/open_source_licenses.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,14 @@ export type OpenSourceLicense = Readonly<{
export const OPEN_SOURCE_LICENSES = [
{
packageName: '@anthropic-ai/sdk',
version: '0.124.0',
version: '0.129.0',
license: 'MIT',
projectUrl: 'https://github.com/anthropics/anthropic-sdk-typescript',
},
{
packageName: '@earendil-works/pi-ai',
displayName: 'Earendil Works AI',
version: '1.0.0',
version: '1.3.0',
license: 'MIT',
projectUrl: 'https://github.com/earendil-works/pi',
},
Expand Down
5 changes: 3 additions & 2 deletions tests/live/pi_st_configured_live_smoke.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -434,6 +434,8 @@ export async function runPiStConfiguredLiveSmoke(options) {
: []),
];
})
// 先分配预期值再筛选,单项复测与完整矩阵使用相同输入。
.map((scenario, index) => ({ ...scenario, value: 7 + index }))
.filter(scenario => {
const filters = (process.env.MVU_PI_ST_CONFIGURED_CASE || '')
.split(',')
Expand All @@ -454,8 +456,7 @@ export async function runPiStConfiguredLiveSmoke(options) {
},
};
try {
for (const [index, scenario] of scenarios.entries()) {
const testCase = { ...scenario, value: 7 + index };
for (const testCase of scenarios) {
process.stderr.write(`[pi-st-live] ${testCase.id}\n`);
const result = {
id: testCase.id,
Expand Down
Loading
Loading