@crazx/dsh-llm-pi-ai 0.1.5-rc.1.zw.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 DeepSeek
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,6 @@
1
+ # Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
2
+ # side as of the last confirmed-consistent state. Both languages carry equal authority;
3
+ # after editing either side, bring the other along and re-record with:
4
+ # pnpm run verify-translation-pairing --write packages/llm/llm-pi-ai/README.md
5
+ README.md: 5b4eef21bdc03011aa47612c9f0aadfc5a750842
6
+ README.zh.md: 120668342bc01936be549a34c411ae60eb71486f
package/README.md ADDED
@@ -0,0 +1,251 @@
1
+ ---
2
+ description: "The pi-ai-backed multi-provider adapter for users and maintainers routing the harness LLM service through pi-ai catalogs and hand-declared gateways."
3
+ kind: "package-reference"
4
+ ---
5
+
6
+ # @deepseek-ai/dsh-llm-pi-ai
7
+
8
+ English | [中文](README.zh.md)
9
+
10
+ ## Summary
11
+
12
+ `@deepseek-ai/dsh-llm-pi-ai` routes model requests to multiple pi-ai providers, OpenAI-compatible gateways, or self-hosted servers from one configuration. Installed pi-ai providers supply endpoint, protocol, and model-catalog defaults; custom routes can declare those values without code changes. Profiles and credentials are resolved for each request, so settings changes take effect on the next request without a restart. Supported providers can use stored OAuth or interactive-key sign-in with cross-process refresh locking. The package may start with no routes and activate when user settings add them.
13
+
14
+ ## Table of Contents
15
+
16
+ - [Use this package](#use-this-package)
17
+ - [Understand the implementation](#understand-the-implementation)
18
+ - [Further Exploration](#further-exploration)
19
+ - [Model Experience](#model-experience)
20
+ - [Known Limitations and Deferred Work](#known-limitations-and-deferred-work)
21
+ - [Dev Note](#dev-note)
22
+
23
+ -----
24
+
25
+ <a id="use-this-package"></a>
26
+ ## Use this package
27
+
28
+ Mount this plugin when a composition routes model requests through pi-ai's provider catalogs or through gateways that pi-ai's installed catalog does not describe. The `providers` dictionary is the whole configuration surface: each key is the provider route name a request selects with `GenerateOptions.provider`.
29
+
30
+ ### When to choose it
31
+
32
+ Choose this adapter when the same composition serves several providers, when a route needs pi-ai's catalog defaults with a few fields corrected, or when a hand-declared gateway must be reached through its own endpoint and protocol. Choose `dsh-llm-deepseek` for the direct DeepSeek route when the deployment needs no other provider. Both adapters can be mounted together because their route names do not collide; registering a route another adapter already owns fails plugin loading.
33
+
34
+ ### Configure provider routes
35
+
36
+ Each profile may set a `retryPolicy`; omission uses normal mode with five retries. `apiKeyEnv` is a credential reference resolved per request through the harness credential seam, so no secret enters the configuration file; a reference that resolves to nothing fails the request with `MISSING_CREDENTIAL`. Omitting it leaves the route configured-but-keyless, which for an installed catalog route defers to pi-ai's provider-native ambient discovery.
37
+
38
+ ```yaml
39
+ - name: '@deepseek-ai/dsh-llm-pi-ai'
40
+ config:
41
+ providers:
42
+ openai:
43
+ apiKeyEnv: OPENAI_API_KEY
44
+ baseURL: https://proxy.example.com:8443
45
+ reasoning: high
46
+ requestImagePixelBudget: 4194304 # total pixels; 2048 by 2048 default
47
+ requestImageMaxBytes: 1048576 # raw bytes before base64 expansion
48
+ maxRequestImageBytes: 20971520 # accumulated base64 payload
49
+ retryPolicy:
50
+ mode: normal
51
+ maxRetries: 3
52
+ anthropic:
53
+ apiKeyEnv: ANTHROPIC_API_KEY
54
+ models:
55
+ - id: claude-sonnet-4-5
56
+ contextWindow: 200000
57
+ acme-gateway:
58
+ displayName: Acme Gateway
59
+ apiKeyEnv: ACME_GATEWAY_API_KEY
60
+ api: openai-completions
61
+ baseURL: https://gateway.acme.example/v1
62
+ compat:
63
+ thinkingFormat: deepseek
64
+ models:
65
+ - id: acme-think
66
+ name: Acme Think
67
+ contextWindow: 262144
68
+ reasoningEfforts:
69
+ off:
70
+ high: high
71
+ opencode-go:
72
+ apiKeyEnv: OPENCODE_API_KEY
73
+ sessionAffinityHeaders:
74
+ - x-opencode-session
75
+ - x-client-request-id
76
+ ```
77
+
78
+ | Field | Default | Meaning |
79
+ |---|---|---|
80
+ | `apiKeyEnv` | absent | Credential reference resolved per request; omission defers to pi-ai ambient discovery |
81
+ | `displayName` | provider name | Label shown by selector surfaces |
82
+ | `api` | catalog protocol | Wire protocol; only needed for routes the catalog does not supply |
83
+ | `baseURL` | catalog endpoint | Endpoint of every model on the route |
84
+ | `models` | installed catalog | Replaces the route's catalog wholesale; each entry defaults from the installed model |
85
+ | `modelOverrides` | none | Reshapes individual installed-catalog models without replacing the rest |
86
+ | `compat` | catalog detection | Wire-compatibility switches for unrecognized endpoints |
87
+ | `defaultContextWindow` | `262,144` | Capacity fallback for undescribed models |
88
+ | `defaultMaxTokens` | `32,768` | Output-cap fallback for undescribed models |
89
+ | `requestImagePixelBudget` | `4,194,304` | Total-pixel budget for each deterministic request image |
90
+ | `requestImageMaxBytes` | `1 MiB` | Encoded-byte target for each request image before base64 expansion |
91
+ | `maxRequestImageBytes` | `20 MiB` | Aggregate base64 image-payload bound with oldest-first offload |
92
+ | `retryPolicy` | normal, 5 retries | Provider-owned retry policy executed by `dsh-llm-retry` |
93
+ | `sessionAffinityHeaders` | absent | Header names written with the calling session id on every request of this route, enabling per-conversation routing and prompt caching on affinity gateways; undeclared names send nothing, and the attribution reserved set is refused |
94
+
95
+ `sessionAffinityHeaders` serves gateways that route and cache per conversation: every declared name is written with the calling session's id (same conversation, same value; a fork is a new conversation), while a route without the field — or a call without a session id — sends nothing, so unrelated providers never see the id. Names must be valid Fetch header names and may not collide with the Harness attribution set; the configuration catalog is the exhaustive source for every accepted field and its JSDoc.
96
+
97
+ The generated [configuration catalog](../../../docs/config-catalog.md#deepseek-aidsh-llm-pi-ai) is the exhaustive source for every accepted field and its JSDoc.
98
+
99
+ ### Sign in to a provider
100
+
101
+ A provider pi-ai ships a login for can be signed into through the harness authorization seam: the flow offers OAuth or an interactive key prompt (a key is typed into pi-ai's own login prompt, not into the settings form), and the resulting credential is stored in the harness credential store at `llm-pi-ai/<provider id>`. The stored sign-in authenticates its route beneath any `apiKeyEnv` override and refreshes itself under the store's cross-process lock; signing out deletes the stored record. A hand-declared route key outside the record grammar — a lowercase hyphenated identifier — cannot be signed into, because a record write for it refuses with `LlmError('UNSTORABLE_PROVIDER_ID')`; such a route authenticates through `apiKeyEnv` or ambient provider settings instead.
102
+
103
+ ### Resolve the model catalog
104
+
105
+ A profile's `models` list replaces the route's installed catalog rather than extending it; each entry defaults its unset fields from the installed model of the same id, so narrowing a route to two models, correcting one capacity, or adding a model newer than the installed catalog are one-line edits. `modelOverrides` reshapes individual installed-catalog models without that cost — correct one model, keep the other thirty-seven — and is refused when set beside a `models` list, on a hand-declared route, or naming a model the catalog does not describe, because a silently unchanged model would be a typo someone hunts for later.
106
+
107
+ ### Run with reasoning and wire compatibility
108
+
109
+ `reasoningEfforts` declares a model's selectable thinking levels: each key is a level selectors offer, its value the spelling dispatch sends on the wire, so `max: ultra` renames a level for a gateway with its own vocabulary. Omitting the field keeps the installed catalog entry's capability; `false` declares a non-reasoning model. `compat` switches reshape the request for endpoints pi-ai cannot recognize — which role carries the system prompt, which field caps output, how a thinking level travels — configurable per route and per model. A model neither the entry nor the installed catalog sizes takes the route's `defaultContextWindow` and `defaultMaxTokens` fallbacks.
110
+
111
+ For self-hosted Chat Completions endpoints, `thinkingTokenBudgetField` selects the reasoning-budget parameter, and `vllmPriority` sets an integer scheduler priority when the server enables priority scheduling. Template arguments accept `$var: thinking.budget`. `openai-responses` gateways can set `supportsMaxOutputTokens: false` to omit `max_output_tokens`; Azure and Codex transports ignore this shared compatibility field. These controls are opt-in; catalog-owned Anthropic effort and fallback capabilities are not configurable switches.
112
+
113
+ ### Change configuration at runtime
114
+
115
+ Profiles are re-read once per operation through the optional settings seam: the base and the user's `llm-pi-ai:` settings section merge per provider, so a user can add a route, override one field of a composition route, or point a route at another proxy, all effective on the next request with no restart. A section the adapter could not serve is refused where it is written — `settings.mutate` answers `settings-rejected` — and a stored section that later fails keeps the namespace's last good value. When the route set or a route's retry policy changes, the plugin re-registers atomically: a conflicting route leaves the previous routes serving.
116
+
117
+ ### Discover models from endpoints
118
+
119
+ The plugin answers "which models can this provider serve?" for a route a configuration surface is editing or drafting. A route the installed catalog ships is answered from that catalog with no network call; only a route the catalog does not describe is interrogated over the wire. `openai-completions` and `openai-responses` use `GET {baseURL}/models` with bearer auth, while `anthropic-messages` uses native `GET /v1/models?limit=1000` semantics with `x-api-key` and `anthropic-version`; its listing URL accepts the API root with or without a trailing `/v1` because gateway documentation publishes both spellings, and only that listing URL normalizes the segment, so model requests receive the configured `baseURL` unchanged. A named configured route supplies its stored credential and profile `headers` inside the Host, so deployment headers configured through `settings.yaml` or Cordis config reach model discovery without becoming discovery-request or Models-page fields; a key typed into the form still wins over the stored credential. The parser accepts either the standard `data` array or an enriched `models` map, normalizing each candidate's id, display name, context window, and output-token cap; Anthropic's `max_input_tokens` and `max_tokens` feed the same capacity fields, a map key remains the request id even when its entry names a different canonical id, primitive-valued map properties are ignored, and a missing display name falls back to that request id. The reply is candidate metadata a surface may offer for adoption — nothing is stored, and `settings.yaml` remains the only thing that decides what a route serves.
120
+
121
+ ### Failures and recovery
122
+
123
+ A route pi-ai does not ship needs `api`, `baseURL`, and a non-empty `models` list; an unserviceable profile is refused where it is written, naming the route and model. Failures carry stable codes: a credential that cannot be used fails with `INVALID_CREDENTIAL` naming the route and reference, a route whose `apiKeyEnv` reference resolves to nothing fails with `MISSING_CREDENTIAL`, an unconfigured model fails with `UNKNOWN_MODEL`, and terminal provider failures distinguish `QUOTA` from transient `RATE_LIMIT`. `GenerateOptions.stop` is rejected with `UNSUPPORTED_OPTION` because pi-ai's common streaming UI cannot guarantee it across providers.
124
+
125
+ Settings writes strictly validate each new or changed provider after merging its composition and user layers. During namespace registration, stored catalog failures retain the namespace and provider rows, with the first available model diagnostic or route failure in `LlmConfigurableProvider.error`; unchanged failed providers do not block edits elsewhere. Serviceable models remain selectable, while unresolved models remain in the editable configuration and fail with `INVALID_CONFIG` before network I/O if requested directly. Repairing or deleting the offending configuration clears its diagnostic. Schema and self-contained profile errors still reject loading. Later external edits validate changed providers and retain the last accepted section on failure.
126
+
127
+ Changing `displayName`, `apiKeyEnv`, or `baseURL` without resolving the provider's model errors still rejects the save. For example, renaming an OpenRouter route whose model `111` needs an `api` cannot be saved on its own: repair or remove that model in the same editor draft, then save the complete provider configuration. Intermediate repairs remain in the draft until the whole provider validates; other providers can be saved independently.
128
+
129
+ -----
130
+
131
+ <a id="understand-the-implementation"></a>
132
+ ## Understand the implementation
133
+
134
+ <details>
135
+ <summary>Implementation internals — click to expand</summary>
136
+
137
+ This section explains the design behind the adapter; the observable behavior is fully covered in [Use this package](#use-this-package).
138
+
139
+ ### Design philosophy
140
+
141
+ The adapter is built on immutable snapshots and per-operation resolution. Each operation captures a whole snapshot — the profiles plus a `createModels()` collection holding the `Provider` each route built — before its first `await`, and a configuration change builds a new collection rather than mutating the one in use, so a request that started under one configuration never finishes under another. A route's own credential reference resolves through the harness seam and rides as the request's `apiKey` option, which pi-ai treats as the highest-priority auth override — that is what keeps the fail-loud reference semantics. Everything that override does not cover reaches pi-ai through the collection's own auth: the credential store holds the records a login wrote and a refresh rotates (addressed as `llm-pi-ai/<provider id>`), and the auth context answers the ambient questions a provider asks while resolving. Both are stable across snapshots, so a configuration change rebuilds the collection without forgetting who is signed in.
142
+
143
+ ### Source map
144
+
145
+ | File | Role |
146
+ |---|---|
147
+ | [`src/index.ts`](src/index.ts) | Plugin entry: profile resolution, settings wiring, directory and route registration |
148
+ | [`src/auth.ts`](src/auth.ts) | The credential store and ambient auth context over the harness credential plane |
149
+ | [`src/login.ts`](src/login.ts) | Authorization flows for the installed providers that ship a login |
150
+ | [`src/config.ts`](src/config.ts) | Profile schema, resolution, and serviceability checks |
151
+ | [`src/catalog.ts`](src/catalog.ts) | Installed-catalog integration and drift gates |
152
+ | [`src/provider.ts`](src/provider.ts) | The supported-protocol table and provider construction |
153
+ | [`src/context.ts`](src/context.ts) | Harness-to-pi-ai context conversion, image handling, replay restore |
154
+ | [`src/stream.ts`](src/stream.ts) | pi-ai event conversion into harness `StreamChunk` values |
155
+ | [`src/replay.ts`](src/replay.ts) | Versioned `ReplayEnvelope` storage and validation |
156
+ | [`src/discovery.ts`](src/discovery.ts) | Endpoint interrogation for configuration surfaces |
157
+
158
+ ### Registration and directory
159
+
160
+ The plugin declares every installed catalog provider it can authenticate in the configurable-provider directory, joined with every route the current profiles declare, so configuration surfaces can offer the full catalog before any route exists. Each entry carries `declared` — whether pi-ai ships nothing under that key — because only the adapter can distinguish a hand-declared route from a narrowed catalog route. Route registration is atomic: a candidate set that collides with another adapter leaves the previous routes serving. A bare mount with zero routes is the dormant posture: nothing registers until a settings section supplies profiles, and routes drop when it empties.
161
+
162
+ ### Replay and vocabulary
163
+
164
+ 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 separately restores an Anthropic response model when the provider resolved an alias or fallback. An unusable state degrades to provider-neutral content instead of failing the request. 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.
165
+
166
+ </details>
167
+
168
+ -----
169
+
170
+ <a id="further-exploration"></a>
171
+ ## Further Exploration
172
+
173
+ Read these pages when the package-level contract is not enough. They move from the service contract to the twin adapter and the shared types.
174
+
175
+ - [dsh-llm service](../llm/README.md) — the provider-neutral service this adapter registers on.
176
+ - [llm-deepseek adapter](../llm-deepseek/README.md) — the direct DeepSeek twin for the `deepseek-official` route.
177
+ - [LLM streaming subsystem](../../../docs/subsystems/llm-streaming.md) — the `StreamChunk` protocol and adapter contract.
178
+ - [llm-retry](../llm-retry/README.md) — the retry executor that applies each profile's `retryPolicy`.
179
+ - [Twin LLM adapters](../../../.agents/notes/implemented/architecture/2026-06-13-twin-llm-adapters.md) — why the DeepSeek route ships two structurally different adapters.
180
+ - [Generated configuration catalog](../../../docs/config-catalog.md#deepseek-aidsh-llm-pi-ai) — every accepted config field and its source declaration.
181
+
182
+ -----
183
+
184
+ <a id="model-experience"></a>
185
+ ## Model Experience
186
+
187
+ ### Provider request through pi-ai
188
+
189
+ #### What the model sees
190
+
191
+ The selected catalog model receives one system prompt (`GenerateOptions.system`, otherwise the text of a leading `system` history message; a leading system message with empty text sends none), the remaining history, tools, and sampling fields supported by pi-ai's common streaming API. Each retained image is preceded by text naming its complete attachment id and actual request dimensions. When the current execution filesystem maps the attachment provider's host object, the text also carries a read-only normalized-object path and warns that normalization or request projection may have resized or re-encoded the upload. When accumulated base64 image payload exceeds the route's `maxRequestImageBytes`, each offloaded image keeps its own identity and currently resolved access in replacement text. Offloaded normalized attachments are not read or transformed. Provider-native replay metadata is restored only when the adapter validates it for the historical content.
192
+
193
+ #### Token effect
194
+
195
+ Provider tokenization governs exact input. Retained images add the stable attachment and coordinate descriptor; the offload placeholder replaces an omitted image's visual tokens. Replay metadata may let a native API reuse provider-side state.
196
+
197
+ #### KV Cache effect
198
+
199
+ Conversion preserves logical request order, while image handles and offload placeholders add model-visible text. A changed execution-world path rewrites a historical handle and can prevent reuse from that image even when attachment identity and request bytes stay stable. Changing adapter instance, provider, model, or another upstream token has the same suffix effect. Crossing the image bound replaces an earlier image with placeholder text, so reuse ends at that message until the offloaded prefix stabilizes.
200
+
201
+ ### Provider response
202
+
203
+ #### What the model sees
204
+
205
+ 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.
206
+
207
+ #### Token effect
208
+
209
+ Generated content affects later inputs only after the loop records it. pi-ai folds reasoning tokens into output usage when the provider does not report them separately, and preserves its exact `totalTokens` value unchanged.
210
+
211
+ #### KV Cache effect
212
+
213
+ Recorded response content appends to the next request and does not invalidate its earlier reusable prefix. Unrecorded transport metadata and usage accounting do not affect cache identity.
214
+
215
+ ## Known Limitations and Deferred Work
216
+
217
+ <a id="known-limitations-and-deferred-work"></a>
218
+
219
+
220
+ These limits define where the adapter stops and future work begins. They are current package constraints, not a general pi-ai comparison or a task backlog.
221
+
222
+ - **`maxRequestImageBytes` counts base64 image payload only** — text, tools, descriptors, and JSON structure ride outside the bound, so it must sit below the gateway's request-body cap with headroom. Offload is a deterministic request projection and is not recorded as a session event.
223
+ - **A sign-in lives only in the process that started it** — an authorization attempt is not durable, so reloading the page mid-login abandons it and the human starts over. Signing out is `deleteRecord` on the stored record, which forgets it locally without telling the issuer.
224
+ - **Provider-native discovery answers through this plugin's ambient context** — a route naming no credential defers to the catalog provider's own resolution, which asks for environment values (`AZURE_OPENAI_API_KEY`, `AWS_PROFILE`, and each provider's own set) and for local credential files. Both questions are answered here: the credential seam is consulted before the process environment, and file existence is checked against the host process's filesystem with `~` expanded. What it cannot do is *read* a credential file's contents — a provider that parses `~/.aws/credentials` itself does so directly, outside the seam.
225
+ - **Settings can add or override routes, not remove composition routes** — the user layer merges over the composition base, so deleting a `cordis.yml`-provided provider is a composition change.
226
+ - **The layered merge has no delete for dict keys** — a `reasoningEfforts` level, `modelOverrides` entry, or `compat` field the base declares can be overridden but not removed by the user layer.
227
+ - **`headers` can carry a credential the redactor never sees** — profile resolution rejects names and values Fetch cannot represent, but the dict remains plain strings; store credentials as `apiKeyEnv` references.
228
+ - **A route's catalog never refreshes itself** — the catalog is whatever `settings.yaml` says; nothing here queries a provider for the models it serves.
229
+ - **Anthropic discovery reads at most 1,000 models** — the request uses the API's maximum page size but does not traverse `has_more`; entries beyond the first page must be added by hand.
230
+ - **One wire protocol per route** — a mixed-protocol catalog route cannot host a model of the other protocol; splitting the provider across two route keys is the workaround.
231
+ - **A modality declaration is not verified** — a model declaring `image` its gateway does not serve is refused by the provider after prompt admission. The durable image remains in history and the same misdeclared model can fail again; switching to a text-only model remains possible because the shared LLM runtime projects image references into stable text for that request.
232
+ - **An unauthenticated route depends on its protocol** — a route naming no credential resolves as configured-but-keyless, but pi-ai's OpenAI-compatible implementation still requires an API key or an `Authorization` header, so a keyless local server needs a placeholder credential referenced by `apiKeyEnv` or an `Authorization` entry in `headers`.
233
+ - **`GenerateOptions.stop` is unsupported** — pi-ai's common stream options cannot guarantee stop-sequence behavior across providers.
234
+ - **Only a leading in-history `system` message becomes pi-ai's `systemPrompt`** — pi-ai has one system slot, so a later `system` message, or a leading one when `GenerateOptions.system` is also set, folds into a `user` message at its position; provider-specific placement of the prompt follows pi-ai rather than a harness-owned wire override. Images in system or assistant history, including the leading system message, fail with `UNSUPPORTED_CONTENT` on both conversion paths.
235
+ - **Provider HTTP status is unavailable** — pi-ai error events do not expose a stable HTTP status across providers.
236
+ - **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.
237
+
238
+ <a id="dev-note"></a>
239
+ ### Dev Note
240
+
241
+ <details>
242
+ <summary>Working context for maintainers — click to expand</summary>
243
+
244
+ This Dev Note is non-authoritative working context: undecided directions and notes for maintainers. Shipped behavior and accepted rationale live in the sections above, the package code, and the linked Agent Notes.
245
+
246
+ - The offered protocol set is deliberately narrower than pi-ai's full API set: Bedrock, Vertex, Azure, and Codex authenticate through flows a profile cannot completely describe with a key, an endpoint, and headers; catalog routes still reach them through their own provider, and only an explicit override is refused. Codex is sign-in-able through the authorization flow's OAuth grant.
247
+ - The `compat` switch set is pinned to pi-ai's compat types by drift gates; an upstream upgrade that adds a field, gives a further protocol a compat type, or widens a value union fails the build until someone classifies it.
248
+
249
+ </details>
250
+
251
+ **Runtime invariant:** No companion is published. This package exposes no independent event sequence or mutable data relation beyond contracts enforced at its owning seam.
package/README.zh.md ADDED
@@ -0,0 +1,249 @@
1
+ ---
2
+ description: "面向用户与维护者的 pi-ai 多提供方适配器说明:通过 pi-ai 目录与手工声明网关路由 harness LLM(大语言模型)服务。"
3
+ kind: "package-reference"
4
+ ---
5
+
6
+ # @deepseek-ai/dsh-llm-pi-ai
7
+
8
+ [English](README.md) | 中文
9
+
10
+ ## 概述
11
+
12
+ `@deepseek-ai/dsh-llm-pi-ai` 通过一份配置把模型请求路由到多个 pi-ai 提供方、OpenAI 兼容网关或自托管服务器。已安装的 pi-ai 提供方会提供端点、协议和模型目录默认值;自定义路由可以直接声明这些值,无需修改代码。profile 与凭据按请求解析,因此设置变更会在下一个请求生效,无需重启。受支持的提供方可以使用已存储的 OAuth 或交互式密钥登录,并通过跨进程锁刷新凭据。本包可以在没有路由时启动,并在用户设置添加路由后将其激活。
13
+
14
+ ## 目录
15
+
16
+ - [使用本包](#use-this-package)
17
+ - [理解实现](#understand-the-implementation)
18
+ - [进一步探索](#further-exploration)
19
+ - [模型体验](#model-experience)
20
+ - [已知限制与延期工作](#known-limitations-and-deferred-work)
21
+ - [开发备注](#dev-note)
22
+
23
+ -----
24
+
25
+ <a id="use-this-package"></a>
26
+ ## 使用本包
27
+
28
+ 当组合需要通过 pi-ai 的提供方目录、或通过 pi-ai 已安装目录未描述的网关路由模型请求时挂载本插件。`providers` 字典就是整个配置面:每个键都是请求用 `GenerateOptions.provider` 选择的提供方路由名。
29
+
30
+ ### 何时选择
31
+
32
+ 当同一组合服务多个提供方、某条路由需要 pi-ai 目录默认值并修正少数字段、或必须通过自有端点与协议到达手工声明网关时,选择本适配器。当部署不需要其他提供方时,选择 `dsh-llm-deepseek` 直连 DeepSeek 路由。两个适配器可以同时挂载,因为它们的路由名不冲突;注册其他适配器已拥有的路由会导致插件加载失败。
33
+
34
+ ### 配置提供方路由
35
+
36
+ 每个 profile 都可以设置 `retryPolicy`;省略时使用 normal mode、最多重试五次。`apiKeyEnv` 是按请求经 harness 凭据 seam 解析的凭据引用,因此配置文件绝不包含密钥;解析为空的引用会让请求以 `MISSING_CREDENTIAL` 失败。省略它会让路由保持已配置但无密钥(configured-but-keyless)状态,对已安装目录路由而言即交由 pi-ai 提供方原生的环境发现。
37
+
38
+ ```yaml
39
+ - name: '@deepseek-ai/dsh-llm-pi-ai'
40
+ config:
41
+ providers:
42
+ openai:
43
+ apiKeyEnv: OPENAI_API_KEY
44
+ baseURL: https://proxy.example.com:8443
45
+ reasoning: high
46
+ requestImagePixelBudget: 4194304 # total pixels; 2048 by 2048 default
47
+ requestImageMaxBytes: 1048576 # raw bytes before base64 expansion
48
+ maxRequestImageBytes: 20971520 # accumulated base64 payload
49
+ retryPolicy:
50
+ mode: normal
51
+ maxRetries: 3
52
+ anthropic:
53
+ apiKeyEnv: ANTHROPIC_API_KEY
54
+ models:
55
+ - id: claude-sonnet-4-5
56
+ contextWindow: 200000
57
+ acme-gateway:
58
+ displayName: Acme Gateway
59
+ apiKeyEnv: ACME_GATEWAY_API_KEY
60
+ api: openai-completions
61
+ baseURL: https://gateway.acme.example/v1
62
+ compat:
63
+ thinkingFormat: deepseek
64
+ models:
65
+ - id: acme-think
66
+ name: Acme Think
67
+ contextWindow: 262144
68
+ reasoningEfforts:
69
+ off:
70
+ high: high
71
+ opencode-go:
72
+ apiKeyEnv: OPENCODE_API_KEY
73
+ sessionAffinityHeaders:
74
+ - x-opencode-session
75
+ - x-client-request-id
76
+ ```
77
+
78
+ | 字段 | 默认值 | 含义 |
79
+ |---|---|---|
80
+ | `apiKeyEnv` | 无 | 按请求解析的凭据引用;省略时交由 pi-ai 环境发现 |
81
+ | `displayName` | 提供方名 | 选择器界面显示的标签 |
82
+ | `api` | 目录协议 | 协议格式;仅目录不提供的路由需要 |
83
+ | `baseURL` | 目录端点 | 路由上所有模型的端点 |
84
+ | `models` | 已安装目录 | 整体替换路由目录;每个条目从已安装模型取默认值 |
85
+ | `modelOverrides` | 无 | 重塑个别已安装目录模型,而不替换其余模型 |
86
+ | `compat` | 目录检测 | 无法识别端点的协议兼容开关 |
87
+ | `defaultContextWindow` | `262,144` | 未描述模型的容量回退 |
88
+ | `defaultMaxTokens` | `32,768` | 未描述模型的输出上限回退 |
89
+ | `requestImagePixelBudget` | `4,194,304` | 每张确定性请求图片的总像素预算 |
90
+ | `requestImageMaxBytes` | `1 MiB` | 每张请求图片在 base64 扩展前的编码字节目标 |
91
+ | `maxRequestImageBytes` | `20 MiB` | 带最旧优先卸载的 base64 图片载荷总上限 |
92
+ | `retryPolicy` | normal,5 次重试 | 由 `dsh-llm-retry` 执行的提供方自有重试策略 |
93
+ | `sessionAffinityHeaders` | 缺省 | 该路由每次请求携带的会话亲和头名列表,值为当前会话 ID,用于亲和网关的按会话路由与提示词缓存;未声明的路由不发送任何此类头,attribution 保留名被拒绝 |
94
+
95
+ 生成的[配置目录](../../../docs/config-catalog.zh.md#deepseek-aidsh-llm-pi-ai)是每个受支持字段及其 JSDoc 的穷尽式真源。
96
+
97
+ ### 登录提供方
98
+
99
+ pi-ai 提供登录的提供方可以通过 harness 授权 seam 登录:流程提供 OAuth 或交互式密钥提示(密钥键入 pi-ai 自己的登录提示,而非设置表单),得到的凭据存储在 harness 凭据存储的 `llm-pi-ai/<provider id>` 记录中。存储的登录在其路由的 `apiKeyEnv` 覆盖之下完成认证,并在存储的跨进程锁下自行刷新;退出登录即删除存储记录。落在记录文法之外——小写连字符标识符——的手工声明路由键无法登录,因为对它的记录写入会以 `LlmError('UNSTORABLE_PROVIDER_ID')` 拒绝;这类路由改用 `apiKeyEnv` 或提供方 ambient 设置认证。
100
+
101
+ ### 解析模型目录
102
+
103
+ profile 的 `models` 列表会替换而非扩展路由的已安装目录;每个条目从同 id 已安装模型取未设置字段的默认值,因此把路由收窄到两个模型、修正一个容量或添加比已安装目录更新的模型都是一行编辑。`modelOverrides` 无需该代价即可重塑个别已安装目录模型——修正一个模型,保留其余三十七个——当它与 `models` 列表并存、位于手工声明路由上、或点名目录未描述的模型时会被拒绝,因为静默不变的模型会成为别人日后寻找的拼写错误。
104
+
105
+ ### 带推理(reasoning)与协议兼容运行
106
+
107
+ `reasoningEfforts` 声明模型可选择的 thinking 等级:每个键都是选择器提供的等级,其值是分派时在协议中发送的拼写,因此 `max: ultra` 可以为拥有自有词汇的网关重命名等级。省略该字段时保留已安装目录条目的能力;`false` 声明非推理模型。对于 pi-ai 无法识别的端点,`compat` 开关重塑请求——哪个角色携带系统提示词、哪个字段限制输出、thinking 等级如何传递——可逐路由、逐模型配置。条目与已安装目录都没有尺寸的模型,会采用路由的 `defaultContextWindow` 与 `defaultMaxTokens` 回退值。
108
+
109
+ 对于自托管 Chat Completions 端点,`thinkingTokenBudgetField` 选择推理预算参数,`vllmPriority` 在服务端启用优先级调度时设置整数调度优先级。模板参数接受 `$var: thinking.budget`。`openai-responses` 网关可设置 `supportsMaxOutputTokens: false` 来省略 `max_output_tokens`;Azure 与 Codex 传输会忽略这个共享兼容字段。这些控制均需显式启用;目录拥有的 Anthropic effort 和回退能力不是可配置开关。
110
+
111
+ ### 运行时更改配置
112
+
113
+ profile 通过可选 settings seam 每次操作重新读取:base 与用户的 `llm-pi-ai:` 设置分节按提供方合并,因此用户可以新增路由、覆盖组合路由的一个字段或把路由指向另一个代理,全部在下一个请求生效、无需重启。适配器无法服务的分节会在写入处被拒绝——`settings.mutate` 回答 `settings-rejected`——之后失效的已存储分节会保留 namespace 最后有效值。当路由集合或某路由的重试策略变化时,插件会原子地重新注册:冲突路由会让此前路由继续服务。
114
+
115
+ ### 从端点发现模型
116
+
117
+ 插件会回答「该提供方可以提供哪些模型?」,供配置界面正在编辑或起草的路由使用。已安装目录提供的路由直接由目录回答,不发网络请求;只有目录未描述的路由才会经网络询问。`openai-completions` 与 `openai-responses` 使用带 bearer 鉴权的 `GET {baseURL}/models`,`anthropic-messages` 则以 `x-api-key` 和 `anthropic-version` 使用原生 `GET /v1/models?limit=1000` 语义;其列表 URL 接受带或不带末尾 `/v1` 的 API 根地址,因为网关文档两种写法都会发布,且只有该列表 URL 会归一化这一段,模型请求收到的仍是配置原样的 `baseURL`。已配置且具名的路由会在 Host 内部提供已存凭据与 profile `headers`,因此通过 `settings.yaml` 或 Cordis 配置设置的部署标头可以到达模型发现请求,但不会成为发现请求或 Models 页面的字段;表单中新键入的密钥仍优先于已存凭据。解析器接受标准 `data` 数组或富信息 `models` 对象,并归一化每个候选的 id、显示名、上下文窗口与最大输出 token 数;Anthropic 的 `max_input_tokens` 与 `max_tokens` 会进入相同容量字段,即使对象条目点名了另一个规范 id,对象键仍是请求 id,原始类型的对象属性会被忽略,缺失的显示名则回退到该请求 id。回答是界面可以提供给用户采纳的候选元数据——不存储任何内容,`settings.yaml` 仍然是决定路由服务内容的唯一事实。
118
+
119
+ ### 失败与恢复
120
+
121
+ pi-ai 不提供的路由需要 `api`、`baseURL` 与非空 `models` 列表;无法服务的 profile 会在写入处被拒绝,并点名路由与模型。失败携带稳定 code:无法使用的凭据以 `INVALID_CREDENTIAL` 失败并点名路由与引用,`apiKeyEnv` 引用解析为空的路由以 `MISSING_CREDENTIAL` 失败,未配置模型以 `UNKNOWN_MODEL` 失败,终止性提供方失败则区分 `QUOTA` 与暂时性 `RATE_LIMIT`。`GenerateOptions.stop` 以 `UNSUPPORTED_OPTION` 被拒绝,因为 pi-ai 的通用流式 UI 无法跨提供方保证它。
122
+
123
+ Settings 写入会在合并组合层与用户层后严格校验每个新增或修改的提供方。命名空间注册时,已存储配置的目录解析错误会保留命名空间与提供方行,并通过 `LlmConfigurableProvider.error` 优先返回首个模型诊断,无模型诊断时返回路由错误;未修改的错误提供方不会阻止其他编辑。可解析的模型仍可选择,无法解析的模型保留在可编辑配置中,直接请求时会在网络 I/O 前以 `INVALID_CONFIG` 失败。修复或删除错误配置会清除诊断。Schema 与 profile 自身的约束错误仍会拒绝加载。后续外部文件编辑会校验变化的提供方,失败时保留最后一次接受的分节。
124
+
125
+ 只修改 `displayName`、`apiKeyEnv` 或 `baseURL` 而未解决提供方的模型配置错误时,保存仍会被拒绝。例如,OpenRouter 路由的模型 `111` 缺少 `api` 时,不能单独保存路由名称的修改:需要在同一份编辑草稿中修复或删除该模型,再保存完整的提供方配置。中间修复状态保留在草稿中,直到整条提供方配置通过校验;其他提供方可以独立保存。
126
+
127
+ -----
128
+
129
+ <a id="understand-the-implementation"></a>
130
+ ## 理解实现
131
+
132
+ <details>
133
+ <summary>实现细节——点击展开</summary>
134
+
135
+ 本节解释适配器背后的设计;可观察行为已在[使用本包](#use-this-package)中完整说明。
136
+
137
+ ### 设计理念
138
+
139
+ 适配器建立在不可变快照与按操作解析之上。每个操作都会在第一次 `await` 前捕获整个快照——profile 加一个持有每条路由所构建 `Provider` 的 `createModels()` 集合——配置变更会构建新集合而非修改使用中的集合,因此在一个配置下开始的请求绝不会在另一个配置下结束。路由自己的凭据引用经 harness seam 解析,并以请求 `apiKey` 选项传入,pi-ai 将其视为优先级最高的 auth 覆盖——这正是明确失败引用语义的所在。该覆盖未覆盖的一切都经集合自身的 auth 到达 pi-ai:凭据存储持有登录写入、刷新轮换的记录(以 `llm-pi-ai/<provider id>` 寻址),auth context 回答提供方解析时提出的 ambient 问题。两者跨快照保持稳定,因此配置变更重建集合时不会忘记谁已登录。
140
+
141
+ ### 源码地图
142
+
143
+ | 文件 | 职责 |
144
+ |---|---|
145
+ | [`src/index.ts`](src/index.ts) | 插件入口:profile 解析、settings 接线、目录与路由注册 |
146
+ | [`src/auth.ts`](src/auth.ts) | 覆盖 harness 凭据平面的凭据存储与 ambient auth context |
147
+ | [`src/login.ts`](src/login.ts) | 面向提供登录的已安装提供方的授权流程 |
148
+ | [`src/config.ts`](src/config.ts) | Profile schema、解析与可服务性校验 |
149
+ | [`src/catalog.ts`](src/catalog.ts) | 已安装目录集成与漂移门禁 |
150
+ | [`src/provider.ts`](src/provider.ts) | 受支持协议表与提供方构建 |
151
+ | [`src/context.ts`](src/context.ts) | Harness 到 pi-ai 的上下文转换、图片处理、回放恢复 |
152
+ | [`src/stream.ts`](src/stream.ts) | 把 pi-ai 事件转换为 harness `StreamChunk` 值 |
153
+ | [`src/replay.ts`](src/replay.ts) | 带版本的 `ReplayEnvelope` 存储与校验 |
154
+ | [`src/discovery.ts`](src/discovery.ts) | 面向配置界面的端点询问 |
155
+
156
+ ### 注册与目录
157
+
158
+ 插件会在可配置提供方目录中声明它能认证的每个已安装目录提供方,并加入当前 profile 声明的每条路由,因此配置界面可以在任何路由存在之前提供完整目录。每个条目都携带 `declared`——pi-ai 是否在该键下不提供任何内容——因为只有适配器能区分手工声明路由与收窄目录路由。路由注册具有原子性:与其他适配器冲突的候选集合会让此前路由继续服务。零路由的裸挂载即休眠姿态:settings 分节提供 profile 前不注册任何内容,分节清空时路由随之消失。
159
+
160
+ ### 回放与词汇
161
+
162
+ 成功 assistant 响应会存储带版本的、无损 JSON 回放状态,与产生它们的提供方和模型放在一起——响应级事实加每个流式块一条逐块条目。请求时,`LlmRuntime` 仅当同一适配器实例拥有两条路由时才传递回放状态;适配器校验它并恢复原生响应 id、提供方签名与可选的 `providerThinkingLevel` effort 元数据,缺失的 effort 元数据仍保持缺失。回放会对照 assistant 来源校验请求模型身份,并在提供方解析别名或回退时单独恢复 Anthropic 响应模型。无法使用的状态会降级为提供方无关内容而不是让请求失败。pi-ai 工具调用参数是解析后的对象,因此适配器解析输入并重新字符串化输出,以符合 harness 原始 JSON 约定;pi-ai 流内错误事件映射为终止 `finish` 分片。
163
+
164
+ </details>
165
+
166
+ -----
167
+
168
+ <a id="further-exploration"></a>
169
+ ## 进一步探索
170
+
171
+ 当包级约定不够用时阅读以下页面。它们从服务约定逐步进入孪生适配器与共享类型。
172
+
173
+ - [dsh-llm 服务](../llm/README.zh.md)——本适配器注册其上的提供方无关服务。
174
+ - [llm-deepseek 适配器](../llm-deepseek/README.zh.md)——`deepseek-official` 路由的 DeepSeek 直连孪生。
175
+ - [LLM 流式子系统](../../../docs/subsystems/llm-streaming.zh.md)——`StreamChunk` 协议与适配器约定。
176
+ - [llm-retry](../llm-retry/README.zh.md)——应用每个 profile `retryPolicy` 的重试执行器。
177
+ - [孪生 LLM 适配器](../../../.agents/notes/implemented/architecture/2026-06-13-twin-llm-adapters.zh.md)——为什么 DeepSeek 路由交付两个结构不同的适配器。
178
+ - [生成配置目录](../../../docs/config-catalog.zh.md#deepseek-aidsh-llm-pi-ai)——每个受支持配置字段及其源声明。
179
+
180
+ -----
181
+
182
+ <a id="model-experience"></a>
183
+ ## 模型体验
184
+
185
+ ### 经 pi-ai 的提供方请求
186
+
187
+ #### 模型看到什么
188
+
189
+ 所选目录模型会收到一条系统提示词(`GenerateOptions.system`,否则取历史中首条 `system` 消息的文本;首条 system 消息文本为空时不发送系统提示词)、其余历史、工具与 pi-ai 通用流式 API 支持的采样字段。每张保留图片前都会有文本,注明其完整附件 id 与实际请求尺寸。当前执行文件系统可以映射附件提供方的宿主对象时,该文本还会携带只读规范化对象路径,并警告规范化或请求投影可能缩放或重新编码上传内容。当累计 base64 图片载荷超过路由的 `maxRequestImageBytes` 时,每张卸载图片都会在替换文本中保留自己的身份与当前已解析访问方式。卸载的规范化附件不会读取或变换。提供方原生回放元数据只在适配器针对历史内容校验通过后恢复。
190
+
191
+ #### Token 影响
192
+
193
+ 提供方分词决定精确输入。保留图片会添加稳定的附件与坐标描述符;卸载占位符会替代省略图片的视觉 token。回放元数据可能让原生 API 复用提供方侧状态。
194
+
195
+ #### KV Cache 影响
196
+
197
+ 转换保持逻辑请求顺序,图片句柄与卸载占位符则会添加模型可见文本。即使附件身份与请求字节保持稳定,执行世界路径变化也会改写历史句柄,并可能从该图片起阻止复用。更换适配器实例、提供方、模型或其他上游 token 具有相同的后缀影响。越过图片上限会把较早图片替换为占位文本,因此复用在该消息处结束,直到被卸载前缀稳定。
198
+
199
+ ### 提供方响应
200
+
201
+ #### 模型看到什么
202
+
203
+ pi-ai 事件变成 harness 的推理、文本、工具调用、用量与 finish 分片。适配器把解析后的工具参数以原始 JSON 字符串传给 harness。
204
+
205
+ #### Token 影响
206
+
207
+ 生成内容只在 loop 记录后才影响后续输入。提供方未单独报告推理 token 时,pi-ai 会把推理 token 并入输出用量,并原样保留其精确 `totalTokens` 值。
208
+
209
+ #### KV Cache 影响
210
+
211
+ 已记录的响应内容会追加到下一个请求,不会使其更早可复用前缀失效。未记录的传输元数据与用量计量不影响缓存标识。
212
+
213
+ ## 已知限制与延期工作
214
+
215
+ <a id="known-limitations-and-deferred-work"></a>
216
+
217
+
218
+ 这些限制说明适配器在哪里停止、由未来工作接续。它们是当前包约束,不是通用 pi-ai 对比或任务积压。
219
+
220
+ - **`maxRequestImageBytes` 只计算 base64 图片载荷**——文本、工具、描述符与 JSON 结构在该上限之外,因此它必须留有余量地低于网关请求体上限。卸载是确定性请求投影,不会记录为会话事件。
221
+ - **登录只存在于发起它的进程中**——授权尝试不持久,因此登录中途刷新页面会放弃它,用户需要重新开始。退出登录是对已存储记录执行 `deleteRecord`,只在本地忘记它,不会告知签发方。
222
+ - **提供方原生发现经本插件的 ambient context 回答**——不点名凭据的路由交由目录提供方自身解析,它会询问环境值(`AZURE_OPENAI_API_KEY`、`AWS_PROFILE` 及各提供方自有集合)与本地凭据文件。两个问题都在这里得到回答:凭据 seam 先于进程环境被查询,文件存在性则针对宿主进程的文件系统以 `~` 展开后检查。它做不到的是*读取*凭据文件内容——自行解析 `~/.aws/credentials` 的提供方会直接读取,不经该 seam。
223
+ - **设置可以新增或覆盖路由,不能移除组合路由**——用户层覆盖组合 base,因此删除 `cordis.yml` 提供的提供方属于组合变更。
224
+ - **分层合并对字典键没有删除**——base 声明的 `reasoningEfforts` 等级、`modelOverrides` 条目或 `compat` 字段可以被用户层覆盖,但不能被移除。
225
+ - **`headers` 可以携带 redactor 永远看不到的凭据**——profile 解析会拒绝 Fetch 无法表示的名称与值,但该字典仍是纯字符串;以 `apiKeyEnv` 引用存储凭据。
226
+ - **路由目录不会自行刷新**——目录就是 `settings.yaml` 的内容;这里没有任何机制向提供方查询它提供的模型。
227
+ - **Anthropic 模型发现最多读取 1,000 个模型**——请求使用 API 的最大页大小,但不会遍历 `has_more`;第一页之外的条目需要手工添加。
228
+ - **每条路由一种协议格式**——混合协议目录路由无法承载另一协议格式的模型;把提供方拆到两个路由键是变通办法。
229
+ - **模态声明不受校验**——声明 `image` 而其网关不支持的模型会在提示词准入后被提供方拒绝。持久图片仍留在历史中,同一误声明模型可能再次失败;切换到纯文本模型仍然可行,因为共享 LLM 运行时会针对该请求把图片引用投影为稳定文本。
230
+ - **未认证路由取决于其协议**——不点名凭据的路由解析为已配置但无密钥,但 pi-ai 的 OpenAI 兼容实现仍要求 API 密钥或 `Authorization` 标头,因此无密钥本地服务器需要由 `apiKeyEnv` 引用或 `headers` 中的 `Authorization` 条目提供的占位凭据。
231
+ - **不支持 `GenerateOptions.stop`**——pi-ai 的通用流式选项无法跨提供方保证停止序列行为。
232
+ - **只有历史中首条 `system` 消息会成为 pi-ai 的 `systemPrompt`**——pi-ai 只有一个系统槽位,因此后续的 `system` 消息,或在同时设置了 `GenerateOptions.system` 时的首条消息,会在原位置折叠为 `user` 消息;系统提示词的提供方专属放置遵循 pi-ai,而非 harness 自有的协议覆盖。system 或 assistant 历史中的图片(包括首条系统消息中的图片)在两条转换路径上都会以 `UNSUPPORTED_CONTENT` 失败。
233
+ - **提供方 HTTP 状态不可用**——pi-ai 错误事件不跨提供方暴露稳定 HTTP 状态。
234
+ - **重试策略由提供方自有,而非 SDK 重试**——pi-ai SDK 重试保持禁用,因此持久 agent(智能体)步骤与 `llm/retry` 事件拥有每个可见尝试,直接 `ctx.llm.stream()` 调用仍是单次尝试。
235
+
236
+ <a id="dev-note"></a>
237
+ ### 开发备注
238
+
239
+ <details>
240
+ <summary>维护者的工作上下文——点击展开</summary>
241
+
242
+ 本开发备注是不具权威性的工作上下文:尚未决定的探索方向与维护者备注。已交付的行为与既定理由以上文、包代码和相关 Agent Note 为准。
243
+
244
+ - 提供的协议集合刻意比 pi-ai 的完整 API 集合更窄:Bedrock、Vertex、Azure 与 Codex 通过 profile 无法以密钥、端点与标头完整描述的流程认证;目录路由仍可经自有提供方到达它们,只有显式覆盖会被拒绝。Codex 可经授权流程的 OAuth grant 登录。
245
+ - `compat` 开关集合由漂移门禁钉在 pi-ai 的 compat 类型上;上游升级若新增字段、为更多协议赋予 compat 类型或扩大值联合,会在有人分类前让构建失败。
246
+
247
+ </details>
248
+
249
+ **运行时不变式:** 不发布伴生入口。本包没有独立事件序列或可变数据关系,相关约定在所属 seam 强制执行。