Merge pinned Pi upgrade base into worker context fix
This commit is contained in:
commit
c12e805dc7
31 files changed
+580
-60
No files matched your search
@@ -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.3.0` — [`patches/@earendil-works__pi-ai@1.3.0.patch`](patches/@earendil-works__pi-ai@1.3.0.patch)
|
||||
- `@electron/[email protected]` — [`patches/@[email protected]`](patches/@[email protected])
|
||||
- `@fortune-sheet/[email protected]` — [`patches/@[email protected]`](patches/@[email protected])
|
||||
- `@fortune-sheet/[email protected]` — [`patches/@[email protected]`](patches/@[email protected])
|
||||
|
||||
@@ -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<Record<string, DesktopRuntimePatch>> = {
|
||||
'@earendil-works/pi-ai@1.1.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/[email protected]': { scope: 'workspace-only', reason: 'Signing executes on the build host.' },
|
||||
'@fortune-sheet/[email protected]': { scope: 'workspace-only', reason: 'Patched spreadsheet code is embedded in the client bundle.' },
|
||||
'@fortune-sheet/[email protected]': { scope: 'workspace-only', reason: 'Patched spreadsheet code is embedded in the client bundle.' },
|
||||
@@ -20,6 +20,6 @@ export const DESKTOP_RUNTIME_PATCHES: Readonly<Record<string, DesktopRuntimePatc
|
||||
|
||||
/** Reviewed Desktop dependency constraints, independent of patch selection. */
|
||||
export const DESKTOP_RUNTIME_DEPENDENCY_OVERRIDES: Readonly<Record<string, string>> = {
|
||||
// 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.3.0; its patch is qualified against 1.3.0.
|
||||
'@earendil-works/pi-ai@^1.3.0': '1.3.0',
|
||||
}
|
||||
@@ -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', '[email protected]'])
|
||||
expect(plan.patches.map(entry => entry.spec)).toEqual(['@earendil-works/pi-ai@1.3.0', '[email protected]'])
|
||||
expect(plan.workspaceOnly).toHaveLength(6)
|
||||
const settings = yaml.load(readFileSync(join(project, 'pnpm-workspace.yaml'), 'utf8')) as { overrides: Record<string, string> }
|
||||
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.3.0']).toBe('1.3.0')
|
||||
expect(readFileSync(join(repository, 'pnpm-lock.yaml'))).toEqual(before)
|
||||
})
|
||||
|
||||
|
||||
@@ -6,8 +6,8 @@
|
||||
en: 3e8ecd3509fa944f
|
||||
zh: 3fd3ff9fcd1e1e90
|
||||
/apps-web-browser-e2e/completion-observations:
|
||||
en: db3a0b5c98fcb03c
|
||||
zh: b58d917ee693cdc4
|
||||
en: 1f9c004f3eb465fb
|
||||
zh: 3e45ef8014bad48d
|
||||
/apps-web-browser-e2e/these-are-host-face-tests:
|
||||
en: 8c406d423c8e2137
|
||||
zh: a66a144fd146f56c
|
||||
|
||||
@@ -24,6 +24,10 @@ 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.
|
||||
|
||||
[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.
|
||||
|
||||
@@ -24,6 +24,10 @@ pnpm --filter @deepseek-ai/dsh-web-frontend exec playwright install --with-deps
|
||||
|
||||
显式滚动使用 `support.ts` 的 `scrollIntoView`:旧元素脱离 DOM 时重新解析 locator,并在同一个浏览器任务中检查连接状态、执行原生滚动。各场景保留滚动后的可见性与几何断言。
|
||||
|
||||
[侧栏窗格关闭后的快照](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 服务访问都无法编译。
|
||||
|
||||
@@ -282,9 +282,14 @@ 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()
|
||||
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'),
|
||||
|
||||
@@ -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<undefined>()
|
||||
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])
|
||||
|
||||
@@ -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-rc.2/pi-ai-model-catalog/guide.md
|
||||
/:
|
||||
en: 95f84e4861e282ee
|
||||
zh: ee2bf9db07fcc61e
|
||||
/pi-ai-1-3-0-model-catalog-changes:
|
||||
en: 201fd0e2296a9792
|
||||
zh: a95449587c9e7e5c
|
||||
/pi-ai-1-3-0-model-catalog-changes/change:
|
||||
en: 4e1d3521fd5f9dec
|
||||
zh: 9978497665279e28
|
||||
/pi-ai-1-3-0-model-catalog-changes/migration:
|
||||
en: 6f1313d45d05c97d
|
||||
zh: a5d396d5c9316031
|
||||
@@ -0,0 +1,53 @@
|
||||
---
|
||||
kind: upgrade-guide
|
||||
description: "The pi-ai 1.3.0 catalog removes model IDs and lowers some model capacities."
|
||||
---
|
||||
|
||||
# pi-ai 1.3.0 model catalog changes
|
||||
|
||||
English | [中文](guide.zh.md)
|
||||
|
||||
## Change
|
||||
|
||||
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` |
|
||||
| `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` |
|
||||
|
||||
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, 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.<provider>.modelOverrides.<id>` entries and replace removed IDs in `providers.<provider>.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.
|
||||
@@ -0,0 +1,53 @@
|
||||
---
|
||||
kind: upgrade-guide
|
||||
description: "pi-ai 1.3.0 目录移除了部分模型 ID,并降低了部分模型容量。"
|
||||
---
|
||||
|
||||
# pi-ai 1.3.0 模型目录变更
|
||||
|
||||
[English](guide.md) | 中文
|
||||
|
||||
## 变更
|
||||
|
||||
pi-ai 依赖从 1.1.0 升级到 1.3.0。以下聊天模型 ID 不再包含在目录中。已保存的 `modelOverrides` 会报告目录条目缺失诊断;新的覆盖配置会被拒绝。显式 `models` 条目会失去目录默认值。
|
||||
|
||||
| 提供商 | 已移除的模型 ID |
|
||||
|---|---|
|
||||
| `google-vertex` | `gemini-flash-latest`, `gemini-flash-lite-latest` |
|
||||
| `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` |
|
||||
|
||||
以下目录限制降低。数值以 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 替换为受支持的模型。检查端点、能力和价格;ID 没有自动替代项。
|
||||
2. 更新 `cordis.patch.yml`、preset patch 和受影响 Session 中的模型选择。移除过时的 `providers.<provider>.modelOverrides.<id>` 条目,并替换 `providers.<provider>.models` 列表中已移除的 ID。参见[配置参考](../../../../packages/llm/llm-pi-ai/README.zh.md)。
|
||||
3. 根据端点支持的值检查容量与推理覆盖配置。启动 profile,确认模型没有配置诊断,再发送一个简短提示。
|
||||
@@ -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-rc.2/pi-ai-protocol-replay/guide.md
|
||||
/:
|
||||
en: 1e2bdc1e7e35ea20
|
||||
zh: 503fcf05bceec218
|
||||
/pi-ai-1-3-0-protocol-and-replay-changes:
|
||||
en: c59f3b45f3ab51cd
|
||||
zh: 34363c450eb55190
|
||||
/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
|
||||
@@ -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.<provider>.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).
|
||||
@@ -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.<provider>.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)。
|
||||
@@ -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(<TextPreview {...h.props()} renderSlot={renderSlot} useDocumentPreviews={useDocumentPreviews} />)
|
||||
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()
|
||||
|
||||
@@ -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: ee870ac2ac87decc
|
||||
zh: 9e66c5e7400c4bc6
|
||||
en: ccaea557a3a3ffaf
|
||||
zh: 5f70d824d39fb457
|
||||
/deepseek-ai-dsh-llm-pi-ai/known-limitations-and-deferred-work/dev-note:
|
||||
en: 3678dd0f6642c2a5
|
||||
zh: 75cbdd064cc64282
|
||||
@@ -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.
|
||||
|
||||
</details>
|
||||
|
||||
@@ -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.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.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.
|
||||
|
||||
<a id="dev-note"></a>
|
||||
### Dev Note
|
||||
|
||||
@@ -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 或模型的请求会省略不受支持的签名和推理块。缺少原生元数据的降级回放也会省略推理。切回原模型时,有效的原生回放会保留已存储的推理。
|
||||
|
||||
</details>
|
||||
|
||||
@@ -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.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.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 时都要重新应用或撤销该补丁。
|
||||
|
||||
<a id="dev-note"></a>
|
||||
### 开发备注
|
||||
|
||||
@@ -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.1.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": {
|
||||
|
||||
@@ -287,6 +287,7 @@ const RESPONSES_COMPAT_GATE = {
|
||||
supportsToolSearch: 'withhold',
|
||||
supportsExplicitPromptCacheMode: 'withhold',
|
||||
supportsMidConvoSystemMessages: 'withhold',
|
||||
supportsMidConvoEffort: 'withhold',
|
||||
} as const satisfies Record<keyof OpenAIResponsesCompat, CompatDisposition>
|
||||
|
||||
/** Disposition of every `AnthropicMessagesCompat` field; a drift gate like the one above. */
|
||||
|
||||
@@ -22,7 +22,7 @@ export const PROVIDER_DATA: Record<BuiltinProvider, ProviderData> & Partial<Reco
|
||||
'google-vertex': { env: ['GOOGLE_CLOUD_API_KEY', 'GOOGLE_APPLICATION_CREDENTIALS', 'GOOGLE_CLOUD_PROJECT', 'GCLOUD_PROJECT', 'GOOGLE_CLOUD_LOCATION'], baseURLEnv: ['GOOGLE_CLOUD_PROJECT', 'GCLOUD_PROJECT', 'GOOGLE_CLOUD_LOCATION'] },
|
||||
'groq': { env: ['GROQ_API_KEY'], defaultBaseURL: 'https://api.groq.com/openai/v1' },
|
||||
'huggingface': { env: ['HF_TOKEN'], defaultBaseURL: 'https://router.huggingface.co/v1' },
|
||||
'kimi-coding': { env: ['KIMI_API_KEY'], defaultBaseURL: 'https://api.kimi.com/coding' },
|
||||
'kimi-coding': { env: ['KIMI_API_KEY'], defaultBaseURL: 'https://api.kimi.com/coding/v1' },
|
||||
'meta': { env: ['META_API_KEY'], defaultBaseURL: 'https://api.meta.ai/v1' },
|
||||
'minimax': { env: ['MINIMAX_API_KEY'], defaultBaseURL: 'https://api.minimax.io/anthropic' },
|
||||
'minimax-cn': { env: ['MINIMAX_CN_API_KEY'], defaultBaseURL: 'https://api.minimaxi.com/anthropic' },
|
||||
|
||||
@@ -137,7 +137,9 @@ export function mapStopReason(message: AssistantMessage, contextWindow?: number)
|
||||
* in-band terminal error an aborted finish.
|
||||
* @param requestedModel - request model identity recorded for durable replay.
|
||||
* @returns the harness chunks, ending with `usage` then `finish`; throws
|
||||
* `LlmError` (`STREAM_CLOSED`) if the source ends without a terminal event.
|
||||
* `LlmError` (`STREAM_CLOSED`) if the source ends without a terminal event, or
|
||||
* `MALFORMED_RESPONSE` when completed tool arguments are invalid. Length-limited
|
||||
* output retains its terminal reason so the assembler can discard incomplete calls.
|
||||
*/
|
||||
export async function* toStreamChunks(
|
||||
events: AsyncIterable<AssistantMessageEvent>,
|
||||
@@ -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<number, { id: string; name: string }>()
|
||||
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),
|
||||
|
||||
@@ -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<void>((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)
|
||||
})
|
||||
})
|
||||
@@ -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, {
|
||||
|
||||
@@ -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: [],
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
diff --git a/dist/api/anthropic-messages.js b/dist/api/anthropic-messages.js
|
||||
index 38571b31e143e913c2bf59dad73382982e93664a..950e546197d8e84cb3edfca0012939f1db2488d8 100644
|
||||
index befb18b2e7d519b3731f6355921de1cb7892c7ac..7b7da85be215d3f91235ef4c32d4aa0f512ea6a5 100644
|
||||
--- a/dist/api/anthropic-messages.js
|
||||
+++ b/dist/api/anthropic-messages.js
|
||||
@@ -553,7 +553,6 @@ export const stream = (model, context, options) => {
|
||||
@@ -588,7 +588,6 @@
|
||||
const block = blocks[index];
|
||||
if (block && block.type === "toolCall") {
|
||||
block.partialJson += event.delta.partial_json;
|
||||
@@ -11,10 +11,10 @@ index 38571b31e143e913c2bf59dad73382982e93664a..950e546197d8e84cb3edfca0012939f1
|
||||
type: "toolcall_delta",
|
||||
contentIndex: index,
|
||||
diff --git a/dist/api/bedrock-converse-stream.js b/dist/api/bedrock-converse-stream.js
|
||||
index 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 @@ 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 9038a1c498e0ea0d305fcfdc946455e72714683b..afda5ea6c3bed6abee984d0cb308a769636d01a7 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;
|
||||
@@ -35,10 +35,10 @@ index 2fc3ad343af090a919f5b71454b29f23b009fc21..64aa8a4ab2461484f22066913455518f
|
||||
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 @@ export const stream = (model, context, options) => {
|
||||
@@ -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 @@ export async function processResponsesStream(openaiStream, output, stream, model
|
||||
@@ -561,7 +561,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);
|
||||
Generated
+19
-10
@@ -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.3.0': 97749fcd291996bcd92ab46822f9c410ee5162b1b52a4d87b3f48b1a681a1378
|
||||
'@electron/[email protected]': 280e7b2816caf01423097d24e79107a66afeb563a348630ce9928ae8499a5861
|
||||
'@fortune-sheet/[email protected]': 8791a230763dbf853fd65d627c68c31de37209992851377855e16cf08ea70532
|
||||
'@fortune-sheet/[email protected]': 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/[email protected])(@modelcontextprotocol/[email protected]([email protected])([email protected]))(@smithy/[email protected])([email protected])([email protected])([email protected])([email protected])
|
||||
specifier: ^1.3.0
|
||||
version: 1.3.0(patch_hash=97749fcd291996bcd92ab46822f9c410ee5162b1b52a4d87b3f48b1a681a1378)(@aws-sdk/[email protected])(@modelcontextprotocol/[email protected]([email protected])([email protected]))(@smithy/[email protected])([email protected])([email protected])([email protected])([email protected])
|
||||
devDependencies:
|
||||
'@deepseek-ai/cordis':
|
||||
specifier: workspace:~
|
||||
@@ -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.1.0':
|
||||
resolution: {integrity: sha512-1T7LAkc/5Bvc0v6w4vAGVdCrli0o/E0pEmYKTnixu95vSFArBjvbhS/G4ZwI0RUePgf0Imcu0VyqlM4EcXxqfw==}
|
||||
'@earendil-works/pi-ai@1.3.0':
|
||||
resolution: {integrity: sha512-KaBI8x7q7sPoFUXICNGD9efHzDG+oRNwVvkA9jdIsSQWH1RjFssOlEGsbLtewUPPgCrvmz0l2KTgSLXTyj3acw==}
|
||||
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.3.0':
|
||||
resolution: {integrity: sha512-ERGZr+aXjNJzoUbB7ZzHVH3g5otzMHjHLs2Fyizm12By5X+gBEhod9cJXFOk13S7M8a29Am+Mq21zm73mRJFLg==}
|
||||
engines: {node: '>=22.19.0'}
|
||||
|
||||
'@electron-internal/[email protected]':
|
||||
@@ -24342,11 +24351,11 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- '@algolia/client-search'
|
||||
|
||||
'@earendil-works/pi-ai@1.1.0(patch_hash=66eaf960054f10441b93ac3857114e96c1901f6d183713e2f21437b007111644)(@aws-sdk/[email protected])(@modelcontextprotocol/[email protected]([email protected])([email protected]))(@smithy/[email protected])([email protected])([email protected])([email protected])([email protected])':
|
||||
'@earendil-works/pi-ai@1.3.0(patch_hash=97749fcd291996bcd92ab46822f9c410ee5162b1b52a4d87b3f48b1a681a1378)(@aws-sdk/[email protected])(@modelcontextprotocol/[email protected]([email protected])([email protected]))(@smithy/[email protected])([email protected])([email protected])([email protected])([email protected])':
|
||||
dependencies:
|
||||
'@anthropic-ai/sdk': 0.129.0([email protected])
|
||||
'@aws-sdk/client-bedrock-runtime': 3.1127.0
|
||||
'@earendil-works/pi-telemetry': 1.1.0
|
||||
'@earendil-works/pi-telemetry': 1.3.0
|
||||
'@google/genai': 2.21.0(@modelcontextprotocol/[email protected]([email protected])([email protected]))([email protected])
|
||||
'@smithy/node-http-handler': 4.12.1
|
||||
http-proxy-agent: 9.1.0([email protected])
|
||||
@@ -24367,7 +24376,7 @@ snapshots:
|
||||
- ws
|
||||
- zod
|
||||
|
||||
'@earendil-works/pi-telemetry@1.1.0': {}
|
||||
'@earendil-works/pi-telemetry@1.3.0': {}
|
||||
|
||||
'@electron-internal/[email protected]': {}
|
||||
|
||||
|
||||
+3
-3
@@ -64,8 +64,8 @@ minimumReleaseAgeExclude:
|
||||
- '@deepseek-ai/[email protected]'
|
||||
# Fresh pi-ai releases carry the model catalog updates that are the whole
|
||||
# point of bumping it; waiting out the release age would defeat that.
|
||||
- '@earendil-works/pi-ai@1.1.0'
|
||||
- '@earendil-works/pi-telemetry@1.1.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.
|
||||
- [email protected]
|
||||
- [email protected]
|
||||
@@ -106,7 +106,7 @@ minimumReleaseAgeExclude:
|
||||
# Desktop packaging requires this Electron fingerprint with node-addon-require-builtin 0.1.9.
|
||||
- [email protected]
|
||||
patchedDependencies:
|
||||
'@earendil-works/pi-ai@1.1.0': patches/@earendil-works__pi-ai@1.1.0.patch
|
||||
'@earendil-works/pi-ai@1.3.0': patches/@earendil-works__pi-ai@1.3.0.patch
|
||||
'@electron/[email protected]': patches/@[email protected]
|
||||
'@fortune-sheet/[email protected]': patches/@[email protected]
|
||||
'@fortune-sheet/[email protected]': patches/@[email protected]
|
||||
|
||||
@@ -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"}}}}
|
||||
@@ -0,0 +1,9 @@
|
||||
version: 1
|
||||
scenario: pi-ai-malformed-tool-arguments
|
||||
profile: headless
|
||||
composition: default
|
||||
recording: authored
|
||||
header:
|
||||
class: default
|
||||
workspace:
|
||||
final: true
|
||||
Whitespace-only changes.
Reference in new issue
Block a user