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
2 changes: 1 addition & 1 deletion artifact/bundle.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion artifact/bundle.js.map

Large diffs are not rendered by default.

20 changes: 15 additions & 5 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@0.85.1` 和
当前使用 `@earendil-works/pi-ai@1.0.0` 和
`@google/genai@2.21.0`。Pi 的间接依赖沿用其发布版本声明(包括 Anthropic SDK
0.123.0 和 Pi 内部的 Google SDK 1.52.0),不强行替换上游依赖。
0.124.0 和 Google SDK 2.21.0),不强行替换上游依赖。

Pi 1.x 在 provider 边界将 `Context.systemPrompt` 和 `Context.tools` 规范化为 transcript
中的系统消息。Google 自定义传输适配器从该消息序列读取提示词和工具;直接调用 adapter 的测试也先调用
`normalizeContext()`。酒馆中途 system 消息仍沿用现有的按原位置恢复机制。

生产构建通过 jsDelivr 的版本化 `+esm` 地址加载 Pi、模型目录、各 API adapter 和直接使用的 Google
SDK,第三方实现不再打入 MVU。Webpack 从 `package.json`
Expand Down Expand Up @@ -162,13 +166,19 @@ fetch,而原本就通过 SillyTavern 模型状态接口读取 OpenAI 结构目
4. 填写模型 ID,也可以从 Pi 内置模型目录选择;“获取模型列表”会按当前来源/API/认证请求上游可见模型。
5. 设置 `contextWindow` 和现有“最大回复 token 数”。
6. 如果当前路由标有 `(Proxy)`,先确认 SillyTavern 已启用 Proxy 且来源下方没有 Proxy 警告。
7. 在来源上方选择破限方案、应答格式和“兼容假流式”,并按需调整采样参数。
7. 在来源上方选择破限方案、应答格式和“兼容假流式”,并按需调整高级参数中的采样参数和“思考等级”。
8. 在“模型来源”顶部的“API 方案”中保存配置,或选择已有方案。

高级参数中的“思考等级”支持默认、关闭/最低、minimal、low、medium、high、xhigh 和 max,随 Pi API 方案保存。
默认(包括旧配置)保留原始请求行为;显式选择时由 Pi 的 simple adapter 按模型能力映射等级,部分模型无法完全关闭思考。
目录外模型或自定义端点按协议转换等级;OpenAI/Mistral 会原样尝试 xhigh/max,是否接受由服务端决定,Google/预算式思考仍按协议映射。思考与回答的共享 token 预算不会突破配置的回复上限。
开启思考会禁用不兼容的采样参数;Anthropic 不支持同时强制工具调用,预算式思考至少需要 2048 回复 token。
自定义请求体覆盖保持原有优先级。

“更多”默认发送非流式请求,勾选“兼容假流式”后才发送流式请求;两种模式都等完整回复后再更新变量。OpenAI
Codex 的账号接口要求 `stream: true`,该来源的开关显示为固定开启。

Pi 0.85.1 的 `complete()` 仍通过流式 HTTP 实现,因此普通应答由 `non_streaming_fetch.ts`
Pi 1.0.0 的 `complete()` 仍通过流式 HTTP 实现,因此普通应答由 `non_streaming_fetch.ts`
适配:保留 Pi 的鉴权和请求构建,在 HTTP 层发送 `stream: false`(Google 使用
`generateContent`),再将完整 JSON 应答转换为 Pi 解析器可读取的事件。该转换在收到完整应答后进行,保持工具调用、结束原因和取消信号,并与 Proxy 组合;OAuth 和模型列表请求不受影响。

Expand Down Expand Up @@ -201,7 +211,7 @@ API 方案通过 `backend` 区分两类快照:

- `backend: 'custom'`:保存原有自定义 API 字段。
- `backend: 'pi'`:保存结构完整的 Pi 连接快照,包括 provider、API、认证方式、endpoint、`useProxy`、model、
`contextWindow`、`customHeaders`、`customIncludeBody`、`customExcludeBody` 和 OAuth 凭证引用 `credentialIds`。
`contextWindow`、`thinkingLevel`、`customHeaders`、`customIncludeBody`、`customExcludeBody` 和 OAuth 凭证引用 `credentialIds`。

旧方案没有 `backend` 时按 `custom`
迁移。保存、另存、切换和删除 Pi 方案时会深拷贝连接字段并保留未知字段,避免响应式对象共享或前向兼容数据丢失。
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": "0.85.1",
"@earendil-works/pi-ai": "1.0.0",
"@google/genai": "2.21.0",
"compare-versions": "^6.1.1",
"corepack": "^0.32.0",
Expand Down
4 changes: 4 additions & 0 deletions src/function/update/extra_model_api_profiles.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { normalizePiThinkingLevel, type PiThinkingLevel } from './pi/thinking_setting';
import { tr } from '@/i18n';
import {
getPiProviderTargetDefinition,
Expand Down Expand Up @@ -47,6 +48,7 @@ export type ExtraModelPiConnectionFields = {
authType: string;
endpoint: string;
useProxy: boolean;
thinkingLevel?: PiThinkingLevel;
model: string;
contextWindow: number | string;
customHeaders: string;
Expand Down Expand Up @@ -148,6 +150,7 @@ function clonePiConnectionFields(
// Profiles saved before this option existed, and malformed imported values, must retain the
// direct-transport default rather than becoming enabled through truthiness coercion.
cloned.useProxy = cloned.useProxy === true;
cloned.thinkingLevel = normalizePiThinkingLevel(cloned.thinkingLevel);
const context_window = normalizeExtraModelPiProfileContextWindow(cloned.contextWindow);
if (context_window !== undefined) {
cloned.contextWindow = context_window;
Expand Down Expand Up @@ -290,6 +293,7 @@ function clearPiConnectionFields(pi: ExtraModelPiSettings): ExtraModelPiSettings
authType: 'api_key',
endpoint: '',
useProxy: false,
thinkingLevel: 'default',
model: '',
contextWindow: 0,
credentials: klona(cloned.credentials),
Expand Down
7 changes: 4 additions & 3 deletions src/function/update/pi/context_adapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import type {
UserMessage,
} from '@earendil-works/pi-ai';

/** Pi Context 无法表达的系统消息,交由请求载荷 hook 在原位置恢复。 */
/** 需要按酒馆原始位置保留的系统消息,交由请求载荷 hook 在原位置恢复。 */
export type PiLateSystemMessage = {
sourceIndex: number;
/** 插在转换后第几条普通消息之前;等于 messages.length 表示位于末尾。 */
Expand Down Expand Up @@ -581,7 +581,7 @@ function addNamePrefix(content: PiInputContent[], name: string | undefined): PiI
}

/** 将历史工具参数解析为普通对象,拒绝无效 JSON 或非对象参数。 */
function parseToolArguments(argumentsValue: string, sourceIndex: number): Record<string, unknown> {
function parseToolArguments(argumentsValue: string, sourceIndex: number): ToolCall['arguments'] {
let parsed: unknown;
try {
parsed = JSON.parse(argumentsValue);
Expand All @@ -599,7 +599,8 @@ function parseToolArguments(argumentsValue: string, sourceIndex: number): Record
sourceIndex
);
}
return parsed;
// JSON.parse 已排除非 JSON 值,且上方校验保证参数是对象而非数组或标量。
return parsed as ToolCall['arguments'];
}

/** 校验历史工具调用标识和参数,并转换为 Pi 工具调用块。 */
Expand Down
40 changes: 25 additions & 15 deletions src/function/update/pi/google_proxy_adapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,12 @@ import {
buildBaseOptions,
calculateCost,
clampThinkingLevel,
collapseSystemMessages,
convertGoogleMessages,
convertGoogleTools,
createAssistantMessageEventStream,
getCurrentSystemPrompt,
getCurrentTools,
isGoogleThinkingPart,
mapGoogleStopReason,
resolveGoogleFunctionCallingMode,
Expand All @@ -22,7 +25,6 @@ import {
type Api,
type AssistantMessage,
type AssistantMessageEventStream,
type Context,
type FetchFunction,
type GoogleApiThinkingLevel,
type Model,
Expand All @@ -35,6 +37,7 @@ import {
type ThinkingBudgets,
type ThinkingContent,
type ToolCall,
type TranscriptContext,
} from './pi_gateway';

/**
Expand Down Expand Up @@ -237,10 +240,14 @@ function createClient(
/** 将 Pi 上下文、工具、采样和思考配置转换为 Google 生成参数,并保留取消信号。 */
function buildParams(
model: Model<'google-generative-ai'>,
context: Context,
context: TranscriptContext,
options: GoogleProxyOptions = {}
): GenerateContentParameters {
const contents = convertGoogleMessages(model, context);
// Pi 1.x 将系统提示词和工具声明放进 transcript;Google 使用折叠后的系统消息。
const normalized_context = collapseSystemMessages(context);
const contents = convertGoogleMessages(model, normalized_context);
const system_prompt = getCurrentSystemPrompt(normalized_context.messages);
const tools = getCurrentTools(normalized_context.messages);
const generation_config: GenerateContentConfig = {};
if (options.temperature !== undefined) {
generation_config.temperature = options.temperature;
Expand All @@ -250,17 +257,13 @@ function buildParams(
}

const supports_strict_mode = supportsGoogleStrictToolSampling(model.id);
const function_calling_mode = context.tools?.length
? resolveGoogleFunctionCallingMode(context.tools, options.toolChoice, supports_strict_mode)
const function_calling_mode = tools.length
? resolveGoogleFunctionCallingMode(tools, options.toolChoice, supports_strict_mode)
: undefined;
const config: GenerateContentConfig = {
...(Object.keys(generation_config).length === 0 ? {} : generation_config),
...(context.systemPrompt
? { systemInstruction: sanitizeSurrogates(context.systemPrompt) }
: {}),
...(context.tools?.length
? { tools: convertGoogleTools(context.tools, false, supports_strict_mode) }
: {}),
...(system_prompt ? { systemInstruction: sanitizeSurrogates(system_prompt) } : {}),
...(tools.length ? { tools: convertGoogleTools(tools, false, supports_strict_mode) } : {}),
...(function_calling_mode === undefined
? {}
: { toolConfig: { functionCallingConfig: { mode: function_calling_mode } } }),
Expand Down Expand Up @@ -301,7 +304,7 @@ let tool_call_counter = 0;
*/
function streamWithInjectedFetch(
model: Model<'google-generative-ai'>,
context: Context,
context: TranscriptContext,
options: GoogleProxyOptions,
fetch_impl: FetchFunction
): AssistantMessageEventStream {
Expand Down Expand Up @@ -447,8 +450,7 @@ function streamWithInjectedFetch(
type: 'toolCall',
id: tool_call_id,
name: part.functionCall.name || '',
arguments:
(part.functionCall.args as Record<string, unknown>) ?? {},
arguments: (part.functionCall.args as ToolCall['arguments']) ?? {},
...(part.thoughtSignature
? { thoughtSignature: part.thoughtSignature }
: {}),
Expand Down Expand Up @@ -638,7 +640,7 @@ function getGoogleBudget(
/** 将 Pi 简化选项解析为模型适用的思考等级或预算,再复用注入传输的流实现。 */
function streamSimpleWithInjectedFetch(
model: Model<'google-generative-ai'>,
context: Context,
context: TranscriptContext,
options: SimpleStreamOptions,
fetch_impl: FetchFunction
): AssistantMessageEventStream {
Expand All @@ -660,6 +662,14 @@ function streamSimpleWithInjectedFetch(
}

const clamped_reasoning = clampThinkingLevel(model, options.reasoning);
if (clamped_reasoning === 'off') {
return streamWithInjectedFetch(
model,
context,
{ ...base, thinking: { enabled: false } },
fetch_impl
);
}
const resolved_level = resolveGoogleThinkingLevel(model, clamped_reasoning);
if (isGemini3ProModel(model) || isGemini3FlashModel(model) || isGemma4Model(model)) {
return streamWithInjectedFetch(
Expand Down
5 changes: 5 additions & 0 deletions src/function/update/pi/pi_gateway.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,13 @@ export {
Type,
calculateCost,
clampThinkingLevel,
collapseSystemMessages,
createAssistantMessageEventStream,
createModels,
createProvider,
getCurrentSystemPrompt,
getCurrentTools,
normalizeContext,
} from '@earendil-works/pi-ai';

export type {
Expand Down Expand Up @@ -55,6 +59,7 @@ export type {
Tool,
ToolCall,
ToolChoice,
TranscriptContext,
Usage,
UserMessage,
} from '@earendil-works/pi-ai';
Expand Down
12 changes: 10 additions & 2 deletions src/function/update/pi/provider_registry.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { resolvePiThinkingLevel } from './thinking';
import { isPiThinkingEnabled } from './thinking_setting';
import {
ANT_LING_MODELS,
ANTHROPIC_MODELS,
Expand Down Expand Up @@ -656,7 +658,7 @@ function verifiedCatalogModel(
export function resolvePiCapabilities(
definition: PiProviderDefinition,
api: PiWireApi,
options: { model?: Model<Api>; catalogHit: boolean }
options: { model?: Model<Api>; catalogHit: boolean; thinkingLevel?: string }
): Readonly<PiApiCapabilities> | undefined {
const registered = definition.apiCapabilities[api];
if (!registered) {
Expand All @@ -665,6 +667,9 @@ export function resolvePiCapabilities(

const catalogModel = verifiedCatalogModel(definition, api, options);
const samplingModel = catalogModel ?? options.model;
const thinkingEnabled = isPiThinkingEnabled(
resolvePiThinkingLevel(options.thinkingLevel, catalogModel)
);
const anthropicSamplingAllowed =
definition.key !== 'anthropic' ||
catalogModel === undefined ||
Expand All @@ -683,7 +688,10 @@ export function resolvePiCapabilities(
!catalogModel ||
!GOOGLE_NO_SAMPLING_MODEL_IDS.has(catalogModel.id);
const samplingAllowed =
anthropicSamplingAllowed && openAISamplingAllowed && googleSamplingAllowed;
anthropicSamplingAllowed &&
openAISamplingAllowed &&
googleSamplingAllowed &&
!(thinkingEnabled && (api === 'anthropic-messages' || definition.key === 'openai'));
const streamingAllowed =
definition.key !== 'openai' ||
!catalogModel ||
Expand Down
Loading
Loading