Merge pull request #2972 from deepseek-harness/worktree/upgrade-pi-ai-0.84.2

chore(llm): bump pi-ai to 0.84.2
This commit is contained in:
Tianyi Cui
2026-08-24 02:22:50 +08:00
committed by GitHub
25 changed files with 266 additions and 116 deletions
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-08-18-pi-ai-wire-compat-surface.md
2026-08-18-pi-ai-wire-compat-surface.md: 3da2db1ebdf67bcfaf8c872491356b0ef7d0ca89
2026-08-18-pi-ai-wire-compat-surface.zh.md: ff9870f5863fb96ee026dfab1b96b4e1f3e6e238
2026-08-18-pi-ai-wire-compat-surface.md: 280ce2dd16bb7f89f238c8e0fab07ed74de943f1
2026-08-18-pi-ai-wire-compat-surface.zh.md: 1a5704dc8c462a8007d1129de4ec7a51e9bd22a4
@@ -14,13 +14,13 @@ Writing the field anyway was worse than unsupported. schemastery passes unknown
## Decision
One drift gate per pi-ai compat type — keyed `Record<keyof OpenAICompletionsCompat | …, CompatDisposition>` — classifies every upstream field as `offer` or `withhold`. Thirty distinct fields, twenty offered. The line is what a private URL can imply: a deployment must be able to state what nothing can infer from an unrecognized endpoint, while a field pi-ai's installed catalog sets for a named vendor stays withheld, because a route reaching for `openRouterRouting` or `deferredToolsMode` is a catalog route that should be named as such and inherit the value.
One drift gate per pi-ai compat type — keyed `Record<keyof OpenAICompletionsCompat | …, CompatDisposition>` — classifies every upstream field as `offer` or `withhold`. Thirty-four distinct fields, twenty-three offered. The line is what a private URL can imply: a deployment must be able to state what nothing can infer from an unrecognized endpoint, while a field pi-ai's installed catalog sets for a named vendor stays withheld, because a route reaching for `openRouterRouting` or `deferredToolsMode` is a catalog route that should be named as such and inherit the value.
`PiAiCompatProfile` stays an explicit interface with per-field JSDoc — it is what a configuration surface renders and what `docs/config-catalog.md` pastes — and a type-level `AssertNever` over the symmetric difference proves it names exactly the offered set. The schemastery schema is declared `z<PiAiCompatProfile>`, and `exactOptionalPropertyTypes` is what makes that annotation load-bearing in both directions, so the four faces lock together: an upstream field added, a gate entry missing, an interface field forgotten, or a schema key omitted each fails compilation. Field *types* are derived from upstream rather than restated, and a second proof pins the profile assignable to the upstream compat types, so a widened value union cannot silently narrow what configuration accepts — the cast to `ModelCompat` at materialization would otherwise hide it.
Protocol applicability is per field, and grouping follows the compat *type* rather than the protocol name: pi-ai gives `openai-responses`, `azure-openai-responses`, and `openai-codex-responses` one `OpenAIResponsesCompat`, so a switch settable on one is settable on all three. Keying by protocol name alone refused two shipped catalog routes the fields their own models declare. The protocol set is derived from `Model.compat`'s own conditional, so a release that gives a further protocol a compat type fails the gate list by name. A model-level switch its protocol does not take fails resolution naming what that protocol does offer; a route-level one lands on the models that read it and skips the rest, and is refused only when no model on the route could read it. `chatTemplateKwargs` is offered, which is what makes the two `chat-template` thinking formats nameable; nothing cross-checks that pairing, because the format in force may come from the catalog entry or from pi-ai's detection, neither of which resolution can read.
Protocol applicability is per field, and grouping follows the compat *type* rather than the protocol name: pi-ai gives `openai-responses`, `azure-openai-responses`, and `openai-codex-responses` one `OpenAIResponsesCompat`, so a switch settable on one is settable on all three. Keying by protocol name alone refused two shipped catalog routes the fields their own models declare. The protocol set is derived from `Model.compat`'s own conditional, so a release that gives a further protocol a compat type fails the gate list by name. A model-level switch its protocol does not take fails resolution naming what that protocol does offer; a route-level one lands on the models that read it and skips the rest, and is refused only when no model on the route could read it. `chatTemplateKwargs` and `chatTemplateArgs` are offered, which makes the two `chat-template` formats and `baseten` nameable; nothing cross-checks those pairings, because the format in force may come from the catalog entry or from pi-ai's detection, neither of which resolution can read.
Three kinds of `compat` key are refused where they are written rather than dropped: one no protocol declares, one a gate withholds, and one written with no value. The check runs over every key before any protocol resolves, so a misspelling fails even on a route whose models never reach the protocol that would have taken it. It reads raw keys deliberately: a withheld or undeclared name is absent from the schema, so schemastery cannot have materialized it and a person wrote it. The valueless case is the one that has to fail rather than be ignored — schemastery passes a YAML bare key through as null, and carrying it forward writes null over the installed catalog's value, leaving pi-ai's `??` reaching for its baseURL detection with the catalog layer skipped entirely. Fields carrying a value are then filtered separately, because schemastery materializes an absent dict as `{}` and `chatTemplateKwargs` is present on every parsed profile whether or not anyone wrote one.
Three kinds of `compat` key are refused where they are written rather than dropped: one no protocol declares, one a gate withholds, and one written with no value. The check runs over every key before any protocol resolves, so a misspelling fails even on a route whose models never reach the protocol that would have taken it. It reads raw keys deliberately: a withheld or undeclared name is absent from the schema, so schemastery cannot have materialized it and a person wrote it. The valueless case is the one that has to fail rather than be ignored — schemastery passes a YAML bare key through as null, and carrying it forward writes null over the installed catalog's value, leaving pi-ai's `??` reaching for its baseURL detection with the catalog layer skipped entirely. Fields carrying a value are then filtered separately, because schemastery materializes absent dicts as `{}` and both template-argument fields are present on every parsed profile whether or not anyone wrote them.
## Where a refusal lands
@@ -45,7 +45,7 @@ An external edit to the settings file is the one path that cannot report: the pr
## Consequences
- An OpenAI-compatible gateway that rejects the `developer` role, `max_completion_tokens`, `store`, `stream_options`, or `strict` is now configuration rather than an unreachable provider, and the same holds for an Anthropic-compatible gateway rejecting `temperature` or tool `cache_control`.
- A pi-ai upgrade that adds a compat field fails the build until someone classifies it, which is how `chatTemplateKwargs` and the `chat-template` formats stopped being a standing exception.
- A pi-ai upgrade that adds a compat field or thinking format fails the build until someone classifies it; the gates include both template-argument fields and every current format.
- Unknown compat keys join every other configuration error's failure model. The improvement over the previous silent drop is bounded by the settings seam: an external file edit still keeps its last good value and warns, so the operator's signal is a restart rather than the write.
- **Deferred, not closed:** a route that repoints `api` and configures no compat at all keeps the installed entry's `compat` through the model literal's `...base` spread, in the *other* protocol's shape. Fields several compat types share (`supportsLongCacheRetention`, `sendSessionAffinityHeaders`) therefore cross protocols. It predates this surface — the early return it rides existed before — and is left for its own change.
- **Deferred, not closed:** `publish()` reports a rejected stored section only through `ctx.logger.warn`, with no user-visible channel. It affects every settings namespace and is owned by `dsh-settings`.
@@ -14,13 +14,13 @@ pi-ai 依据提供方 id 与 baseURL 决定每个请求的形状——系统提
## Decision
每个 pi-ai compat 类型一张漂移门禁——以 `Record<keyof OpenAICompletionsCompat | …, CompatDisposition>` 为键——把每一个上游字段分类为 `offer``withhold`。去重后三十个字段,开放二十个。分界线在于私有 URL 能推出什么:凡是无法从未识别端点推断的,部署方必须能够说出口;而 pi-ai 已安装 catalog 为具名厂商设定的字段保持扣留,因为伸手去够 `openRouterRouting``deferredToolsMode` 的路由,本就是一条应当以该厂商命名、并继承其值的 catalog 路由。
每个 pi-ai compat 类型一张漂移门禁——以 `Record<keyof OpenAICompletionsCompat | …, CompatDisposition>` 为键——把每一个上游字段分类为 `offer``withhold`。去重后三十个字段,开放二十个。分界线在于私有 URL 能推出什么:凡是无法从未识别端点推断的,部署方必须能够说出口;而 pi-ai 已安装 catalog 为具名厂商设定的字段保持扣留,因为伸手去够 `openRouterRouting``deferredToolsMode` 的路由,本就是一条应当以该厂商命名、并继承其值的 catalog 路由。
`PiAiCompatProfile` 保持为带逐字段 JSDoc 的显式 interface——它是配置界面所渲染、也是 `docs/config-catalog.md` 所粘贴的东西——并由一个作用在对称差上的类型级 `AssertNever` 证明它恰好命名了开放集。schemastery schema 声明为 `z<PiAiCompatProfile>`,而使这条标注在两个方向上都真正吃劲的是 `exactOptionalPropertyTypes`,于是四个面互锁:上游新增字段、门禁漏一条、interface 忘记一个字段、schema 少一个键,都会在编译期失败。字段的**类型**派生自上游而非重述,另有一条证明把 profile 钉为可赋值给上游 compat 类型,因此被拓宽的值并集不会悄悄收窄配置所接受的范围——否则物化处对 `ModelCompat` 的强转会把它洗掉。
协议适用性逐字段判断,且归组依据是 compat **类型**而非协议名:pi-ai 让 `openai-responses``azure-openai-responses``openai-codex-responses` 共用同一个 `OpenAIResponsesCompat`,因此可设在其中之一的开关,三者皆可设。仅按协议名归组曾使两条随附的 catalog 路由拿不到其自身模型所声明的字段。协议集派生自 `Model.compat` 自身的条件类型,因此某个版本若给别的协议加上 compat 类型,门禁列表会以点名的方式失败。模型级开关若其协议并不接受,解析失败并点名该协议实际提供哪些开关;路由级开关则落在读取它的模型上、跳过其余模型,只有当路由上没有任何模型能读取它时才被拒绝。`chatTemplateKwargs` 予以开放,这正是两个 `chat-template` 思考格式得以命名的前提;两者的配对不做交叉校验,因为实际生效的格式可能来自 catalog 条目或 pi-ai 的检测,而解析读不到那两层。
协议适用性逐字段判断,且归组依据是 compat **类型**而非协议名:pi-ai 让 `openai-responses``azure-openai-responses``openai-codex-responses` 共用同一个 `OpenAIResponsesCompat`,因此可设在其中之一的开关,三者皆可设。仅按协议名归组曾使两条随附的 catalog 路由拿不到其自身模型所声明的字段。协议集派生自 `Model.compat` 自身的条件类型,因此某个版本若给别的协议加上 compat 类型,门禁列表会以点名的方式失败。模型级开关若其协议并不接受,解析失败并点名该协议实际提供哪些开关;路由级开关则落在读取它的模型上、跳过其余模型,只有当路由上没有任何模型能读取它时才被拒绝。`chatTemplateKwargs``chatTemplateArgs` 予以开放,因此两个 `chat-template` 格式和 `baseten` 都可命名;这些配对不做交叉校验,因为实际生效的格式可能来自 catalog 条目或 pi-ai 的检测,而解析读不到那两层。
三类 `compat` 键在其被写下之处遭到拒绝而非丢弃:没有任何协议声明的键、被门禁扣留的键,以及完全没有写值的键。该检查在任何协议解析之前遍历全部键,因此即便路由上的模型永远不会走到那个本会接受它的协议,笔误同样失败。它刻意读取原始键:被扣留或未声明的名字不在 schema 中,所以 schemastery 不可能物化它,写下它的必然是人。无值那一类是必须失败而不能忽略的:schemastery 会把 YAML 裸键放行为 null,照单收下就会用 null 写覆盖已安装 catalog 的值,随后 pi-ai 的 `??` 转而去够它的 baseURL 检测,catalog 这一层被整个跳过。随后再单独过滤携带值的字段,因为 schemastery 会把缺省的 dict 物化成 `{}`,于是无论有没有人写过,`chatTemplateKwargs`出现在每一个解析过的 profile 上。
三类 `compat` 键在其被写下之处遭到拒绝而非丢弃:没有任何协议声明的键、被门禁扣留的键,以及完全没有写值的键。该检查在任何协议解析之前遍历全部键,因此即便路由上的模型永远不会走到那个本会接受它的协议,笔误同样失败。它刻意读取原始键:被扣留或未声明的名字不在 schema 中,所以 schemastery 不可能物化它,写下它的必然是人。无值那一类是必须失败而不能忽略的:schemastery 会把 YAML 裸键放行为 null,照单收下就会用 null 写覆盖已安装 catalog 的值,随后 pi-ai 的 `??` 转而去够它的 baseURL 检测,catalog 这一层被整个跳过。随后再单独过滤携带值的字段,因为 schemastery 会把缺省的 dict 物化成 `{}`,于是无论有没有人写过,两个模板参数字段都会出现在每一个解析过的 profile 上。
## Where a refusal lands
@@ -45,7 +45,7 @@ pi-ai 依据提供方 id 与 baseURL 决定每个请求的形状——系统提
## Consequences
- 拒绝 `developer` 角色、`max_completion_tokens``store``stream_options``strict` 的 OpenAI 兼容网关,如今属于配置问题而非无法接入的提供方;拒绝 `temperature` 或工具 `cache_control` 的 Anthropic 兼容网关同理。
- pi-ai 升级新增 compat 字段会使构建失败,直到有人为它做出分类——`chatTemplateKwargs` 与那两个 `chat-template` 格式正是因此不再是一项长期例外
- pi-ai 升级新增 compat 字段或思考格式会使构建失败,直到有人为它做出分类;门禁包含两个模板参数字段和当前每一种格式
- 未知 compat 键并入了其余所有配置错误的失败模型。相对此前静默丢弃的改善程度受 settings seam 限制:外部文件编辑仍会保留其上一个有效值并告警,因此运维拿到的信号是一次重启,而不是那次写入。
- **搁置而非解决:** 改指 `api` 且完全未配置 compat 的路由,会经模型字面量的 `...base` 展开保留已安装条目的 `compat`,且形状属于**另一个**协议。多个 compat 类型共有的字段(`supportsLongCacheRetention``sendSessionAffinityHeaders`)因而会跨协议串味。它早于本面存在——其所依附的提前返回本就在那里——留给独立的一次改动处理。
- **搁置而非解决:** `publish()` 对被拒绝的已存 section 只通过 `ctx.logger.warn` 报告,没有面向用户的通道。它影响每一个 settings namespace,归属 `dsh-settings`
@@ -26,6 +26,7 @@
- option "ant-ling"
- option "anthropic"
- option "azure-openai-responses"
- option "baseten"
- option "cerebras"
- option "cloudflare-ai-gateway"
- option "cloudflare-workers-ai"
@@ -50,6 +51,7 @@
- option "openrouter"
- option "qwen-token-plan"
- option "qwen-token-plan-cn"
- option "qwen-token-plan-individual"
- option "together"
- option "vercel-ai-gateway"
- option "xai"
@@ -30,6 +30,7 @@
- option "ant-ling"
- option "anthropic"
- option "azure-openai-responses"
- option "baseten"
- option "cerebras"
- option "cloudflare-ai-gateway"
- option "cloudflare-workers-ai"
@@ -54,6 +55,7 @@
- option "openrouter"
- option "qwen-token-plan"
- option "qwen-token-plan-cn"
- option "qwen-token-plan-individual"
- option "together"
- option "vercel-ai-gateway"
- option "xai"
+2 -2
View File
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write docs/config-catalog.md
config-catalog.md: 2fa4a59d00867c4be168347a4e1dfd827ea443f4
config-catalog.zh.md: c2d64b1a5cef1fa20a37d954afc36f29cd3147c9
config-catalog.md: 9685f4689a6ebd6994c1fc6dccbae04af1658a15
config-catalog.zh.md: 51575325b3656487e2fe852a6c8cf0b47b2b6fe8
+9
View File
@@ -1133,6 +1133,11 @@ export interface PiAiCompatProfile {
supportsReasoningEffort?: boolean
/** Whether the endpoint accepts `stream_options: {include_usage: true}`; `openai-completions`. */
supportsUsageInStreaming?: boolean
/**
* Whether streams include `finish_reason`; `false` lets pi-ai infer the
* terminal reason when the stream ends; `openai-completions`.
*/
supportsFinishReason?: boolean
/** Which output-cap field the endpoint reads; `openai-completions`. */
maxTokensField?: NonNullable<OpenAICompletionsCompat['maxTokensField']>
/** Whether tool results must carry `name`; `openai-completions`. */
@@ -1153,6 +1158,10 @@ export interface PiAiCompatProfile {
* can read, so kwargs set beside another format are sent nowhere.
*/
chatTemplateKwargs?: NonNullable<OpenAICompletionsCompat['chatTemplateKwargs']>
/** Arguments sent as `chat_template_args` under the `baseten` thinking format; `openai-completions`. */
chatTemplateArgs?: NonNullable<OpenAICompletionsCompat['chatTemplateArgs']>
/** Whether the endpoint accepts `thinking_token_budget` to cap vLLM reasoning; `openai-completions`. */
supportsThinkingTokenBudget?: boolean
/**
* Whether the endpoint accepts `strict` in tool definitions;
* `openai-completions`, the three Responses protocols, `bedrock-converse-stream`.
+9
View File
@@ -1135,6 +1135,11 @@ export interface PiAiCompatProfile {
supportsReasoningEffort?: boolean
/** Whether the endpoint accepts `stream_options: {include_usage: true}`; `openai-completions`. */
supportsUsageInStreaming?: boolean
/**
* Whether streams include `finish_reason`; `false` lets pi-ai infer the
* terminal reason when the stream ends; `openai-completions`.
*/
supportsFinishReason?: boolean
/** Which output-cap field the endpoint reads; `openai-completions`. */
maxTokensField?: NonNullable<OpenAICompletionsCompat['maxTokensField']>
/** Whether tool results must carry `name`; `openai-completions`. */
@@ -1155,6 +1160,10 @@ export interface PiAiCompatProfile {
* can read, so kwargs set beside another format are sent nowhere.
*/
chatTemplateKwargs?: NonNullable<OpenAICompletionsCompat['chatTemplateKwargs']>
/** Arguments sent as `chat_template_args` under the `baseten` thinking format; `openai-completions`. */
chatTemplateArgs?: NonNullable<OpenAICompletionsCompat['chatTemplateArgs']>
/** Whether the endpoint accepts `thinking_token_budget` to cap vLLM reasoning; `openai-completions`. */
supportsThinkingTokenBudget?: boolean
/**
* Whether the endpoint accepts `strict` in tool definitions;
* `openai-completions`, the three Responses protocols, `bedrock-converse-stream`.
@@ -0,0 +1,33 @@
- id: base
name: '@deepseek-ai/cordis-plugin-include'
config:
path: ../../cordis.yml
patches:
- id: llm-deepseek
name: '@deepseek-ai/dsh-llm-deepseek'
disabled: true
- insert:
- id: llm-pi-ai
name: '@deepseek-ai/dsh-llm-pi-ai'
config:
providers:
deepseek:
apiKeyEnv: DEEPSEEK_API_KEY
baseURL: !!js process.env.DSH_SNAPSHOT_BASE_URL
reasoning: low
modelOverrides:
deepseek-v4-flash:
maxTokens: 1024
streamIdleTimeoutMs: 1000
- id: agent-spine
config:
agents:
- id: main
provider: deepseek
model: deepseek-v4-flash
cwd: !!js process.cwd()
workspaceContext: false
persona: 'Keyless pi-ai DeepSeek compatibility snapshot.'
- id: persistence
config:
root: './.sessions'
@@ -56,6 +56,7 @@ const dshBinScript = fileURLToPath(new URL('../../../apps/cli/src/bin.ts', impor
const tsconfigPath = fileURLToPath(new URL('../../../tsconfig.json', import.meta.url))
const reasoningConfigPath = fileURLToPath(new URL('./fixtures/cli.cordis.yml', import.meta.url))
const deepseekDefaultsConfigPath = fileURLToPath(new URL('./fixtures/deepseek-defaults.cordis.yml', import.meta.url))
const piAiDefaultsConfigPath = fileURLToPath(new URL('./fixtures/pi-ai-defaults.cordis.yml', import.meta.url))
const headlessOverlayPath = fileURLToPath(new URL('./fixtures/headless-profile.cordis.yml', import.meta.url))
const headlessSessionExpected = join(snapshotsDir, 'headless-profile', 'session.expected.jsonl')
const headlessFailureExpected = join(snapshotsDir, 'headless-profile', 'stderr.expected.txt')
@@ -579,6 +580,57 @@ describe('headless stream-json snapshots', () => {
}
}, LOADER_SMOKE_TEST_TIMEOUT_MS)
it('sends pi-ai DeepSeek compatibility through the one-shot app', async () => {
const server = await deepseekDefaultsServer()
try {
const result = await runLoaderSmoke({
label: 'pi-ai DeepSeek compatibility headless stream-json snapshot',
tempDirPrefix: 'headless-snapshot-pi-ai-defaults-',
binScript,
libBinScript: binScript,
configPath: piAiDefaultsConfigPath,
binArgs: [
piAiDefaultsConfigPath,
'return the deterministic response',
],
tsconfigPath,
env: {
DEEPSEEK_API_KEY: 'snapshot-key',
DSH_SNAPSHOT_BASE_URL: server.url,
NODE_OPTIONS: [process.env.NODE_OPTIONS, '--disable-warning=ExperimentalWarning'].filter(Boolean).join(' '),
},
})
expect(result.stderr).toBe('')
expect(server.requests).toHaveLength(1)
expect(server.requests[0]?.max_tokens).toBe(1024)
expect(server.requests[0]).not.toHaveProperty('max_completion_tokens')
const header = (parseJsonl(result.stdout)
.map(record => record.event)
.find((event): event is JsonObject => (
event !== null
&& typeof event === 'object'
&& !Array.isArray(event)
&& 'type' in event
&& event.type === 'request/header'
))?.data as JsonObject | undefined)?.header as JsonObject | undefined
expect(header?.config).toMatchInlineSnapshot(`
{
"maxTokens": 1024,
"model": "deepseek-v4-flash",
"provider": "deepseek",
"reasoningEffort": "low",
}
`)
expect(header?.adapterDefaults).toEqual({
maxTokens: true,
reasoningEffort: true,
})
} finally {
await server.close()
}
}, LOADER_SMOKE_TEST_TIMEOUT_MS)
it('replays the advanced toolchain through the one-shot app', async () => {
const prompt = await scenarioPrompt(advancedScenarioDir, 'advanced-toolchain')
const fixtureFiles = [
@@ -26,13 +26,14 @@ export const getSupportedThinkingLevels = notImplementedFail(MODULE, 'getSupport
/** Context-overflow predicate (unavailable). */
export const isContextOverflow = notImplementedFail(MODULE, 'isContextOverflow')
/** Builtin provider ids of pi-ai 0.82.1, in catalog order. */
/** Builtin provider ids of pi-ai 0.84.2, in catalog order. */
const BUILTIN_PROVIDER_IDS: readonly string[] = [
'amazon-bedrock', 'ant-ling', 'anthropic', 'azure-openai-responses', 'cerebras',
'amazon-bedrock', 'ant-ling', 'anthropic', 'azure-openai-responses', 'baseten', 'cerebras',
'cloudflare-ai-gateway', 'cloudflare-workers-ai', 'deepseek', 'fireworks', 'github-copilot',
'google', 'google-vertex', 'groq', 'huggingface', 'kimi-coding', 'minimax', 'minimax-cn',
'mistral', 'moonshotai', 'moonshotai-cn', 'nvidia', 'openai', 'openai-codex', 'opencode',
'opencode-go', 'openrouter', 'qwen-token-plan', 'qwen-token-plan-cn', 'together',
'opencode-go', 'openrouter', 'qwen-token-plan', 'qwen-token-plan-cn',
'qwen-token-plan-individual', 'together',
'vercel-ai-gateway', 'xai', 'xiaomi', 'xiaomi-token-plan-ams', 'xiaomi-token-plan-cn',
'xiaomi-token-plan-sgp', 'zai', 'zai-coding-cn',
]
+2 -2
View File
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write packages/llm/llm-pi-ai/README.md
README.md: 43472de90803481deebb9bc91586a4b85e443db5
README.zh.md: 76b3dc9dec2a4bf83933319aa065954a191e595b
README.md: 9ef6596490b614a3d4af3dd6b52eb9b9fe87a335
README.zh.md: 10f366659a38f52f7700c0db7953b983fd0e623a
+1 -1
View File
@@ -97,7 +97,7 @@ pi-ai shapes each request from the provider id and baseURL: which role carries t
Each switch belongs to the protocols whose pi-ai compat type declares it, and grouping follows the compat *type* rather than the protocol name: the three Responses protocols (`openai-responses`, `azure-openai-responses`, `openai-codex-responses`) share one compat type, so a switch settable on one is settable on all three. `supportsDeveloperRole` is settable on `openai-completions` and on those three; `thinkingFormat` only on `openai-completions`; `supportsTemperature` only on `anthropic-messages`; `supportsStrictMode` also reaches `bedrock-converse-stream`. A model-level switch its protocol does not take fails resolution naming what that protocol does offer; a route-level one lands on the models that read it and skips the rest, and is refused only when no model on the route could read it at all.
Three kinds of key are refused rather than dropped: one no protocol declares (a misspelling), one pi-ai's installed catalog owns for a named vendor (`openRouterRouting`, `zaiToolStream`, `deferredToolsMode`, `sessionAffinityFormat`, `supportsOpenAIGrammarTools`, `supportsToolSearch`, `supportsExplicitPromptCacheMode`, `supportsToolReferences`, `vercelGatewayRouting`, `sendSessionAffinityHeaders`) — a route needing a vendor's own switch is a catalog route that should be named as such — and one written with no value at all (`supportsDeveloperRole:`), which schemastery passes through as null and which would otherwise replace the installed catalog's value with nothing. The offered set is pinned to pi-ai's four compat types by drift gates, the protocols carrying them are derived from `Model.compat` itself, and each field's type is derived from upstream rather than restated, so an upgrade that adds a field, gives a further protocol a compat type, or widens a value union fails the build until someone classifies it.
Three kinds of key are refused rather than dropped: one no protocol declares (a misspelling), one pi-ai's installed catalog owns for a named vendor (`openRouterRouting`, `zaiToolStream`, `deferredToolsMode`, `sessionAffinityFormat`, `supportsOpenAIGrammarTools`, `supportsAdditionalTools`, `supportsToolSearch`, `supportsExplicitPromptCacheMode`, `supportsToolReferences`, `vercelGatewayRouting`, `sendSessionAffinityHeaders`) — a route needing a vendor's own switch is a catalog route that should be named as such — and one written with no value at all (`supportsDeveloperRole:`), which schemastery passes through as null and which would otherwise replace the installed catalog's value with nothing. The offered set is pinned to pi-ai's four compat types by drift gates, the protocols carrying them are derived from `Model.compat` itself, and each field's type is derived from upstream rather than restated, so an upgrade that adds a field, gives a further protocol a compat type, or widens a value union fails the build until someone classifies it.
A model neither the entry nor the installed catalog sizes takes the route's `defaultContextWindow` (262,144) and `defaultMaxTokens` (32,768), so a listing that discloses nothing but ids still yields a serviceable route. Both fallbacks are guesses by construction, which is why they are route fields a deployment whose gateway serves smaller models corrects once rather than constants buried in the adapter; the fallback sizes the model and never becomes a per-request cap.
+1 -1
View File
@@ -97,7 +97,7 @@ pi-ai 依据提供方 id 与 baseURL 决定每个请求的形状:系统提示
每个开关归属于其 pi-ai compat 类型声明了它的那些协议,且归组依据是 compat **类型**而非协议名:三个 Responses 协议(`openai-responses``azure-openai-responses``openai-codex-responses`)共用同一个 compat 类型,因此可设在其中之一的开关,三者皆可设。`supportsDeveloperRole` 可设在 `openai-completions` 与这三者上;`thinkingFormat` 只能设在 `openai-completions``supportsTemperature` 只能设在 `anthropic-messages``supportsStrictMode` 还可达 `bedrock-converse-stream`。模型级开关若其协议并不接受,解析失败并点名该协议实际提供哪些开关;路由级开关则落在读取它的模型上、跳过其余模型,只有当路由上没有任何模型能读取它时才被拒绝。
三类键会被拒绝而非丢弃:没有任何协议声明的键(笔误);pi-ai 已安装 catalog 为具名厂商掌管的键(`openRouterRouting``zaiToolStream``deferredToolsMode``sessionAffinityFormat``supportsOpenAIGrammarTools``supportsToolSearch``supportsExplicitPromptCacheMode``supportsToolReferences``vercelGatewayRouting``sendSessionAffinityHeaders`)——需要某厂商专属开关的路由,本就是一条应当以该厂商命名的 catalog 路由;以及完全没有写值的键(`supportsDeveloperRole:`),schemastery 会把它放行为 null,若照单收下就会用空值替换已安装 catalog 的值。开放集由漂移门禁钉在 pi-ai 的四个 compat 类型上,承载它们的协议集派生自 `Model.compat` 本身,每个字段的类型也派生自上游而非重述,因此上游新增字段、给别的协议加上 compat 类型、或拓宽某个值并集,都会使构建失败,直到有人为它做出分类。
三类键会被拒绝而非丢弃:没有任何协议声明的键(笔误);pi-ai 已安装 catalog 为具名厂商掌管的键(`openRouterRouting``zaiToolStream``deferredToolsMode``sessionAffinityFormat``supportsOpenAIGrammarTools``supportsAdditionalTools``supportsToolSearch``supportsExplicitPromptCacheMode``supportsToolReferences``vercelGatewayRouting``sendSessionAffinityHeaders`)——需要某厂商专属开关的路由,本就是一条应当以该厂商命名的 catalog 路由;以及完全没有写值的键(`supportsDeveloperRole:`),schemastery 会把它放行为 null,若照单收下就会用空值替换已安装 catalog 的值。开放集由漂移门禁钉在 pi-ai 的四个 compat 类型上,承载它们的协议集派生自 `Model.compat` 本身,每个字段的类型也派生自上游而非重述,因此上游新增字段、给别的协议加上 compat 类型、或拓宽某个值并集,都会使构建失败,直到有人为它做出分类。
条目与已安装 catalog 都没有给出尺寸的模型,会采用该路由的 `defaultContextWindow`262,144)与 `defaultMaxTokens`(32,768),因此一份只公布 id 的列表同样能产出可服务的路由。两个回退值本质上都是猜测,这正是它们作为路由字段、供网关服务更小模型的部署一次性更正的原因,而不是埋在适配器里的常量;回退值只用于给模型定尺寸,绝不会变成单次请求上限。
+1 -1
View File
@@ -43,7 +43,7 @@
"@deepseek-ai/cordis": "workspace:^"
},
"dependencies": {
"@earendil-works/pi-ai": "^0.82.1",
"@earendil-works/pi-ai": "^0.84.2",
"@deepseek-ai/schemastery": "workspace:^"
},
"devDependencies": {
+1 -1
View File
@@ -374,7 +374,7 @@ export class PiAiAdapter extends LlmAdapter {
// Harness-owned and therefore win collisions.
headers: requestHeaders(profile.headers),
})
const iterator = toStreamChunks(events, model.contextWindow)[Symbol.asyncIterator]()
const iterator = toStreamChunks(events, model.contextWindow, options.signal)[Symbol.asyncIterator]()
let exhausted = false
try {
while (true) {
+24 -9
View File
@@ -89,17 +89,19 @@ export type PiAiThinkingFormat = NonNullable<OpenAICompletionsCompat['thinkingFo
/**
* The nameable reasoning-dispatch formats, most-reached first. The `Record`
* key type is a drift gate: a pi-ai upgrade that adds a format (0.84 added
* `baseten`) fails compilation here until the new format is named, so the
* offer never silently lags the upstream set. The two `chat-template` variants
* are nameable because {@link PiAiCompatProfile.chatTemplateKwargs} carries
* the kwargs they dispatch through.
* key type is a drift gate: an upstream format addition fails compilation
* here until it is named, so the offer never silently lags the upstream set.
* The two `chat-template` variants are nameable because
* {@link PiAiCompatProfile.chatTemplateKwargs} carries their kwargs;
* `baseten` is nameable because {@link PiAiCompatProfile.chatTemplateArgs}
* carries its arguments.
*/
const THINKING_FORMAT_GATE: Record<PiAiThinkingFormat, true> = {
'openai': true,
'deepseek': true,
'openrouter': true,
'together': true,
'baseten': true,
'zai': true,
'qwen': true,
'chat-template': true,
@@ -217,6 +219,7 @@ const COMPLETIONS_COMPAT_GATE = {
supportsDeveloperRole: 'offer',
supportsReasoningEffort: 'offer',
supportsUsageInStreaming: 'offer',
supportsFinishReason: 'offer',
maxTokensField: 'offer',
requiresToolResultName: 'offer',
requiresAssistantAfterToolResult: 'offer',
@@ -224,6 +227,8 @@ const COMPLETIONS_COMPAT_GATE = {
requiresReasoningContentOnAssistantMessages: 'offer',
thinkingFormat: 'offer',
chatTemplateKwargs: 'offer',
chatTemplateArgs: 'offer',
supportsThinkingTokenBudget: 'offer',
supportsStrictMode: 'offer',
cacheControlFormat: 'offer',
supportsLongCacheRetention: 'offer',
@@ -243,6 +248,7 @@ const RESPONSES_COMPAT_GATE = {
supportsLongCacheRetention: 'offer',
sessionAffinityFormat: 'withhold',
supportsOpenAIGrammarTools: 'withhold',
supportsAdditionalTools: 'withhold',
supportsToolSearch: 'withhold',
supportsExplicitPromptCacheMode: 'withhold',
} as const satisfies Record<keyof OpenAIResponsesCompat, CompatDisposition>
@@ -344,6 +350,11 @@ export interface PiAiCompatProfile {
supportsReasoningEffort?: boolean
/** Whether the endpoint accepts `stream_options: {include_usage: true}`; `openai-completions`. */
supportsUsageInStreaming?: boolean
/**
* Whether streams include `finish_reason`; `false` lets pi-ai infer the
* terminal reason when the stream ends; `openai-completions`.
*/
supportsFinishReason?: boolean
/** Which output-cap field the endpoint reads; `openai-completions`. */
maxTokensField?: NonNullable<OpenAICompletionsCompat['maxTokensField']>
/** Whether tool results must carry `name`; `openai-completions`. */
@@ -364,6 +375,10 @@ export interface PiAiCompatProfile {
* can read, so kwargs set beside another format are sent nowhere.
*/
chatTemplateKwargs?: NonNullable<OpenAICompletionsCompat['chatTemplateKwargs']>
/** Arguments sent as `chat_template_args` under the `baseten` thinking format; `openai-completions`. */
chatTemplateArgs?: NonNullable<OpenAICompletionsCompat['chatTemplateArgs']>
/** Whether the endpoint accepts `thinking_token_budget` to cap vLLM reasoning; `openai-completions`. */
supportsThinkingTokenBudget?: boolean
/**
* Whether the endpoint accepts `strict` in tool definitions;
* `openai-completions`, the three Responses protocols, `bedrock-converse-stream`.
@@ -432,10 +447,10 @@ export type EveryProfileFieldMatchesUpstream = AssertTrue<
*
* schemastery materializes an absent dict as `{}` the behavior
* `reasoningEfforts` works around with a union so every parsed profile
* carries a `chatTemplateKwargs` key whether or not anyone wrote one. An empty
* one states nothing here: it would send no kwargs, which is exactly what
* leaving the field out does, so absent and empty are the same request and
* neither may make a route look like it configured a switch. A valueless
* carries both template-argument keys whether or not anyone wrote them. An
* empty one states nothing here: it would send no arguments, which is exactly
* what leaving the field out does, so absent and empty are the same request
* and neither may make a route look like it configured a switch. A valueless
* scalar is the other thing schemastery lets through, and it is refused by
* {@link assertOfferedCompatFields} before this runs rather than filtered.
* @param compat - the configured switches, when any.
+7 -3
View File
@@ -227,9 +227,10 @@ const thinkingBudgets = z.object({
})
/**
* One `chat_template_kwargs` value. The `$var` member is pi-ai's placeholder
* for a value dispatch fills from the request's thinking state, which is what
* makes a chat-template gateway configurable without restating its template.
* One `chat_template_kwargs` or `chat_template_args` value. The `$var` member
* is pi-ai's placeholder for a value dispatch fills from the request's
* thinking state, which makes a template-driven gateway configurable without
* restating its template.
*/
const chatTemplateKwarg: z<ChatTemplateKwargValue> = z.union([
z.string(),
@@ -247,6 +248,7 @@ const compatProfile: z<PiAiCompatProfile> = z.object({
supportsDeveloperRole: z.boolean(),
supportsReasoningEffort: z.boolean(),
supportsUsageInStreaming: z.boolean(),
supportsFinishReason: z.boolean(),
maxTokensField: z.union(MAX_TOKENS_FIELDS),
requiresToolResultName: z.boolean(),
requiresAssistantAfterToolResult: z.boolean(),
@@ -254,6 +256,8 @@ const compatProfile: z<PiAiCompatProfile> = z.object({
requiresReasoningContentOnAssistantMessages: z.boolean(),
thinkingFormat: z.union(SUPPORTED_THINKING_FORMATS),
chatTemplateKwargs: z.dict(chatTemplateKwarg),
chatTemplateArgs: z.dict(chatTemplateKwarg),
supportsThinkingTokenBudget: z.boolean(),
supportsStrictMode: z.boolean(),
cacheControlFormat: z.union(CACHE_CONTROL_FORMATS),
supportsLongCacheRetention: z.boolean(),
+20 -2
View File
@@ -71,7 +71,8 @@ function classifyPiAiError(message: string): string {
* @returns the mapped harness reason. Recognized error text, `stop` usage above
* `contextWindow`, and zero-output `length` usage that fills the window map
* to `CONTEXT_WINDOW_EXCEEDED`; a `stop` with no content blocks maps to an
* `EMPTY_RESPONSE` error.
* `EMPTY_RESPONSE` error, while terminal `pending` and `deferred` states map
* to non-retryable `PI_AI_ERROR` failures.
*/
export function mapStopReason(message: AssistantMessage, contextWindow?: number): FinishReason {
const piAiOverflow = isContextOverflow(message, contextWindow)
@@ -104,6 +105,14 @@ export function mapStopReason(message: AssistantMessage, contextWindow?: number)
return { kind: 'stop' }
case 'length': return { kind: 'max-tokens' }
case 'toolUse': return { kind: 'tool-calls' }
case 'pending': return {
kind: 'error',
failure: { message: `pi-ai stream for model "${message.model}" ended pending`, code: 'PI_AI_ERROR' },
}
case 'deferred': return {
kind: 'error',
failure: { message: `pi-ai deferred response for model "${message.model}" is not supported`, code: 'PI_AI_ERROR' },
}
case 'aborted': return {
kind: 'aborted',
failure: { message: message.errorMessage ?? 'pi-ai stream aborted', code: 'ABORTED' },
@@ -121,12 +130,15 @@ export function mapStopReason(message: AssistantMessage, contextWindow?: number)
* `finish` chunks (the harness protocol's other error-delivery style).
* @param events - one assistant turn's pi-ai event stream.
* @param contextWindow - resolved catalog capacity for usage-based overflow detection.
* @param callerSignal - caller cancellation state; an aborted caller makes any
* in-band terminal error an aborted finish.
* @returns the harness chunks, ending with `usage` then `finish`; throws
* `LlmError` (`STREAM_CLOSED`) if the source ends without a terminal event.
*/
export async function* toStreamChunks(
events: AsyncIterable<AssistantMessageEvent>,
contextWindow?: number,
callerSignal?: AbortSignal,
): AsyncGenerator<StreamChunk> {
// 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.
@@ -200,7 +212,13 @@ export async function* toStreamChunks(
// In-stream error delivery (pi-ai's style) → error finish chunk
// (the harness's other sanctioned error path besides throwing).
yield { type: 'usage', usage: mapUsage(event.error.usage) }
yield { type: 'finish', reason: mapStopReason(event.error, contextWindow) }
yield {
type: 'finish',
reason: mapStopReason(
callerSignal?.aborted ? { ...event.error, stopReason: 'aborted' } : event.error,
contextWindow,
),
}
return
// no default: AssistantMessageEvent is pi-ai's closed union; a new
// event type should fail compilation here via tsc's exhaustiveness
+6 -2
View File
@@ -132,7 +132,7 @@ describe('PiAiAdapter provider routing', () => {
expect(server.requests[0]).toMatchObject({
model: 'deepseek-v4-flash',
temperature: 0.2,
max_completion_tokens: 77,
max_tokens: 77,
thinking: { type: 'enabled' },
reasoning_effort: 'max',
})
@@ -483,6 +483,7 @@ describe('provider profile lifecycle', () => {
reasoning: {
efforts: [
{ id: ReasoningEffortId('off'), name: 'Off' },
{ id: ReasoningEffortId('low'), name: 'Low' },
{ id: ReasoningEffortId('high'), name: 'High' },
{ id: ReasoningEffortId('max'), name: 'Max' },
],
@@ -975,7 +976,10 @@ describe('abort wiring', () => {
messages: [],
signal: controller.signal,
})) chunks.push(chunk)
expect(chunks.at(-1)).toMatchObject({ type: 'finish', reason: { kind: 'aborted' } })
expect(chunks.at(-1)).toMatchObject({
type: 'finish',
reason: { kind: 'aborted', failure: { code: 'ABORTED' } },
})
})
it('honors a pre-aborted caller signal', async () => {
+36 -9
View File
@@ -637,7 +637,7 @@ describe('per-model reasoning efforts', () => {
it('narrows a catalog models levels in place', () => {
const [catalogModel] = getBuiltinModels('deepseek')
if (catalogModel === undefined) throw new Error('the installed catalog ships no deepseek model')
expect(getSupportedThinkingLevels(catalogModel as Model<Api>)).toEqual(['off', 'high', 'max'])
expect(getSupportedThinkingLevels(catalogModel as Model<Api>)).toEqual(['off', 'low', 'high', 'max'])
const model = modelOf({
deepseek: { models: [{ id: catalogModel.id, reasoningEfforts: { off: null, high: 'high' } }] },
@@ -920,6 +920,31 @@ describe('compat switches', () => {
})
})
it('carries private-endpoint stream and reasoning controls', () => {
const models = modelsOf({
'acme-baseten': {
api: 'openai-completions',
baseURL: 'https://acme.test',
models: [{
id: 'reasoning-local',
compat: {
supportsFinishReason: false,
thinkingFormat: 'baseten',
chatTemplateArgs: { enable_thinking: { $var: 'thinking.enabled' } },
supportsThinkingTokenBudget: true,
},
}],
},
}, 'acme-baseten')
expect(models.get('reasoning-local')?.compat).toEqual({
supportsFinishReason: false,
thinkingFormat: 'baseten',
chatTemplateArgs: { enable_thinking: { $var: 'thinking.enabled' } },
supportsThinkingTokenBudget: true,
})
})
it('rejects a model switch on an unrecognized protocol as having no configurable compat', () => {
expect(() => resolveProfiles({
'acme-gateway': {
@@ -1044,14 +1069,16 @@ describe('compat switches', () => {
})).toThrow(/compat "supportsDevelperRole", which no wire protocol declares; the configurable switches are .*\bsupportsDeveloperRole\b/)
})
it('refuses a compat key pi-ais catalog owns, pointing at the catalog route', () => {
expect(() => resolveProfiles({
'acme-gateway': {
api: 'openai-completions',
baseURL: 'https://acme.test',
models: [{ id: 'acme-a', compat: { openRouterRouting: {} } as never }],
},
})).toThrow(/compat "openRouterRouting", which is not configurable here/)
it('refuses compat keys pi-ais catalog owns, pointing at the catalog route', () => {
for (const compat of [{ openRouterRouting: {} }, { supportsAdditionalTools: true }]) {
expect(() => resolveProfiles({
'acme-gateway': {
api: 'openai-completions',
baseURL: 'https://acme.test',
models: [{ id: 'acme-a', compat: compat as never }],
},
})).toThrow(/which is not configurable here/)
}
})
})
@@ -35,6 +35,17 @@ describe('reasoning schema boundary', () => {
it('rejects a thinking format outside the offered set', () => {
expect(configWith({ compat: { thinkingFormat: 'quantum' } })).toThrow(/expected/)
})
it('accepts Baseten template arguments and completion controls', () => {
expect(configWith({
compat: {
supportsFinishReason: false,
thinkingFormat: 'baseten',
chatTemplateArgs: { enable_thinking: { $var: 'thinking.enabled' } },
supportsThinkingTokenBudget: true,
},
})).not.toThrow()
})
})
describe('modality schema boundary', () => {
@@ -758,6 +758,14 @@ describe('mapStopReason / mapUsage', () => {
['stop', { kind: 'stop' }],
['length', { kind: 'max-tokens' }],
['toolUse', { kind: 'tool-calls' }],
['pending', {
kind: 'error',
failure: { message: 'pi-ai stream for model "deepseek-v4-flash" ended pending', code: 'PI_AI_ERROR' },
}],
['deferred', {
kind: 'error',
failure: { message: 'pi-ai deferred response for model "deepseek-v4-flash" is not supported', code: 'PI_AI_ERROR' },
}],
['aborted', { kind: 'aborted', failure: { message: 'pi-ai stream aborted', code: 'ABORTED' } }],
] as const)('maps %s', (stopReason, expected) => {
expect(mapStopReason(assistant({ stopReason, content: [{ type: 'text', text: 'ok' }] }))).toEqual(expected)
+24 -69
View File
@@ -441,7 +441,7 @@ importers:
version: 6.4.3(@types/node@22.20.0)(jiti@2.7.0)(lightningcss@1.32.0)(tsx@4.22.4)(yaml@2.9.0)
vitest:
specifier: ^4.1.8
version: 4.1.8(@opentelemetry/api@1.9.0)(@types/node@22.20.0)(@vitest/coverage-v8@4.1.8)(jsdom@29.1.1(@noble/hashes@2.3.0))(vite@6.4.3(@types/node@22.20.0)(jiti@2.7.0)(lightningcss@1.32.0)(tsx@4.22.4)(yaml@2.9.0))
version: 4.1.8(@opentelemetry/api@1.9.1)(@types/node@22.20.0)(@vitest/coverage-v8@4.1.8)(jsdom@29.1.1(@noble/hashes@2.3.0))(vite@6.4.3(@types/node@22.20.0)(jiti@2.7.0)(lightningcss@1.32.0)(tsx@4.22.4)(yaml@2.9.0))
examples:
dependencies:
@@ -6193,8 +6193,8 @@ importers:
specifier: link:../../../vendor/schemastery
version: link:../../../vendor/schemastery
'@earendil-works/pi-ai':
specifier: ^0.82.1
version: 0.82.1(@modelcontextprotocol/sdk@1.29.0(zod@4.4.3))(ws@8.21.0)(zod@4.4.3)
specifier: ^0.84.2
version: 0.84.2(@modelcontextprotocol/sdk@1.29.0(zod@4.4.3))(ws@8.21.0)(zod@4.4.3)
devDependencies:
'@deepseek-ai/cordis':
specifier: workspace:^
@@ -8774,7 +8774,7 @@ importers:
version: 4.2.0
vitest:
specifier: ^4.1.8
version: 4.1.8(@opentelemetry/api@1.9.0)(@types/node@25.9.3)(@vitest/coverage-v8@4.1.8)(jsdom@29.1.1(@noble/hashes@2.3.0))(vite@8.0.16(@types/node@26.1.2)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(yaml@2.9.0))
version: 4.1.8(@opentelemetry/api@1.9.1)(@types/node@25.9.3)(@vitest/coverage-v8@4.1.8)(jsdom@29.1.1(@noble/hashes@2.3.0))(vite@8.0.16(@types/node@26.1.2)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(yaml@2.9.0))
devDependencies:
'@deepseek-ai/cordis':
specifier: workspace:^
@@ -10554,11 +10554,15 @@ packages:
search-insights:
optional: true
'@earendil-works/pi-ai@0.82.1':
resolution: {integrity: sha512-3WFYRhEp3lQB3444EhPMBcM7zSaEUE3eJgHOR7s4081NLqbw/FsWilIKWXSua0Gv3sRr7m9xMidR3pPDE7jI/A==}
'@earendil-works/pi-ai@0.84.2':
resolution: {integrity: sha512-6MzsrYIYNVlE7SfpbL2yYb67Qo58p/7Q+xWG1RZvoX1P80aRCHSod2/13aFpxkow1lPO2LEh3c495J0Gwmyjig==}
engines: {node: '>=22.19.0'}
hasBin: true
'@earendil-works/pi-telemetry@0.84.2':
resolution: {integrity: sha512-wg5caea7uIv1BHRBm2Y116RvFG4oSAiP5qk9tA2463PDGIr4K8M1Ceyyg5DOpF/shUUl0gk826yQJAeAcHYB9g==}
engines: {node: '>=22.19.0'}
'@emnapi/core@1.10.0':
resolution: {integrity: sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==}
@@ -11387,14 +11391,6 @@ packages:
'@mermaid-js/parser@1.2.0':
resolution: {integrity: sha512-oYPyv8A4As1yH5Bx+04iQEQxXuIQDe0GKCNSRgao6z8AM9jixXIfP0vsppRLvGf+nKIOb9/LdpWA4YuJiVvESA==}
'@mistralai/mistralai@2.2.6':
resolution: {integrity: sha512-W8pX7zHxjJvMIpw8JMxeJEleapXX0Q9NPszdNzqkM3MIEoIGPObdodujj+WHteXEvGfaP/AMwlNyRfEzSY6dQQ==}
peerDependencies:
'@opentelemetry/api': ^1.9.0
peerDependenciesMeta:
'@opentelemetry/api':
optional: true
'@mixmark-io/domino@2.2.0':
resolution: {integrity: sha512-Y28PR25bHXUg88kCV7nivXrP2Nj2RueZ3/l/jdx6J9f8J4nsEGcgX0Qe6lt7Pa+J79+kPiJU3LguR6O/6zrLOw==}
@@ -14643,9 +14639,8 @@ packages:
resolution: {integrity: sha512-smsWv2LzFjP03xmvFoJ331ss6h+jixfA4UUV/Bsiyuu4YJPfN+FIQGOIiv4w9/+MoHkfkJ22UIaQWRVFRfH6Vw==}
engines: {node: '>=20'}
openai@6.26.0:
resolution: {integrity: sha512-zd23dbWTjiJ6sSAX6s0HrCZi41JwTA1bQVs0wLQPZ2/5o2gxOJA5wh7yOAUgwYybfhDXyhwlpeQf7Mlgx8EOCA==}
hasBin: true
openai@6.40.0:
resolution: {integrity: sha512-MWtTjd/gQt4jpbji61NTgFWJLoY/PdRJ6wG9/ZDRMYNMlBKrCrSlkLI+KgHP1vR1qT6LKSAyAqIxno6lcK9JiA==}
peerDependencies:
ws: ^8.18.0
zod: ^3.25 || ^4.0
@@ -15299,8 +15294,8 @@ packages:
resolution: {integrity: sha512-faYHw0anBbc/kWF3zFTEnxSFOAGUX9GFbOBthvDdLsIlEoWOFOtS0zgCiQYwIskL9iGXZL3kAXD8OoZ4GmMATA==}
engines: {node: '>= 18'}
typebox@1.1.38:
resolution: {integrity: sha512-pZ0aQPmMmXoUvSbeuWf/Hzsc+avNw/Zd6VeE8CFgkVGWyuHPJvqeJJDeJqLve+K70LvjYIoleGcoJHPT17cWoA==}
typebox@1.3.7:
resolution: {integrity: sha512-meKuifc33Pccx0O6PdIzYMq3Og8zvP4TIi/a+Bw3AEMZMxOD0+RHGQvpglEe6Zdy3wZ8nqn/j95h8LUZLk/6Hg==}
typescript-language-server@5.3.0:
resolution: {integrity: sha512-5puofxZHgFdAYtfNpmwCAvgtaYgg8wrUnH30m7Ze3QuguId5RNRadKASpOpyDxTyUdAF51FjhTdjntLw/EuWcQ==}
@@ -16360,19 +16355,19 @@ snapshots:
transitivePeerDependencies:
- '@algolia/client-search'
'@earendil-works/pi-ai@0.82.1(@modelcontextprotocol/sdk@1.29.0(zod@4.4.3))(ws@8.21.0)(zod@4.4.3)':
'@earendil-works/pi-ai@0.84.2(@modelcontextprotocol/sdk@1.29.0(zod@4.4.3))(ws@8.21.0)(zod@4.4.3)':
dependencies:
'@anthropic-ai/sdk': 0.91.1(zod@4.4.3)
'@aws-sdk/client-bedrock-runtime': 3.1048.0
'@earendil-works/pi-telemetry': 0.84.2
'@google/genai': 1.52.0(@modelcontextprotocol/sdk@1.29.0(zod@4.4.3))
'@mistralai/mistralai': 2.2.6(@opentelemetry/api@1.9.0)
'@opentelemetry/api': 1.9.0
'@smithy/node-http-handler': 4.7.3
http-proxy-agent: 7.0.2
https-proxy-agent: 7.0.6
openai: 6.26.0(ws@8.21.0)(zod@4.4.3)
openai: 6.40.0(ws@8.21.0)(zod@4.4.3)
partial-json: 0.1.7
typebox: 1.1.38
typebox: 1.3.7
transitivePeerDependencies:
- '@modelcontextprotocol/sdk'
- bufferutil
@@ -16381,6 +16376,8 @@ snapshots:
- ws
- zod
'@earendil-works/pi-telemetry@0.84.2': {}
'@emnapi/core@1.10.0':
dependencies:
'@emnapi/wasi-threads': 1.2.1
@@ -16924,18 +16921,6 @@ snapshots:
dependencies:
'@chevrotain/types': 11.1.2
'@mistralai/mistralai@2.2.6(@opentelemetry/api@1.9.0)':
dependencies:
'@opentelemetry/semantic-conventions': 1.43.0
ws: 8.21.0
zod: 4.4.3
zod-to-json-schema: 3.25.2(zod@4.4.3)
optionalDependencies:
'@opentelemetry/api': 1.9.0
transitivePeerDependencies:
- bufferutil
- utf-8-validate
'@mixmark-io/domino@2.2.0': {}
'@modelcontextprotocol/sdk@1.29.0(zod@4.4.3)':
@@ -20237,7 +20222,7 @@ snapshots:
powershell-utils: 0.1.0
wsl-utils: 0.3.1
openai@6.26.0(ws@8.21.0)(zod@4.4.3):
openai@6.40.0(ws@8.21.0)(zod@4.4.3):
optionalDependencies:
ws: 8.21.0
zod: 4.4.3
@@ -20998,7 +20983,7 @@ snapshots:
media-typer: 1.1.0
mime-types: 3.0.2
typebox@1.1.38: {}
typebox@1.3.7: {}
typescript-language-server@5.3.0:
dependencies:
@@ -21213,7 +21198,7 @@ snapshots:
- typescript
- universal-cookie
vitest@4.1.8(@opentelemetry/api@1.9.0)(@types/node@22.20.0)(@vitest/coverage-v8@4.1.8)(jsdom@29.1.1(@noble/hashes@2.3.0))(vite@6.4.3(@types/node@22.20.0)(jiti@2.7.0)(lightningcss@1.32.0)(tsx@4.22.4)(yaml@2.9.0)):
vitest@4.1.8(@opentelemetry/api@1.9.1)(@types/node@22.20.0)(@vitest/coverage-v8@4.1.8)(jsdom@29.1.1(@noble/hashes@2.3.0))(vite@6.4.3(@types/node@22.20.0)(jiti@2.7.0)(lightningcss@1.32.0)(tsx@4.22.4)(yaml@2.9.0)):
dependencies:
'@vitest/expect': 4.1.8
'@vitest/mocker': 4.1.8(vite@6.4.3(@types/node@22.20.0)(jiti@2.7.0)(lightningcss@1.32.0)(tsx@4.22.4)(yaml@2.9.0))
@@ -21236,43 +21221,13 @@ snapshots:
vite: 6.4.3(@types/node@22.20.0)(jiti@2.7.0)(lightningcss@1.32.0)(tsx@4.22.4)(yaml@2.9.0)
why-is-node-running: 2.3.0
optionalDependencies:
'@opentelemetry/api': 1.9.0
'@opentelemetry/api': 1.9.1
'@types/node': 22.20.0
'@vitest/coverage-v8': 4.1.8(vitest@4.1.8)
jsdom: 29.1.1(@noble/hashes@2.3.0)
transitivePeerDependencies:
- msw
vitest@4.1.8(@opentelemetry/api@1.9.0)(@types/node@25.9.3)(@vitest/coverage-v8@4.1.8)(jsdom@29.1.1(@noble/hashes@2.3.0))(vite@8.0.16(@types/node@26.1.2)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(yaml@2.9.0)):
dependencies:
'@vitest/expect': 4.1.8
'@vitest/mocker': 4.1.8(vite@8.0.16(@types/node@26.1.2)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(yaml@2.9.0))
'@vitest/pretty-format': 4.1.8
'@vitest/runner': 4.1.8
'@vitest/snapshot': 4.1.8
'@vitest/spy': 4.1.8
'@vitest/utils': 4.1.8
es-module-lexer: 2.1.0
expect-type: 1.3.0
magic-string: 0.30.21
obug: 2.1.3
pathe: 2.0.3
picomatch: 4.0.4
std-env: 4.1.0
tinybench: 2.9.0
tinyexec: 1.2.4
tinyglobby: 0.2.17
tinyrainbow: 3.1.0
vite: 8.0.16(@types/node@26.1.2)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(yaml@2.9.0)
why-is-node-running: 2.3.0
optionalDependencies:
'@opentelemetry/api': 1.9.0
'@types/node': 25.9.3
'@vitest/coverage-v8': 4.1.8(vitest@4.1.8)
jsdom: 29.1.1(@noble/hashes@2.3.0)
transitivePeerDependencies:
- msw
vitest@4.1.8(@opentelemetry/api@1.9.1)(@types/node@22.20.0)(@vitest/coverage-v8@4.1.8)(jsdom@29.1.1(@noble/hashes@2.3.0))(vite@8.0.16(@types/node@22.20.0)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(yaml@2.9.0)):
dependencies:
'@vitest/expect': 4.1.8
+1 -1
View File
@@ -57,7 +57,7 @@ allowBuilds:
minimumReleaseAgeExclude:
# 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@0.82.1'
- '@earendil-works/pi-ai@0.84.2'
- node-addon-native-custom-loader@0.1.4
- node-addon-require-builtin-darwin-arm64@0.1.4
- node-addon-require-builtin-darwin-x64@0.1.4