feat(llm-pi-ai): adopt pi-ai 1.1.0 and Azure provider rename

This commit is contained in:
Yichen Jiang committed 2026-10-12 17:33:47 +08:00
1 parent 3aa8872e67
commit d79f13a4d4
12 files changed
+125 -26

No files matched your search

+1 -1
View File
@@ -141,7 +141,7 @@ External packages installed for runtime use or distributed inside the prebuilt b
pnpm applies local patches to the following packages at install time, so shipped artifacts carry modified copies; each patch file is the complete record of the modification:
- `@earendil-works/pi-ai@1.0.2` — [`patches/@earendil-works__pi-ai@1.0.2.patch`](patches/@earendil-works__pi-ai@1.0.2.patch)
- `@earendil-works/pi-ai@1.1.0` — [`patches/@earendil-works__pi-ai@1.1.0.patch`](patches/@earendil-works__pi-ai@1.1.0.patch)
- `@electron/[email protected]` — [`patches/@[email protected]`](patches/@[email protected])
- `@fortune-sheet/[email protected]` — [`patches/@[email protected]`](patches/@[email protected])
- `@fortune-sheet/[email protected]` — [`patches/@[email protected]`](patches/@[email protected])
@@ -0,0 +1,16 @@
# Bilingual-pair consistency record for guide.md (docs/i18n/README.md): per heading
# section, a hash of its English and Chinese blocks outside code blocks and generated regions.
# After editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write docs/upgrade-guide/v0.2.1-alpha.2/pi-ai-azure-provider/guide.md
/:
en: 51d5b5b4d2fb1b8a
zh: 4dc9af2458a8fcac
/azure-provider-route-rename:
en: 105c73820d057e28
zh: 91a7bfc5c282e164
/azure-provider-route-rename/change:
en: d5ce9089d8aa7119
zh: f337926f85b01bc1
/azure-provider-route-rename/migration:
en: 8fb23fbb8184fa00
zh: 8c44fb293a8dd70a
@@ -0,0 +1,19 @@
---
kind: upgrade-guide
description: "The pi-ai Azure provider route is renamed from azure-openai-responses to azure."
---
# Azure provider route rename
English | [中文](guide.zh.md)
## Change
The installed pi-ai catalog identifies the Azure provider as `azure` instead of `azure-openai-responses`. The `azure` route includes both Responses and Chat Completions models. The Responses API identifier remains `azure-openai-responses`, and the `AZURE_OPENAI_*` environment variables keep their names. Configurations and model selections that use the old provider key no longer resolve the Azure catalog.
## Migration
1. In the `llm-pi-ai` configuration in `$DSH_HOME/profiles/<profile>/cordis.patch.yml` or another supplying layer, rename `config.providers.azure-openai-responses` to `config.providers.azure`. Preserve its existing fields and any `apiKeyEnv` reference.
2. Use catalog model IDs under `models`; map a custom Azure deployment name with `AZURE_OPENAI_DEPLOYMENT_NAME_MAP`. The mixed catalog cannot infer a protocol for a model ID absent from the catalog. An explicit route-level `api` overrides every model on that route; it does not select the Azure provider.
3. Update explicit request/default provider selections to `azure`, and reselect an Azure model when continuing an existing Session. Configure the new Azure entry in Settings → Models if its key was stored for the old entry. Do not edit historical Session events.
4. Confirm that Settings → Models offers `azure` and lists the expected models, then send a short request using the configured deployment.
@@ -0,0 +1,19 @@
---
kind: upgrade-guide
description: "pi-ai 的 Azure 提供方路由从 azure-openai-responses 更名为 azure。"
---
# Azure 提供方路由更名
[English](guide.md) | 中文
## 变更
已安装的 pi-ai 目录使用 `azure` 标识 Azure 提供方,替代 `azure-openai-responses`。`azure` 路由同时包含 Responses 和 Chat Completions 模型。Responses API 标识仍为 `azure-openai-responses`,`AZURE_OPENAI_*` 环境变量名称也保持不变。使用旧提供方键的配置和模型选择无法再解析 Azure 目录。
## 迁移
1. 在 `$DSH_HOME/profiles/<profile>/cordis.patch.yml` 或其他提供配置的层中,找到 `llm-pi-ai` 配置,将 `config.providers.azure-openai-responses` 改为 `config.providers.azure`。保留原有字段及任何 `apiKeyEnv` 引用。
2. 在 `models` 中使用目录模型 ID;自定义 Azure 部署名称通过 `AZURE_OPENAI_DEPLOYMENT_NAME_MAP` 映射。混合协议目录无法为目录外的模型 ID 推断协议。显式设置路由级 `api` 会覆盖该路由所有模型的协议,并不表示选择 Azure 提供方。
3. 将显式请求及默认模型配置中的提供方改为 `azure`,继续现有 Session 时重新选择 Azure 模型。若密钥存储在旧条目下,请在“设置 → 模型”中配置新的 Azure 条目。不要修改历史 Session 事件。
4. 确认“设置 → 模型”提供 `azure` 且列出预期模型,再向所配置的部署发送一条简短请求。
+2 -2
View File
@@ -87,8 +87,8 @@
en: b6bd570c0159ba08
zh: 7d2d91eea6a8d830
/deepseek-ai-dsh-llm-pi-ai/known-limitations-and-deferred-work:
en: 552281e47ba3980f
zh: 4af4dff3d9fa52d5
en: de6e60be33ddcd82
zh: dfd0f00289200b77
/deepseek-ai-dsh-llm-pi-ai/known-limitations-and-deferred-work/dev-note:
en: 3678dd0f6642c2a5
zh: 75cbdd064cc64282
+2 -2
View File
@@ -229,7 +229,7 @@ These limits define where the adapter stops and future work begins. They are cur
- **`headers` can carry a credential the redactor never sees** — profile resolution rejects names and values Fetch cannot represent, but the dict remains plain strings; store credentials as `apiKeyEnv` references.
- **Discovery does not change configured models** — adopt discovery results explicitly into the route configuration.
- **Anthropic discovery reads at most 1,000 models** — the request uses the API's maximum page size but does not traverse `has_more`; entries beyond the first page must be added by hand.
- **One wire protocol per route** — a mixed-protocol catalog route cannot host a model of the other protocol; splitting the provider across two route keys is the workaround.
- **Uncataloged models need an unambiguous protocol** — catalog routes preserve each model's protocol, including both Azure APIs under `azure`. A model absent from a mixed-protocol catalog needs an explicit route-level `api`, which overrides every model on the route and must be one of the supported custom protocols. For Azure deployment names, keep the catalog model ID and set `AZURE_OPENAI_DEPLOYMENT_NAME_MAP`.
- **A modality declaration is not verified** — a model declaring `image` its gateway does not serve is refused by the provider after prompt admission. The durable image remains in history and the same misdeclared model can fail again; switching to a text-only model remains possible because the shared LLM runtime projects image references into stable text for that request.
- **An unauthenticated route depends on its protocol** — a route naming no credential resolves as configured-but-keyless, but pi-ai's OpenAI-compatible implementation still requires an API key or an `Authorization` header, so a keyless local server needs a placeholder credential referenced by `apiKeyEnv` or an `Authorization` entry in `headers`.
- **`GenerateOptions.stop` is unsupported** — pi-ai's common stream options cannot guarantee stop-sequence behavior across providers.
@@ -237,7 +237,7 @@ These limits define where the adapter stops and future work begins. They are cur
- **Images are accepted only in user and tool-result messages** — images in system, developer, or assistant history fail with `UNSUPPORTED_CONTENT` on both conversion paths.
- **Provider HTTP status is unavailable** — pi-ai error events do not expose a stable HTTP status across providers.
- **Retry policy is provider-owned, not an SDK retry** — pi-ai SDK retries stay disabled so durable agent steps and `llm/retry` events own every visible attempt, and direct `ctx.llm.stream()` calls remain single-attempt.
- **Streamed tool-call arguments are parsed once, when the call ends** — the installed pi-ai carries [`patches/@earendil-works__pi-ai@1.0.2.patch`](../../../patches/@earendil-works__pi-ai@1.0.2.patch), which removes the per-delta re-parse of the whole accumulated argument JSON in every stream adapter (upstream [earendil-works/pi#9265](https://github.com/earendil-works/pi/issues/9265)); unpatched, a multi-megabyte argument stream costs O(n²) CPU on the event loop and stalls every session in the process. Until `toolcall_end`, a pi-ai partial's tool-call `arguments` stays `{}`; this adapter reads only the delta strings and the finalized arguments. Re-apply or retire the patch on every pi-ai upgrade.
- **Streamed tool-call arguments are parsed once, when the call ends** — the installed pi-ai carries [`patches/@earendil-works__pi-ai@1.1.0.patch`](../../../patches/@earendil-works__pi-ai@1.1.0.patch), which removes the per-delta re-parse of the whole accumulated argument JSON in every stream adapter (upstream [earendil-works/pi#9265](https://github.com/earendil-works/pi/issues/9265)); unpatched, a multi-megabyte argument stream costs O(n²) CPU on the event loop and stalls every session in the process. Until `toolcall_end`, a pi-ai partial's tool-call `arguments` stays `{}`; this adapter reads only the delta strings and the finalized arguments. Re-apply or retire the patch on every pi-ai upgrade.
<a id="dev-note"></a>
### Dev Note
+2 -2
View File
@@ -229,7 +229,7 @@ pi-ai 事件变成 harness 的推理、文本、工具调用、用量与 finish
- **`headers` 可以携带 redactor 永远看不到的凭据**——profile 解析会拒绝 Fetch 无法表示的名称与值,但该字典仍是纯字符串;以 `apiKeyEnv` 引用存储凭据。
- **发现操作不更改已配置模型**——需显式将发现结果采纳到路由配置中。
- **Anthropic 模型发现最多读取 1,000 个模型**——请求使用 API 的最大页大小,但不会遍历 `has_more`;第一页之外的条目需要手工添加。
- **每条路由一种协议格式**——混合协议目录路由无法承载另一协议格式的模型;把提供方拆到两个路由键是变通办法。
- **目录外模型需要明确的协议**——目录路由保留每个模型的协议,包括 `azure` 下的两种 Azure API。混合协议目录中不存在的模型需要显式设置路由级 `api`;该设置覆盖路由上的所有模型,且必须属于受支持的自定义协议。Azure 部署名称应通过 `AZURE_OPENAI_DEPLOYMENT_NAME_MAP` 设置,模型 ID 保持为目录中的 ID。
- **模态声明不受校验**——声明 `image` 而其网关不支持的模型会在提示词准入后被提供方拒绝。持久图片仍留在历史中,同一误声明模型可能再次失败;切换到纯文本模型仍然可行,因为共享 LLM 运行时会针对该请求把图片引用投影为稳定文本。
- **未认证路由取决于其协议**——不点名凭据的路由解析为已配置但无密钥,但 pi-ai 的 OpenAI 兼容实现仍要求 API 密钥或 `Authorization` 标头,因此无密钥本地服务器需要由 `apiKeyEnv` 引用或 `headers` 中的 `Authorization` 条目提供的占位凭据。
- **不支持 `GenerateOptions.stop`**——pi-ai 的通用流式选项无法跨提供方保证停止序列行为。
@@ -237,7 +237,7 @@ pi-ai 事件变成 harness 的推理、文本、工具调用、用量与 finish
- **图片只在 user 与工具结果消息中被接受**——system、developer 或 assistant 历史中的图片在两条转换路径上都会以 `UNSUPPORTED_CONTENT` 失败。
- **提供方 HTTP 状态不可用**——pi-ai 错误事件不跨提供方暴露稳定 HTTP 状态。
- **重试策略由提供方自有,而非 SDK 重试**——pi-ai SDK 重试保持禁用,因此持久 agent(智能体)步骤与 `llm/retry` 事件拥有每个可见尝试,直接 `ctx.llm.stream()` 调用仍是单次尝试。
- **流式工具调用参数只在调用结束时解析一次**——安装的 pi-ai 带有 [`patches/@earendil-works__pi-ai@1.0.2.patch`](../../../patches/@earendil-works__pi-ai@1.0.2.patch),它移除了每个流适配器中对整段累计参数 JSON 的逐 delta 重新解析(上游 [earendil-works/pi#9265](https://github.com/earendil-works/pi/issues/9265));未打补丁时,数 MB 的参数流会在事件循环上消耗 O(n²) CPU,并使进程内所有会话停滞。在 `toolcall_end` 之前,pi-ai partial 的工具调用 `arguments` 保持为 `{}`;本适配器只读取 delta 字符串与最终参数。每次升级 pi-ai 时都要重新应用或撤销该补丁。
- **流式工具调用参数只在调用结束时解析一次**——安装的 pi-ai 带有 [`patches/@earendil-works__pi-ai@1.1.0.patch`](../../../patches/@earendil-works__pi-ai@1.1.0.patch),它移除了每个流适配器中对整段累计参数 JSON 的逐 delta 重新解析(上游 [earendil-works/pi#9265](https://github.com/earendil-works/pi/issues/9265));未打补丁时,数 MB 的参数流会在事件循环上消耗 O(n²) CPU,并使进程内所有会话停滞。在 `toolcall_end` 之前,pi-ai partial 的工具调用 `arguments` 保持为 `{}`;本适配器只读取 delta 字符串与最终参数。每次升级 pi-ai 时都要重新应用或撤销该补丁。
<a id="dev-note"></a>
### 开发备注
+1 -1
View File
@@ -43,7 +43,7 @@
"@deepseek-ai/dsh-brand": "workspace:*",
"@deepseek-ai/dsh-util-values": "workspace:*",
"@deepseek-ai/schemastery": "workspace:~",
"@earendil-works/pi-ai": "^1.0.2"
"@earendil-works/pi-ai": "^1.1.0"
},
"devDependencies": {
"@deepseek-ai/cordis": "workspace:~",
+48 -3
View File
@@ -440,6 +440,48 @@ describe('hand-declared providers', () => {
})
describe('catalog routes with per-model configuration', () => {
it('keeps both Azure protocols under the azure catalog route', () => {
const provider = resolveProfiles({ azure: {} }).get('azure')?.piProvider
const models = provider?.getModels()
expect(provider?.id).toBe('azure')
expect(models).toEqual(expect.arrayContaining([
expect.objectContaining({ provider: 'azure', api: 'azure-openai-responses', id: 'gpt-6.1-sol' }),
expect.objectContaining({ provider: 'azure', api: 'openai-completions', id: 'deepseek-v4-pro' }),
]))
expect(catalogProvider('azure-openai-responses')).toBeUndefined()
})
it('requires a protocol for an uncataloged model on the mixed Azure route', () => {
expect(() => resolveProfiles({ azure: { models: [{ id: 'private-deployment' }] } }))
.toThrow(/needs an api/)
})
it.each(['azure-openai-responses', 'openai-completions'])(
'sends Azure %s requests with the configured deployment name',
async (api) => {
const events = api === 'openai-completions' ? textEvents : [
'{"type":"response.created","response":{"id":"resp_azure"}}',
'{"type":"response.completed","response":{"id":"resp_azure","status":"completed","output":[],"usage":{"input_tokens":3,"output_tokens":1,"total_tokens":4}}}',
]
const server = await mockServer([{ events }])
const provider = resolveProfiles({ azure: {} }).get('azure')!.piProvider!
const model = provider.getModels().find(model => model.api === api)!
const stream = provider.streamSimple(model, normalizeContext({ messages: [] }), {
apiKey: 'test-key',
env: {
AZURE_OPENAI_BASE_URL: server.url,
AZURE_OPENAI_API_VERSION: 'v1',
AZURE_OPENAI_DEPLOYMENT_NAME_MAP: `${model.id}=private-deployment`,
},
})
expect(await stream.result()).toMatchObject({ provider: 'azure', stopReason: 'stop' })
expect(server.requests).toEqual([expect.objectContaining({ model: 'private-deployment' })])
expect(server.paths).toEqual([api === 'openai-completions' ? '/chat/completions' : '/responses?api-version=v1'])
},
)
it('serves the installed catalog untouched when the profile lists no models', async () => {
const server = await mockServer([])
const ctx = await harness({ providers: { deepseek: { baseURL: server.url } } })
@@ -1079,10 +1121,11 @@ describe('compat switches', () => {
it('serves the Responses compat type on every protocol pi-ai gives it to', () => {
// pi-ai types azure-openai-responses and openai-codex-responses with the
// same OpenAIResponsesCompat, so a switch settable on one is settable on all.
for (const route of ['azure-openai-responses', 'openai-codex']) {
for (const route of ['azure', 'openai-codex']) {
const models = modelsOf({ [route]: { compat: { supportsDeveloperRole: false } } }, route)
const [first] = [...models.values()]
expect((first?.compat as { supportsDeveloperRole?: boolean }).supportsDeveloperRole).toBe(false)
const model = [...models.values()].find(model => model.api === (route === 'azure' ? 'azure-openai-responses' : 'openai-codex-responses'))
expect(model).toBeDefined()
expect(model?.compat).toMatchObject({ supportsDeveloperRole: false })
}
})
@@ -1237,6 +1280,8 @@ describe('configurable-provider directory', () => {
expect(offered).toContain('openai-codex')
expect(offered).toContain('anthropic')
expect(offered).toContain('openai')
expect(offered).toContain('azure')
expect(offered).not.toContain('azure-openai-responses')
})
it('leaves out an installed catalog route that ships no chat model', async () => {
@@ -11,7 +11,7 @@ index 38571b31e143e913c2bf59dad73382982e93664a..950e546197d8e84cb3edfca0012939f1
type: "toolcall_delta",
contentIndex: index,
diff --git a/dist/api/bedrock-converse-stream.js b/dist/api/bedrock-converse-stream.js
index 428af807620993a4c90ceb1a1a120d553844be32..d81d290d8b8b97f8e2cdd97cff1ebaf019c5733e 100644
index ca40f1b4a4e99f441503cf364822073a7a6c478a..14c204261b3f6c47f8950cf44f0fb9f86cf2cda7 100644
--- a/dist/api/bedrock-converse-stream.js
+++ b/dist/api/bedrock-converse-stream.js
@@ -461,7 +461,6 @@ function handleContentBlockDelta(event, blocks, output, stream) {
@@ -23,7 +23,7 @@ index 428af807620993a4c90ceb1a1a120d553844be32..d81d290d8b8b97f8e2cdd97cff1ebaf0
}
else if (delta?.reasoningContent) {
diff --git a/dist/api/mistral-conversations.js b/dist/api/mistral-conversations.js
index fbcc5e25ee6752a9c5461b41fa8544a090a17b3f..f540825c4f934f342200db72190599fe95f01d0c 100644
index 2fc3ad343af090a919f5b71454b29f23b009fc21..64aa8a4ab2461484f22066913455518f69157857 100644
--- a/dist/api/mistral-conversations.js
+++ b/dist/api/mistral-conversations.js
@@ -554,7 +554,6 @@ async function consumeChatStream(model, output, stream, mistralStream, onProvide
+10 -10
View File
@@ -10,7 +10,7 @@ overrides:
'@deepseek-ai/schemastery': link:vendor/schemastery
patchedDependencies:
'@earendil-works/pi-ai@1.0.2': 1cc211bc358d30cb58bd975f39c03e296f8a277c3de845514d12f9ae77ebab5f
'@earendil-works/pi-ai@1.1.0': 66eaf960054f10441b93ac3857114e96c1901f6d183713e2f21437b007111644
'@electron/[email protected]': 280e7b2816caf01423097d24e79107a66afeb563a348630ce9928ae8499a5861
'@fortune-sheet/[email protected]': 8791a230763dbf853fd65d627c68c31de37209992851377855e16cf08ea70532
'@fortune-sheet/[email protected]': 2932e52e79afcd4ced08cfa123b5eded786c85043da32f622f3e80af6fc96c37
@@ -10617,8 +10617,8 @@ importers:
specifier: link:../../../vendor/schemastery
version: link:../../../vendor/schemastery
'@earendil-works/pi-ai':
specifier: ^1.0.2
version: 1.0.2(patch_hash=1cc211bc358d30cb58bd975f39c03e296f8a277c3de845514d12f9ae77ebab5f)(@aws-sdk/[email protected])(@modelcontextprotocol/[email protected]([email protected])([email protected]))(@smithy/[email protected])([email protected])([email protected])([email protected])([email protected])
specifier: ^1.1.0
version: 1.1.0(patch_hash=66eaf960054f10441b93ac3857114e96c1901f6d183713e2f21437b007111644)(@aws-sdk/[email protected])(@modelcontextprotocol/[email protected]([email protected])([email protected]))(@smithy/[email protected])([email protected])([email protected])([email protected])([email protected])
devDependencies:
'@deepseek-ai/cordis':
specifier: workspace:~
@@ -15961,13 +15961,13 @@ packages:
search-insights:
optional: true
'@earendil-works/pi-ai@1.0.2':
resolution: {integrity: sha512-JP59xGlSAQ/HhQ6EHN5qmneBlWfDyu6FPPryAvAaSo8A38ubAsEpS9YisCM5Jbbns0srrDffLZonE0OXorHI5A==}
'@earendil-works/pi-ai@1.1.0':
resolution: {integrity: sha512-1T7LAkc/5Bvc0v6w4vAGVdCrli0o/E0pEmYKTnixu95vSFArBjvbhS/G4ZwI0RUePgf0Imcu0VyqlM4EcXxqfw==}
engines: {node: '>=22.19.0'}
hasBin: true
'@earendil-works/pi-telemetry@1.0.2':
resolution: {integrity: sha512-Ev5h9TE8nEXHCfRKhWj0qpUFhQgVqNteS95vsM2KNGRGm533vOO2ruDPaEwImBKPMBKXy4omz3D6V2cm41gLOQ==}
'@earendil-works/pi-telemetry@1.1.0':
resolution: {integrity: sha512-8gAK05/2pPozZZz6hCLOi8x2vsqzvsmO9gG92kbvuvTPm0qMHaFlaXX98ndXsrOi5pmzha7vNLguuG4+0Rgxjw==}
engines: {node: '>=22.19.0'}
'@electron-internal/[email protected]':
@@ -23697,11 +23697,11 @@ snapshots:
transitivePeerDependencies:
- '@algolia/client-search'
'@earendil-works/pi-ai@1.0.2(patch_hash=1cc211bc358d30cb58bd975f39c03e296f8a277c3de845514d12f9ae77ebab5f)(@aws-sdk/[email protected])(@modelcontextprotocol/[email protected]([email protected])([email protected]))(@smithy/[email protected])([email protected])([email protected])([email protected])([email protected])':
'@earendil-works/pi-ai@1.1.0(patch_hash=66eaf960054f10441b93ac3857114e96c1901f6d183713e2f21437b007111644)(@aws-sdk/[email protected])(@modelcontextprotocol/[email protected]([email protected])([email protected]))(@smithy/[email protected])([email protected])([email protected])([email protected])([email protected])':
dependencies:
'@anthropic-ai/sdk': 0.129.0([email protected])
'@aws-sdk/client-bedrock-runtime': 3.1127.0
'@earendil-works/pi-telemetry': 1.0.2
'@earendil-works/pi-telemetry': 1.1.0
'@google/genai': 2.21.0(@modelcontextprotocol/[email protected]([email protected])([email protected]))([email protected])
'@smithy/node-http-handler': 4.12.1
http-proxy-agent: 9.1.0([email protected])
@@ -23722,7 +23722,7 @@ snapshots:
- ws
- zod
'@earendil-works/pi-telemetry@1.0.2': {}
'@earendil-works/pi-telemetry@1.1.0': {}
'@electron-internal/[email protected]': {}
+3 -3
View File
@@ -64,8 +64,8 @@ minimumReleaseAgeExclude:
- '@deepseek-ai/[email protected]'
# Fresh pi-ai releases carry the model catalog updates that are the whole
# point of bumping it; waiting out the release age would defeat that.
- '@earendil-works/pi-ai@1.0.2'
- '@earendil-works/pi-telemetry@1.0.2'
- '@earendil-works/pi-ai@1.1.0'
- '@earendil-works/pi-telemetry@1.1.0'
# This addon release recognizes the Electron 44.7.0 Node/V8 fingerprint used by Desktop.
- [email protected]
- [email protected]
@@ -106,7 +106,7 @@ minimumReleaseAgeExclude:
# Desktop packaging requires this Electron fingerprint with node-addon-require-builtin 0.1.9.
- [email protected]
patchedDependencies:
'@earendil-works/pi-ai@1.0.2': patches/@earendil-works__pi-ai@1.0.2.patch
'@earendil-works/pi-ai@1.1.0': patches/@earendil-works__pi-ai@1.1.0.patch
'@electron/[email protected]': patches/@[email protected]
'@fortune-sheet/[email protected]': patches/@[email protected]
'@fortune-sheet/[email protected]': patches/@[email protected]