From 09b83c3c5326277e5e225841c4942e9b13d0ed0e Mon Sep 17 00:00:00 2001 From: Tianyi Cui <53024+tianyicui@users.noreply.github.com> Date: Fri, 16 Oct 2026 01:17:29 +0800 Subject: [PATCH 1/8] test(web): clear hover before keyboard panel snapshots --- apps/web/tests/README.i18n.yaml | 4 ++-- apps/web/tests/README.md | 2 ++ apps/web/tests/README.zh.md | 2 ++ apps/web/tests/shortcuts-panels.e2e.ts | 4 ++++ 4 files changed, 10 insertions(+), 2 deletions(-) diff --git a/apps/web/tests/README.i18n.yaml b/apps/web/tests/README.i18n.yaml index 9517ab8962..c60764a7d4 100644 --- a/apps/web/tests/README.i18n.yaml +++ b/apps/web/tests/README.i18n.yaml @@ -6,8 +6,8 @@ en: 3e8ecd3509fa944f zh: 3fd3ff9fcd1e1e90 /apps-web-browser-e2e/completion-observations: - en: db3a0b5c98fcb03c - zh: b58d917ee693cdc4 + en: 6f8629c8d036f142 + zh: 21c10231c062817c /apps-web-browser-e2e/these-are-host-face-tests: en: 8c406d423c8e2137 zh: a66a144fd146f56c diff --git a/apps/web/tests/README.md b/apps/web/tests/README.md index fa5f5735ba..05cd758c7a 100644 --- a/apps/web/tests/README.md +++ b/apps/web/tests/README.md @@ -24,6 +24,8 @@ Plugin-manager and HMR lifecycle switches wait up to `PLUGIN_TOGGLE_SETTLE_MS` ( Explicit scrolling uses `scrollIntoView` from `support.ts`: it resolves the locator again when its old element detaches and checks connection in the same browser task as native scrolling. Scenarios retain their visibility and geometry assertions after scrolling. +[Sidebar keyboard snapshots](shortcuts-panels.e2e.ts) move the pointer outside the controls and wait for hover tooltips to close before capture. Keyboard focus remains on the pane. + ## These are Host-face tests They type-check in the root `tsconfig.host.json`, not in the Client aggregate, because they read Host services directly: `ctx.connection`, the Host `SessionStore`, and `ctx.sessionProjectionCache`. Driving a browser at runtime does not make a file part of the Client program — the two faces merge Cordis `Context` under the same keys with different services, so one program cannot see both. Moving these files into the Client aggregate makes every Host-service access fail to compile. diff --git a/apps/web/tests/README.zh.md b/apps/web/tests/README.zh.md index a41c87c3a6..fa0892224f 100644 --- a/apps/web/tests/README.zh.md +++ b/apps/web/tests/README.zh.md @@ -24,6 +24,8 @@ pnpm --filter @deepseek-ai/dsh-web-frontend exec playwright install --with-deps 显式滚动使用 `support.ts` 的 `scrollIntoView`:旧元素脱离 DOM 时重新解析 locator,并在同一个浏览器任务中检查连接状态、执行原生滚动。各场景保留滚动后的可见性与几何断言。 +[侧栏键盘快照](shortcuts-panels.e2e.ts)在捕获前将指针移出控件,并等待悬停提示关闭。键盘焦点保持在窗格上。 + ## 这些是 Host 面的测试 它们在根 `tsconfig.host.json` 中做类型检查,而不在 Client aggregate 中,因为它们直接读取 Host 服务:`ctx.connection`、Host 侧 `SessionStore` 与 `ctx.sessionProjectionCache`。运行时驱动浏览器并不使一个文件成为 Client 程序的一部分——两个 face 在相同的键上以不同服务合并 Cordis `Context`,因此单个程序无法同时看见两者。把这些文件挪进 Client aggregate 会让每一处 Host 服务访问都无法编译。 diff --git a/apps/web/tests/shortcuts-panels.e2e.ts b/apps/web/tests/shortcuts-panels.e2e.ts index 9968341f1b..ad117b88c4 100644 --- a/apps/web/tests/shortcuts-panels.e2e.ts +++ b/apps/web/tests/shortcuts-panels.e2e.ts @@ -282,9 +282,13 @@ describe.skipIf(process.platform === 'win32')('Web sidebar shortcuts', () => { await panel.getByRole('button', { name: 'Fullscreen', exact: true }).click() await panel.locator('[data-dockkit-split-button]').click() await expect.poll(() => panel.locator('[data-dockkit-pane]').count()).toBe(2) + await right.locator('[data-dockkit-split-button]').locator('..').hover() + await page.getByRole('tooltip', { name: 'Two panes is the limit', exact: true }).waitFor() await (closeFirst === 'left' ? left : right).getByRole('tab').focus() await page.keyboard.press(`${primary}+Shift+,`) await expect.poll(() => panel.getByRole('tab').count()).toBe(1) + await page.mouse.move(10, 10) + await expect.poll(() => panel.getByRole('tooltip').count()).toBe(0) expect(await panel.locator('[data-dockkit-pane]').evaluate(pane => pane === document.activeElement)).toBe(true) expect(await paneAppearance()).toEqual([plainPane]) await compareOrRefreshGolden(join(expected, 'guide-close.expected.md'), From 64be7f033bad9cdf194af5f50457467955d96d23 Mon Sep 17 00:00:00 2001 From: Tianyi Cui <53024+tianyicui@users.noreply.github.com> Date: Fri, 16 Oct 2026 02:52:21 +0800 Subject: [PATCH 2/8] docs(web): clarify sidebar hover setup --- apps/web/tests/README.i18n.yaml | 4 ++-- apps/web/tests/README.md | 2 +- apps/web/tests/README.zh.md | 2 +- apps/web/tests/shortcuts-panels.e2e.ts | 1 + 4 files changed, 5 insertions(+), 4 deletions(-) diff --git a/apps/web/tests/README.i18n.yaml b/apps/web/tests/README.i18n.yaml index c60764a7d4..09561f47f9 100644 --- a/apps/web/tests/README.i18n.yaml +++ b/apps/web/tests/README.i18n.yaml @@ -6,8 +6,8 @@ en: 3e8ecd3509fa944f zh: 3fd3ff9fcd1e1e90 /apps-web-browser-e2e/completion-observations: - en: 6f8629c8d036f142 - zh: 21c10231c062817c + en: 97cefc8839aea4c5 + zh: 0f719c9064b249ab /apps-web-browser-e2e/these-are-host-face-tests: en: 8c406d423c8e2137 zh: a66a144fd146f56c diff --git a/apps/web/tests/README.md b/apps/web/tests/README.md index 05cd758c7a..57f3d5f4b5 100644 --- a/apps/web/tests/README.md +++ b/apps/web/tests/README.md @@ -24,7 +24,7 @@ Plugin-manager and HMR lifecycle switches wait up to `PLUGIN_TOGGLE_SETTLE_MS` ( Explicit scrolling uses `scrollIntoView` from `support.ts`: it resolves the locator again when its old element detaches and checks connection in the same browser task as native scrolling. Scenarios retain their visibility and geometry assertions after scrolling. -[Sidebar keyboard snapshots](shortcuts-panels.e2e.ts) move the pointer outside the controls and wait for hover tooltips to close before capture. Keyboard focus remains on the pane. +[The snapshot after closing a sidebar pane](shortcuts-panels.e2e.ts) moves the pointer outside the controls and waits for hover tooltips to close before capture. Keyboard focus remains on the pane. ## These are Host-face tests diff --git a/apps/web/tests/README.zh.md b/apps/web/tests/README.zh.md index fa0892224f..2bbd1e06b9 100644 --- a/apps/web/tests/README.zh.md +++ b/apps/web/tests/README.zh.md @@ -24,7 +24,7 @@ pnpm --filter @deepseek-ai/dsh-web-frontend exec playwright install --with-deps 显式滚动使用 `support.ts` 的 `scrollIntoView`:旧元素脱离 DOM 时重新解析 locator,并在同一个浏览器任务中检查连接状态、执行原生滚动。各场景保留滚动后的可见性与几何断言。 -[侧栏键盘快照](shortcuts-panels.e2e.ts)在捕获前将指针移出控件,并等待悬停提示关闭。键盘焦点保持在窗格上。 +[侧栏窗格关闭后的快照](shortcuts-panels.e2e.ts)在捕获前将指针移出控件,并等待悬停提示关闭。键盘焦点保持在窗格上。 ## 这些是 Host 面的测试 diff --git a/apps/web/tests/shortcuts-panels.e2e.ts b/apps/web/tests/shortcuts-panels.e2e.ts index ad117b88c4..2036dad448 100644 --- a/apps/web/tests/shortcuts-panels.e2e.ts +++ b/apps/web/tests/shortcuts-panels.e2e.ts @@ -282,6 +282,7 @@ describe.skipIf(process.platform === 'win32')('Web sidebar shortcuts', () => { await panel.getByRole('button', { name: 'Fullscreen', exact: true }).click() await panel.locator('[data-dockkit-split-button]').click() await expect.poll(() => panel.locator('[data-dockkit-pane]').count()).toBe(2) + // Pane closure can leave the Split tooltip open beneath the stationary pointer. await right.locator('[data-dockkit-split-button]').locator('..').hover() await page.getByRole('tooltip', { name: 'Two panes is the limit', exact: true }).waitFor() await (closeFirst === 'left' ? left : right).getByRole('tab').focus() From ef055d25d07268dd6e337e97e1a62337fce3aaef Mon Sep 17 00:00:00 2001 From: Tianyi Cui <53024+tianyicui@users.noreply.github.com> Date: Thu, 15 Oct 2026 21:53:07 +0800 Subject: [PATCH 3/8] test(preview): await renderer version effects --- .../tests/renderer-loading.client.spec.tsx | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/packages/client/ui-sidebar-documentpreview/tests/renderer-loading.client.spec.tsx b/packages/client/ui-sidebar-documentpreview/tests/renderer-loading.client.spec.tsx index 8566142c5c..84fdab2c51 100644 --- a/packages/client/ui-sidebar-documentpreview/tests/renderer-loading.client.spec.tsx +++ b/packages/client/ui-sidebar-documentpreview/tests/renderer-loading.client.spec.tsx @@ -27,9 +27,11 @@ it('lets a non-Office renderer load content, report its version, and reload thro const custom: DocumentPreviewDefinition = { id: 'custom-viewer', extensions: ['md'], binaryExtensions: ['md'], title: () => 'Custom', loading: 'renderer', } + const initialRead = Promise.withResolvers<{ text: string; version: string }>() + const reloadedRead = Promise.withResolvers<{ text: string; version: string }>() const read = vi.fn<(signal: AbortSignal) => Promise<{ text: string; version: string }>>() - .mockResolvedValueOnce({ text: 'Custom content v1', version: 'v1' }) - .mockResolvedValueOnce({ text: 'Custom content v2', version: 'v2' }) + .mockReturnValueOnce(initialRead.promise) + .mockReturnValueOnce(reloadedRead.promise) function CustomBody({ content }: { content: DocumentContent }) { const [file, setFile] = useState<{ text: string; version: string; revision: number }>() const request = content.kind === 'renderer' ? content : undefined @@ -55,7 +57,11 @@ it('lets a non-Office renderer load content, report its version, and reload thro const useDocumentPreviews: TextPreviewProps['useDocumentPreviews'] = selector => selector([custom]) const view = render() expect(read).toHaveBeenCalledTimes(1) - expect(await screen.findByText('Custom content v1')).toBeTruthy() + expect(screen.getByText('Loading custom content')).toBeTruthy() + expect(h.instance.getSnapshot().byTab[TAB_ID]?.version).toBeUndefined() + // Resolving inside act waits for the DOM commit and the version-reporting effect. + await act(async () => { initialRead.resolve({ text: 'Custom content v1', version: 'v1' }) }) + expect(screen.getByText('Custom content v1')).toBeTruthy() expect(h.instance.getSnapshot().byTab[TAB_ID]?.version).toBe('v1') act(() => { h.instance.actions.toggledAutoRefresh(TAB_ID) }) h.setVersion('v2') @@ -63,7 +69,9 @@ it('lets a non-Office renderer load content, report its version, and reload thro expect(screen.getByText('changed')).toBeTruthy() expect(read).toHaveBeenCalledTimes(1) fireEvent.click(screen.getByRole('button', { name: 'reloadNow' })) - expect(await screen.findByText('Custom content v2')).toBeTruthy() + expect(screen.getByText('Loading custom content')).toBeTruthy() + await act(async () => { reloadedRead.resolve({ text: 'Custom content v2', version: 'v2' }) }) + expect(screen.getByText('Custom content v2')).toBeTruthy() expect(read).toHaveBeenCalledTimes(2) expect(h.instance.getSnapshot().byTab[TAB_ID]?.version).toBe('v2') expect(screen.queryByText('changed')).toBeNull() From d849cfdd15eac0c35734b77a6a439cbe6a732d5d Mon Sep 17 00:00:00 2001 From: Tianyi Cui <53024+tianyicui@users.noreply.github.com> Date: Sat, 17 Oct 2026 22:43:54 +0800 Subject: [PATCH 4/8] test(web): await terminal close before checking process cleanup --- apps/web/tests/README.i18n.yaml | 4 ++-- apps/web/tests/README.md | 2 ++ apps/web/tests/README.zh.md | 2 ++ apps/web/tests/sidebar-terminal.e2e.ts | 12 ++++++++++-- 4 files changed, 16 insertions(+), 4 deletions(-) diff --git a/apps/web/tests/README.i18n.yaml b/apps/web/tests/README.i18n.yaml index 09561f47f9..9dc3bf2779 100644 --- a/apps/web/tests/README.i18n.yaml +++ b/apps/web/tests/README.i18n.yaml @@ -6,8 +6,8 @@ en: 3e8ecd3509fa944f zh: 3fd3ff9fcd1e1e90 /apps-web-browser-e2e/completion-observations: - en: 97cefc8839aea4c5 - zh: 0f719c9064b249ab + en: 1f9c004f3eb465fb + zh: 3e45ef8014bad48d /apps-web-browser-e2e/these-are-host-face-tests: en: 8c406d423c8e2137 zh: a66a144fd146f56c diff --git a/apps/web/tests/README.md b/apps/web/tests/README.md index 57f3d5f4b5..55b9ed5d4c 100644 --- a/apps/web/tests/README.md +++ b/apps/web/tests/README.md @@ -26,6 +26,8 @@ Explicit scrolling uses `scrollIntoView` from `support.ts`: it resolves the loca [The snapshot after closing a sidebar pane](shortcuts-panels.e2e.ts) moves the pointer outside the controls and waits for hover tooltips to close before capture. Keyboard focus remains on the pane. +[The same-origin terminal case](sidebar-terminal.e2e.ts) waits for the UI-triggered Host close operation before checking process termination. It also checks the first window's process, saved bindings and layout after reload. + ## These are Host-face tests They type-check in the root `tsconfig.host.json`, not in the Client aggregate, because they read Host services directly: `ctx.connection`, the Host `SessionStore`, and `ctx.sessionProjectionCache`. Driving a browser at runtime does not make a file part of the Client program — the two faces merge Cordis `Context` under the same keys with different services, so one program cannot see both. Moving these files into the Client aggregate makes every Host-service access fail to compile. diff --git a/apps/web/tests/README.zh.md b/apps/web/tests/README.zh.md index 2bbd1e06b9..1350b245bf 100644 --- a/apps/web/tests/README.zh.md +++ b/apps/web/tests/README.zh.md @@ -26,6 +26,8 @@ pnpm --filter @deepseek-ai/dsh-web-frontend exec playwright install --with-deps [侧栏窗格关闭后的快照](shortcuts-panels.e2e.ts)在捕获前将指针移出控件,并等待悬停提示关闭。键盘焦点保持在窗格上。 +[同源窗口终端用例](sidebar-terminal.e2e.ts)等待 UI 触发的 Host 关闭操作完成后,再检查进程终止。它还检查第一个窗口的进程、保存的绑定以及重载后的布局。 + ## 这些是 Host 面的测试 它们在根 `tsconfig.host.json` 中做类型检查,而不在 Client aggregate 中,因为它们直接读取 Host 服务:`ctx.connection`、Host 侧 `SessionStore` 与 `ctx.sessionProjectionCache`。运行时驱动浏览器并不使一个文件成为 Client 程序的一部分——两个 face 在相同的键上以不同服务合并 Cordis `Context`,因此单个程序无法同时看见两者。把这些文件挪进 Client aggregate 会让每一处 Host 服务访问都无法编译。 diff --git a/apps/web/tests/sidebar-terminal.e2e.ts b/apps/web/tests/sidebar-terminal.e2e.ts index 07ee0c02f2..6709a336ab 100644 --- a/apps/web/tests/sidebar-terminal.e2e.ts +++ b/apps/web/tests/sidebar-terminal.e2e.ts @@ -478,10 +478,18 @@ describe.skipIf(process.platform === 'win32')('Web sidebar terminal', () => { .filter(key => key.startsWith('dsh.terminal.binding.v1.')).map(key => localStorage.getItem(key))) const saved = await bindings() expect(saved).toHaveLength(2) + const closed = Promise.withResolvers() + const close = scaffold.ctx.terminalController.close.bind(scaffold.ctx.terminalController) + vi.spyOn(scaffold.ctx.terminalController, 'close').mockImplementation((agent, id) => { + const result = close(agent, id) + closed.resolve(result.then(() => undefined)) + return result + }) const selected = second.locator('[data-dockkit-tab][aria-selected="true"]') await selected.hover() - await selected.locator('[data-dockkit-tab-close]').click() - await expect.poll(() => alive(secondProcess)).toBe(false) + // Tab removal starts background cleanup; the Host promise includes process termination. + await Promise.all([selected.locator('[data-dockkit-tab-close]').click(), closed.promise]) + expect(alive(secondProcess)).toBe(false) expect(alive(firstProcess)).toBe(true) await expect.poll(() => bindings()).toHaveLength(1) expect(saved).toContain((await bindings())[0]) From 8fd91f311ece4fd186df9afecf05ff89c274edca Mon Sep 17 00:00:00 2001 From: Tianyi Cui <53024+tianyicui@users.noreply.github.com> Date: Thu, 15 Oct 2026 21:37:23 +0800 Subject: [PATCH 5/8] chore(deps): upgrade pi-ai to 1.2.0 --- THIRD_PARTY_NOTICES.md | 2 +- .../pi-ai-model-catalog/guide.i18n.yaml | 16 +++++++++++ .../pi-ai-model-catalog/guide.md | 27 +++++++++++++++++++ .../pi-ai-model-catalog/guide.zh.md | 27 +++++++++++++++++++ packages/llm/llm-pi-ai/README.i18n.yaml | 4 +-- packages/llm/llm-pi-ai/README.md | 2 +- packages/llm/llm-pi-ai/README.zh.md | 2 +- packages/llm/llm-pi-ai/package.json | 2 +- ...tch => @earendil-works__pi-ai@1.2.0.patch} | 16 +++++------ pnpm-lock.yaml | 20 +++++++------- pnpm-workspace.yaml | 6 ++--- 11 files changed, 97 insertions(+), 27 deletions(-) create mode 100644 docs/upgrade-guide/v0.2.1-alpha.4/pi-ai-model-catalog/guide.i18n.yaml create mode 100644 docs/upgrade-guide/v0.2.1-alpha.4/pi-ai-model-catalog/guide.md create mode 100644 docs/upgrade-guide/v0.2.1-alpha.4/pi-ai-model-catalog/guide.zh.md rename patches/{@earendil-works__pi-ai@1.1.0.patch => @earendil-works__pi-ai@1.2.0.patch} (84%) diff --git a/THIRD_PARTY_NOTICES.md b/THIRD_PARTY_NOTICES.md index 67f867a790..74759ee557 100644 --- a/THIRD_PARTY_NOTICES.md +++ b/THIRD_PARTY_NOTICES.md @@ -145,7 +145,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.1.0` — [`patches/@earendil-works__pi-ai@1.1.0.patch`](patches/@earendil-works__pi-ai@1.1.0.patch) +- `@earendil-works/pi-ai@1.2.0` — [`patches/@earendil-works__pi-ai@1.2.0.patch`](patches/@earendil-works__pi-ai@1.2.0.patch) - `@electron/osx-sign@1.3.3` — [`patches/@electron__osx-sign@1.3.3.patch`](patches/@electron__osx-sign@1.3.3.patch) - `@fortune-sheet/core@1.0.4` — [`patches/@fortune-sheet__core@1.0.4.patch`](patches/@fortune-sheet__core@1.0.4.patch) - `@fortune-sheet/react@1.0.4` — [`patches/@fortune-sheet__react@1.0.4.patch`](patches/@fortune-sheet__react@1.0.4.patch) diff --git a/docs/upgrade-guide/v0.2.1-alpha.4/pi-ai-model-catalog/guide.i18n.yaml b/docs/upgrade-guide/v0.2.1-alpha.4/pi-ai-model-catalog/guide.i18n.yaml new file mode 100644 index 0000000000..08329e03d8 --- /dev/null +++ b/docs/upgrade-guide/v0.2.1-alpha.4/pi-ai-model-catalog/guide.i18n.yaml @@ -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.4/pi-ai-model-catalog/guide.md +/: + en: 3d2fec5fa8405395 + zh: b8617482168609b1 +/pi-ai-model-catalog: + en: ad7d66838bb7f924 + zh: d4b120332acf786f +/pi-ai-model-catalog/change: + en: 0948c8c82d4f8256 + zh: 763504917909d130 +/pi-ai-model-catalog/migration: + en: 6db9f70e6db44b3c + zh: 34156f7446a78bc1 diff --git a/docs/upgrade-guide/v0.2.1-alpha.4/pi-ai-model-catalog/guide.md b/docs/upgrade-guide/v0.2.1-alpha.4/pi-ai-model-catalog/guide.md new file mode 100644 index 0000000000..72c9d8eaf6 --- /dev/null +++ b/docs/upgrade-guide/v0.2.1-alpha.4/pi-ai-model-catalog/guide.md @@ -0,0 +1,27 @@ +--- +kind: upgrade-guide +description: "The pi-ai 1.2.0 catalog removes model IDs and lowers the default context limit for Anthropic Sonnet 4.5." +--- + +# pi-ai model catalog + +English | [中文](guide.zh.md) + +## Change + +The pi-ai dependency changes from 1.1.0 to 1.2.0. Routes that use its catalog lose the following model IDs. Stored `modelOverrides` for these IDs report a missing-catalog diagnostic; new overrides are rejected. An explicit `models` entry can still declare an ID, but it loses its catalog defaults. + +| Provider | Removed model IDs | +|---|---| +| `google-vertex` | `gemini-flash-latest`, `gemini-flash-lite-latest` | +| `opencode` | `fledge-alpha-free` | +| `openrouter` | `inclusionai/ling-3.0-flash-sante:free`, `qwen/qwen-plus-2025-07-28`, `qwen/qwen3-235b-a22b`, `qwen/qwen3-30b-a3b-thinking-2507`, `qwen/qwen3-8b`, `qwen/qwen3-coder-plus`, `qwen/qwen3-max`, `qwen/qwen3-max-thinking`, `qwen/qwen3-vl-235b-a22b-thinking`, `qwen/qwen3-vl-32b-instruct`, `qwen/qwen3-vl-8b-thinking`, `qwen/qwen3.6-max-preview`, `sao10k/l3.1-euryale-70b` | +| `vercel-ai-gateway` | `alibaba/qwen-3.6-max-preview`, `alibaba/qwen3-235b-a22b-thinking`, `alibaba/qwen3-coder-plus`, `alibaba/qwen3-max`, `alibaba/qwen3-max-preview`, `alibaba/qwen3-vl-235b-a22b-instruct`, `alibaba/qwen3-vl-thinking`, `anthropic/claude-3-haiku`, `anthropic/claude-opus-4` | + +For the `anthropic` provider, `claude-sonnet-4-5` and `claude-sonnet-4-5-20250929` have a default `contextWindow` of 200,000 tokens, down from 1,000,000. Explicit configuration overrides still take precedence. The `vercel-ai-gateway` model `alibaba/qwen-3-235b` is now declared non-reasoning. + +## Migration + +1. In Settings → Models, select a model from the updated provider catalog. Check that its endpoint, capabilities, and price fit your use; removed IDs have no automatic replacement. +2. Update model selections in your profile's `cordis.patch.yml`, preset patches, and affected Sessions. Remove obsolete `providers..modelOverrides.` entries. Replace removed IDs in explicit `providers..models` lists with supported models and review their declared capacities and capabilities. See the [pi-ai configuration reference](../../../../packages/llm/llm-pi-ai/README.md). +3. Review explicit Sonnet 4.5 context overrides against the endpoint's supported limit. Start the affected profile, confirm its model has no configuration diagnostic, and send a short prompt to verify the selected route. diff --git a/docs/upgrade-guide/v0.2.1-alpha.4/pi-ai-model-catalog/guide.zh.md b/docs/upgrade-guide/v0.2.1-alpha.4/pi-ai-model-catalog/guide.zh.md new file mode 100644 index 0000000000..ad87c026b4 --- /dev/null +++ b/docs/upgrade-guide/v0.2.1-alpha.4/pi-ai-model-catalog/guide.zh.md @@ -0,0 +1,27 @@ +--- +kind: upgrade-guide +description: "pi-ai 1.2.0 目录移除了部分模型 ID,并降低了 Anthropic Sonnet 4.5 的默认上下文限制。" +--- + +# pi-ai 模型目录 + +[English](guide.md) | 中文 + +## 变更 + +pi-ai 依赖从 1.1.0 升级到 1.2.0。使用其目录的路由不再包含以下模型 ID。已保存的 `modelOverrides` 若引用这些 ID,会报告目录条目缺失的诊断;新的覆盖配置会被拒绝。显式 `models` 条目仍可声明这些 ID,但会失去目录默认值。 + +| 提供商 | 已移除的模型 ID | +|---|---| +| `google-vertex` | `gemini-flash-latest`, `gemini-flash-lite-latest` | +| `opencode` | `fledge-alpha-free` | +| `openrouter` | `inclusionai/ling-3.0-flash-sante:free`, `qwen/qwen-plus-2025-07-28`, `qwen/qwen3-235b-a22b`, `qwen/qwen3-30b-a3b-thinking-2507`, `qwen/qwen3-8b`, `qwen/qwen3-coder-plus`, `qwen/qwen3-max`, `qwen/qwen3-max-thinking`, `qwen/qwen3-vl-235b-a22b-thinking`, `qwen/qwen3-vl-32b-instruct`, `qwen/qwen3-vl-8b-thinking`, `qwen/qwen3.6-max-preview`, `sao10k/l3.1-euryale-70b` | +| `vercel-ai-gateway` | `alibaba/qwen-3.6-max-preview`, `alibaba/qwen3-235b-a22b-thinking`, `alibaba/qwen3-coder-plus`, `alibaba/qwen3-max`, `alibaba/qwen3-max-preview`, `alibaba/qwen3-vl-235b-a22b-instruct`, `alibaba/qwen3-vl-thinking`, `anthropic/claude-3-haiku`, `anthropic/claude-opus-4` | + +在 `anthropic` 提供商中,`claude-sonnet-4-5` 和 `claude-sonnet-4-5-20250929` 的默认 `contextWindow` 从 1,000,000 token 降为 200,000 token。显式配置的覆盖值仍然优先。`vercel-ai-gateway` 的 `alibaba/qwen-3-235b` 模型现在被声明为非推理模型。 + +## 迁移 + +1. 在“设置 → 模型”中,从更新后的提供商目录选择模型。确认其端点、能力和价格符合用途;已移除的 ID 没有自动替代项。 +2. 更新 profile 的 `cordis.patch.yml`、preset patch 和受影响 Session 中的模型选择。移除过时的 `providers..modelOverrides.` 条目。将显式 `providers..models` 列表中已移除的 ID 替换为受支持的模型,并检查其声明的容量和能力。参见 [pi-ai 配置参考](../../../../packages/llm/llm-pi-ai/README.zh.md)。 +3. 根据端点支持的限制检查显式 Sonnet 4.5 上下文覆盖值。启动受影响的 profile,确认模型没有配置诊断,再发送一个简短提示,验证所选路由。 diff --git a/packages/llm/llm-pi-ai/README.i18n.yaml b/packages/llm/llm-pi-ai/README.i18n.yaml index b751a19d3c..ab600163c4 100644 --- a/packages/llm/llm-pi-ai/README.i18n.yaml +++ b/packages/llm/llm-pi-ai/README.i18n.yaml @@ -90,8 +90,8 @@ en: b6bd570c0159ba08 zh: 7d2d91eea6a8d830 /deepseek-ai-dsh-llm-pi-ai/known-limitations-and-deferred-work: - en: ee870ac2ac87decc - zh: 9e66c5e7400c4bc6 + en: 21b4435e0a9f3564 + zh: 7bc2dba9294b0da3 /deepseek-ai-dsh-llm-pi-ai/known-limitations-and-deferred-work/dev-note: en: 3678dd0f6642c2a5 zh: 75cbdd064cc64282 diff --git a/packages/llm/llm-pi-ai/README.md b/packages/llm/llm-pi-ai/README.md index af66591776..f8db8f5c8a 100644 --- a/packages/llm/llm-pi-ai/README.md +++ b/packages/llm/llm-pi-ai/README.md @@ -257,7 +257,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 JSON tool-call arguments avoid per-delta reparsing** — 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. Completion events supply parsed arguments; Responses may parse at both argument completion and output-item completion. This adapter reads only delta strings and finalized arguments. Re-apply or retire the patch on every pi-ai upgrade. +- **Streamed JSON tool-call arguments avoid per-delta reparsing** — the installed pi-ai carries [`patches/@earendil-works__pi-ai@1.2.0.patch`](../../../patches/@earendil-works__pi-ai@1.2.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. Completion events supply parsed arguments; Responses may parse at both argument completion and output-item completion. This adapter reads only delta strings and finalized arguments. Re-apply or retire the patch on every pi-ai upgrade. ### Dev Note diff --git a/packages/llm/llm-pi-ai/README.zh.md b/packages/llm/llm-pi-ai/README.zh.md index b5e79c14a2..3837f4b99e 100644 --- a/packages/llm/llm-pi-ai/README.zh.md +++ b/packages/llm/llm-pi-ai/README.zh.md @@ -257,7 +257,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()` 调用仍是单次尝试。 -- **流式 JSON 工具调用参数避免逐 delta 重复解析**——安装的 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,并使进程内所有会话停滞。完成事件提供解析后的参数;Responses 可能在参数完成和输出项完成时分别解析。本适配器只读取 delta 字符串与最终参数。每次升级 pi-ai 时都要重新应用或撤销该补丁。 +- **流式 JSON 工具调用参数避免逐 delta 重复解析**——安装的 pi-ai 带有 [`patches/@earendil-works__pi-ai@1.2.0.patch`](../../../patches/@earendil-works__pi-ai@1.2.0.patch),它移除了每个流适配器中对整段累计参数 JSON 的逐 delta 重新解析(上游 [earendil-works/pi#9265](https://github.com/earendil-works/pi/issues/9265));未打补丁时,数 MB 的参数流会在事件循环上消耗 O(n²) CPU,并使进程内所有会话停滞。完成事件提供解析后的参数;Responses 可能在参数完成和输出项完成时分别解析。本适配器只读取 delta 字符串与最终参数。每次升级 pi-ai 时都要重新应用或撤销该补丁。 ### 开发备注 diff --git a/packages/llm/llm-pi-ai/package.json b/packages/llm/llm-pi-ai/package.json index df62e71425..7c0d07c428 100644 --- a/packages/llm/llm-pi-ai/package.json +++ b/packages/llm/llm-pi-ai/package.json @@ -43,7 +43,7 @@ "@deepseek-ai/dsh-brand": "workspace:*", "@deepseek-ai/dsh-util-values": "workspace:*", "@deepseek-ai/schemastery": "workspace:~", - "@earendil-works/pi-ai": "^1.1.0" + "@earendil-works/pi-ai": "^1.2.0" }, "devDependencies": { "@deepseek-ai/cordis": "workspace:~", diff --git a/patches/@earendil-works__pi-ai@1.1.0.patch b/patches/@earendil-works__pi-ai@1.2.0.patch similarity index 84% rename from patches/@earendil-works__pi-ai@1.1.0.patch rename to patches/@earendil-works__pi-ai@1.2.0.patch index d1a3b542c2..b992efd197 100644 --- a/patches/@earendil-works__pi-ai@1.1.0.patch +++ b/patches/@earendil-works__pi-ai@1.2.0.patch @@ -1,8 +1,8 @@ diff --git a/dist/api/anthropic-messages.js b/dist/api/anthropic-messages.js -index 38571b31e143e913c2bf59dad73382982e93664a..950e546197d8e84cb3edfca0012939f1db2488d8 100644 +index d0a5b0f1bcd77e4afff2c92b068a6e3150dcdd86..999a32b27b53838851eb0578fafadea5cb1343f6 100644 --- a/dist/api/anthropic-messages.js +++ b/dist/api/anthropic-messages.js -@@ -553,7 +553,6 @@ export const stream = (model, context, options) => { +@@ -585,7 +585,6 @@ const block = blocks[index]; if (block && block.type === "toolCall") { block.partialJson += event.delta.partial_json; @@ -14,7 +14,7 @@ diff --git a/dist/api/bedrock-converse-stream.js b/dist/api/bedrock-converse-str 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) { +@@ -461,7 +461,6 @@ } else if (delta?.toolUse && block?.type === "toolCall") { block.partialJson = (block.partialJson || "") + (delta.toolUse.input || ""); @@ -23,10 +23,10 @@ index ca40f1b4a4e99f441503cf364822073a7a6c478a..14c204261b3f6c47f8950cf44f0fb9f8 } else if (delta?.reasoningContent) { diff --git a/dist/api/mistral-conversations.js b/dist/api/mistral-conversations.js -index 2fc3ad343af090a919f5b71454b29f23b009fc21..64aa8a4ab2461484f22066913455518f69157857 100644 +index eb52675006e4f048aeefde05acceb474e918473e..0cfa1ebc2b277d38fa9515262ca4445c7e22a23e 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 +@@ -570,7 +570,6 @@ ? toolCall.function.arguments : JSON.stringify(toolCall.function.arguments || {}); block.partialArgs = (block.partialArgs || "") + argsDelta; @@ -38,7 +38,7 @@ diff --git a/dist/api/openai-completions.js b/dist/api/openai-completions.js index a60f39352b776d71497e55b33b3334bacfbeb2f5..6c9b98e35606c52e4f0fb29d8fd0b5ba02d92e09 100644 --- a/dist/api/openai-completions.js +++ b/dist/api/openai-completions.js -@@ -438,7 +438,6 @@ export const stream = (model, context, options) => { +@@ -438,7 +438,6 @@ if (toolCall.function?.arguments) { delta = toolCall.function.arguments; block.partialArgs = (block.partialArgs ?? "") + toolCall.function.arguments; @@ -50,7 +50,7 @@ diff --git a/dist/api/openai-responses-shared.js b/dist/api/openai-responses-sha index f413fe5f2a1abcac39c38bbd4a77b38c349a5918..108f599ae1d985d2f465e8cdf99ce0624acea6aa 100644 --- a/dist/api/openai-responses-shared.js +++ b/dist/api/openai-responses-shared.js -@@ -548,7 +548,6 @@ export async function processResponsesStream(openaiStream, output, stream, model +@@ -548,7 +548,6 @@ if (!slot || slot.block.partialJson === undefined) continue; slot.block.partialJson += event.delta; @@ -62,7 +62,7 @@ diff --git a/dist/api/pi-messages.js b/dist/api/pi-messages.js index 7da1c45de8a106a68411cf79bec75f553534a1c7..296d131b59b1cf75f2516c39e85bba72972b2c17 100644 --- a/dist/api/pi-messages.js +++ b/dist/api/pi-messages.js -@@ -155,8 +155,6 @@ function createEventConverter(model) { +@@ -155,8 +155,6 @@ case "toolcall_delta": { const json = `${toolJson.get(event.contentIndex) ?? ""}${event.delta}`; toolJson.set(event.contentIndex, json); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5f38bae356..f6162aae1f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -10,7 +10,7 @@ overrides: '@deepseek-ai/schemastery': link:vendor/schemastery patchedDependencies: - '@earendil-works/pi-ai@1.1.0': 66eaf960054f10441b93ac3857114e96c1901f6d183713e2f21437b007111644 + '@earendil-works/pi-ai@1.2.0': 4202cd05e0250bf7bc1b010b9aa8a50eb4bb7d0c5364195397842d26972b4b6f '@electron/osx-sign@1.3.3': 280e7b2816caf01423097d24e79107a66afeb563a348630ce9928ae8499a5861 '@fortune-sheet/core@1.0.4': 8791a230763dbf853fd65d627c68c31de37209992851377855e16cf08ea70532 '@fortune-sheet/react@1.0.4': 2932e52e79afcd4ced08cfa123b5eded786c85043da32f622f3e80af6fc96c37 @@ -11167,8 +11167,8 @@ importers: specifier: link:../../../vendor/schemastery version: link:../../../vendor/schemastery '@earendil-works/pi-ai': - specifier: ^1.1.0 - version: 1.1.0(patch_hash=66eaf960054f10441b93ac3857114e96c1901f6d183713e2f21437b007111644)(@aws-sdk/credential-provider-node@3.972.84)(@modelcontextprotocol/sdk@1.31.0(supports-color@9.4.0)(zod@4.4.3))(@smithy/signature-v4@5.7.4)(supports-color@9.4.0)(undici@8.11.2)(ws@8.21.3)(zod@4.4.3) + specifier: ^1.2.0 + version: 1.2.0(patch_hash=4202cd05e0250bf7bc1b010b9aa8a50eb4bb7d0c5364195397842d26972b4b6f)(@aws-sdk/credential-provider-node@3.972.84)(@modelcontextprotocol/sdk@1.31.0(supports-color@9.4.0)(zod@4.4.3))(@smithy/signature-v4@5.7.4)(supports-color@9.4.0)(undici@8.11.2)(ws@8.21.3)(zod@4.4.3) devDependencies: '@deepseek-ai/cordis': specifier: workspace:~ @@ -16591,13 +16591,13 @@ packages: search-insights: optional: true - '@earendil-works/pi-ai@1.1.0': - resolution: {integrity: sha512-1T7LAkc/5Bvc0v6w4vAGVdCrli0o/E0pEmYKTnixu95vSFArBjvbhS/G4ZwI0RUePgf0Imcu0VyqlM4EcXxqfw==} + '@earendil-works/pi-ai@1.2.0': + resolution: {integrity: sha512-U5/1zXqfV1on7/9sbez03tT4Ru49Lvfb4+zXSDK3DBKF5cRk6KxvAOTOY3HJ4jimlZth+rs7vbJqN6iBQAoNuQ==} engines: {node: '>=22.19.0'} hasBin: true - '@earendil-works/pi-telemetry@1.1.0': - resolution: {integrity: sha512-8gAK05/2pPozZZz6hCLOi8x2vsqzvsmO9gG92kbvuvTPm0qMHaFlaXX98ndXsrOi5pmzha7vNLguuG4+0Rgxjw==} + '@earendil-works/pi-telemetry@1.2.0': + resolution: {integrity: sha512-NZ3aJE6dr7aTgzzXDe53OW7BsAsF8ZUKHPJTtnjaQcxtlagkUT0Ew8Qas0EvYQsZqR/KpF1lB+z209XBzIC7xg==} engines: {node: '>=22.19.0'} '@electron-internal/extract-zip@1.0.5': @@ -24342,11 +24342,11 @@ snapshots: transitivePeerDependencies: - '@algolia/client-search' - '@earendil-works/pi-ai@1.1.0(patch_hash=66eaf960054f10441b93ac3857114e96c1901f6d183713e2f21437b007111644)(@aws-sdk/credential-provider-node@3.972.84)(@modelcontextprotocol/sdk@1.31.0(supports-color@9.4.0)(zod@4.4.3))(@smithy/signature-v4@5.7.4)(supports-color@9.4.0)(undici@8.11.2)(ws@8.21.3)(zod@4.4.3)': + '@earendil-works/pi-ai@1.2.0(patch_hash=4202cd05e0250bf7bc1b010b9aa8a50eb4bb7d0c5364195397842d26972b4b6f)(@aws-sdk/credential-provider-node@3.972.84)(@modelcontextprotocol/sdk@1.31.0(supports-color@9.4.0)(zod@4.4.3))(@smithy/signature-v4@5.7.4)(supports-color@9.4.0)(undici@8.11.2)(ws@8.21.3)(zod@4.4.3)': dependencies: '@anthropic-ai/sdk': 0.129.0(zod@4.4.3) '@aws-sdk/client-bedrock-runtime': 3.1127.0 - '@earendil-works/pi-telemetry': 1.1.0 + '@earendil-works/pi-telemetry': 1.2.0 '@google/genai': 2.21.0(@modelcontextprotocol/sdk@1.31.0(supports-color@9.4.0)(zod@4.4.3))(supports-color@9.4.0) '@smithy/node-http-handler': 4.12.1 http-proxy-agent: 9.1.0(supports-color@9.4.0) @@ -24367,7 +24367,7 @@ snapshots: - ws - zod - '@earendil-works/pi-telemetry@1.1.0': {} + '@earendil-works/pi-telemetry@1.2.0': {} '@electron-internal/extract-zip@1.0.5': {} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 290650169a..db9147bf89 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -64,8 +64,8 @@ minimumReleaseAgeExclude: - '@deepseek-ai/libreoffice-kit-wasm@0.1.5' # 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.1.0' - - '@earendil-works/pi-telemetry@1.1.0' + - '@earendil-works/pi-ai@1.2.0' + - '@earendil-works/pi-telemetry@1.2.0' # This addon release recognizes the Electron 44.7.0 Node/V8 fingerprint used by Desktop. - node-addon-native-custom-loader@0.1.9 - node-addon-require-builtin-darwin-arm64@0.1.9 @@ -106,7 +106,7 @@ minimumReleaseAgeExclude: # Desktop packaging requires this Electron fingerprint with node-addon-require-builtin 0.1.9. - electron@44.7.0 patchedDependencies: - '@earendil-works/pi-ai@1.1.0': patches/@earendil-works__pi-ai@1.1.0.patch + '@earendil-works/pi-ai@1.2.0': patches/@earendil-works__pi-ai@1.2.0.patch '@electron/osx-sign@1.3.3': patches/@electron__osx-sign@1.3.3.patch '@fortune-sheet/core@1.0.4': patches/@fortune-sheet__core@1.0.4.patch '@fortune-sheet/react@1.0.4': patches/@fortune-sheet__react@1.0.4.patch From f9cb7ea9f536987fe50f07f32167cf93f61cd320 Mon Sep 17 00:00:00 2001 From: Tianyi Cui <53024+tianyicui@users.noreply.github.com> Date: Thu, 15 Oct 2026 21:49:40 +0800 Subject: [PATCH 6/8] fix(desktop): align runtime patch policy with pi-ai 1.2.0 --- apps/desktop/scripts/runtime-patch-policy.ts | 6 +++--- apps/desktop/tests/prepare-runtime-patches.spec.ts | 4 ++-- .../v0.2.1-alpha.4/pi-ai-model-catalog/guide.i18n.yaml | 10 +++++----- .../v0.2.1-alpha.4/pi-ai-model-catalog/guide.md | 2 +- .../v0.2.1-alpha.4/pi-ai-model-catalog/guide.zh.md | 2 +- 5 files changed, 12 insertions(+), 12 deletions(-) diff --git a/apps/desktop/scripts/runtime-patch-policy.ts b/apps/desktop/scripts/runtime-patch-policy.ts index d2ea174f2c..fd54b6f292 100644 --- a/apps/desktop/scripts/runtime-patch-policy.ts +++ b/apps/desktop/scripts/runtime-patch-policy.ts @@ -8,7 +8,7 @@ export type DesktopRuntimePatch = /** Every workspace patch requires an explicit Desktop decision; runtime-only entries may add or replace a patch. */ export const DESKTOP_RUNTIME_PATCHES: Readonly> = { - '@earendil-works/pi-ai@1.1.0': { scope: 'shared', reason: 'Runtime providers need the streamed argument parsing fix.' }, + '@earendil-works/pi-ai@1.2.0': { scope: 'shared', reason: 'Runtime providers need the streamed argument parsing fix.' }, '@electron/osx-sign@1.3.3': { scope: 'workspace-only', reason: 'Signing executes on the build host.' }, '@fortune-sheet/core@1.0.4': { scope: 'workspace-only', reason: 'Patched spreadsheet code is embedded in the client bundle.' }, '@fortune-sheet/react@1.0.4': { scope: 'workspace-only', reason: 'Patched spreadsheet code is embedded in the client bundle.' }, @@ -20,6 +20,6 @@ export const DESKTOP_RUNTIME_PATCHES: Readonly> = { - // The provider declares ^1.1.0; its patch is qualified against 1.1.0. - '@earendil-works/pi-ai@^1.1.0': '1.1.0', + // The provider declares ^1.2.0; its patch is qualified against 1.2.0. + '@earendil-works/pi-ai@^1.2.0': '1.2.0', } diff --git a/apps/desktop/tests/prepare-runtime-patches.spec.ts b/apps/desktop/tests/prepare-runtime-patches.spec.ts index 84c71a1884..7230382a67 100644 --- a/apps/desktop/tests/prepare-runtime-patches.spec.ts +++ b/apps/desktop/tests/prepare-runtime-patches.spec.ts @@ -42,11 +42,11 @@ it('classifies the current workspace and shares only runtime patch bytes', () => const repository = resolve(import.meta.dirname, '../../..') const before = readFileSync(join(repository, 'pnpm-lock.yaml')) const plan = prepareRuntimePatches(project, repository) - expect(plan.patches.map(entry => entry.spec)).toEqual(['@earendil-works/pi-ai@1.1.0', 'node-pty@1.2.0-beta.15']) + expect(plan.patches.map(entry => entry.spec)).toEqual(['@earendil-works/pi-ai@1.2.0', 'node-pty@1.2.0-beta.15']) expect(plan.workspaceOnly).toHaveLength(6) const settings = yaml.load(readFileSync(join(project, 'pnpm-workspace.yaml'), 'utf8')) as { overrides: Record } expect(settings.overrides['@earendil-works/pi-ai']).toBeUndefined() - expect(settings.overrides['@earendil-works/pi-ai@^1.1.0']).toBe('1.1.0') + expect(settings.overrides['@earendil-works/pi-ai@^1.2.0']).toBe('1.2.0') expect(readFileSync(join(repository, 'pnpm-lock.yaml'))).toEqual(before) }) diff --git a/docs/upgrade-guide/v0.2.1-alpha.4/pi-ai-model-catalog/guide.i18n.yaml b/docs/upgrade-guide/v0.2.1-alpha.4/pi-ai-model-catalog/guide.i18n.yaml index 08329e03d8..ffe72f22d5 100644 --- a/docs/upgrade-guide/v0.2.1-alpha.4/pi-ai-model-catalog/guide.i18n.yaml +++ b/docs/upgrade-guide/v0.2.1-alpha.4/pi-ai-model-catalog/guide.i18n.yaml @@ -5,12 +5,12 @@ /: en: 3d2fec5fa8405395 zh: b8617482168609b1 -/pi-ai-model-catalog: - en: ad7d66838bb7f924 - zh: d4b120332acf786f -/pi-ai-model-catalog/change: +/pi-ai-1-2-0-removes-model-ids-and-lowers-sonnet-4-5-context: + en: 793381c1cb58d941 + zh: de53660729d48116 +/pi-ai-1-2-0-removes-model-ids-and-lowers-sonnet-4-5-context/change: en: 0948c8c82d4f8256 zh: 763504917909d130 -/pi-ai-model-catalog/migration: +/pi-ai-1-2-0-removes-model-ids-and-lowers-sonnet-4-5-context/migration: en: 6db9f70e6db44b3c zh: 34156f7446a78bc1 diff --git a/docs/upgrade-guide/v0.2.1-alpha.4/pi-ai-model-catalog/guide.md b/docs/upgrade-guide/v0.2.1-alpha.4/pi-ai-model-catalog/guide.md index 72c9d8eaf6..a7ac30061a 100644 --- a/docs/upgrade-guide/v0.2.1-alpha.4/pi-ai-model-catalog/guide.md +++ b/docs/upgrade-guide/v0.2.1-alpha.4/pi-ai-model-catalog/guide.md @@ -3,7 +3,7 @@ kind: upgrade-guide description: "The pi-ai 1.2.0 catalog removes model IDs and lowers the default context limit for Anthropic Sonnet 4.5." --- -# pi-ai model catalog +# pi-ai 1.2.0 removes model IDs and lowers Sonnet 4.5 context English | [中文](guide.zh.md) diff --git a/docs/upgrade-guide/v0.2.1-alpha.4/pi-ai-model-catalog/guide.zh.md b/docs/upgrade-guide/v0.2.1-alpha.4/pi-ai-model-catalog/guide.zh.md index ad87c026b4..b9d61ceff1 100644 --- a/docs/upgrade-guide/v0.2.1-alpha.4/pi-ai-model-catalog/guide.zh.md +++ b/docs/upgrade-guide/v0.2.1-alpha.4/pi-ai-model-catalog/guide.zh.md @@ -3,7 +3,7 @@ kind: upgrade-guide description: "pi-ai 1.2.0 目录移除了部分模型 ID,并降低了 Anthropic Sonnet 4.5 的默认上下文限制。" --- -# pi-ai 模型目录 +# pi-ai 1.2.0 移除部分模型 ID 并降低 Sonnet 4.5 上下文限制 [English](guide.md) | 中文 From d6398b3c864f81c4925662b0dee79d9e01169310 Mon Sep 17 00:00:00 2001 From: Tianyi Cui <53024+tianyicui@users.noreply.github.com> Date: Fri, 16 Oct 2026 04:00:44 +0800 Subject: [PATCH 7/8] chore(llm): upgrade pi-ai to 1.3.0 --- THIRD_PARTY_NOTICES.md | 2 +- apps/desktop/scripts/runtime-patch-policy.ts | 6 +- .../tests/prepare-runtime-patches.spec.ts | 4 +- .../pi-ai-model-catalog/guide.i18n.yaml | 22 +- .../pi-ai-model-catalog/guide.md | 46 +++- .../pi-ai-model-catalog/guide.zh.md | 46 +++- .../pi-ai-protocol-replay/guide.i18n.yaml | 16 ++ .../pi-ai-protocol-replay/guide.md | 30 +++ .../pi-ai-protocol-replay/guide.zh.md | 30 +++ packages/llm/llm-pi-ai/README.i18n.yaml | 12 +- packages/llm/llm-pi-ai/README.md | 6 +- packages/llm/llm-pi-ai/README.zh.md | 6 +- packages/llm/llm-pi-ai/package.json | 15 +- packages/llm/llm-pi-ai/src/catalog.ts | 1 + .../llm-pi-ai/src/provider-data.generated.ts | 2 +- packages/llm/llm-pi-ai/src/stream.ts | 12 +- .../tests/completed-tool-arguments.spec.ts | 199 ++++++++++++++++++ .../tests/conversation-updates.spec.ts | 47 ++++- packages/llm/llm-pi-ai/tests/convert.spec.ts | 2 +- ...tch => @earendil-works__pi-ai@1.3.0.patch} | 16 +- pnpm-lock.yaml | 29 ++- pnpm-workspace.yaml | 6 +- .../session.v4.jsonl | 17 ++ .../snapshot.yml | 9 + .../workspace.expected/.empty | 0 25 files changed, 501 insertions(+), 80 deletions(-) create mode 100644 docs/upgrade-guide/v0.2.1-alpha.4/pi-ai-protocol-replay/guide.i18n.yaml create mode 100644 docs/upgrade-guide/v0.2.1-alpha.4/pi-ai-protocol-replay/guide.md create mode 100644 docs/upgrade-guide/v0.2.1-alpha.4/pi-ai-protocol-replay/guide.zh.md create mode 100644 packages/llm/llm-pi-ai/tests/completed-tool-arguments.spec.ts rename patches/{@earendil-works__pi-ai@1.2.0.patch => @earendil-works__pi-ai@1.3.0.patch} (86%) create mode 100644 snapshots/session/pi-ai-malformed-tool-arguments/session.v4.jsonl create mode 100644 snapshots/session/pi-ai-malformed-tool-arguments/snapshot.yml create mode 100644 snapshots/session/pi-ai-malformed-tool-arguments/workspace.expected/.empty diff --git a/THIRD_PARTY_NOTICES.md b/THIRD_PARTY_NOTICES.md index 74759ee557..62fd638aec 100644 --- a/THIRD_PARTY_NOTICES.md +++ b/THIRD_PARTY_NOTICES.md @@ -145,7 +145,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.2.0` — [`patches/@earendil-works__pi-ai@1.2.0.patch`](patches/@earendil-works__pi-ai@1.2.0.patch) +- `@earendil-works/pi-ai@1.3.0` — [`patches/@earendil-works__pi-ai@1.3.0.patch`](patches/@earendil-works__pi-ai@1.3.0.patch) - `@electron/osx-sign@1.3.3` — [`patches/@electron__osx-sign@1.3.3.patch`](patches/@electron__osx-sign@1.3.3.patch) - `@fortune-sheet/core@1.0.4` — [`patches/@fortune-sheet__core@1.0.4.patch`](patches/@fortune-sheet__core@1.0.4.patch) - `@fortune-sheet/react@1.0.4` — [`patches/@fortune-sheet__react@1.0.4.patch`](patches/@fortune-sheet__react@1.0.4.patch) diff --git a/apps/desktop/scripts/runtime-patch-policy.ts b/apps/desktop/scripts/runtime-patch-policy.ts index fd54b6f292..6460915837 100644 --- a/apps/desktop/scripts/runtime-patch-policy.ts +++ b/apps/desktop/scripts/runtime-patch-policy.ts @@ -8,7 +8,7 @@ export type DesktopRuntimePatch = /** Every workspace patch requires an explicit Desktop decision; runtime-only entries may add or replace a patch. */ export const DESKTOP_RUNTIME_PATCHES: Readonly> = { - '@earendil-works/pi-ai@1.2.0': { scope: 'shared', reason: 'Runtime providers need the streamed argument parsing fix.' }, + '@earendil-works/pi-ai@1.3.0': { scope: 'shared', reason: 'Runtime providers need the streamed argument parsing fix.' }, '@electron/osx-sign@1.3.3': { scope: 'workspace-only', reason: 'Signing executes on the build host.' }, '@fortune-sheet/core@1.0.4': { scope: 'workspace-only', reason: 'Patched spreadsheet code is embedded in the client bundle.' }, '@fortune-sheet/react@1.0.4': { scope: 'workspace-only', reason: 'Patched spreadsheet code is embedded in the client bundle.' }, @@ -20,6 +20,6 @@ export const DESKTOP_RUNTIME_PATCHES: Readonly> = { - // The provider declares ^1.2.0; its patch is qualified against 1.2.0. - '@earendil-works/pi-ai@^1.2.0': '1.2.0', + // The provider declares ^1.3.0; its patch is qualified against 1.3.0. + '@earendil-works/pi-ai@^1.3.0': '1.3.0', } diff --git a/apps/desktop/tests/prepare-runtime-patches.spec.ts b/apps/desktop/tests/prepare-runtime-patches.spec.ts index 7230382a67..ac4611b8c3 100644 --- a/apps/desktop/tests/prepare-runtime-patches.spec.ts +++ b/apps/desktop/tests/prepare-runtime-patches.spec.ts @@ -42,11 +42,11 @@ it('classifies the current workspace and shares only runtime patch bytes', () => const repository = resolve(import.meta.dirname, '../../..') const before = readFileSync(join(repository, 'pnpm-lock.yaml')) const plan = prepareRuntimePatches(project, repository) - expect(plan.patches.map(entry => entry.spec)).toEqual(['@earendil-works/pi-ai@1.2.0', 'node-pty@1.2.0-beta.15']) + expect(plan.patches.map(entry => entry.spec)).toEqual(['@earendil-works/pi-ai@1.3.0', 'node-pty@1.2.0-beta.15']) expect(plan.workspaceOnly).toHaveLength(6) const settings = yaml.load(readFileSync(join(project, 'pnpm-workspace.yaml'), 'utf8')) as { overrides: Record } expect(settings.overrides['@earendil-works/pi-ai']).toBeUndefined() - expect(settings.overrides['@earendil-works/pi-ai@^1.2.0']).toBe('1.2.0') + expect(settings.overrides['@earendil-works/pi-ai@^1.3.0']).toBe('1.3.0') expect(readFileSync(join(repository, 'pnpm-lock.yaml'))).toEqual(before) }) diff --git a/docs/upgrade-guide/v0.2.1-alpha.4/pi-ai-model-catalog/guide.i18n.yaml b/docs/upgrade-guide/v0.2.1-alpha.4/pi-ai-model-catalog/guide.i18n.yaml index ffe72f22d5..3a074bf961 100644 --- a/docs/upgrade-guide/v0.2.1-alpha.4/pi-ai-model-catalog/guide.i18n.yaml +++ b/docs/upgrade-guide/v0.2.1-alpha.4/pi-ai-model-catalog/guide.i18n.yaml @@ -3,14 +3,14 @@ # 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.4/pi-ai-model-catalog/guide.md /: - en: 3d2fec5fa8405395 - zh: b8617482168609b1 -/pi-ai-1-2-0-removes-model-ids-and-lowers-sonnet-4-5-context: - en: 793381c1cb58d941 - zh: de53660729d48116 -/pi-ai-1-2-0-removes-model-ids-and-lowers-sonnet-4-5-context/change: - en: 0948c8c82d4f8256 - zh: 763504917909d130 -/pi-ai-1-2-0-removes-model-ids-and-lowers-sonnet-4-5-context/migration: - en: 6db9f70e6db44b3c - zh: 34156f7446a78bc1 + en: 95f84e4861e282ee + zh: ee2bf9db07fcc61e +/pi-ai-1-3-0-model-catalog-changes: + en: ffd3cd2159b8c733 + zh: bf7e8f5a62d432f4 +/pi-ai-1-3-0-model-catalog-changes/change: + en: 66ff01b4e9eb0e15 + zh: 6c71de9b23ebc8b9 +/pi-ai-1-3-0-model-catalog-changes/migration: + en: 6f1313d45d05c97d + zh: a5d396d5c9316031 diff --git a/docs/upgrade-guide/v0.2.1-alpha.4/pi-ai-model-catalog/guide.md b/docs/upgrade-guide/v0.2.1-alpha.4/pi-ai-model-catalog/guide.md index a7ac30061a..a431dbb15a 100644 --- a/docs/upgrade-guide/v0.2.1-alpha.4/pi-ai-model-catalog/guide.md +++ b/docs/upgrade-guide/v0.2.1-alpha.4/pi-ai-model-catalog/guide.md @@ -1,27 +1,53 @@ --- kind: upgrade-guide -description: "The pi-ai 1.2.0 catalog removes model IDs and lowers the default context limit for Anthropic Sonnet 4.5." +description: "The pi-ai 1.3.0 catalog removes model IDs and lowers some model capacities." --- -# pi-ai 1.2.0 removes model IDs and lowers Sonnet 4.5 context +# pi-ai 1.3.0 model catalog changes English | [中文](guide.zh.md) ## Change -The pi-ai dependency changes from 1.1.0 to 1.2.0. Routes that use its catalog lose the following model IDs. Stored `modelOverrides` for these IDs report a missing-catalog diagnostic; new overrides are rejected. An explicit `models` entry can still declare an ID, but it loses its catalog defaults. +The pi-ai dependency changes from 1.1.0 to 1.3.0. These chat model IDs leave the catalog. Stored `modelOverrides` report missing-catalog diagnostics; new overrides are rejected. Explicit `models` entries lose their catalog defaults. | Provider | Removed model IDs | |---|---| | `google-vertex` | `gemini-flash-latest`, `gemini-flash-lite-latest` | -| `opencode` | `fledge-alpha-free` | -| `openrouter` | `inclusionai/ling-3.0-flash-sante:free`, `qwen/qwen-plus-2025-07-28`, `qwen/qwen3-235b-a22b`, `qwen/qwen3-30b-a3b-thinking-2507`, `qwen/qwen3-8b`, `qwen/qwen3-coder-plus`, `qwen/qwen3-max`, `qwen/qwen3-max-thinking`, `qwen/qwen3-vl-235b-a22b-thinking`, `qwen/qwen3-vl-32b-instruct`, `qwen/qwen3-vl-8b-thinking`, `qwen/qwen3.6-max-preview`, `sao10k/l3.1-euryale-70b` | -| `vercel-ai-gateway` | `alibaba/qwen-3.6-max-preview`, `alibaba/qwen3-235b-a22b-thinking`, `alibaba/qwen3-coder-plus`, `alibaba/qwen3-max`, `alibaba/qwen3-max-preview`, `alibaba/qwen3-vl-235b-a22b-instruct`, `alibaba/qwen3-vl-thinking`, `anthropic/claude-3-haiku`, `anthropic/claude-opus-4` | +| `nvidia` | `moonshotai/kimi-k3`, `nvidia/nemotron-3-super-120b-a12b` | +| `opencode` | `exo-free`, `fledge-alpha-free`, `ling-3.0-flash-fin-free`, `ling-3.1-flash-free` | +| `opencode-go` | `deepseek-v4-flash`, `deepseek-v4-flash-vision-exp`, `grok-4.6`, `minimax-m2.7` | +| `openrouter` | `anthropic/claude-sonnet-4`, `apodex/apodex-1.1-mini:free`, `inclusionai/ling-3.0-flash-fin`, `inclusionai/ling-3.0-flash-sante:free`, `qwen/qwen-plus-2025-07-28`, `qwen/qwen3-235b-a22b`, `qwen/qwen3-30b-a3b-thinking-2507`, `qwen/qwen3-8b`, `qwen/qwen3-coder-plus`, `qwen/qwen3-max`, `qwen/qwen3-max-thinking`, `qwen/qwen3-vl-235b-a22b-thinking`, `qwen/qwen3-vl-32b-instruct`, `qwen/qwen3-vl-8b-thinking`, `qwen/qwen3.6-max-preview`, `sao10k/l3.1-euryale-70b`, `unbiased/pareto`, `unbiased/pareto-26.10-preview` | +| `vercel-ai-gateway` | `alibaba/qwen-3.6-max-preview`, `alibaba/qwen3-235b-a22b-thinking`, `alibaba/qwen3-coder-plus`, `alibaba/qwen3-max`, `alibaba/qwen3-max-preview`, `alibaba/qwen3-vl-235b-a22b-instruct`, `alibaba/qwen3-vl-thinking`, `anthropic/claude-3-haiku`, `anthropic/claude-opus-4`, `inclusionai/ling-3.1-flash-free` | -For the `anthropic` provider, `claude-sonnet-4-5` and `claude-sonnet-4-5-20250929` have a default `contextWindow` of 200,000 tokens, down from 1,000,000. Explicit configuration overrides still take precedence. The `vercel-ai-gateway` model `alibaba/qwen-3-235b` is now declared non-reasoning. +These catalog limits decrease. Values are tokens; `—` means that limit did not decrease. Explicit overrides still take precedence. `maxTokens` describes model capacity, not an implicit request limit. + +| Provider/model | New contextWindow / maxTokens | +|---|---| +| `anthropic/claude-sonnet-4-5` | 200000 / — | +| `anthropic/claude-sonnet-4-5-20250929` | 200000 / — | +| `openrouter/deepseek/deepseek-v4-flash` | 1048575 / 943717 | +| `openrouter/inclusionai/ling-3.0-flash-vl` | 131072 / — | +| `openrouter/meta-llama/llama-4-scout` | 131072 / — | +| `openrouter/minimax/minimax-m2` | — / 131072 | +| `openrouter/minimax/minimax-m2.5` | 198000 / 32768 | +| `openrouter/minimax/minimax-m2.7` | — / 131072 | +| `openrouter/moonshotai/kimi-k2-thinking` | — / 98304 | +| `openrouter/nvidia/nemotron-3-ultra-550b-a55b` | 202800 / — | +| `openrouter/openai/gpt-oss-120b` | — / 65536 | +| `openrouter/qwen/qwen3-235b-a22b-thinking-2507` | 128000 / 16384 | +| `openrouter/qwen/qwen3.5-35b-a3b` | 256000 / 16384 | +| `openrouter/qwen/qwen3.8-2.4t-a95b` | 1000000 / — | +| `openrouter/qwen/qwen3.8-27b` | 262144 / — | +| `openrouter/~z-ai/glm-latest` | — / 131072 | +| `radius/claude-sonnet-4-5` | 200000 / — | +| `vercel-ai-gateway/alibaba/qwen3-coder` | — / 32768 | +| `vercel-ai-gateway/deepseek/deepseek-v4.1-flash-fast` | — / 32768 | + +The `vercel-ai-gateway` model `alibaba/qwen-3-235b` is now non-reasoning. Protocol and effort changes have a [separate guide](../pi-ai-protocol-replay/guide.md). ## Migration -1. In Settings → Models, select a model from the updated provider catalog. Check that its endpoint, capabilities, and price fit your use; removed IDs have no automatic replacement. -2. Update model selections in your profile's `cordis.patch.yml`, preset patches, and affected Sessions. Remove obsolete `providers..modelOverrides.` entries. Replace removed IDs in explicit `providers..models` lists with supported models and review their declared capacities and capabilities. See the [pi-ai configuration reference](../../../../packages/llm/llm-pi-ai/README.md). -3. Review explicit Sonnet 4.5 context overrides against the endpoint's supported limit. Start the affected profile, confirm its model has no configuration diagnostic, and send a short prompt to verify the selected route. +1. In Settings → Models, replace removed IDs with supported models. Check endpoints, capabilities, and prices; IDs have no automatic replacement. +2. Update selections in `cordis.patch.yml`, preset patches, and affected Sessions. Remove obsolete `providers..modelOverrides.` entries and replace removed IDs in `providers..models` lists. See the [configuration reference](../../../../packages/llm/llm-pi-ai/README.md). +3. Review capacity and reasoning overrides against the endpoint's supported values. Start the profile, confirm the model has no configuration diagnostic, and send a short prompt. diff --git a/docs/upgrade-guide/v0.2.1-alpha.4/pi-ai-model-catalog/guide.zh.md b/docs/upgrade-guide/v0.2.1-alpha.4/pi-ai-model-catalog/guide.zh.md index b9d61ceff1..2a85d4a276 100644 --- a/docs/upgrade-guide/v0.2.1-alpha.4/pi-ai-model-catalog/guide.zh.md +++ b/docs/upgrade-guide/v0.2.1-alpha.4/pi-ai-model-catalog/guide.zh.md @@ -1,27 +1,53 @@ --- kind: upgrade-guide -description: "pi-ai 1.2.0 目录移除了部分模型 ID,并降低了 Anthropic Sonnet 4.5 的默认上下文限制。" +description: "pi-ai 1.3.0 目录移除了部分模型 ID,并降低了部分模型容量。" --- -# pi-ai 1.2.0 移除部分模型 ID 并降低 Sonnet 4.5 上下文限制 +# pi-ai 1.3.0 模型目录变更 [English](guide.md) | 中文 ## 变更 -pi-ai 依赖从 1.1.0 升级到 1.2.0。使用其目录的路由不再包含以下模型 ID。已保存的 `modelOverrides` 若引用这些 ID,会报告目录条目缺失的诊断;新的覆盖配置会被拒绝。显式 `models` 条目仍可声明这些 ID,但会失去目录默认值。 +pi-ai 依赖从 1.1.0 升级到 1.3.0。以下聊天模型 ID 不再包含在目录中。已保存的 `modelOverrides` 会报告目录条目缺失诊断;新的覆盖配置会被拒绝。显式 `models` 条目会失去目录默认值。 | 提供商 | 已移除的模型 ID | |---|---| | `google-vertex` | `gemini-flash-latest`, `gemini-flash-lite-latest` | -| `opencode` | `fledge-alpha-free` | -| `openrouter` | `inclusionai/ling-3.0-flash-sante:free`, `qwen/qwen-plus-2025-07-28`, `qwen/qwen3-235b-a22b`, `qwen/qwen3-30b-a3b-thinking-2507`, `qwen/qwen3-8b`, `qwen/qwen3-coder-plus`, `qwen/qwen3-max`, `qwen/qwen3-max-thinking`, `qwen/qwen3-vl-235b-a22b-thinking`, `qwen/qwen3-vl-32b-instruct`, `qwen/qwen3-vl-8b-thinking`, `qwen/qwen3.6-max-preview`, `sao10k/l3.1-euryale-70b` | -| `vercel-ai-gateway` | `alibaba/qwen-3.6-max-preview`, `alibaba/qwen3-235b-a22b-thinking`, `alibaba/qwen3-coder-plus`, `alibaba/qwen3-max`, `alibaba/qwen3-max-preview`, `alibaba/qwen3-vl-235b-a22b-instruct`, `alibaba/qwen3-vl-thinking`, `anthropic/claude-3-haiku`, `anthropic/claude-opus-4` | +| `nvidia` | `moonshotai/kimi-k3`, `nvidia/nemotron-3-super-120b-a12b` | +| `opencode` | `exo-free`, `fledge-alpha-free`, `ling-3.0-flash-fin-free`, `ling-3.1-flash-free` | +| `opencode-go` | `deepseek-v4-flash`, `deepseek-v4-flash-vision-exp`, `grok-4.6`, `minimax-m2.7` | +| `openrouter` | `anthropic/claude-sonnet-4`, `apodex/apodex-1.1-mini:free`, `inclusionai/ling-3.0-flash-fin`, `inclusionai/ling-3.0-flash-sante:free`, `qwen/qwen-plus-2025-07-28`, `qwen/qwen3-235b-a22b`, `qwen/qwen3-30b-a3b-thinking-2507`, `qwen/qwen3-8b`, `qwen/qwen3-coder-plus`, `qwen/qwen3-max`, `qwen/qwen3-max-thinking`, `qwen/qwen3-vl-235b-a22b-thinking`, `qwen/qwen3-vl-32b-instruct`, `qwen/qwen3-vl-8b-thinking`, `qwen/qwen3.6-max-preview`, `sao10k/l3.1-euryale-70b`, `unbiased/pareto`, `unbiased/pareto-26.10-preview` | +| `vercel-ai-gateway` | `alibaba/qwen-3.6-max-preview`, `alibaba/qwen3-235b-a22b-thinking`, `alibaba/qwen3-coder-plus`, `alibaba/qwen3-max`, `alibaba/qwen3-max-preview`, `alibaba/qwen3-vl-235b-a22b-instruct`, `alibaba/qwen3-vl-thinking`, `anthropic/claude-3-haiku`, `anthropic/claude-opus-4`, `inclusionai/ling-3.1-flash-free` | -在 `anthropic` 提供商中,`claude-sonnet-4-5` 和 `claude-sonnet-4-5-20250929` 的默认 `contextWindow` 从 1,000,000 token 降为 200,000 token。显式配置的覆盖值仍然优先。`vercel-ai-gateway` 的 `alibaba/qwen-3-235b` 模型现在被声明为非推理模型。 +以下目录限制降低。数值以 token 为单位;`—` 表示该限制未降低。显式覆盖值仍然优先。`maxTokens` 描述模型容量,不是隐式的请求限制。 + +| 提供商/模型 | 新 contextWindow / maxTokens | +|---|---| +| `anthropic/claude-sonnet-4-5` | 200000 / — | +| `anthropic/claude-sonnet-4-5-20250929` | 200000 / — | +| `openrouter/deepseek/deepseek-v4-flash` | 1048575 / 943717 | +| `openrouter/inclusionai/ling-3.0-flash-vl` | 131072 / — | +| `openrouter/meta-llama/llama-4-scout` | 131072 / — | +| `openrouter/minimax/minimax-m2` | — / 131072 | +| `openrouter/minimax/minimax-m2.5` | 198000 / 32768 | +| `openrouter/minimax/minimax-m2.7` | — / 131072 | +| `openrouter/moonshotai/kimi-k2-thinking` | — / 98304 | +| `openrouter/nvidia/nemotron-3-ultra-550b-a55b` | 202800 / — | +| `openrouter/openai/gpt-oss-120b` | — / 65536 | +| `openrouter/qwen/qwen3-235b-a22b-thinking-2507` | 128000 / 16384 | +| `openrouter/qwen/qwen3.5-35b-a3b` | 256000 / 16384 | +| `openrouter/qwen/qwen3.8-2.4t-a95b` | 1000000 / — | +| `openrouter/qwen/qwen3.8-27b` | 262144 / — | +| `openrouter/~z-ai/glm-latest` | — / 131072 | +| `radius/claude-sonnet-4-5` | 200000 / — | +| `vercel-ai-gateway/alibaba/qwen3-coder` | — / 32768 | +| `vercel-ai-gateway/deepseek/deepseek-v4.1-flash-fast` | — / 32768 | + +`vercel-ai-gateway` 的 `alibaba/qwen-3-235b` 模型现在被声明为非推理模型。协议与推理强度变更见[单独的指南](../pi-ai-protocol-replay/guide.zh.md)。 ## 迁移 -1. 在“设置 → 模型”中,从更新后的提供商目录选择模型。确认其端点、能力和价格符合用途;已移除的 ID 没有自动替代项。 -2. 更新 profile 的 `cordis.patch.yml`、preset patch 和受影响 Session 中的模型选择。移除过时的 `providers..modelOverrides.` 条目。将显式 `providers..models` 列表中已移除的 ID 替换为受支持的模型,并检查其声明的容量和能力。参见 [pi-ai 配置参考](../../../../packages/llm/llm-pi-ai/README.zh.md)。 -3. 根据端点支持的限制检查显式 Sonnet 4.5 上下文覆盖值。启动受影响的 profile,确认模型没有配置诊断,再发送一个简短提示,验证所选路由。 +1. 在“设置 → 模型”中,将已移除的 ID 替换为受支持的模型。检查端点、能力和价格;ID 没有自动替代项。 +2. 更新 `cordis.patch.yml`、preset patch 和受影响 Session 中的模型选择。移除过时的 `providers..modelOverrides.` 条目,并替换 `providers..models` 列表中已移除的 ID。参见[配置参考](../../../../packages/llm/llm-pi-ai/README.zh.md)。 +3. 根据端点支持的值检查容量与推理覆盖配置。启动 profile,确认模型没有配置诊断,再发送一个简短提示。 diff --git a/docs/upgrade-guide/v0.2.1-alpha.4/pi-ai-protocol-replay/guide.i18n.yaml b/docs/upgrade-guide/v0.2.1-alpha.4/pi-ai-protocol-replay/guide.i18n.yaml new file mode 100644 index 0000000000..2b5c9f6580 --- /dev/null +++ b/docs/upgrade-guide/v0.2.1-alpha.4/pi-ai-protocol-replay/guide.i18n.yaml @@ -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.4/pi-ai-protocol-replay/guide.md +/: + en: 1e2bdc1e7e35ea20 + zh: 503fcf05bceec218 +/pi-ai-1-3-0-protocol-and-replay-changes: + en: 58e9a6339aa3c1f4 + zh: 3a11bad804ca9b66 +/pi-ai-1-3-0-protocol-and-replay-changes/change: + en: 308406476f8470f0 + zh: ecb22ad91d460769 +/pi-ai-1-3-0-protocol-and-replay-changes/migration: + en: 24c50a0f234a7ee8 + zh: 6ac8b31f88d22c37 diff --git a/docs/upgrade-guide/v0.2.1-alpha.4/pi-ai-protocol-replay/guide.md b/docs/upgrade-guide/v0.2.1-alpha.4/pi-ai-protocol-replay/guide.md new file mode 100644 index 0000000000..6e324bdb68 --- /dev/null +++ b/docs/upgrade-guide/v0.2.1-alpha.4/pi-ai-protocol-replay/guide.md @@ -0,0 +1,30 @@ +--- +kind: upgrade-guide +description: "pi-ai 1.3.0 changes provider protocols, reasoning choices, history replay, and invalid tool-argument handling." +--- + +# pi-ai 1.3.0 protocol and replay changes + +English | [中文](guide.zh.md) + +## Change + +The pi-ai upgrade from 1.1.0 to 1.3.0 changes these defaults. Explicit profile overrides still win. + +| Route or behavior | New behavior | +|---|---| +| `kimi-coding` | Uses `openai-completions` at `https://api.kimi.com/coding/v1` instead of `anthropic-messages` at `/coding` | +| `opencode`, `opencode-go` | Endpoints replace `/zen` with `/inference`; `/go` and `/v1` suffixes remain model-specific | +| Xiaomi MiMo catalogs, including token-plan routes | Offer only `off` and `high` reasoning | +| `kimi-coding/kimi-for-coding-highspeed` | Does not offer reasoning `off` | +| Assistant reasoning history | Outgoing requests omit thinking from another provider, API, or model instead of converting it to text; degraded replay without native metadata also omits thinking | +| Invalid completed tool arguments | The attempt fails with `MALFORMED_RESPONSE`; it executes no tools and publishes no successful replay state. The default normal retry policy excludes this code | + +Stored history is retained. Returning to the original model restores its valid native replay metadata. Output truncated by the token limit still drops incomplete tool calls and can continue normally. Credentials do not require migration. + +## Migration + +1. Review `providers..baseURL`, `api`, and `compat` overrides in `cordis.patch.yml`. For overrides that follow the upstream Kimi Coding endpoint, use the Completions protocol and `/v1` path. Custom gateways must explicitly retain their actual protocol and path. Update OpenCode URLs to `/inference` where applicable. Omit overrides when catalog defaults are intended. +2. Replace unsupported reasoning selections in profiles, presets, and Sessions with a level shown by the current model metadata. +3. Put facts needed across model changes in user messages or normal assistant replies; do not rely on prior private reasoning becoming visible text. +4. For `MALFORMED_RESPONSE`, inspect the reported invalid arguments and correct the model or gateway output. Verify the affected route with a short prompt and a tool call. See the [configuration reference](../../../../packages/llm/llm-pi-ai/README.md). diff --git a/docs/upgrade-guide/v0.2.1-alpha.4/pi-ai-protocol-replay/guide.zh.md b/docs/upgrade-guide/v0.2.1-alpha.4/pi-ai-protocol-replay/guide.zh.md new file mode 100644 index 0000000000..df1ed7b6dc --- /dev/null +++ b/docs/upgrade-guide/v0.2.1-alpha.4/pi-ai-protocol-replay/guide.zh.md @@ -0,0 +1,30 @@ +--- +kind: upgrade-guide +description: "pi-ai 1.3.0 改变了提供方协议、推理选项、历史回放以及无效工具参数的处理方式。" +--- + +# pi-ai 1.3.0 协议与回放变更 + +[English](guide.md) | 中文 + +## 变更 + +pi-ai 从 1.1.0 升级到 1.3.0 时改变了以下默认行为。显式 profile 覆盖值仍然优先。 + +| 路由或行为 | 新行为 | +|---|---| +| `kimi-coding` | 使用 `https://api.kimi.com/coding/v1` 上的 `openai-completions`,替代 `/coding` 上的 `anthropic-messages` | +| `opencode`、`opencode-go` | 端点将 `/zen` 替换为 `/inference`;`/go` 和 `/v1` 后缀仍由模型决定 | +| Xiaomi MiMo 目录,包括 token-plan 路由 | 只提供 `off` 和 `high` 推理选项 | +| `kimi-coding/kimi-for-coding-highspeed` | 不提供推理 `off` 选项 | +| assistant 推理历史 | 发出的请求会省略来自另一提供方、API 或模型的思考,而不再将其转换为文本;缺少原生元数据的降级回放也会省略思考 | +| 已完成调用的工具参数无效 | 本次尝试以 `MALFORMED_RESPONSE` 失败,不执行任何工具,也不发布成功的回放状态。默认的普通重试策略不重试此错误码 | + +存储的历史保持不变。切回原模型时会恢复其有效的原生回放元数据。达到 token 限制而截断的输出仍会丢弃未完成的工具调用,并可正常续写。凭据无需迁移。 + +## 迁移 + +1. 检查 `cordis.patch.yml` 中的 `providers..baseURL`、`api` 和 `compat` 覆盖配置。对于跟随上游 Kimi Coding 端点的覆盖配置,使用 Completions 协议和 `/v1` 路径。自定义网关必须显式保留其实际协议和路径。按需将 OpenCode URL 更新为 `/inference`。若希望使用目录默认值,请省略覆盖配置。 +2. 将 profile、preset 和 Session 中不受支持的推理选项替换为当前模型元数据列出的级别。 +3. 将切换模型后仍需要的事实放入用户消息或普通 assistant 回复;不要依赖先前的私有推理转换为可见文本。 +4. 出现 `MALFORMED_RESPONSE` 时,检查诊断中的无效参数,并修正模型或网关输出。用一个简短提示和一次工具调用验证受影响的路由。参见[配置参考](../../../../packages/llm/llm-pi-ai/README.zh.md)。 diff --git a/packages/llm/llm-pi-ai/README.i18n.yaml b/packages/llm/llm-pi-ai/README.i18n.yaml index ab600163c4..1a5caa57c6 100644 --- a/packages/llm/llm-pi-ai/README.i18n.yaml +++ b/packages/llm/llm-pi-ai/README.i18n.yaml @@ -57,8 +57,8 @@ en: 477d277e9ac35c8f zh: 96870ca333fb6510 /deepseek-ai-dsh-llm-pi-ai/understand-the-implementation/replay-and-vocabulary: - en: 73242ca9b8a98c76 - zh: 4ab98526ae70bd23 + en: e64173763fdbcfae + zh: 73ed2c2af54b62f6 /deepseek-ai-dsh-llm-pi-ai/further-exploration: en: a2a2ccdeae59d408 zh: c47ee091aa90ad91 @@ -81,8 +81,8 @@ en: 802d7ff60a1d03ff zh: c41c56e28f3e8680 /deepseek-ai-dsh-llm-pi-ai/model-experience/provider-response/what-the-model-sees: - en: 9b7d6b95ec2c4351 - zh: 356ac01aef9e8bed + en: 94ca496012c92fa9 + zh: 9539b08924b5022d /deepseek-ai-dsh-llm-pi-ai/model-experience/provider-response/token-effect: en: f002b21d0f4c3df0 zh: 388c8ed4676ed1af @@ -90,8 +90,8 @@ en: b6bd570c0159ba08 zh: 7d2d91eea6a8d830 /deepseek-ai-dsh-llm-pi-ai/known-limitations-and-deferred-work: - en: 21b4435e0a9f3564 - zh: 7bc2dba9294b0da3 + en: ccaea557a3a3ffaf + zh: 5f70d824d39fb457 /deepseek-ai-dsh-llm-pi-ai/known-limitations-and-deferred-work/dev-note: en: 3678dd0f6642c2a5 zh: 75cbdd064cc64282 diff --git a/packages/llm/llm-pi-ai/README.md b/packages/llm/llm-pi-ai/README.md index f8db8f5c8a..c30a414465 100644 --- a/packages/llm/llm-pi-ai/README.md +++ b/packages/llm/llm-pi-ai/README.md @@ -182,7 +182,7 @@ The `pre-commit` hook runs `node_modules/.bin/tsx packages/llm/llm-pi-ai/scripts ### Replay and vocabulary -Successful assistant responses store a versioned, lossless-JSON replay state beside the provider and model that produced them — response-level facts plus one per-block entry per streamed block. At request time, `LlmRuntime` passes replay state only when the same adapter instance owns both routes; the adapter validates it and restores native response ids, provider signatures, and optional `providerThinkingLevel` effort metadata, keeping absent effort metadata absent. Replay validates the requested model identity against the assistant source and uses it to retain same-model signatures; provider-reported response models remain diagnostic metadata. Absent or unusable replay state degrades to provider-neutral content while preserving the assistant source’s required provider and model. pi-ai tool-call arguments are parsed objects, so the adapter parses input and re-stringifies output to the harness raw-JSON convention; pi-ai in-stream error events map to terminal `finish` chunks. Protocol conversion operates on request-local copies. Switching from A to B and back to A preserves the stored content, including empty text blocks, tool ids, original block order, and native replay metadata; unsupported signatures are omitted only from the foreign request. +Successful assistant responses store a versioned, lossless-JSON replay state beside the provider and model that produced them — response-level facts plus one per-block entry per streamed block. At request time, `LlmRuntime` passes replay state only when the same adapter instance owns both routes; the adapter validates it and restores native response ids, provider signatures, and optional `providerThinkingLevel` effort metadata, keeping absent effort metadata absent. Replay validates the requested model identity against the assistant source and uses it to retain same-model signatures; provider-reported response models remain diagnostic metadata. Absent or unusable replay state degrades to provider-neutral content while preserving the assistant source’s required provider and model. pi-ai tool-call arguments are parsed objects, so the adapter parses input and re-stringifies output to the harness raw-JSON convention; pi-ai in-stream error events map to terminal `finish` chunks. Protocol conversion operates on request-local copies. Switching from A to B and back to A preserves the stored content, including empty text blocks, tool ids, original block order, and native replay metadata; requests to a different provider, API, or model omit unsupported signatures and reasoning blocks. Degraded replay without native metadata also omits reasoning. Valid native replay retains the stored reasoning when the original model resumes. @@ -223,7 +223,7 @@ Conversion preserves logical request order, while image handles and offload plac #### What the model sees -pi-ai events become harness reasoning, text, tool-call, usage, and finish chunks. The adapter passes parsed tool arguments to the harness as raw JSON strings. +pi-ai events become harness reasoning, text, tool-call, usage, and finish chunks. The adapter passes parsed tool arguments to the harness as raw JSON strings. If pi-ai reports invalid completed arguments, the attempt fails with `MALFORMED_RESPONSE` before tool dispatch and publishes no successful replay state. The default normal retry policy excludes this code; an explicit retry policy still controls recovery. #### Token effect @@ -257,7 +257,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 JSON tool-call arguments avoid per-delta reparsing** — the installed pi-ai carries [`patches/@earendil-works__pi-ai@1.2.0.patch`](../../../patches/@earendil-works__pi-ai@1.2.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. Completion events supply parsed arguments; Responses may parse at both argument completion and output-item completion. This adapter reads only delta strings and finalized arguments. Re-apply or retire the patch on every pi-ai upgrade. +- **Streamed JSON tool-call arguments avoid per-delta reparsing** — the installed pi-ai carries [`patches/@earendil-works__pi-ai@1.3.0.patch`](../../../patches/@earendil-works__pi-ai@1.3.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. Completion events supply parsed arguments; Responses may parse at both argument completion and output-item completion. This adapter reads only delta strings and finalized arguments. Re-apply or retire the patch on every pi-ai upgrade. ### Dev Note diff --git a/packages/llm/llm-pi-ai/README.zh.md b/packages/llm/llm-pi-ai/README.zh.md index 3837f4b99e..0d7d5c22fe 100644 --- a/packages/llm/llm-pi-ai/README.zh.md +++ b/packages/llm/llm-pi-ai/README.zh.md @@ -182,7 +182,7 @@ git add packages/llm/llm-pi-ai/src/provider-data.generated.ts ### 回放与词汇 -成功 assistant 响应会存储带版本的、无损 JSON 回放状态,与产生它们的提供方和模型放在一起——响应级事实加每个流式块一条逐块条目。请求时,`LlmRuntime` 仅当同一适配器实例拥有两条路由时才传递回放状态;适配器校验它并恢复原生响应 id、提供方签名与可选的 `providerThinkingLevel` effort 元数据,缺失的 effort 元数据仍保持缺失。回放会对照 assistant 来源校验请求模型身份,并用它保留同模型签名;提供方报告的响应模型仅作为诊断元数据。回放状态缺失或无法使用时会降级为提供方无关内容,并保留 assistant 来源中必需的提供方和模型。pi-ai 工具调用参数是解析后的对象,因此适配器解析输入并重新字符串化输出,以符合 harness 原始 JSON 约定;pi-ai 流内错误事件映射为终止 `finish` 分片。 协议转换仅作用于请求副本。从 A 切换到 B 再切回 A 时,存储的内容保持完整,包括空文本块、工具 id、原始块顺序和原生回放元数据;不受支持的签名仅从发给其他协议的请求中省略。 +成功 assistant 响应会存储带版本的、无损 JSON 回放状态,与产生它们的提供方和模型放在一起——响应级事实加每个流式块一条逐块条目。请求时,`LlmRuntime` 仅当同一适配器实例拥有两条路由时才传递回放状态;适配器校验它并恢复原生响应 id、提供方签名与可选的 `providerThinkingLevel` effort 元数据,缺失的 effort 元数据仍保持缺失。回放会对照 assistant 来源校验请求模型身份,并用它保留同模型签名;提供方报告的响应模型仅作为诊断元数据。回放状态缺失或无法使用时会降级为提供方无关内容,并保留 assistant 来源中必需的提供方和模型。pi-ai 工具调用参数是解析后的对象,因此适配器解析输入并重新字符串化输出,以符合 harness 原始 JSON 约定;pi-ai 流内错误事件映射为终止 `finish` 分片。 协议转换仅作用于请求副本。从 A 切换到 B 再切回 A 时,存储的内容保持完整,包括空文本块、工具 id、原始块顺序和原生回放元数据;发给不同提供方、API 或模型的请求会省略不受支持的签名和推理块。缺少原生元数据的降级回放也会省略推理。切回原模型时,有效的原生回放会保留已存储的推理。 @@ -223,7 +223,7 @@ git add packages/llm/llm-pi-ai/src/provider-data.generated.ts #### 模型看到什么 -pi-ai 事件变成 harness 的推理、文本、工具调用、用量与 finish 分片。适配器把解析后的工具参数以原始 JSON 字符串传给 harness。 +pi-ai 事件变成 harness 的推理、文本、工具调用、用量与 finish 分片。适配器把解析后的工具参数以原始 JSON 字符串传给 harness。如果 pi-ai 报告已完成调用的参数无效,本次尝试会在分派工具前以 `MALFORMED_RESPONSE` 失败,且不发布成功的回放状态。默认的普通重试策略不重试此错误码;显式重试策略仍然控制恢复。 #### Token 影响 @@ -257,7 +257,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()` 调用仍是单次尝试。 -- **流式 JSON 工具调用参数避免逐 delta 重复解析**——安装的 pi-ai 带有 [`patches/@earendil-works__pi-ai@1.2.0.patch`](../../../patches/@earendil-works__pi-ai@1.2.0.patch),它移除了每个流适配器中对整段累计参数 JSON 的逐 delta 重新解析(上游 [earendil-works/pi#9265](https://github.com/earendil-works/pi/issues/9265));未打补丁时,数 MB 的参数流会在事件循环上消耗 O(n²) CPU,并使进程内所有会话停滞。完成事件提供解析后的参数;Responses 可能在参数完成和输出项完成时分别解析。本适配器只读取 delta 字符串与最终参数。每次升级 pi-ai 时都要重新应用或撤销该补丁。 +- **流式 JSON 工具调用参数避免逐 delta 重复解析**——安装的 pi-ai 带有 [`patches/@earendil-works__pi-ai@1.3.0.patch`](../../../patches/@earendil-works__pi-ai@1.3.0.patch),它移除了每个流适配器中对整段累计参数 JSON 的逐 delta 重新解析(上游 [earendil-works/pi#9265](https://github.com/earendil-works/pi/issues/9265));未打补丁时,数 MB 的参数流会在事件循环上消耗 O(n²) CPU,并使进程内所有会话停滞。完成事件提供解析后的参数;Responses 可能在参数完成和输出项完成时分别解析。本适配器只读取 delta 字符串与最终参数。每次升级 pi-ai 时都要重新应用或撤销该补丁。 ### 开发备注 diff --git a/packages/llm/llm-pi-ai/package.json b/packages/llm/llm-pi-ai/package.json index 7c0d07c428..572c90e9fb 100644 --- a/packages/llm/llm-pi-ai/package.json +++ b/packages/llm/llm-pi-ai/package.json @@ -30,35 +30,38 @@ "license": "MIT", "peerDependencies": { "@deepseek-ai/cordis": "workspace:~", + "@deepseek-ai/cordis-plugin-loader": "workspace:~", "@deepseek-ai/dsh-attachment": "workspace:*", "@deepseek-ai/dsh-authorization": "workspace:*", "@deepseek-ai/dsh-credentials": "workspace:*", "@deepseek-ai/dsh-fs": "workspace:*", "@deepseek-ai/dsh-launch-environment": "workspace:*", "@deepseek-ai/dsh-llm": "workspace:*", - "@deepseek-ai/dsh-timeout": "workspace:*", - "@deepseek-ai/cordis-plugin-loader": "workspace:~" + "@deepseek-ai/dsh-timeout": "workspace:*" }, "dependencies": { "@deepseek-ai/dsh-brand": "workspace:*", "@deepseek-ai/dsh-util-values": "workspace:*", "@deepseek-ai/schemastery": "workspace:~", - "@earendil-works/pi-ai": "^1.2.0" + "@earendil-works/pi-ai": "^1.3.0" }, "devDependencies": { "@deepseek-ai/cordis": "workspace:~", + "@deepseek-ai/cordis-plugin-loader": "workspace:~", + "@deepseek-ai/dsh-agent-loop-testkit": "workspace:*", "@deepseek-ai/dsh-attachment": "workspace:*", "@deepseek-ai/dsh-authorization": "workspace:*", "@deepseek-ai/dsh-credentials": "workspace:*", "@deepseek-ai/dsh-fs": "workspace:*", + "@deepseek-ai/dsh-http-proxy": "workspace:*", "@deepseek-ai/dsh-launch-environment": "workspace:*", "@deepseek-ai/dsh-llm": "workspace:*", "@deepseek-ai/dsh-llm-deepseek": "workspace:*", "@deepseek-ai/dsh-llm-deepseek-api-key": "workspace:*", + "@deepseek-ai/dsh-session": "workspace:*", + "@deepseek-ai/dsh-settings": "workspace:*", "@deepseek-ai/dsh-timeout": "workspace:*", - "@deepseek-ai/dsh-http-proxy": "workspace:*", - "@deepseek-ai/cordis-plugin-loader": "workspace:~", - "@deepseek-ai/dsh-settings": "workspace:*" + "@deepseek-ai/dsh-tools": "workspace:*" }, "peerDependenciesMeta": { "@deepseek-ai/cordis-plugin-loader": { diff --git a/packages/llm/llm-pi-ai/src/catalog.ts b/packages/llm/llm-pi-ai/src/catalog.ts index 6ccd0cae05..b210309574 100644 --- a/packages/llm/llm-pi-ai/src/catalog.ts +++ b/packages/llm/llm-pi-ai/src/catalog.ts @@ -287,6 +287,7 @@ const RESPONSES_COMPAT_GATE = { supportsToolSearch: 'withhold', supportsExplicitPromptCacheMode: 'withhold', supportsMidConvoSystemMessages: 'withhold', + supportsMidConvoEffort: 'withhold', } as const satisfies Record /** Disposition of every `AnthropicMessagesCompat` field; a drift gate like the one above. */ diff --git a/packages/llm/llm-pi-ai/src/provider-data.generated.ts b/packages/llm/llm-pi-ai/src/provider-data.generated.ts index 749957a3eb..69e641bdd9 100644 --- a/packages/llm/llm-pi-ai/src/provider-data.generated.ts +++ b/packages/llm/llm-pi-ai/src/provider-data.generated.ts @@ -22,7 +22,7 @@ export const PROVIDER_DATA: Record & Partial, @@ -148,6 +150,7 @@ export async function* toStreamChunks( // pi-ai contentIndex ↔ our block index map 1:1 (both count blocks from 0 // in stream order), but we track ids per index for tool calls. const toolIds = new Map() + let argumentsError: string | undefined for await (const event of events) { switch (event.type) { @@ -192,6 +195,10 @@ export async function* toStreamChunks( break } case 'toolcall_end': + if (event.toolCall.argumentsError !== undefined) { + argumentsError ??= `Tool call "${event.toolCall.name}": ${event.toolCall.argumentsError}` + break + } yield { type: 'block-end', index: event.contentIndex, @@ -207,6 +214,9 @@ export async function* toStreamChunks( break case 'done': yield { type: 'usage', usage: mapUsage(event.message.usage) } + if (argumentsError !== undefined && event.message.stopReason !== 'length') { + throw new LlmError(argumentsError, 'MALFORMED_RESPONSE') + } yield { type: 'finish', reason: mapStopReason(event.message, contextWindow), diff --git a/packages/llm/llm-pi-ai/tests/completed-tool-arguments.spec.ts b/packages/llm/llm-pi-ai/tests/completed-tool-arguments.spec.ts new file mode 100644 index 0000000000..9bc6c42757 --- /dev/null +++ b/packages/llm/llm-pi-ai/tests/completed-tool-arguments.spec.ts @@ -0,0 +1,199 @@ +/** Completed provider arguments fail before a malformed call can enter successful history or dispatch. */ + +import { afterEach, describe, expect, it, onTestFinished } from 'vitest' +import { Context } from '@deepseek-ai/cordis' +import LlmRuntime, { BlockAssembler, createUserMessage } from '@deepseek-ai/dsh-llm' +import type { StreamChunk } from '@deepseek-ai/dsh-llm' +import { PiAiAdapter } from '@deepseek-ai/dsh-llm-pi-ai' +import { mountAgentLoopTestDependencies, mountAgentLoopTestHarness } from '@deepseek-ai/dsh-agent-loop-testkit' +import { SessionId } from '@deepseek-ai/dsh-session' +import { defineContentToolFixture } from '@deepseek-ai/dsh-tools' +import { resolveProfiles } from '../src/config.ts' +import { memoryAuth } from './auth-double.ts' +import { anthropicFrame, anthropicMessageStart, closeMockServers, mockServer } from './mock-server.ts' + +afterEach(closeMockServers) + +type Api = 'anthropic-messages' | 'openai-completions' | 'openai-responses' +type WireEvent = string | { event: string; data: string } + +function completionsEvents(raw: string, stop: 'tool_calls' | 'length' = 'tool_calls'): WireEvent[] { + return [ + JSON.stringify({ choices: [{ index: 0, delta: { + role: 'assistant', content: 'before tool', + tool_calls: [{ index: 0, id: 'call_invalid', type: 'function', function: { name: 'count', arguments: raw } }], + }, finish_reason: null }] }), + JSON.stringify({ choices: [{ index: 0, delta: {}, finish_reason: stop }], usage: { prompt_tokens: 3, completion_tokens: 2 } }), + '[DONE]', + ] +} + +function anthropicEvents(raw: string, stop: 'tool_use' | 'max_tokens' = 'tool_use'): WireEvent[] { + return [ + anthropicMessageStart, + anthropicFrame({ type: 'content_block_start', index: 0, content_block: { type: 'text', text: 'before tool' } }), + anthropicFrame({ type: 'content_block_stop', index: 0 }), + anthropicFrame({ type: 'content_block_start', index: 1, content_block: { type: 'tool_use', id: 'call_invalid', name: 'count', input: {} } }), + anthropicFrame({ type: 'content_block_delta', index: 1, delta: { type: 'input_json_delta', partial_json: raw } }), + anthropicFrame({ type: 'content_block_stop', index: 1 }), + // Anthropic reports the reason after the tool block closes. + anthropicFrame({ type: 'message_delta', delta: { stop_reason: stop, stop_sequence: null }, usage: { output_tokens: 2 } }), + anthropicFrame({ type: 'message_stop' }), + ] +} + +function responsesEvents(raw: string, prefix?: string, priorValid = false, priorArguments = '{}'): WireEvent[] { + const valid = { type: 'function_call', call_id: 'call_valid', id: 'item_valid', name: 'count', arguments: priorArguments } + const invalid = { type: 'function_call', call_id: 'call_invalid', id: 'item_invalid', name: 'count', arguments: raw } + const outputIndex = priorValid ? 1 : 0 + return [ + JSON.stringify({ type: 'response.created', response: { id: 'response_arguments' } }), + ...priorValid ? [ + JSON.stringify({ type: 'response.output_item.added', output_index: 0, item: { ...valid, arguments: '' } }), + JSON.stringify({ type: 'response.output_item.done', output_index: 0, item: valid }), + ] : [], + JSON.stringify({ type: 'response.output_item.added', output_index: outputIndex, item: { ...invalid, arguments: '' } }), + ...prefix === undefined ? [] : [JSON.stringify({ type: 'response.function_call_arguments.delta', output_index: outputIndex, delta: prefix })], + JSON.stringify({ type: 'response.output_item.done', output_index: outputIndex, item: invalid }), + JSON.stringify({ type: 'response.completed', response: { + id: 'response_arguments', status: 'completed', output: priorValid ? [valid, invalid] : [invalid], + usage: { input_tokens: 3, output_tokens: 2, total_tokens: 5 }, + } }), + ] +} + +function registerAdapter(ctx: Context, api: Api, baseURL: string): void { + ctx.effect(() => ctx.llm.registerAdapter(['fixture'], new PiAiAdapter({ + profiles: () => resolveProfiles({ fixture: { api, baseURL, models: [{ id: 'm' }], transport: 'sse' } }), + resolveApiKey: () => Promise.resolve('fixture-key'), + auth: memoryAuth(), + }))) +} + +async function collect(api: Api, events: WireEvent[]) { + const server = await mockServer([{ events }]) + const ctx = new Context() + onTestFinished(() => ctx.fiber.dispose()) + await ctx.plugin(LlmRuntime) + registerAdapter(ctx, api, server.url) + const chunks: StreamChunk[] = [] + const assembler = new BlockAssembler() + for await (const chunk of ctx.llm.stream({ provider: 'fixture', model: 'm', messages: [] })) { + chunks.push(chunk) + assembler.push(chunk) + } + return { chunks, assembler, server } +} + +describe('completed tool-call arguments through the public runtime', () => { + it.each([ + ['anthropic-messages', anthropicEvents], + ['openai-completions', completionsEvents], + ['openai-responses', responsesEvents], + ] as const)('%s rejects malformed completed arguments without successful replay', async (api, events) => { + const raw = '{"value":"unfinished' + const { chunks, assembler, server } = await collect(api, events(raw)) + expect(assembler.finish).toMatchObject({ kind: 'error', failure: { code: 'MALFORMED_RESPONSE' } }) + expect(assembler.finish).toHaveProperty('failure.message', expect.stringContaining(raw)) + expect(assembler.replayState).toBeUndefined() + expect(chunks.filter(chunk => chunk.type === 'finish')).toHaveLength(1) + expect(chunks.some(chunk => chunk.type === 'block-end' && chunk.block.type === 'tool-call')).toBe(false) + expect(server.requests).toHaveLength(1) + }) + + it.each(['null', '[]', '42'])('rejects non-object completed arguments %s', async (raw) => { + const { assembler } = await collect('openai-completions', completionsEvents(raw)) + expect(assembler.finish).toMatchObject({ kind: 'error', failure: { code: 'MALFORMED_RESPONSE' } }) + expect(assembler.replayState).toBeUndefined() + }) + + it('uses authoritative invalid Responses arguments after a valid streamed prefix', async () => { + const raw = '{"value":"final-invalid' + const { chunks, assembler } = await collect('openai-responses', responsesEvents(raw, '{"value":"valid-prefix"}')) + expect(chunks).toContainEqual(expect.objectContaining({ type: 'tool-call-delta', argumentsDelta: '{"value":"valid-prefix"}' })) + expect(assembler.finish).toMatchObject({ kind: 'error', failure: { code: 'MALFORMED_RESPONSE' } }) + expect(assembler.finish).toHaveProperty('failure.message', expect.stringContaining(raw)) + expect(assembler.replayState).toBeUndefined() + }) + + it('retains the first completed-argument failure when several calls are invalid', async () => { + const { assembler } = await collect('openai-responses', responsesEvents('[]', undefined, true, '[1]')) + expect(assembler.finish).toEqual({ kind: 'error', failure: { + code: 'MALFORMED_RESPONSE', message: 'Tool call "count": Tool arguments must be a JSON object\n\nReceived arguments:\n[1]', + } }) + expect(assembler.replayState).toBeUndefined() + }) + + it('retains a native provider failure after malformed argument output', async () => { + const events = responsesEvents('[]').slice(0, -1) + events.push(JSON.stringify({ type: 'response.failed', response: { + id: 'response_arguments', status: 'failed', error: { code: 'server_error', message: 'upstream failure' }, + } })) + const { assembler } = await collect('openai-responses', events) + expect(assembler.finish).toMatchObject({ kind: 'error' }) + expect(assembler.finish).toHaveProperty('failure.message', expect.stringContaining('upstream failure')) + expect(assembler.finish).not.toMatchObject({ failure: { code: 'MALFORMED_RESPONSE' } }) + expect(assembler.replayState).toBeUndefined() + }) + + it('accepts authoritative valid Responses arguments after an incomplete prefix', async () => { + const { assembler } = await collect('openai-responses', responsesEvents('{"value":"complete"}', '{"value":')) + expect(assembler.finish).toEqual({ kind: 'tool-calls' }) + expect(assembler.blocks()).toEqual([{ type: 'tool-call', id: 'call_invalid|item_invalid', name: 'count', arguments: '{"value":"complete"}' }]) + expect(assembler.replayState?.blocks).toEqual([{ type: 'tool-call' }]) + }) + + it('preserves repaired invalid Unicode escapes as complete arguments', async () => { + const { assembler } = await collect('openai-completions', completionsEvents(String.raw`{"value":"\usepackage{x}"}`)) + expect(assembler.finish).toEqual({ kind: 'tool-calls' }) + expect(assembler.blocks()).toContainEqual(expect.objectContaining({ type: 'tool-call', arguments: JSON.stringify({ value: String.raw`\usepackage{x}` }) })) + expect(assembler.replayState).toBeDefined() + }) + + it.each([ + ['anthropic-messages', anthropicEvents('{"value":', 'max_tokens')], + ['openai-completions', completionsEvents('{"value":', 'length')], + ] as const)('%s keeps max-token truncation and prunes its incomplete call', async (api, events) => { + const { assembler } = await collect(api, [...events]) + expect(assembler.finish).toEqual({ kind: 'max-tokens' }) + expect(assembler.blocks()).toEqual([{ type: 'text', text: 'before tool' }]) + expect(assembler.replayState?.blocks).toEqual([{ type: 'text' }]) + expect(assembler.replayState?.response).toMatchObject({ stopReason: 'length' }) + }) + + it('does not dispatch an earlier valid parallel call when a later call is malformed', async () => { + const raw = '{"value":"unfinished' + const server = await mockServer([{ events: responsesEvents(raw, undefined, true) }]) + const ctx = new Context() + onTestFinished(() => ctx.fiber.dispose()) + await mountAgentLoopTestDependencies(ctx) + registerAdapter(ctx, 'openai-responses', server.url) + const loop = await mountAgentLoopTestHarness(ctx) + let executions = 0 + ctx.effect(() => ctx.tools.register(defineContentToolFixture({ + name: 'count', description: 'Count executions', parameters: {}, + isConcurrencySafe: () => true, + async execute() { executions++; return [{ type: 'text', text: 'counted' }] }, + }))) + const agent = await loop.create(SessionId('malformed-parallel'), { provider: 'fixture', model: 'm' }) + const idle = new Promise((resolve) => { + const dispose = ctx.on('agent/status', ({ agent: subject, status }) => { + if (subject === agent && status === 'idle') { dispose(); resolve() } + }) + }) + agent.followup(createUserMessage({ source: { kind: 'user' }, content: [{ type: 'text', text: 'count twice' }] })) + await idle + const events = agent.session.snapshotEvents() + expect(executions).toBe(0) + expect(events.filter(event => event.type === 'tool/call' || event.type === 'tool/result' || event.type === 'assistant/message')).toEqual([]) + const attempts = events.filter(event => event.type === 'assistant/attempt') + expect(attempts).toHaveLength(1) + expect(JSON.stringify(attempts)).toContain('call_valid|item_valid') + expect(JSON.stringify(attempts)).toContain('MALFORMED_RESPONSE') + expect(JSON.stringify(attempts)).not.toContain('replayState') + expect(events.find(event => event.type === 'turn/end')).toMatchObject({ + data: { reason: { kind: 'error', error: { code: 'MALFORMED_RESPONSE' } } }, + }) + expect(server.requests).toHaveLength(1) + }) +}) diff --git a/packages/llm/llm-pi-ai/tests/conversation-updates.spec.ts b/packages/llm/llm-pi-ai/tests/conversation-updates.spec.ts index 756683d718..77b554ec3b 100644 --- a/packages/llm/llm-pi-ai/tests/conversation-updates.spec.ts +++ b/packages/llm/llm-pi-ai/tests/conversation-updates.spec.ts @@ -1,7 +1,7 @@ /** Capability-driven transcript updates through the real pi-ai serializers and shared runtime. */ import { afterEach, describe, expect, it } from 'vitest' import { Context } from '@deepseek-ai/cordis' -import LlmRuntime, { createAssistantMessage, createDeveloperMessage, createMessage, createUserMessage } from '@deepseek-ai/dsh-llm' +import LlmRuntime, { createAssistantMessage, createDeveloperMessage, createMessage, createUserMessage, ReasoningEffortId } from '@deepseek-ai/dsh-llm' import type { GenerateOptions, ToolSchema, ToolUpdate } from '@deepseek-ai/dsh-llm' import * as LlmPiAi from '@deepseek-ai/dsh-llm-pi-ai' import type { Api, Model } from '@earendil-works/pi-ai' @@ -79,6 +79,51 @@ const addition = () => createDeveloperMessage({ source: { kind: 'tool-registry' ] }) describe('serialized conversation updates', () => { + it('retains catalog OpenAI effort in replay while changing the next effort', async () => { + const item = { type: 'message', id: 'message_effort', role: 'assistant', content: [{ type: 'output_text', text: 'reply', annotations: [] }] } + const events = [ + JSON.stringify({ type: 'response.created', response: { id: 'response_effort' } }), + JSON.stringify({ type: 'response.output_item.added', output_index: 0, item }), + JSON.stringify({ type: 'response.output_item.done', output_index: 0, item }), + JSON.stringify({ type: 'response.completed', response: { + id: 'response_effort', status: 'completed', output: [item], usage: { input_tokens: 3, output_tokens: 1, total_tokens: 4 }, + } }), + ] + const server = await mockServer([{ events }, { events }]) + const ctx = new Context() + try { + await ctx.plugin(LlmRuntime) + const adapter = new LlmPiAi.PiAiAdapter({ + profiles: () => resolveProfiles({ openai: { baseURL: server.url, transport: 'sse' } }), + resolveApiKey: () => Promise.resolve('sk-fixture-key'), + auth: memoryAuth(), + }) + ctx.effect(() => ctx.llm.registerAdapter(['openai'], adapter)) + const first = await assemble(ctx, { + provider: 'openai', model: 'gpt-6-sol', reasoningEffort: ReasoningEffortId('low'), messages: [user('first')], + }) + expect(first.finish).toEqual({ kind: 'stop' }) + expect(first.message.source).toMatchObject({ replayState: { response: { providerThinkingLevel: 'low' } } }) + const second = await assemble(ctx, { + provider: 'openai', model: 'gpt-6-sol', reasoningEffort: ReasoningEffortId('high'), + messages: [user('first'), first.message, user('next')], + }) + expect(second.finish).toEqual({ kind: 'stop' }) + expect(second.message.source).toMatchObject({ replayState: { response: { providerThinkingLevel: 'high' } } }) + expect(server.requests).toHaveLength(2) + expect(server.requests[0]).toMatchObject({ reasoning: { effort: 'medium' }, input: [ + expect.objectContaining({ role: 'user' }), { type: 'configuration_update', reasoning: { effort: 'low' } }, + ] }) + expect(server.requests[1]).toMatchObject({ reasoning: { effort: 'medium' }, input: [ + expect.objectContaining({ role: 'user' }), { type: 'configuration_update', reasoning: { effort: 'low' } }, + expect.objectContaining({ role: 'assistant' }), expect.objectContaining({ role: 'user' }), + { type: 'configuration_update', reasoning: { effort: 'high' } }, + ] }) + } finally { + await ctx.fiber.dispose() + } + }) + it.each(['anthropic', 'openai'] as const)('omits an empty initial prompt while retaining the later update through %s', async (provider) => { const modelId = provider === 'anthropic' ? 'claude-opus-4-8' : 'gpt-5.4' const { requests } = await send(provider, modelId, { diff --git a/packages/llm/llm-pi-ai/tests/convert.spec.ts b/packages/llm/llm-pi-ai/tests/convert.spec.ts index d6c86ba4b8..a4b3e85955 100644 --- a/packages/llm/llm-pi-ai/tests/convert.spec.ts +++ b/packages/llm/llm-pi-ai/tests/convert.spec.ts @@ -724,7 +724,7 @@ describe('toStreamChunks', () => { if (requested === undefined) throw new Error('missing Anthropic catalog model') expect(transformMessages(context.messages, requested)[0]).toMatchObject({ content: native.content }) expect(transformMessages(context.messages, { ...requested, id: 'other-model' })[0]).toMatchObject({ - content: [{ type: 'text', text: 'reason' }], + content: [], }) }) diff --git a/patches/@earendil-works__pi-ai@1.2.0.patch b/patches/@earendil-works__pi-ai@1.3.0.patch similarity index 86% rename from patches/@earendil-works__pi-ai@1.2.0.patch rename to patches/@earendil-works__pi-ai@1.3.0.patch index b992efd197..fa81bf4837 100644 --- a/patches/@earendil-works__pi-ai@1.2.0.patch +++ b/patches/@earendil-works__pi-ai@1.3.0.patch @@ -1,8 +1,8 @@ diff --git a/dist/api/anthropic-messages.js b/dist/api/anthropic-messages.js -index d0a5b0f1bcd77e4afff2c92b068a6e3150dcdd86..999a32b27b53838851eb0578fafadea5cb1343f6 100644 +index befb18b2e7d519b3731f6355921de1cb7892c7ac..7b7da85be215d3f91235ef4c32d4aa0f512ea6a5 100644 --- a/dist/api/anthropic-messages.js +++ b/dist/api/anthropic-messages.js -@@ -585,7 +585,6 @@ +@@ -588,7 +588,6 @@ const block = blocks[index]; if (block && block.type === "toolCall") { block.partialJson += event.delta.partial_json; @@ -11,7 +11,7 @@ index d0a5b0f1bcd77e4afff2c92b068a6e3150dcdd86..999a32b27b53838851eb0578fafadea5 type: "toolcall_delta", contentIndex: index, diff --git a/dist/api/bedrock-converse-stream.js b/dist/api/bedrock-converse-stream.js -index ca40f1b4a4e99f441503cf364822073a7a6c478a..14c204261b3f6c47f8950cf44f0fb9f86cf2cda7 100644 +index 6ec62d045eab17f1298c39962cbada2073cb1bd3..abe5eeee081f608be3a5eb7c866684e807c0521c 100644 --- a/dist/api/bedrock-converse-stream.js +++ b/dist/api/bedrock-converse-stream.js @@ -461,7 +461,6 @@ @@ -23,7 +23,7 @@ index ca40f1b4a4e99f441503cf364822073a7a6c478a..14c204261b3f6c47f8950cf44f0fb9f8 } else if (delta?.reasoningContent) { diff --git a/dist/api/mistral-conversations.js b/dist/api/mistral-conversations.js -index eb52675006e4f048aeefde05acceb474e918473e..0cfa1ebc2b277d38fa9515262ca4445c7e22a23e 100644 +index 9038a1c498e0ea0d305fcfdc946455e72714683b..afda5ea6c3bed6abee984d0cb308a769636d01a7 100644 --- a/dist/api/mistral-conversations.js +++ b/dist/api/mistral-conversations.js @@ -570,7 +570,6 @@ @@ -35,10 +35,10 @@ index eb52675006e4f048aeefde05acceb474e918473e..0cfa1ebc2b277d38fa9515262ca4445c type: "toolcall_delta", contentIndex: toolBlocksByKey.get(key), diff --git a/dist/api/openai-completions.js b/dist/api/openai-completions.js -index a60f39352b776d71497e55b33b3334bacfbeb2f5..6c9b98e35606c52e4f0fb29d8fd0b5ba02d92e09 100644 +index 0eeaffa968628593cf6e9f8648650149a97ed55b..fdafb4cfdee8a65bbef86cd1c3a06317c1d87857 100644 --- a/dist/api/openai-completions.js +++ b/dist/api/openai-completions.js -@@ -438,7 +438,6 @@ +@@ -453,7 +453,6 @@ if (toolCall.function?.arguments) { delta = toolCall.function.arguments; block.partialArgs = (block.partialArgs ?? "") + toolCall.function.arguments; @@ -47,10 +47,10 @@ index a60f39352b776d71497e55b33b3334bacfbeb2f5..6c9b98e35606c52e4f0fb29d8fd0b5ba else if (toolCall.custom?.input) { const nextInput = getCustomToolCallInput(block) + toolCall.custom.input; diff --git a/dist/api/openai-responses-shared.js b/dist/api/openai-responses-shared.js -index f413fe5f2a1abcac39c38bbd4a77b38c349a5918..108f599ae1d985d2f465e8cdf99ce0624acea6aa 100644 +index af5f042285334542b9b39239cd9ea540916a1c32..b9f00fbe3a2ba4e7dfabfe4bcca3c54cb2d55f9f 100644 --- a/dist/api/openai-responses-shared.js +++ b/dist/api/openai-responses-shared.js -@@ -548,7 +548,6 @@ +@@ -561,7 +561,6 @@ if (!slot || slot.block.partialJson === undefined) continue; slot.block.partialJson += event.delta; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f6162aae1f..b96dcbfed3 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -10,7 +10,7 @@ overrides: '@deepseek-ai/schemastery': link:vendor/schemastery patchedDependencies: - '@earendil-works/pi-ai@1.2.0': 4202cd05e0250bf7bc1b010b9aa8a50eb4bb7d0c5364195397842d26972b4b6f + '@earendil-works/pi-ai@1.3.0': 97749fcd291996bcd92ab46822f9c410ee5162b1b52a4d87b3f48b1a681a1378 '@electron/osx-sign@1.3.3': 280e7b2816caf01423097d24e79107a66afeb563a348630ce9928ae8499a5861 '@fortune-sheet/core@1.0.4': 8791a230763dbf853fd65d627c68c31de37209992851377855e16cf08ea70532 '@fortune-sheet/react@1.0.4': 2932e52e79afcd4ced08cfa123b5eded786c85043da32f622f3e80af6fc96c37 @@ -11167,8 +11167,8 @@ importers: specifier: link:../../../vendor/schemastery version: link:../../../vendor/schemastery '@earendil-works/pi-ai': - specifier: ^1.2.0 - version: 1.2.0(patch_hash=4202cd05e0250bf7bc1b010b9aa8a50eb4bb7d0c5364195397842d26972b4b6f)(@aws-sdk/credential-provider-node@3.972.84)(@modelcontextprotocol/sdk@1.31.0(supports-color@9.4.0)(zod@4.4.3))(@smithy/signature-v4@5.7.4)(supports-color@9.4.0)(undici@8.11.2)(ws@8.21.3)(zod@4.4.3) + specifier: ^1.3.0 + version: 1.3.0(patch_hash=97749fcd291996bcd92ab46822f9c410ee5162b1b52a4d87b3f48b1a681a1378)(@aws-sdk/credential-provider-node@3.972.84)(@modelcontextprotocol/sdk@1.31.0(supports-color@9.4.0)(zod@4.4.3))(@smithy/signature-v4@5.7.4)(supports-color@9.4.0)(undici@8.11.2)(ws@8.21.3)(zod@4.4.3) devDependencies: '@deepseek-ai/cordis': specifier: workspace:~ @@ -11176,6 +11176,9 @@ importers: '@deepseek-ai/cordis-plugin-loader': specifier: workspace:~ version: link:../../../vendor/loader + '@deepseek-ai/dsh-agent-loop-testkit': + specifier: workspace:* + version: link:../../test-support/agent-loop-testkit '@deepseek-ai/dsh-attachment': specifier: workspace:* version: link:../../attachment/attachment @@ -11203,12 +11206,18 @@ importers: '@deepseek-ai/dsh-llm-deepseek-api-key': specifier: workspace:* version: link:../llm-deepseek-api-key + '@deepseek-ai/dsh-session': + specifier: workspace:* + version: link:../../core/session '@deepseek-ai/dsh-settings': specifier: workspace:* version: link:../../settings/settings '@deepseek-ai/dsh-timeout': specifier: workspace:* version: link:../../util/timeout + '@deepseek-ai/dsh-tools': + specifier: workspace:* + version: link:../../core/tools packages/llm/llm-retry: dependencies: @@ -16591,13 +16600,13 @@ packages: search-insights: optional: true - '@earendil-works/pi-ai@1.2.0': - resolution: {integrity: sha512-U5/1zXqfV1on7/9sbez03tT4Ru49Lvfb4+zXSDK3DBKF5cRk6KxvAOTOY3HJ4jimlZth+rs7vbJqN6iBQAoNuQ==} + '@earendil-works/pi-ai@1.3.0': + resolution: {integrity: sha512-KaBI8x7q7sPoFUXICNGD9efHzDG+oRNwVvkA9jdIsSQWH1RjFssOlEGsbLtewUPPgCrvmz0l2KTgSLXTyj3acw==} engines: {node: '>=22.19.0'} hasBin: true - '@earendil-works/pi-telemetry@1.2.0': - resolution: {integrity: sha512-NZ3aJE6dr7aTgzzXDe53OW7BsAsF8ZUKHPJTtnjaQcxtlagkUT0Ew8Qas0EvYQsZqR/KpF1lB+z209XBzIC7xg==} + '@earendil-works/pi-telemetry@1.3.0': + resolution: {integrity: sha512-ERGZr+aXjNJzoUbB7ZzHVH3g5otzMHjHLs2Fyizm12By5X+gBEhod9cJXFOk13S7M8a29Am+Mq21zm73mRJFLg==} engines: {node: '>=22.19.0'} '@electron-internal/extract-zip@1.0.5': @@ -24342,11 +24351,11 @@ snapshots: transitivePeerDependencies: - '@algolia/client-search' - '@earendil-works/pi-ai@1.2.0(patch_hash=4202cd05e0250bf7bc1b010b9aa8a50eb4bb7d0c5364195397842d26972b4b6f)(@aws-sdk/credential-provider-node@3.972.84)(@modelcontextprotocol/sdk@1.31.0(supports-color@9.4.0)(zod@4.4.3))(@smithy/signature-v4@5.7.4)(supports-color@9.4.0)(undici@8.11.2)(ws@8.21.3)(zod@4.4.3)': + '@earendil-works/pi-ai@1.3.0(patch_hash=97749fcd291996bcd92ab46822f9c410ee5162b1b52a4d87b3f48b1a681a1378)(@aws-sdk/credential-provider-node@3.972.84)(@modelcontextprotocol/sdk@1.31.0(supports-color@9.4.0)(zod@4.4.3))(@smithy/signature-v4@5.7.4)(supports-color@9.4.0)(undici@8.11.2)(ws@8.21.3)(zod@4.4.3)': dependencies: '@anthropic-ai/sdk': 0.129.0(zod@4.4.3) '@aws-sdk/client-bedrock-runtime': 3.1127.0 - '@earendil-works/pi-telemetry': 1.2.0 + '@earendil-works/pi-telemetry': 1.3.0 '@google/genai': 2.21.0(@modelcontextprotocol/sdk@1.31.0(supports-color@9.4.0)(zod@4.4.3))(supports-color@9.4.0) '@smithy/node-http-handler': 4.12.1 http-proxy-agent: 9.1.0(supports-color@9.4.0) @@ -24367,7 +24376,7 @@ snapshots: - ws - zod - '@earendil-works/pi-telemetry@1.2.0': {} + '@earendil-works/pi-telemetry@1.3.0': {} '@electron-internal/extract-zip@1.0.5': {} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index db9147bf89..45da5e4ce4 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -64,8 +64,8 @@ minimumReleaseAgeExclude: - '@deepseek-ai/libreoffice-kit-wasm@0.1.5' # 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.2.0' - - '@earendil-works/pi-telemetry@1.2.0' + - '@earendil-works/pi-ai@1.3.0' + - '@earendil-works/pi-telemetry@1.3.0' # This addon release recognizes the Electron 44.7.0 Node/V8 fingerprint used by Desktop. - node-addon-native-custom-loader@0.1.9 - node-addon-require-builtin-darwin-arm64@0.1.9 @@ -106,7 +106,7 @@ minimumReleaseAgeExclude: # Desktop packaging requires this Electron fingerprint with node-addon-require-builtin 0.1.9. - electron@44.7.0 patchedDependencies: - '@earendil-works/pi-ai@1.2.0': patches/@earendil-works__pi-ai@1.2.0.patch + '@earendil-works/pi-ai@1.3.0': patches/@earendil-works__pi-ai@1.3.0.patch '@electron/osx-sign@1.3.3': patches/@electron__osx-sign@1.3.3.patch '@fortune-sheet/core@1.0.4': patches/@fortune-sheet__core@1.0.4.patch '@fortune-sheet/react@1.0.4': patches/@fortune-sheet__react@1.0.4.patch diff --git a/snapshots/session/pi-ai-malformed-tool-arguments/session.v4.jsonl b/snapshots/session/pi-ai-malformed-tool-arguments/session.v4.jsonl new file mode 100644 index 0000000000..3dd638168e --- /dev/null +++ b/snapshots/session/pi-ai-malformed-tool-arguments/session.v4.jsonl @@ -0,0 +1,17 @@ +{"type":"session","version":4,"id":"{{session:1}}","createdAt":0,"cwd":"{{cwd}}","isSeeded":false,"delegationDepth":0} +{"type":"permission/preset","data":{"preset":"danger-full-access"}} +{"type":"sandbox/mode","data":{"mode":"danger-full-access"}} +{"type":"approval/policy","data":{"policy":"never"}} +{"type":"agent/inbox/spliced","data":{"target":"next-turn","start":0,"inserted":[{"content":[{"type":"text","text":"Write two files in this workspace."}],"source":{"kind":"user"},"role":"user","id":"{{message:1}}"}]}} +{"type":"turn/start","data":{"turn":1}} +{"type":"agent/inbox/spliced","data":{"target":"next-turn","start":0,"removedCount":1,"inserted":[]}} +{"type":"step/start","data":{"turn":1,"step":1}} +{"type":"system/message","data":{"turn":1,"step":1,"message":{"role":"system","content":[{"type":"text","text":"{{system}}"}],"source":{"kind":"system-prompt"},"id":"{{message:2}}"}},"surfaceOp":"append"} +{"type":"user/message","data":{"content":[{"type":"text","text":"Write two files in this workspace."}],"source":{"kind":"user"},"role":"user","id":"{{message:1}}"},"surfaceOp":"append"} +{"type":"user/message","data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent working directory: \"{{cwd}}\".\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}],"source":{"kind":"runtime-context","form":"snapshot","sections":[{"name":"working-directory:current","text":"Current working directory: \"{{cwd}}\"."},{"name":"sandbox:policy","text":"Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations."},{"name":"approval:policy","text":"Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}]},"role":"user","id":"{{message:3}}"},"surfaceOp":"append"} +{"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"tools":"{{tools}}"},"reason":"initial"}} +{"type":"request/context","data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}} +{"type":"session/title","data":{"title":"Write two files in this","messageSeqs":[8],"source":{"kind":"fallback"}}} +{"type":"assistant/attempt","data":{"turn":1,"step":1,"stream":[{"type":"chunk","time":0,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}},{"type":"chunk","time":0,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"call_valid","name":"write","arguments":"{\"file_path\":\"should-not-exist.txt\",\"content\":\"unexpected write\"}"}}},{"type":"chunk","time":0,"chunk":{"type":"block-start","index":1,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":0,"index":1,"dt":[],"id":"call_invalid","name":"write","args":["[]"]},{"type":"chunk","time":0,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":2,"totalTokens":5}}},{"type":"chunk","time":0,"chunk":{"type":"finish","reason":{"kind":"error","failure":{"message":"Tool call \"write\": Tool arguments must be a JSON object\n\nReceived arguments:\n[]","code":"MALFORMED_RESPONSE"}}}}]}} +{"type":"step/end","data":{"turn":1,"step":1}} +{"type":"turn/end","data":{"turn":1,"reason":{"kind":"error","error":{"message":"Tool call \"write\": Tool arguments must be a JSON object\n\nReceived arguments:\n[]","code":"MALFORMED_RESPONSE"}}}} diff --git a/snapshots/session/pi-ai-malformed-tool-arguments/snapshot.yml b/snapshots/session/pi-ai-malformed-tool-arguments/snapshot.yml new file mode 100644 index 0000000000..07bfe5c0ca --- /dev/null +++ b/snapshots/session/pi-ai-malformed-tool-arguments/snapshot.yml @@ -0,0 +1,9 @@ +version: 1 +scenario: pi-ai-malformed-tool-arguments +profile: headless +composition: default +recording: authored +header: + class: default +workspace: + final: true diff --git a/snapshots/session/pi-ai-malformed-tool-arguments/workspace.expected/.empty b/snapshots/session/pi-ai-malformed-tool-arguments/workspace.expected/.empty new file mode 100644 index 0000000000..e69de29bb2 From b9e95fd1e5762b27007866510e91ddd6ee004b4b Mon Sep 17 00:00:00 2001 From: Tianyi Cui <53024+tianyicui@users.noreply.github.com> Date: Sat, 17 Oct 2026 18:58:49 +0800 Subject: [PATCH 8/8] docs(upgrade): target rc.2 for the unmerged pi-ai upgrade --- .../pi-ai-model-catalog/guide.i18n.yaml | 10 +++++----- .../pi-ai-model-catalog/guide.md | 0 .../pi-ai-model-catalog/guide.zh.md | 0 .../pi-ai-protocol-replay/guide.i18n.yaml | 6 +++--- .../pi-ai-protocol-replay/guide.md | 0 .../pi-ai-protocol-replay/guide.zh.md | 0 6 files changed, 8 insertions(+), 8 deletions(-) rename docs/upgrade-guide/{v0.2.1-alpha.4 => v0.2.1-rc.2}/pi-ai-model-catalog/guide.i18n.yaml (79%) rename docs/upgrade-guide/{v0.2.1-alpha.4 => v0.2.1-rc.2}/pi-ai-model-catalog/guide.md (100%) rename docs/upgrade-guide/{v0.2.1-alpha.4 => v0.2.1-rc.2}/pi-ai-model-catalog/guide.zh.md (100%) rename docs/upgrade-guide/{v0.2.1-alpha.4 => v0.2.1-rc.2}/pi-ai-protocol-replay/guide.i18n.yaml (86%) rename docs/upgrade-guide/{v0.2.1-alpha.4 => v0.2.1-rc.2}/pi-ai-protocol-replay/guide.md (100%) rename docs/upgrade-guide/{v0.2.1-alpha.4 => v0.2.1-rc.2}/pi-ai-protocol-replay/guide.zh.md (100%) diff --git a/docs/upgrade-guide/v0.2.1-alpha.4/pi-ai-model-catalog/guide.i18n.yaml b/docs/upgrade-guide/v0.2.1-rc.2/pi-ai-model-catalog/guide.i18n.yaml similarity index 79% rename from docs/upgrade-guide/v0.2.1-alpha.4/pi-ai-model-catalog/guide.i18n.yaml rename to docs/upgrade-guide/v0.2.1-rc.2/pi-ai-model-catalog/guide.i18n.yaml index 3a074bf961..0c66d2300b 100644 --- a/docs/upgrade-guide/v0.2.1-alpha.4/pi-ai-model-catalog/guide.i18n.yaml +++ b/docs/upgrade-guide/v0.2.1-rc.2/pi-ai-model-catalog/guide.i18n.yaml @@ -1,16 +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.4/pi-ai-model-catalog/guide.md +# pnpm run verify-translation-pairing --write docs/upgrade-guide/v0.2.1-rc.2/pi-ai-model-catalog/guide.md /: en: 95f84e4861e282ee zh: ee2bf9db07fcc61e /pi-ai-1-3-0-model-catalog-changes: - en: ffd3cd2159b8c733 - zh: bf7e8f5a62d432f4 + en: 201fd0e2296a9792 + zh: a95449587c9e7e5c /pi-ai-1-3-0-model-catalog-changes/change: - en: 66ff01b4e9eb0e15 - zh: 6c71de9b23ebc8b9 + en: 4e1d3521fd5f9dec + zh: 9978497665279e28 /pi-ai-1-3-0-model-catalog-changes/migration: en: 6f1313d45d05c97d zh: a5d396d5c9316031 diff --git a/docs/upgrade-guide/v0.2.1-alpha.4/pi-ai-model-catalog/guide.md b/docs/upgrade-guide/v0.2.1-rc.2/pi-ai-model-catalog/guide.md similarity index 100% rename from docs/upgrade-guide/v0.2.1-alpha.4/pi-ai-model-catalog/guide.md rename to docs/upgrade-guide/v0.2.1-rc.2/pi-ai-model-catalog/guide.md diff --git a/docs/upgrade-guide/v0.2.1-alpha.4/pi-ai-model-catalog/guide.zh.md b/docs/upgrade-guide/v0.2.1-rc.2/pi-ai-model-catalog/guide.zh.md similarity index 100% rename from docs/upgrade-guide/v0.2.1-alpha.4/pi-ai-model-catalog/guide.zh.md rename to docs/upgrade-guide/v0.2.1-rc.2/pi-ai-model-catalog/guide.zh.md diff --git a/docs/upgrade-guide/v0.2.1-alpha.4/pi-ai-protocol-replay/guide.i18n.yaml b/docs/upgrade-guide/v0.2.1-rc.2/pi-ai-protocol-replay/guide.i18n.yaml similarity index 86% rename from docs/upgrade-guide/v0.2.1-alpha.4/pi-ai-protocol-replay/guide.i18n.yaml rename to docs/upgrade-guide/v0.2.1-rc.2/pi-ai-protocol-replay/guide.i18n.yaml index 2b5c9f6580..c57311953a 100644 --- a/docs/upgrade-guide/v0.2.1-alpha.4/pi-ai-protocol-replay/guide.i18n.yaml +++ b/docs/upgrade-guide/v0.2.1-rc.2/pi-ai-protocol-replay/guide.i18n.yaml @@ -1,13 +1,13 @@ # 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.4/pi-ai-protocol-replay/guide.md +# pnpm run verify-translation-pairing --write docs/upgrade-guide/v0.2.1-rc.2/pi-ai-protocol-replay/guide.md /: en: 1e2bdc1e7e35ea20 zh: 503fcf05bceec218 /pi-ai-1-3-0-protocol-and-replay-changes: - en: 58e9a6339aa3c1f4 - zh: 3a11bad804ca9b66 + en: c59f3b45f3ab51cd + zh: 34363c450eb55190 /pi-ai-1-3-0-protocol-and-replay-changes/change: en: 308406476f8470f0 zh: ecb22ad91d460769 diff --git a/docs/upgrade-guide/v0.2.1-alpha.4/pi-ai-protocol-replay/guide.md b/docs/upgrade-guide/v0.2.1-rc.2/pi-ai-protocol-replay/guide.md similarity index 100% rename from docs/upgrade-guide/v0.2.1-alpha.4/pi-ai-protocol-replay/guide.md rename to docs/upgrade-guide/v0.2.1-rc.2/pi-ai-protocol-replay/guide.md diff --git a/docs/upgrade-guide/v0.2.1-alpha.4/pi-ai-protocol-replay/guide.zh.md b/docs/upgrade-guide/v0.2.1-rc.2/pi-ai-protocol-replay/guide.zh.md similarity index 100% rename from docs/upgrade-guide/v0.2.1-alpha.4/pi-ai-protocol-replay/guide.zh.md rename to docs/upgrade-guide/v0.2.1-rc.2/pi-ai-protocol-replay/guide.zh.md