@oh-my-pi/pi-ai 18.1.16 → 18.1.18
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/CHANGELOG.md +29 -0
- package/dist/types/error/flags.d.ts +6 -0
- package/dist/types/providers/anthropic-wire.d.ts +44 -7
- package/dist/types/providers/anthropic.d.ts +42 -0
- package/dist/types/providers/github-copilot-headers.d.ts +71 -1
- package/dist/types/providers/openai-completions.d.ts +1 -1
- package/dist/types/providers/openai-shared.d.ts +8 -0
- package/dist/types/providers/vision-guard.d.ts +16 -2
- package/dist/types/registry/oauth/github-copilot.d.ts +1 -0
- package/dist/types/types.d.ts +47 -1
- package/dist/types/utils/block-symbols.d.ts +41 -0
- package/package.json +6 -6
- package/src/error/finalize.ts +9 -2
- package/src/error/flags.ts +46 -1
- package/src/error/retryable.ts +2 -0
- package/src/providers/amazon-bedrock.ts +35 -25
- package/src/providers/anthropic-wire.ts +42 -9
- package/src/providers/anthropic.ts +648 -82
- package/src/providers/cursor.ts +1 -1
- package/src/providers/devin.ts +1 -1
- package/src/providers/github-copilot-headers.ts +242 -1
- package/src/providers/google-gemini-cli.ts +1 -1
- package/src/providers/google-shared.ts +1 -1
- package/src/providers/ollama.ts +11 -2
- package/src/providers/openai-codex-responses.ts +7 -2
- package/src/providers/openai-completions.ts +23 -4
- package/src/providers/openai-responses.ts +17 -9
- package/src/providers/openai-shared.ts +25 -4
- package/src/providers/pi-native-server.ts +3 -0
- package/src/providers/transform-messages.ts +7 -3
- package/src/providers/vision-guard.ts +28 -2
- package/src/registry/oauth/github-copilot.ts +28 -4
- package/src/stream.ts +1 -0
- package/src/types.ts +49 -1
- package/src/utils/block-symbols.ts +47 -0
- package/src/utils/empty-completion-retry.ts +1 -0
- package/src/utils/http-inspector.ts +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,35 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [18.1.18] - 2026-09-11
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- Anthropic server-side compaction (`compact-2026-01-12` beta): `anthropicCompaction` on `StreamOptions` sends the `compact_20260112` context-management edit, the streamed `compaction` block is surfaced as an `anthropicCompaction` provider payload, the `compaction` stop reason is a normal stop tagged in `stopDetails` (exempt from the empty-completion retry), and usage sums `usage.iterations` whenever a compaction iteration ran. A user-role compaction summary carrying that payload replays as a leading assistant `compaction` block — folded into the retained assistant turn when one follows — with the beta and a never-firing strategy attached automatically; other providers keep reading the summary text. Everything compaction-related is gated on the model line (`compat.supportsServerCompaction`, rule-owned in the catalog) and on the endpoint the request actually reaches (`supportsAnthropicCompaction`: the official API for the first-party provider, resolved through Foundry / `ANTHROPIC_BASE_URL` reroutes, or an explicit `remoteCompaction.enabled` opt-in), so a rerouted session or an older model line falls back to the text summary instead of sending a block the API rejects. Caller-owned clients are gated on their own endpoint (the client's `baseURL`, or an explicit `remoteCompaction.enabled` opt-in when it exposes none) and receive the compaction beta per request, like the effort and control betas. A block held by its originating assistant message — a caller that appends the compacting response itself — replays at the head of that turn. The block's opaque `encrypted_content` is captured from the stream, kept on the payload as `encryptedContent`, and replayed verbatim. A compacting turn is priced per sampling iteration (like a server-side fallback turn), so a long-context tier applies only to an iteration whose own prompt crosses the threshold, never to the summed totals.
|
|
10
|
+
- Added historical decimation prompt-cache breakpoints every 15 user turns on Anthropic requests, so long conversations retain stable cached prefixes during branching, rewinds, and session resume ([#11665](https://github.com/can1357/oh-my-pi/pull/11665) by [@camjac251](https://github.com/camjac251)).
|
|
11
|
+
|
|
12
|
+
### Changed
|
|
13
|
+
|
|
14
|
+
- Defaulted Anthropic OAuth requests to 1h prompt-cache retention where supported, matching Claude Code subscriber behavior and preventing cache expiry during idle intervals ([#11667](https://github.com/can1357/oh-my-pi/pull/11667) by [@camjac251](https://github.com/camjac251)).
|
|
15
|
+
|
|
16
|
+
### Fixed
|
|
17
|
+
|
|
18
|
+
- Fixed Codex HTTP response-body transport failures forwarded through Anthropic-compatible proxies being treated as terminal errors; replay-safe turns now use the existing transient recovery without re-executing completed tools.
|
|
19
|
+
- GitHub Copilot Enterprise requests keep the Copilot CLI identity accepted by private Enterprise endpoints, and Business requests denied with HTTP 400 `model_not_supported` now retry once as the Copilot CLI (matching the existing 403 fallback), restoring models that 18.1.17 rejected as unsupported ([#11669](https://github.com/can1357/oh-my-pi/issues/11669)).
|
|
20
|
+
- Fixed provider stream truncations reported as a bare `unexpected EOF` (and other stream-parse diagnostics) classifying as terminal errors, so they now retry like every other transient transport failure ([#11745](https://github.com/can1357/oh-my-pi/issues/11745)).
|
|
21
|
+
- GitHub Copilot streams remember the working `Copilot-Integration-Id` per credential after a denied chat identity retries as the Copilot CLI, so later streams start at the working shape instead of replaying the denial ([#11669](https://github.com/can1357/oh-my-pi/issues/11669)).
|
|
22
|
+
- Fixed Anthropic OAuth requests omitting the tool-array cache breakpoint, so tool definitions are now cached across session rewrites and sibling subagents ([#11660](https://github.com/can1357/oh-my-pi/pull/11660) by [@camjac251](https://github.com/camjac251)).
|
|
23
|
+
- Fixed Amazon Bedrock OpenAI models rejecting image-bearing tool results by sending each image as a sibling user content block ([#11681](https://github.com/can1357/oh-my-pi/issues/11681)).
|
|
24
|
+
|
|
25
|
+
## [18.1.17] - 2026-09-10
|
|
26
|
+
|
|
27
|
+
### Fixed
|
|
28
|
+
|
|
29
|
+
- Fixed transient Python HTTP/2 stream resets and HTTP/1.1 chunked response interruptions being treated as terminal errors when forwarded by a proxy ([#11160](https://github.com/can1357/oh-my-pi/pull/11160) by [@cyriusweng](https://github.com/cyriusweng)).
|
|
30
|
+
- Ollama cache hits now populate cached-token usage: `prompt_eval_cached_count` from the `/api/chat` done chunk maps to `cacheRead`, with `input` reduced to the uncached portion, so status-line `cache_turn`/`cache_hit` segments and cache-prefix audits report real hit rates instead of false misses.
|
|
31
|
+
- Fixed requests that run across a price change being costed at the newer rate; peak/off-peak estimates now use the rate in effect when the request started.
|
|
32
|
+
- Fixed GitHub Copilot Business seats getting HTTP 403 on every model while the same token succeeds with a Chat client identity: chat and model-policy requests now identify as `copilot-chat`, denied requests retry once as the Copilot CLI (`copilot-developer-cli`), and `COPILOT_INTEGRATION_ID` pins the `Copilot-Integration-Id` header up front; model discovery keeps the CLI identity and the 403 message names the identity and the remedies ([#11372](https://github.com/can1357/oh-my-pi/issues/11372)).
|
|
33
|
+
|
|
5
34
|
## [18.1.16] - 2026-09-09
|
|
6
35
|
|
|
7
36
|
### Fixed
|
|
@@ -28,6 +28,12 @@ export declare const Flag: {
|
|
|
28
28
|
};
|
|
29
29
|
export type Flag = (typeof Flag)[keyof typeof Flag];
|
|
30
30
|
export declare const STREAM_READ_ERROR_PATTERN: RegExp;
|
|
31
|
+
/** Python h2/httpx diagnostics forwarded through provider or proxy error events. */
|
|
32
|
+
export declare const PYTHON_HTTP2_STREAM_RESET_PATTERN: RegExp;
|
|
33
|
+
/** Python h11/httpx EOF while reading an HTTP/1.1 chunked response body. */
|
|
34
|
+
export declare const PYTHON_HTTP_INCOMPLETE_CHUNK_PATTERN: RegExp;
|
|
35
|
+
/** reqwest body-frame failures forwarded by the Codex HTTP proxy. */
|
|
36
|
+
export declare const CODEX_HTTP_BODY_READ_ERROR_PATTERN: RegExp;
|
|
31
37
|
export declare const TRANSIENT_TRANSPORT_PATTERN: RegExp;
|
|
32
38
|
/**
|
|
33
39
|
* Local llama.cpp / Ollama deterministic tool-call argument JSON parse failure.
|
|
@@ -135,7 +135,21 @@ export type FallbackBlockParam = {
|
|
|
135
135
|
model: string;
|
|
136
136
|
};
|
|
137
137
|
};
|
|
138
|
-
|
|
138
|
+
/** Beta enabling server-side compaction (`compact_20260112` edit, `compaction` blocks). */
|
|
139
|
+
export declare const COMPACTION_BETA = "compact-2026-01-12";
|
|
140
|
+
/**
|
|
141
|
+
* Server-side compaction summary (compact-2026-01-12). Returned at the start
|
|
142
|
+
* of the assistant response that crossed the trigger; on replay the API drops
|
|
143
|
+
* every block that precedes it, so it may open the messages array. The
|
|
144
|
+
* `encrypted_content` is opaque provider state, round-tripped verbatim.
|
|
145
|
+
*/
|
|
146
|
+
export type CompactionBlockParam = {
|
|
147
|
+
type: "compaction";
|
|
148
|
+
content: string;
|
|
149
|
+
encrypted_content?: string | null;
|
|
150
|
+
cache_control?: CacheControlEphemeral | null;
|
|
151
|
+
};
|
|
152
|
+
export type ContentBlockParam = TextBlockParam | ImageBlockParam | ToolUseBlockParam | ToolResultBlockParam | ServerToolUseBlockParam | WebSearchToolResultBlockParam | ToolSearchToolResultBlockParam | ToolAdditionBlockParam | ToolRemovalBlockParam | ThinkingBlockParam | RedactedThinkingBlockParam | FallbackBlockParam | CompactionBlockParam;
|
|
139
153
|
/**
|
|
140
154
|
* A single conversation turn.
|
|
141
155
|
*
|
|
@@ -229,12 +243,24 @@ export type FallbackParam = {
|
|
|
229
243
|
output_config?: OutputConfig;
|
|
230
244
|
speed?: "fast";
|
|
231
245
|
};
|
|
246
|
+
/** Server-side compaction edit (compact-2026-01-12). */
|
|
247
|
+
export type CompactionEdit = {
|
|
248
|
+
type: "compact_20260112";
|
|
249
|
+
/** `input_tokens` is the only trigger; `value` must be at least 50,000. */
|
|
250
|
+
trigger?: {
|
|
251
|
+
type: "input_tokens";
|
|
252
|
+
value: number;
|
|
253
|
+
};
|
|
254
|
+
pause_after_compaction?: boolean;
|
|
255
|
+
/** Replaces the API's default summarization prompt entirely. */
|
|
256
|
+
instructions?: string;
|
|
257
|
+
};
|
|
232
258
|
/** Claude Code context-management beta payload. */
|
|
233
259
|
export type ContextManagement = {
|
|
234
260
|
edits: Array<{
|
|
235
261
|
type: "clear_thinking_20251015";
|
|
236
262
|
keep: "all";
|
|
237
|
-
}>;
|
|
263
|
+
} | CompactionEdit>;
|
|
238
264
|
};
|
|
239
265
|
export type MessageCreateParams = {
|
|
240
266
|
model: string;
|
|
@@ -267,7 +293,7 @@ export type MessageCreateParams = {
|
|
|
267
293
|
export type MessageCreateParamsStreaming = MessageCreateParams & {
|
|
268
294
|
stream: true;
|
|
269
295
|
};
|
|
270
|
-
export type StopReason = "end_turn" | "max_tokens" | "stop_sequence" | "tool_use" | "pause_turn" | "refusal" | "sensitive" | "model_context_window_exceeded";
|
|
296
|
+
export type StopReason = "end_turn" | "max_tokens" | "stop_sequence" | "tool_use" | "pause_turn" | "refusal" | "sensitive" | "model_context_window_exceeded" | "compaction";
|
|
271
297
|
export type CacheCreation = {
|
|
272
298
|
ephemeral_5m_input_tokens?: number | null;
|
|
273
299
|
ephemeral_1h_input_tokens?: number | null;
|
|
@@ -278,12 +304,15 @@ export type ServerToolUsage = {
|
|
|
278
304
|
};
|
|
279
305
|
/**
|
|
280
306
|
* Per-attempt token accounting inside a multi-run turn
|
|
281
|
-
* (server-side-fallback-2026-06-01). Populated whenever
|
|
282
|
-
* ran, including sticky-served turns with no `fallback`
|
|
283
|
-
*
|
|
307
|
+
* (server-side-fallback-2026-06-01, compact-2026-01-12). Populated whenever
|
|
308
|
+
* a fallback chain ran, including sticky-served turns with no `fallback`
|
|
309
|
+
* content block, and whenever the compaction beta is active. A
|
|
310
|
+
* `fallback_message` entry is the definitive "served by fallback" signal; a
|
|
311
|
+
* `compaction` entry is the summarization sampling the top-level usage
|
|
312
|
+
* excludes.
|
|
284
313
|
*/
|
|
285
314
|
export type UsageIteration = {
|
|
286
|
-
type?: "message" | "fallback_message" | string;
|
|
315
|
+
type?: "message" | "fallback_message" | "compaction" | string;
|
|
287
316
|
model?: string | null;
|
|
288
317
|
input_tokens?: number | null;
|
|
289
318
|
output_tokens?: number | null;
|
|
@@ -343,6 +372,10 @@ export type ResponseContentBlock = {
|
|
|
343
372
|
to: {
|
|
344
373
|
model: string;
|
|
345
374
|
};
|
|
375
|
+
} | {
|
|
376
|
+
type: "compaction";
|
|
377
|
+
content?: string | null;
|
|
378
|
+
encrypted_content?: string | null;
|
|
346
379
|
};
|
|
347
380
|
export type ContentBlockDelta = {
|
|
348
381
|
type: "text_delta";
|
|
@@ -356,6 +389,10 @@ export type ContentBlockDelta = {
|
|
|
356
389
|
} | {
|
|
357
390
|
type: "signature_delta";
|
|
358
391
|
signature: string;
|
|
392
|
+
} | {
|
|
393
|
+
type: "compaction_delta";
|
|
394
|
+
content?: string | null;
|
|
395
|
+
encrypted_content?: string | null;
|
|
359
396
|
};
|
|
360
397
|
export type StopDetails = {
|
|
361
398
|
type: string;
|
|
@@ -165,6 +165,14 @@ export type AnthropicClientOptionsArgs = {
|
|
|
165
165
|
fetch?: FetchImpl;
|
|
166
166
|
maxRetryDelayMs?: number;
|
|
167
167
|
sessionId?: string;
|
|
168
|
+
/** Working-identity cache key for this credential+host; undefined off the Copilot path. */
|
|
169
|
+
copilotCacheKey?: string;
|
|
170
|
+
/**
|
|
171
|
+
* Build-time cache provenance for the wrapper: the cached value the
|
|
172
|
+
* outgoing headers were built from, or `null` when the cache was empty at
|
|
173
|
+
* build. `undefined` rereads the cache at dispatch.
|
|
174
|
+
*/
|
|
175
|
+
copilotCacheSnapshot?: string | null;
|
|
168
176
|
};
|
|
169
177
|
export type AnthropicClientOptionsResult = {
|
|
170
178
|
isOAuthToken: boolean;
|
|
@@ -204,6 +212,32 @@ export type AnthropicUsageLike = {
|
|
|
204
212
|
* zero-valued objects clear prior extras from earlier stream usage snapshots.
|
|
205
213
|
*/
|
|
206
214
|
export declare function applyAnthropicUsageExtras(usage: Usage, source: AnthropicUsageLike): void;
|
|
215
|
+
/**
|
|
216
|
+
* Whether this model's requests reach the official Anthropic API, resolved the
|
|
217
|
+
* way the transport resolves it — including the Foundry and
|
|
218
|
+
* `ANTHROPIC_BASE_URL` reroutes that leave `compat.officialEndpoint` stale.
|
|
219
|
+
*/
|
|
220
|
+
export declare function resolvesToOfficialAnthropicEndpoint(model: Model<"anthropic-messages">): boolean;
|
|
221
|
+
/**
|
|
222
|
+
* Whether server-side compaction (`compact-2026-01-12`) may be spoken for
|
|
223
|
+
* this model to the endpoint a request actually reaches: a model line the
|
|
224
|
+
* beta supports (`compat.supportsServerCompaction`, rule-owned in the
|
|
225
|
+
* catalog), on the official API for the first-party provider or on any
|
|
226
|
+
* endpoint that opted in through `remoteCompaction.enabled`, and never on one
|
|
227
|
+
* whose deployment contract excludes context management. The same predicate
|
|
228
|
+
* gates emitting the edit, attaching the beta, and replaying a persisted
|
|
229
|
+
* block, so a route or model change can never leave a session sending a block
|
|
230
|
+
* its endpoint rejects.
|
|
231
|
+
*/
|
|
232
|
+
export declare function supportsAnthropicCompaction(model: Model<"anthropic-messages">, effectiveBaseUrl?: string): boolean;
|
|
233
|
+
/**
|
|
234
|
+
* {@link supportsAnthropicCompaction} for a request on a caller-owned client:
|
|
235
|
+
* the endpoint is whatever the client targets (an `AnthropicVertex` client
|
|
236
|
+
* carries an Anthropic model to Vertex), never the model's own routing. SDK
|
|
237
|
+
* clients expose it as `baseURL`; a client that exposes no endpoint only
|
|
238
|
+
* compacts through an explicit `remoteCompaction.enabled` opt-in.
|
|
239
|
+
*/
|
|
240
|
+
export declare function supportsAnthropicCompactionOnClient(model: Model<"anthropic-messages">, client: AnthropicMessagesClientLike): boolean;
|
|
207
241
|
/** Detects the preserved-thinking error caused by rewriting a signed block's conversation prefix. */
|
|
208
242
|
export declare function isThinkingPrefixBindingError(message: string): boolean;
|
|
209
243
|
export declare function isInvalidThinkingSignatureError(message: string): boolean;
|
|
@@ -250,9 +284,17 @@ export type AnthropicMessageParam = MessageParam;
|
|
|
250
284
|
* `fallback` content block from a prior turn be replayed on the wire;
|
|
251
285
|
* otherwise the block is dropped to avoid a 400 on non-fallback requests
|
|
252
286
|
* that don't send the beta.
|
|
287
|
+
*
|
|
288
|
+
* `opts.replayCompaction` — replay a user-role compaction summary that
|
|
289
|
+
* carries an {@link AnthropicCompactionPayload} from this provider as a
|
|
290
|
+
* native `compaction` block instead of its text. The API drops every block
|
|
291
|
+
* before the compaction block, so the assistant turn carrying it may open
|
|
292
|
+
* the conversation; the request must send the compaction beta (the stream
|
|
293
|
+
* entry point adds it whenever such a payload is present).
|
|
253
294
|
*/
|
|
254
295
|
export declare function convertAnthropicMessages(messages: Message[], model: Model<"anthropic-messages">, isOAuthToken: boolean, opts?: {
|
|
255
296
|
serverSideFallbackEnabled?: boolean;
|
|
297
|
+
replayCompaction?: boolean;
|
|
256
298
|
dropAllThinking?: boolean;
|
|
257
299
|
droppedThinkingBlocks?: ReadonlySet<string>;
|
|
258
300
|
}): AnthropicMessageParam[];
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { Message } from "../types.js";
|
|
1
|
+
import type { FetchImpl, Message } from "../types.js";
|
|
2
2
|
/**
|
|
3
3
|
* Infer whether the current request to Copilot is user-initiated or agent-initiated.
|
|
4
4
|
* Accepts `unknown[]` because providers may pass pre-converted message shapes.
|
|
@@ -11,6 +11,70 @@ export type CopilotDynamicHeaders = {
|
|
|
11
11
|
premiumRequests: CopilotPremiumRequests;
|
|
12
12
|
};
|
|
13
13
|
export declare function resolveGitHubCopilotBaseUrl(baseUrl: string | undefined, apiKey: string | undefined): string | undefined;
|
|
14
|
+
/**
|
|
15
|
+
* Opt-in `Copilot-Integration-Id` override for chat and model-policy requests.
|
|
16
|
+
* Reads `COPILOT_INTEGRATION_ID`; unset/invalid keeps the chat-surface default
|
|
17
|
+
* (`COPILOT_CHAT_INTEGRATION_ID`). Model discovery keeps the CLI identity: it
|
|
18
|
+
* unlocks enterprise/experimental models and listing models is not
|
|
19
|
+
* policy-gated the way chat completions are (#11372).
|
|
20
|
+
*/
|
|
21
|
+
export declare function resolveCopilotIntegrationIdOverride(env?: Record<string, string | undefined>): string | undefined;
|
|
22
|
+
/**
|
|
23
|
+
* Effective identity before the chat-surface default: explicit value, then
|
|
24
|
+
* request headers, then `COPILOT_INTEGRATION_ID`. Pure given its inputs, so
|
|
25
|
+
* tests inject literals instead of mutating process state.
|
|
26
|
+
*/
|
|
27
|
+
export declare function resolveCopilotRequestIdentity(headers?: Record<string, string>, explicit?: unknown, env?: Record<string, string | undefined>): string | undefined;
|
|
28
|
+
/**
|
|
29
|
+
* Stable cache key for a raw Copilot API key envelope on one effective host.
|
|
30
|
+
* Hashes the bearer with `Bun.hash` (repo-approved hashing API; same
|
|
31
|
+
* credential-scoped pattern as the GitLab Duo and Codex account keys) so token
|
|
32
|
+
* bytes never sit in the map as keys; enterprise/business routing inputs and
|
|
33
|
+
* the normalized effective base URL participate so the same token on two hosts
|
|
34
|
+
* does not share an entry.
|
|
35
|
+
*/
|
|
36
|
+
export declare function getCopilotIntegrationCacheKey(apiKeyRaw: string | undefined, baseUrl?: string): string | undefined;
|
|
37
|
+
/** Cached working identity for a cache key, if one was learned. */
|
|
38
|
+
export declare function getCachedCopilotIntegrationId(cacheKey: string | undefined): string | undefined;
|
|
39
|
+
/**
|
|
40
|
+
* Remember the identity that cleared the identity gate for a credential.
|
|
41
|
+
* Every store refreshes recency, so hot credentials survive eviction.
|
|
42
|
+
*/
|
|
43
|
+
export declare function rememberCopilotWorkingIntegrationId(cacheKey: string | undefined, integrationId: unknown): void;
|
|
44
|
+
/** Clear one cached identity, or the whole cache when no key is given. */
|
|
45
|
+
export declare function clearCopilotIntegrationCache(cacheKey?: string): void;
|
|
46
|
+
/**
|
|
47
|
+
* Reissue Copilot client-identity denials once with the other surface.
|
|
48
|
+
*
|
|
49
|
+
* Chat is the default surface (`COPILOT_CHAT_INTEGRATION_ID`) because Business
|
|
50
|
+
* organizations that gate premium models per client surface commonly allow
|
|
51
|
+
* chat while blocking CLI/agentic clients (issue #11372). Other Business and
|
|
52
|
+
* Enterprise orgs do the opposite and reject the chat identity — as an HTTP 403
|
|
53
|
+
* or, on `api.business.githubcopilot.com`, an HTTP 400 `model_not_supported`
|
|
54
|
+
* (issue #11669). Both denials retry once as the CLI. The retry fires only for
|
|
55
|
+
* requests carrying the chat default and only when the caller resolved no
|
|
56
|
+
* explicit identity — an explicit choice is never second-guessed. The denied
|
|
57
|
+
* body is drained before reissuing, and the retry carries the CLI identity so
|
|
58
|
+
* the guard passes it through: at most two requests, never a loop.
|
|
59
|
+
*
|
|
60
|
+
* When `cacheKey` is set, a 2xx retry remembers its identity via
|
|
61
|
+
* `rememberCopilotWorkingIntegrationId`, so later streams for the same
|
|
62
|
+
* credential start at the working shape. A cached CLI start that is itself
|
|
63
|
+
* denied (stale after an org-policy flip) retries once as chat and relearns.
|
|
64
|
+
* Only a 2xx retry proves its identity — 401s deny every identity equally and
|
|
65
|
+
* 408/429/5xx are transport-retryable (the transport resends the *original*
|
|
66
|
+
* headers), so those must never be recorded as working. Any non-2xx retry
|
|
67
|
+
* clears the entry instead: the next stream rediscovers rather than pinning a
|
|
68
|
+
* shape that just failed.
|
|
69
|
+
*/
|
|
70
|
+
export declare function wrapFetchForCopilotFallback(base: FetchImpl | undefined, enabled: boolean, integrationId?: unknown, cacheKey?: string,
|
|
71
|
+
/**
|
|
72
|
+
* Build-time cache provenance: the exact cached value the outgoing headers
|
|
73
|
+
* were built from. `undefined` rereads the cache at dispatch (direct
|
|
74
|
+
* callers); `null` pins "cache was empty at build" so a sibling learning
|
|
75
|
+
* mid-flight cannot change this request's retry decision.
|
|
76
|
+
*/
|
|
77
|
+
cacheSnapshot?: string | null): FetchImpl;
|
|
14
78
|
export declare function inferCopilotInitiator(messages: unknown[]): CopilotInitiator;
|
|
15
79
|
/** Check whether any message in the conversation contains image content. */
|
|
16
80
|
export declare function hasCopilotVisionInput(messages: Message[]): boolean;
|
|
@@ -37,4 +101,10 @@ export declare function buildCopilotDynamicHeaders(params: {
|
|
|
37
101
|
headers?: Record<string, string>;
|
|
38
102
|
initiatorOverride?: CopilotInitiator;
|
|
39
103
|
planTier?: string;
|
|
104
|
+
/** Enterprise login domain; Enterprise keeps the CLI identity that its private endpoint accepts. */
|
|
105
|
+
enterpriseUrl?: string;
|
|
106
|
+
/** Raw explicit identity; validated here, chat default when absent/invalid. */
|
|
107
|
+
integrationId?: unknown;
|
|
108
|
+
/** Learned working identity for this credential; explicit still wins, then this, then the defaults. */
|
|
109
|
+
cachedIntegrationId?: unknown;
|
|
40
110
|
}): CopilotDynamicHeaders;
|
|
@@ -42,5 +42,5 @@ export interface OpenAICompletionsOptions extends StreamOptions {
|
|
|
42
42
|
* assistant output commits the attempt.
|
|
43
43
|
*/
|
|
44
44
|
export declare const streamOpenAICompletions: StreamFunction<"openai-completions">;
|
|
45
|
-
export declare function parseChunkUsage(rawUsage: object, model: Model<"openai-completions">, premiumRequests: number | undefined): AssistantMessage["usage"];
|
|
45
|
+
export declare function parseChunkUsage(rawUsage: object, model: Model<"openai-completions">, premiumRequests: number | undefined, timestamp?: number): AssistantMessage["usage"];
|
|
46
46
|
export declare function convertMessages(model: Model<"openai-completions">, context: Context, compat: ResolvedOpenAICompat): ChatCompletionMessageParam[];
|
|
@@ -65,6 +65,14 @@ export interface OpenAIRequestSetup {
|
|
|
65
65
|
headers: Record<string, string>;
|
|
66
66
|
query: Record<string, string> | undefined;
|
|
67
67
|
requestHeaders: Record<string, string>;
|
|
68
|
+
/** Working-identity cache key for this credential+host; undefined off the Copilot path. */
|
|
69
|
+
copilotCacheKey: string | undefined;
|
|
70
|
+
/**
|
|
71
|
+
* Build-time cache provenance for the wrapper: the cached value the
|
|
72
|
+
* outgoing headers were built from, or `null` when the cache was empty at
|
|
73
|
+
* build. `undefined` off the Copilot path (wrapper rereads at dispatch).
|
|
74
|
+
*/
|
|
75
|
+
copilotCacheSnapshot: string | null | undefined;
|
|
68
76
|
}
|
|
69
77
|
export declare function resolveOpenAIRequestSetup(model: OpenAIRequestSetupModel, options: OpenAIRequestSetupOptions): OpenAIRequestSetup;
|
|
70
78
|
export declare function applyOpenAIServiceTier(params: {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ImageContent, Model, TextContent } from "../types.js";
|
|
1
|
+
import type { Api, ImageContent, Model, TextContent } from "../types.js";
|
|
2
2
|
export declare const NON_VISION_IMAGE_PLACEHOLDER = "[image omitted: model does not support vision]";
|
|
3
3
|
export declare function partitionVisionContent(content: ReadonlyArray<TextContent | ImageContent>, supportsImages: boolean): {
|
|
4
4
|
textBlocks: TextContent[];
|
|
@@ -12,4 +12,18 @@ export declare function joinTextWithImagePlaceholder(text: string, omittedImages
|
|
|
12
12
|
* misconfigured provider descriptors or user model entries (e.g. text-only
|
|
13
13
|
* DashScope Qwen SKUs, DeepSeek models) whose endpoints reject `image_url`.
|
|
14
14
|
*/
|
|
15
|
-
export declare function isOpenAICompletionsVisionSupported(model: Model<"openai-completions">): boolean;
|
|
15
|
+
export declare function isOpenAICompletionsVisionSupported(model: Model<"openai-completions" | "openrouter">): boolean;
|
|
16
|
+
/**
|
|
17
|
+
* Whether the transport that will carry `model` sends image content on the wire.
|
|
18
|
+
*
|
|
19
|
+
* The `pi-native` transport forwards the original context (images included) to
|
|
20
|
+
* the gateway, which resolves its own model server-side, so the Chat
|
|
21
|
+
* Completions guard below never runs client-side and the declared input
|
|
22
|
+
* applies. Otherwise the OpenAI Chat Completions path applies the text-only
|
|
23
|
+
* guard, as does the OpenRouter chat fallback (`PI_OPENROUTER_RESPONSES=0`,
|
|
24
|
+
* which dispatches `openrouter` models through `streamOpenAICompletions`);
|
|
25
|
+
* every other API ships the modalities the model declares. Callers that report
|
|
26
|
+
* or gate on the wire (for example the `omp models` table) read this
|
|
27
|
+
* predicate; declared capability reads `model.input`.
|
|
28
|
+
*/
|
|
29
|
+
export declare function sendsImageInputOnWire(model: Model<Api>): boolean;
|
package/dist/types/types.d.ts
CHANGED
|
@@ -195,6 +195,18 @@ export interface CodexCompactionMetadata {
|
|
|
195
195
|
export interface CodexCompactionRequestContext extends CodexCompactionMetadata {
|
|
196
196
|
operationId: string;
|
|
197
197
|
}
|
|
198
|
+
/** Anthropic `compact_20260112` context-management edit (`compact-2026-01-12` beta). */
|
|
199
|
+
export interface AnthropicCompactionRequest {
|
|
200
|
+
/**
|
|
201
|
+
* Prompt input-token count at which the API compacts. The API enforces a
|
|
202
|
+
* 50,000-token floor and defaults to 150,000 when omitted.
|
|
203
|
+
*/
|
|
204
|
+
triggerInputTokens?: number;
|
|
205
|
+
/** Stop after the compaction block instead of continuing the response. */
|
|
206
|
+
pauseAfterCompaction?: boolean;
|
|
207
|
+
/** Custom summarization prompt; replaces the API default entirely when set. */
|
|
208
|
+
instructions?: string;
|
|
209
|
+
}
|
|
198
210
|
/** OpenAI's GPT-5.6+ explicit prompt-cache controls. */
|
|
199
211
|
export interface OpenAIPromptCacheOptions {
|
|
200
212
|
/** `explicit` disables OpenAI's automatic latest-message breakpoint. */
|
|
@@ -243,6 +255,15 @@ export interface StreamOptions {
|
|
|
243
255
|
anthropicPrefixMismatchBehavior?: "drop_block" | "error";
|
|
244
256
|
/** @internal Marks a replay-only Anthropic request that must use non-streaming `max_tokens: 0`. */
|
|
245
257
|
anthropicCacheRefreshRequest?: boolean;
|
|
258
|
+
/**
|
|
259
|
+
* Anthropic server-side compaction (`compact-2026-01-12` beta). Sends the
|
|
260
|
+
* `compact_20260112` context-management edit so the API summarizes the
|
|
261
|
+
* prompt in-band once its input reaches the trigger; the resulting summary
|
|
262
|
+
* arrives as an {@link AnthropicCompactionPayload} on the assistant message.
|
|
263
|
+
* Ignored by every other provider and by Anthropic-compatible endpoints
|
|
264
|
+
* without context-management support.
|
|
265
|
+
*/
|
|
266
|
+
anthropicCompaction?: AnthropicCompactionRequest;
|
|
246
267
|
/**
|
|
247
268
|
* Additional headers to include in provider requests.
|
|
248
269
|
* These are merged on top of model-defined headers.
|
|
@@ -703,7 +724,32 @@ export interface AnthropicMessagePayload {
|
|
|
703
724
|
name: string;
|
|
704
725
|
}>;
|
|
705
726
|
}
|
|
706
|
-
|
|
727
|
+
/**
|
|
728
|
+
* Anthropic server-side compaction summary (`compact-2026-01-12` beta).
|
|
729
|
+
*
|
|
730
|
+
* Produced by the Anthropic provider on the assistant message of a request
|
|
731
|
+
* that streamed a `compaction` content block, and attached to the user-role
|
|
732
|
+
* compaction summary message that replaces the compacted history so the
|
|
733
|
+
* provider can replay the block verbatim: the API drops every block that
|
|
734
|
+
* precedes it. `content` is the plain-text summary, so every other provider
|
|
735
|
+
* reads the message text and ignores the payload.
|
|
736
|
+
*/
|
|
737
|
+
export interface AnthropicCompactionPayload {
|
|
738
|
+
type: "anthropicCompaction";
|
|
739
|
+
/** Provider that produced the summary; only that provider replays it natively. */
|
|
740
|
+
provider: string;
|
|
741
|
+
content: string;
|
|
742
|
+
/** Opaque provider state the API attached to the block; replayed verbatim when present. */
|
|
743
|
+
encryptedContent?: string;
|
|
744
|
+
/**
|
|
745
|
+
* Harness-appended file metadata (`<files>` section) kept out of the
|
|
746
|
+
* byte-identical block. Replayed as a user message after the native block:
|
|
747
|
+
* the converter replaces the summary message with the block and skips its
|
|
748
|
+
* text, so without this the metadata would be invisible to this provider.
|
|
749
|
+
*/
|
|
750
|
+
filesText?: string;
|
|
751
|
+
}
|
|
752
|
+
export type ProviderPayload = OpenAIResponsesHistoryPayload | AnthropicMessagePayload | AnthropicCompactionPayload;
|
|
707
753
|
/** Provider-reported rewrite applied to request content before inference. */
|
|
708
754
|
export interface ProviderInputTransformation {
|
|
709
755
|
type: string;
|
|
@@ -72,3 +72,44 @@ export type DemotedThinkingCarrier = object & {
|
|
|
72
72
|
};
|
|
73
73
|
/** True for text blocks synthesized by cross-model thinking demotion. */
|
|
74
74
|
export declare function isDemotedThinking(block: DemotedThinkingCarrier | null | undefined): boolean;
|
|
75
|
+
/**
|
|
76
|
+
* Marks an Anthropic wire message that was serialized from a source
|
|
77
|
+
* `role: "user"` message.
|
|
78
|
+
*
|
|
79
|
+
* The wire role alone cannot answer this. `convertAnthropicMessages` emits
|
|
80
|
+
* `role: "user"` for several things that are not a conversational turn:
|
|
81
|
+
* `developer` messages on models without mid-conversation `system` support,
|
|
82
|
+
* `tool_result` runs, and the synthetic `Continue.` pads inserted between
|
|
83
|
+
* adjacent assistants. Prompt-cache decimation counts conversational turns,
|
|
84
|
+
* so it reads this marker instead of guessing from wire content.
|
|
85
|
+
*
|
|
86
|
+
* Symbol-keyed so the marker never persists across the JSONL round-trip and
|
|
87
|
+
* never reaches the wire.
|
|
88
|
+
*/
|
|
89
|
+
export declare const kConversationalUser: unique symbol;
|
|
90
|
+
/** Carries the conversational-user marker without exposing a string-keyed property. */
|
|
91
|
+
export type ConversationalUserCarrier = object & {
|
|
92
|
+
[kConversationalUser]?: boolean;
|
|
93
|
+
};
|
|
94
|
+
/** True for wire messages serialized from a source `role: "user"` message. */
|
|
95
|
+
export declare function isConversationalUser(message: ConversationalUserCarrier | null | undefined): boolean;
|
|
96
|
+
/**
|
|
97
|
+
* Marks a `role: "user"` message that `transformMessages` synthesized rather
|
|
98
|
+
* than one the user sent.
|
|
99
|
+
*
|
|
100
|
+
* The stale-tool-result note is deliberately emitted as `user` so no provider
|
|
101
|
+
* elevates untrusted tool output to instruction priority, which leaves it
|
|
102
|
+
* indistinguishable from a real turn by role alone. Prompt-cache decimation
|
|
103
|
+
* must not count it, or an orphan result appearing or disappearing in a
|
|
104
|
+
* compacted history shifts every later checkpoint.
|
|
105
|
+
*
|
|
106
|
+
* Symbol-keyed so the marker never persists across the JSONL round-trip and
|
|
107
|
+
* never reaches the wire.
|
|
108
|
+
*/
|
|
109
|
+
export declare const kSyntheticUser: unique symbol;
|
|
110
|
+
/** Carries the synthetic-user marker without exposing a string-keyed property. */
|
|
111
|
+
export type SyntheticUserCarrier = object & {
|
|
112
|
+
[kSyntheticUser]?: boolean;
|
|
113
|
+
};
|
|
114
|
+
/** True for `user` messages synthesized by message transformation. */
|
|
115
|
+
export declare function isSyntheticUser(message: SyntheticUserCarrier | null | undefined): boolean;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@oh-my-pi/pi-ai",
|
|
3
|
-
"version": "18.1.
|
|
3
|
+
"version": "18.1.18",
|
|
4
4
|
"description": "Unified LLM API with automatic model discovery and provider configuration",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ai",
|
|
@@ -124,11 +124,11 @@
|
|
|
124
124
|
"fmt": "oxfmt --no-error-on-unmatched-pattern 'src/**/*.{ts,tsx}' '{test,bench,examples,scripts}/**/*.ts' '*.ts'"
|
|
125
125
|
},
|
|
126
126
|
"dependencies": {
|
|
127
|
-
"@oh-my-pi/omptype": "18.1.
|
|
128
|
-
"@oh-my-pi/pi-catalog": "18.1.
|
|
129
|
-
"@oh-my-pi/pi-natives": "18.1.
|
|
130
|
-
"@oh-my-pi/pi-utils": "18.1.
|
|
131
|
-
"@oh-my-pi/pi-wire": "18.1.
|
|
127
|
+
"@oh-my-pi/omptype": "18.1.18",
|
|
128
|
+
"@oh-my-pi/pi-catalog": "18.1.18",
|
|
129
|
+
"@oh-my-pi/pi-natives": "18.1.18",
|
|
130
|
+
"@oh-my-pi/pi-utils": "18.1.18",
|
|
131
|
+
"@oh-my-pi/pi-wire": "18.1.18"
|
|
132
132
|
},
|
|
133
133
|
"devDependencies": {
|
|
134
134
|
"@types/bun": "^1.3.14"
|
package/src/error/finalize.ts
CHANGED
|
@@ -45,7 +45,8 @@ export interface FinalizeResult {
|
|
|
45
45
|
*/
|
|
46
46
|
export async function finalize(error: unknown, opts: FinalizeOptions = {}): Promise<FinalizeResult> {
|
|
47
47
|
const aborted = opts.abortTracker ? opts.abortTracker.wasCallerAbort() : opts.signal?.aborted === true;
|
|
48
|
-
const
|
|
48
|
+
const errorStatus = status(error);
|
|
49
|
+
const currentStatus = errorStatus ?? opts.capturedErrorResponse?.status;
|
|
49
50
|
|
|
50
51
|
let message: string;
|
|
51
52
|
try {
|
|
@@ -55,11 +56,17 @@ export async function finalize(error: unknown, opts: FinalizeOptions = {}): Prom
|
|
|
55
56
|
message = error instanceof Error ? error.message : String(error);
|
|
56
57
|
}
|
|
57
58
|
|
|
59
|
+
// A captured status is transport context for the original error. Put it at
|
|
60
|
+
// the root of the classification cause chain so terminal 4xx policy governs
|
|
61
|
+
// nested diagnostics before they can contribute transient flags.
|
|
62
|
+
const classificationError =
|
|
63
|
+
errorStatus === undefined && currentStatus !== undefined ? { status: currentStatus, cause: error } : error;
|
|
64
|
+
|
|
58
65
|
const id = classifyMessage({
|
|
59
66
|
api: opts.api,
|
|
60
67
|
provider: opts.provider,
|
|
61
68
|
model: opts.model,
|
|
62
|
-
errorId: classify(
|
|
69
|
+
errorId: classify(classificationError, opts.api),
|
|
63
70
|
errorMessage: message,
|
|
64
71
|
errorStatus: currentStatus,
|
|
65
72
|
});
|
package/src/error/flags.ts
CHANGED
|
@@ -156,6 +156,13 @@ const TIMEOUT_PATTERN = /\b(?:operation\s+)?timed?\s*out\b|\btimeout\b|\bstream
|
|
|
156
156
|
const TRANSIENT_ENVELOPE_PATTERN = /anthropic stream envelope error:/i;
|
|
157
157
|
const TRANSIENT_ENVELOPE_TRUNCATION_PATTERN = /before message_(?:start|stop)/i;
|
|
158
158
|
export const STREAM_READ_ERROR_PATTERN = /stream[_ -]?read[_ -]?error/i;
|
|
159
|
+
/** Python h2/httpx diagnostics forwarded through provider or proxy error events. */
|
|
160
|
+
export const PYTHON_HTTP2_STREAM_RESET_PATTERN = /<StreamReset stream_id:\d+, error_code:(?:2|7), remote_reset:True>/;
|
|
161
|
+
/** Python h11/httpx EOF while reading an HTTP/1.1 chunked response body. */
|
|
162
|
+
export const PYTHON_HTTP_INCOMPLETE_CHUNK_PATTERN =
|
|
163
|
+
/peer closed connection without sending complete message body \(incomplete chunked read\)/;
|
|
164
|
+
/** reqwest body-frame failures forwarded by the Codex HTTP proxy. */
|
|
165
|
+
export const CODEX_HTTP_BODY_READ_ERROR_PATTERN = /\btransport error reading codex response body\b/i;
|
|
159
166
|
export const TRANSIENT_TRANSPORT_PATTERN =
|
|
160
167
|
/\b(?:no[_ -]?capacity|(?:high|peak)[ _-]?demand|(?:at|over|insufficient)[ _-]?capacity|capacity[ _-]?(?:exceeded|exhausted)|peak[ _-]?load)\b|overloaded|provider.?returned.?error|rate.?limit|too many requests|auth-gateway\s+5\d{2}(?=[:\s]|$)|\b(?:429|500|502|503|504)\b|service.?unavailable|server.?error|internal.?error|retry your request|network.?error|connection.?error|connection.?refused|unable.?to.?connect\.\s*is the computer able to access the url\?|other side closed|fetch failed|upstream.?connect|upstream.?request.?failed|reset before headers|socket hang up|timed? out|timeout|terminated|retry delay|stream stall|no error details in response|HTTP2(?:StreamReset|RefusedStream|EnhanceYourCalm)|nghttp2_(?:internal_error|refused_stream)|stream closed with error code nghttp2_(?:internal_error|refused_stream)|malformed.?function.?call/i;
|
|
161
168
|
const AUTH_FAILURE_PATTERN =
|
|
@@ -396,6 +403,8 @@ function isTransientErrorText(text: string): boolean {
|
|
|
396
403
|
return (
|
|
397
404
|
isUnexpectedSocketCloseMessage(text) ||
|
|
398
405
|
isStreamReadErrorText(text) ||
|
|
406
|
+
PYTHON_HTTP2_STREAM_RESET_PATTERN.test(text) ||
|
|
407
|
+
PYTHON_HTTP_INCOMPLETE_CHUNK_PATTERN.test(text) ||
|
|
399
408
|
(TRANSIENT_ENVELOPE_PATTERN.test(text) && TRANSIENT_ENVELOPE_TRUNCATION_PATTERN.test(text)) ||
|
|
400
409
|
TRANSIENT_TRANSPORT_PATTERN.test(text)
|
|
401
410
|
);
|
|
@@ -432,6 +441,17 @@ function matchesOverflowText(text: string): boolean {
|
|
|
432
441
|
return OVERFLOW_PATTERNS.some(p => p.test(text)) || OVERFLOW_NO_BODY_PATTERN.test(text);
|
|
433
442
|
}
|
|
434
443
|
|
|
444
|
+
/**
|
|
445
|
+
* A 4xx the provider rejected as a deterministic client error — every 4xx
|
|
446
|
+
* except 408 (Request Timeout) and 429 (Too Many Requests), the retryable
|
|
447
|
+
* pair. Mirrors the 4xx policy in {@link isProviderRetryableError}: such a
|
|
448
|
+
* request replays identically, so a transient signal riding on it (e.g. a
|
|
449
|
+
* truncation phrase in the body) must not flip it to retryable.
|
|
450
|
+
*/
|
|
451
|
+
function isTerminalClientErrorStatus(status: number | undefined): boolean {
|
|
452
|
+
return status !== undefined && status >= 400 && status < 500 && status !== 408 && status !== 429;
|
|
453
|
+
}
|
|
454
|
+
|
|
435
455
|
function classifyText(
|
|
436
456
|
errorMessage: string | undefined,
|
|
437
457
|
errorStatus: number | undefined,
|
|
@@ -483,6 +503,24 @@ function classifyText(
|
|
|
483
503
|
}
|
|
484
504
|
if (isTimeoutText(errorMessage)) kinds |= Flag.Transient | Flag.Timeout;
|
|
485
505
|
else if (isTransientErrorText(errorMessage)) kinds |= Flag.Transient;
|
|
506
|
+
// A stream truncation or forwarded Codex HTTP body-read failure may not
|
|
507
|
+
// match TRANSIENT_TRANSPORT_PATTERN. Flag it explicitly so AIError.retriable and
|
|
508
|
+
// the turn-recovery layer treat it as retryable, matching the provider
|
|
509
|
+
// retry path (isProviderRetryableError). Separate `if` (not chained onto
|
|
510
|
+
// the else-if) so a timeout whose text also reads as a truncation keeps
|
|
511
|
+
// Flag.Timeout alongside Flag.Transient. The string arm applies the strict
|
|
512
|
+
// STREAM_PARSE_DIAGNOSTIC_PATTERN, per the rationale on isTransientStreamParseError.
|
|
513
|
+
// Skip a truncation phrase that rides on a terminal 4xx (e.g. a malformed
|
|
514
|
+
// request rejected as "400 unexpected EOF"): that is a deterministic client
|
|
515
|
+
// error that replays identically, so keep it terminal. classify() carries
|
|
516
|
+
// the outer terminal status down the cause chain so a wrapped truncation
|
|
517
|
+
// (ProviderHttpError 400 → cause "unexpected EOF") is caught here too.
|
|
518
|
+
if (
|
|
519
|
+
!isTerminalClientErrorStatus(statusClean) &&
|
|
520
|
+
(isTransientStreamParseError(errorMessage) || CODEX_HTTP_BODY_READ_ERROR_PATTERN.test(errorMessage))
|
|
521
|
+
) {
|
|
522
|
+
kinds |= Flag.Transient;
|
|
523
|
+
}
|
|
486
524
|
// A concurrency cap (e.g. Vertex "Online prediction concurrent requests
|
|
487
525
|
// quota exceeded") is transient — shed-and-backoff. The bare wording need
|
|
488
526
|
// not match TRANSIENT_TRANSPORT_PATTERN, so flag it explicitly to keep
|
|
@@ -521,6 +559,11 @@ export function classify(error: unknown, api?: Api): number {
|
|
|
521
559
|
const seen = new Set<object>();
|
|
522
560
|
const causeTokenEvidence = hasCauseTokenContextOverflowEvidence(error);
|
|
523
561
|
let link: unknown = error;
|
|
562
|
+
// A terminal 4xx on an outer link governs its own cause diagnostics: a
|
|
563
|
+
// wrapped truncation is describing why the deterministic request failed,
|
|
564
|
+
// not an independently retryable transport fault. Carry it down so the
|
|
565
|
+
// stream-parse guard in classifyText sees it on the status-less cause.
|
|
566
|
+
let governingTerminalStatus: number | undefined;
|
|
524
567
|
while (link !== undefined && link !== null) {
|
|
525
568
|
if (typeof link === "object") {
|
|
526
569
|
if (seen.has(link)) break;
|
|
@@ -595,8 +638,10 @@ export function classify(error: unknown, api?: Api): number {
|
|
|
595
638
|
linkMessage = (link as { message: string }).message;
|
|
596
639
|
}
|
|
597
640
|
|
|
598
|
-
const
|
|
641
|
+
const linkStatus = status(link);
|
|
642
|
+
const textId = classifyText(linkMessage, linkStatus ?? governingTerminalStatus, causeTokenEvidence, api);
|
|
599
643
|
kinds |= textId & KIND_MASK;
|
|
644
|
+
if (isTerminalClientErrorStatus(linkStatus)) governingTerminalStatus = linkStatus;
|
|
600
645
|
|
|
601
646
|
link = typeof link === "object" && "cause" in link ? (link as { cause: unknown }).cause : undefined;
|
|
602
647
|
}
|
package/src/error/retryable.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { isRetryableError, isUnexpectedSocketCloseMessage } from "@oh-my-pi/pi-utils";
|
|
2
2
|
import {
|
|
3
|
+
CODEX_HTTP_BODY_READ_ERROR_PATTERN,
|
|
3
4
|
isRetryableStreamEnvelopeError,
|
|
4
5
|
isTransientStreamParseError,
|
|
5
6
|
isUsageLimit,
|
|
@@ -51,6 +52,7 @@ export function isProviderRetryableError(error: unknown): boolean {
|
|
|
51
52
|
isUnexpectedSocketCloseMessage(msg) ||
|
|
52
53
|
isTransientTransportMessage(msg) ||
|
|
53
54
|
TRANSIENT_TRANSPORT_PATTERN.test(msg) ||
|
|
55
|
+
CODEX_HTTP_BODY_READ_ERROR_PATTERN.test(msg) ||
|
|
54
56
|
PROVIDER_TRANSIENT_EXTRA_PATTERN.test(msg) ||
|
|
55
57
|
isTransientStreamParseError(error) ||
|
|
56
58
|
isRetryableStreamEnvelopeError(error)
|