@oh-my-pi/pi-ai 18.10.3 → 18.11.0
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 +28 -0
- package/dist/types/auth/usage-cache.d.ts +3 -1
- package/dist/types/error/flags.d.ts +12 -0
- package/dist/types/provider-session-state.d.ts +2 -2
- package/dist/types/providers/anthropic.d.ts +7 -5
- package/dist/types/providers/openai-codex-responses.d.ts +6 -0
- package/dist/types/types.d.ts +9 -9
- package/dist/types/utils/dsml-leak.d.ts +4 -2
- package/dist/types/utils/tool-call-arguments.d.ts +2 -1
- package/package.json +6 -6
- package/src/auth/usage-cache.ts +23 -7
- package/src/auth/usage.ts +2 -4
- package/src/error/flags.ts +19 -1
- package/src/provider-session-state.ts +2 -2
- package/src/providers/anthropic-messages-server.ts +84 -1
- package/src/providers/anthropic.ts +242 -106
- package/src/providers/openai-codex/access-programs.ts +2 -1
- package/src/providers/openai-codex-responses.ts +31 -27
- package/src/providers/openai-completions.ts +16 -5
- package/src/providers/transform-messages.ts +7 -11
- package/src/stream.ts +1 -1
- package/src/types.ts +9 -9
- package/src/usage/openai-codex.ts +11 -0
- package/src/usage/opencode-go.ts +7 -8
- package/src/utils/dsml-leak.ts +30 -14
- package/src/utils/tool-call-arguments.ts +2 -1
- package/src/utils.ts +37 -18
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,34 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [18.11.0] - 2026-10-14
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
|
|
9
|
+
- Fixed parallel tool calls failing with "Tool call arguments are not valid JSON" on some OpenAI-compatible gateways, such as Gemini ([#15682](https://github.com/can1357/oh-my-pi/pull/15682) by [@Zzz141592653](https://github.com/Zzz141592653))
|
|
10
|
+
- Fixed Anthropic fallback-credit redemption losing its credit on errors that only mention `400` in their text, such as an overload message ([#15702](https://github.com/can1357/oh-my-pi/pull/15702) by [@H4vC](https://github.com/H4vC))
|
|
11
|
+
- Fixed Anthropic fallback-credit redemption sending an extra `fallback-credit-2026-06-01` beta when the refused request used a newer one ([#15702](https://github.com/can1357/oh-my-pi/pull/15702) by [@H4vC](https://github.com/H4vC))
|
|
12
|
+
- Fixed Anthropic fallback-credit redemptions failing when the refused turn included unsigned thinking ([#15702](https://github.com/can1357/oh-my-pi/pull/15702) by [@H4vC](https://github.com/H4vC))
|
|
13
|
+
- Fixed turns ending without automatic retry when a gateway forwards Go's `use of closed network connection` transport error; it now retries like other dropped connections ([#15385](https://github.com/can1357/oh-my-pi/issues/15385))
|
|
14
|
+
- Fixed Codex SSE streams failing with an abort instead of retrying when the first event stalls ([#15542](https://github.com/can1357/oh-my-pi/issues/15542))
|
|
15
|
+
- Fixed Codex (ChatGPT subscription) requests with reasoning disabled, such as judges, titles, commit messages, and thinking `off`, leaving effort to the server default; they now send the lowest supported effort, or `none` where the model accepts it ([#15514](https://github.com/can1357/oh-my-pi/issues/15514))
|
|
16
|
+
- Fixed a Codex account whose token OpenAI rejects (401/403) continuing to show its last quota as healthy; `omp usage` now drops the stale report and `omp auth-gateway check` reports the credential as failing ([#15439](https://github.com/can1357/oh-my-pi/issues/15439))
|
|
17
|
+
- Fixed usage reports being served indefinitely while their provider keeps failing; the last successful report is now shown for at most 24 hours after it was fetched ([#15439](https://github.com/can1357/oh-my-pi/issues/15439))
|
|
18
|
+
- Fixed Responses requests 400ing (DeepSeek: "The `reasoning_text` in the thinking mode must be passed back to the API") after a tool call whose arguments were invalid JSON: the call is now replayed with its parse diagnostic instead of being dropped and leaving a trailing `[Orphan tool result]` note ([#15568](https://github.com/can1357/oh-my-pi/issues/15568))
|
|
19
|
+
- Fixed DeepSeek on OpenAI-compatible gateways leaving doubled-pipe DSML markup (`</||DSML||parameter>`) in assistant text and reasoning ([#15485](https://github.com/can1357/oh-my-pi/issues/15485))
|
|
20
|
+
- Fixed sessions failing every request with `400 … thinking or redacted_thinking blocks in the latest assistant message cannot be modified` after a response fell back to another model mid-turn (e.g. through a gateway that opts into Anthropic server-side fallback); the turn now replays one model's reasoning, and a session already stuck on such a turn recovers by dropping that turn's reasoning and retrying ([#15654](https://github.com/can1357/oh-my-pi/pull/15654) by [@abilliontokens](https://github.com/abilliontokens))
|
|
21
|
+
- Fixed `omp auth-gateway`'s Anthropic `/v1/messages` stream misnumbering content blocks after a redacted reasoning block, which misrouted every later delta in index-addressed clients ([#15654](https://github.com/can1357/oh-my-pi/pull/15654) by [@abilliontokens](https://github.com/abilliontokens))
|
|
22
|
+
|
|
23
|
+
## [18.10.4] - 2026-10-13
|
|
24
|
+
|
|
25
|
+
### Fixed
|
|
26
|
+
|
|
27
|
+
- Fixed Codex cybersecurity policy errors incorrectly retrying without Daybreak and disabling it for subsequent requests, while preserving retries without Daybreak when access is rejected ([#15472](https://github.com/can1357/oh-my-pi/pull/15472) by [@taibenvenuti](https://github.com/taibenvenuti))
|
|
28
|
+
|
|
29
|
+
### Removed
|
|
30
|
+
|
|
31
|
+
- Removed the `toolNamespacesInfo` stream option; Codex requests no longer send `tool_namespaces_info` turn metadata, and `code_mode_tool_names`/`tool_namespaces_info` are no longer reserved client metadata keys.
|
|
32
|
+
|
|
5
33
|
## [18.10.3] - 2026-10-12
|
|
6
34
|
|
|
7
35
|
### Breaking Changes
|
|
@@ -2,7 +2,7 @@ import type { AuthCredentialStore } from "./store.js";
|
|
|
2
2
|
import type { AuthCredential, OAuthCredential } from "./types.js";
|
|
3
3
|
import type { CredentialPool } from "./pool.js";
|
|
4
4
|
import type { Provider } from "../types.js";
|
|
5
|
-
import type { UsageCredential, UsageProvider } from "../usage.js";
|
|
5
|
+
import type { UsageCredential, UsageProvider, UsageReport } from "../usage.js";
|
|
6
6
|
/** Minimum interval between non-exhausted header snapshots; used by UsageService. */
|
|
7
7
|
export declare const USAGE_HEADER_INGEST_INTERVAL_MS = 60000;
|
|
8
8
|
/**
|
|
@@ -56,6 +56,8 @@ export declare class UsageCache {
|
|
|
56
56
|
failureKey(reportKey: string): string;
|
|
57
57
|
get<T>(key: string): UsageCacheEntry<T> | undefined;
|
|
58
58
|
getStale<T>(key: string): UsageCacheEntry<T> | undefined;
|
|
59
|
+
/** Last successful report under `key` while inside its retention window; the failure-path fallback. */
|
|
60
|
+
lastGood(key: string): UsageReport | null;
|
|
59
61
|
set<T>(key: string, entry: UsageCacheEntry<T>): void;
|
|
60
62
|
deletePrefix(prefix: string): boolean;
|
|
61
63
|
markForceRefresh(provider?: Provider): void;
|
|
@@ -68,10 +68,22 @@ export declare function isCodexSteerRejection(message: {
|
|
|
68
68
|
api?: Api;
|
|
69
69
|
errorMessage?: string;
|
|
70
70
|
}): boolean;
|
|
71
|
+
/**
|
|
72
|
+
* Transport-failure wording that marks an error transient. Covers
|
|
73
|
+
* diagnostics forwarded verbatim by gateways/proxies in other runtimes, e.g.
|
|
74
|
+
* undici's "other side closed" and Go's `net.ErrClosed` ("use of closed
|
|
75
|
+
* network connection", #15385).
|
|
76
|
+
*/
|
|
71
77
|
export declare const TRANSIENT_TRANSPORT_PATTERN: RegExp;
|
|
72
78
|
export declare const ANTHROPIC_ACCOUNT_POLICY_PATTERN: RegExp;
|
|
73
79
|
/** Whether an error message represents an Anthropic account-scoped permission/policy denial. */
|
|
74
80
|
export declare function isAnthropicAccountPolicyText(text: string, provider?: string, statusArg?: number): boolean;
|
|
81
|
+
/**
|
|
82
|
+
* Anthropic's rejection of a replayed latest assistant turn whose `thinking` or
|
|
83
|
+
* `redacted_thinking` blocks no longer verify. Resending the same turn fails the
|
|
84
|
+
* same way; only a request that changes that turn's reasoning can succeed.
|
|
85
|
+
*/
|
|
86
|
+
export declare function isImmutableLatestThinkingText(text: string): boolean;
|
|
75
87
|
/**
|
|
76
88
|
* Local llama.cpp / Ollama deterministic tool-call argument JSON parse failure.
|
|
77
89
|
* The model emitted invalid JSON in a tool call and the server returned HTTP 500
|
|
@@ -18,8 +18,8 @@
|
|
|
18
18
|
* siblings are endpoint-scoped and stay: `strictToolsDisabled`
|
|
19
19
|
* (grammar-too-large 400 for the model's tool schema),
|
|
20
20
|
* `replayUnsignedThinkingDisabled` / `thinkingReplayDisabled` (the endpoint
|
|
21
|
-
* is a signing proxy), `prefixDroppedThinkingBlocks` (blocks a
|
|
22
|
-
*
|
|
21
|
+
* is a signing proxy), `prefixDroppedThinkingBlocks` (blocks a rejected
|
|
22
|
+
* replay made the client strip).
|
|
23
23
|
* - **OpenAI Responses** — the `previous_response_id` chain baselines are
|
|
24
24
|
* account-scoped: a stored response belongs to the account that created it.
|
|
25
25
|
* Strict-tools / reasoning-effort fallbacks, replay warmup and the chaining
|
|
@@ -118,7 +118,8 @@ export interface AnthropicOptions extends StreamOptions {
|
|
|
118
118
|
* header is auto-attached; the response parser then honors mid-stream
|
|
119
119
|
* `fallback` content blocks and `usage.iterations` for served-model
|
|
120
120
|
* promotion and per-attempt pricing. Opt-in ONLY — leaving this
|
|
121
|
-
* undefined
|
|
121
|
+
* undefined sends neither and keeps both off; a `fallback` block in the
|
|
122
|
+
* response (a gateway opted in) is still persisted as a replay boundary.
|
|
122
123
|
*/
|
|
123
124
|
fallbacks?: FallbackParam[];
|
|
124
125
|
}
|
|
@@ -283,10 +284,11 @@ export type AnthropicMessageParam = MessageParam;
|
|
|
283
284
|
* Serialize omp {@link Message}s to Anthropic wire messages.
|
|
284
285
|
*
|
|
285
286
|
* `opts.serverSideFallbackEnabled` — when the CURRENT request itself
|
|
286
|
-
* opts into the server-side-fallback beta chain. Only then
|
|
287
|
-
* `fallback` content block from a prior
|
|
288
|
-
* otherwise the block is dropped to avoid a 400
|
|
289
|
-
* that don't send the beta
|
|
287
|
+
* opts into the server-side-fallback beta chain. Only then, and only on the
|
|
288
|
+
* official endpoint, may a persisted `fallback` content block from a prior
|
|
289
|
+
* turn be replayed on the wire; otherwise the block is dropped to avoid a 400
|
|
290
|
+
* on requests that don't send the beta, and its turn keeps one model's
|
|
291
|
+
* reasoning (see {@link collapseFallbackSegments}).
|
|
290
292
|
*
|
|
291
293
|
* `opts.replayCompaction` — replay a user-role compaction summary that
|
|
292
294
|
* carries an {@link AnthropicCompactionPayload} from this provider as a
|
|
@@ -6,6 +6,12 @@ export type { CodexAttestationProvider } from "./openai-codex-attestation.js";
|
|
|
6
6
|
import type { ResponseInput } from "./openai-responses-wire.js";
|
|
7
7
|
export interface OpenAICodexResponsesOptions extends StreamOptions {
|
|
8
8
|
reasoning?: "none" | "minimal" | "low" | "medium" | "high" | "xhigh" | "max";
|
|
9
|
+
/**
|
|
10
|
+
* Run with the least reasoning the model allows; takes precedence over
|
|
11
|
+
* `reasoning`. Sends the `none` tier where the catalog's disable mode is
|
|
12
|
+
* `none-effort`, else the model's lowest supported effort.
|
|
13
|
+
*/
|
|
14
|
+
disableReasoning?: boolean;
|
|
9
15
|
reasoningSummary?: "auto" | "concise" | "detailed" | null;
|
|
10
16
|
/** Explicit `reasoning.context` replay scope. Omitted by default so Codex applies its native request policy. */
|
|
11
17
|
reasoningContext?: CodexReasoningContext;
|
package/dist/types/types.d.ts
CHANGED
|
@@ -9,7 +9,7 @@ import type { ApiKey } from "./auth-retry.js";
|
|
|
9
9
|
import type { OAuthRequestIdentity } from "./auth/types.js";
|
|
10
10
|
import type { BedrockOptions } from "./providers/amazon-bedrock.js";
|
|
11
11
|
import type { AnthropicOptions } from "./providers/anthropic.js";
|
|
12
|
-
import type { FallbackParam, StopDetails } from "./providers/anthropic-wire.js";
|
|
12
|
+
import type { FallbackParam, MessageCreateParamsStreaming, StopDetails } from "./providers/anthropic-wire.js";
|
|
13
13
|
import type { AzureOpenAIResponsesOptions } from "./providers/azure-openai-responses.js";
|
|
14
14
|
import type { CursorOptions } from "./providers/cursor.js";
|
|
15
15
|
import type { AppleFoundationModelsOptions } from "./providers/apple-foundation-models.js";
|
|
@@ -398,8 +398,6 @@ export interface StreamOptions {
|
|
|
398
398
|
liveSteering?: LiveSteering;
|
|
399
399
|
/** Canonical Codex compaction classification; ignored by other providers. */
|
|
400
400
|
codexCompaction?: CodexCompactionRequestContext;
|
|
401
|
-
/** Codex Code Mode tool exposure snapshot emitted as `tool_namespaces_info` turn metadata; ignored by other providers. */
|
|
402
|
-
toolNamespacesInfo?: unknown;
|
|
403
401
|
/**
|
|
404
402
|
* Optional per-provider concurrent request cap for LLM stream calls. Keys are
|
|
405
403
|
* provider ids (`model.provider`); positive numeric values cap in-flight
|
|
@@ -652,11 +650,12 @@ export interface RedactedThinkingContent {
|
|
|
652
650
|
}
|
|
653
651
|
/**
|
|
654
652
|
* Anthropic server-side-fallback boundary marker persisted on assistant
|
|
655
|
-
* turns whose
|
|
656
|
-
* `AnthropicOptions.fallbacks
|
|
657
|
-
*
|
|
658
|
-
*
|
|
659
|
-
*
|
|
653
|
+
* turns whose response switched models mid-stream, whether the request
|
|
654
|
+
* opted into `AnthropicOptions.fallbacks` or a gateway opted in on its
|
|
655
|
+
* behalf. Consumers other than the Anthropic provider MUST ignore it —
|
|
656
|
+
* `transformMessages` strips the block on any non-Anthropic hop, and
|
|
657
|
+
* `convertAnthropicMessages` only puts it on the wire for an opted-in
|
|
658
|
+
* request to the official endpoint.
|
|
660
659
|
*/
|
|
661
660
|
export interface AnthropicFallbackContent {
|
|
662
661
|
type: "fallback";
|
|
@@ -1424,7 +1423,8 @@ export type AssistantMessageEvent = {
|
|
|
1424
1423
|
export interface AnthropicFallbackCreditHandle {
|
|
1425
1424
|
token: string;
|
|
1426
1425
|
prefillClaim?: boolean | null;
|
|
1427
|
-
|
|
1426
|
+
/** The refused request's body, replayed with the credit token on the fallback model. */
|
|
1427
|
+
params: MessageCreateParamsStreaming;
|
|
1428
1428
|
betas?: readonly string[];
|
|
1429
1429
|
betaHeader?: string;
|
|
1430
1430
|
expiresAt: number;
|
|
@@ -23,7 +23,9 @@ export declare function isDsmlLeakRecoveryTarget(model: Model): boolean;
|
|
|
23
23
|
*/
|
|
24
24
|
export declare function stripDsmlToolMarkup(text: string): string | undefined;
|
|
25
25
|
/**
|
|
26
|
-
* Remove leaked DSML tool-call markup from
|
|
27
|
-
*
|
|
26
|
+
* Remove leaked DSML tool-call markup from an assistant message before commit,
|
|
27
|
+
* including reasoning blocks and turns with valid structured calls. Rewritten
|
|
28
|
+
* thinking with an opaque signature is discarded; OpenAI-compatible reasoning
|
|
29
|
+
* field names are replay markers, not signatures, and survive cleanup.
|
|
28
30
|
*/
|
|
29
31
|
export declare function removeDsmlToolMarkupLeak(message: AssistantMessage): boolean;
|
|
@@ -5,7 +5,8 @@ export declare function parseToolCallArguments(json: string | undefined): ToolCa
|
|
|
5
5
|
* Native Responses `function_call.arguments` to persist for replay.
|
|
6
6
|
*
|
|
7
7
|
* {@link parseToolCallArguments} repairs some invalid JSON (e.g. `"name": ,`) and the tool runs with the
|
|
8
|
-
* repaired value; replay
|
|
8
|
+
* repaired value; replay must carry what was executed (#14155), not the {@link invalidToolCallArguments}
|
|
9
|
+
* diagnostic it substitutes for stored arguments that do not parse (#15568).
|
|
9
10
|
* Returns `raw` when it already parses or the arguments were unrepairable, else the executed arguments.
|
|
10
11
|
*/
|
|
11
12
|
export declare function replayableToolCallArguments(raw: string, executed: ToolCall["arguments"]): string;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@oh-my-pi/pi-ai",
|
|
3
|
-
"version": "18.
|
|
3
|
+
"version": "18.11.0",
|
|
4
4
|
"description": "Unified LLM API with automatic model discovery and provider configuration",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ai",
|
|
@@ -155,11 +155,11 @@
|
|
|
155
155
|
"fmt": "oxfmt --no-error-on-unmatched-pattern 'src/**/*.{ts,tsx}' '{test,bench,examples,scripts}/**/*.ts' '*.ts'"
|
|
156
156
|
},
|
|
157
157
|
"dependencies": {
|
|
158
|
-
"@oh-my-pi/omptype": "18.
|
|
159
|
-
"@oh-my-pi/pi-catalog": "18.
|
|
160
|
-
"@oh-my-pi/pi-natives": "18.
|
|
161
|
-
"@oh-my-pi/pi-utils": "18.
|
|
162
|
-
"@oh-my-pi/pi-wire": "18.
|
|
158
|
+
"@oh-my-pi/omptype": "18.11.0",
|
|
159
|
+
"@oh-my-pi/pi-catalog": "18.11.0",
|
|
160
|
+
"@oh-my-pi/pi-natives": "18.11.0",
|
|
161
|
+
"@oh-my-pi/pi-utils": "18.11.0",
|
|
162
|
+
"@oh-my-pi/pi-wire": "18.11.0"
|
|
163
163
|
},
|
|
164
164
|
"devDependencies": {
|
|
165
165
|
"@types/bun": "^1.3.14"
|
package/src/auth/usage-cache.ts
CHANGED
|
@@ -3,6 +3,7 @@ import type { AuthCredential, OAuthCredential } from "./types";
|
|
|
3
3
|
import type { CredentialPool } from "./pool";
|
|
4
4
|
import type { Provider } from "../types";
|
|
5
5
|
import type { UsageCredential, UsageProvider, UsageReport } from "../usage";
|
|
6
|
+
import { isRecord } from "../utils";
|
|
6
7
|
|
|
7
8
|
const USAGE_CACHE_PREFIX = "usage_cache:";
|
|
8
9
|
const USAGE_FORCE_REFRESH_CACHE_PREFIX = "force-refresh:";
|
|
@@ -53,6 +54,17 @@ function withoutRawPayload(value: unknown): unknown {
|
|
|
53
54
|
return rest;
|
|
54
55
|
}
|
|
55
56
|
|
|
57
|
+
/**
|
|
58
|
+
* End of a cached value's durable retention. A report is retained for 24h past
|
|
59
|
+
* its last successful fetch, so re-storing it as a failure fallback never
|
|
60
|
+
* extends that window; other values are retained for 24h from now.
|
|
61
|
+
*/
|
|
62
|
+
function retentionDeadline(value: unknown): number {
|
|
63
|
+
const fetchedAt = isRecord(value) ? value.fetchedAt : undefined;
|
|
64
|
+
const start = typeof fetchedAt === "number" && Number.isFinite(fetchedAt) && fetchedAt > 0 ? fetchedAt : Date.now();
|
|
65
|
+
return start + USAGE_LAST_GOOD_RETENTION_MS;
|
|
66
|
+
}
|
|
67
|
+
|
|
56
68
|
/** Convert a stored credential for usage providers; used by usage and health services. */
|
|
57
69
|
export function buildUsageCredential(credential: AuthCredential): UsageCredential {
|
|
58
70
|
if (credential.type === "api_key") {
|
|
@@ -203,15 +215,19 @@ export class UsageCache {
|
|
|
203
215
|
return parseUsageCacheEntry<T>(raw);
|
|
204
216
|
}
|
|
205
217
|
|
|
218
|
+
/** Last successful report under `key` while inside its retention window; the failure-path fallback. */
|
|
219
|
+
lastGood(key: string): UsageReport | null {
|
|
220
|
+
const report = this.getStale<UsageReport | null>(key)?.value;
|
|
221
|
+
return report && Date.now() < retentionDeadline(report) ? report : null;
|
|
222
|
+
}
|
|
223
|
+
|
|
206
224
|
set<T>(key: string, entry: UsageCacheEntry<T>): void {
|
|
207
225
|
const value = withoutRawPayload(entry.value);
|
|
208
|
-
const
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
});
|
|
212
|
-
|
|
213
|
-
value === null ? entry.expiresAt : Math.max(entry.expiresAt, Date.now() + USAGE_LAST_GOOD_RETENTION_MS);
|
|
214
|
-
this.#store.setCache(`${USAGE_CACHE_PREFIX}${key}`, payload, Math.floor(durableExpiresAt / 1000));
|
|
226
|
+
const deadline = value === null ? undefined : retentionDeadline(value);
|
|
227
|
+
// Failure cooldowns must not make a last-good report fresh beyond retention.
|
|
228
|
+
const expiresAt = deadline === undefined ? entry.expiresAt : Math.min(entry.expiresAt, deadline);
|
|
229
|
+
const payload = JSON.stringify({ value, expiresAt });
|
|
230
|
+
this.#store.setCache(`${USAGE_CACHE_PREFIX}${key}`, payload, Math.floor((deadline ?? expiresAt) / 1000));
|
|
215
231
|
}
|
|
216
232
|
|
|
217
233
|
deletePrefix(prefix: string): boolean {
|
package/src/auth/usage.ts
CHANGED
|
@@ -334,7 +334,7 @@ export class UsageService implements UsageApi {
|
|
|
334
334
|
|
|
335
335
|
const failure = this.#deps.cache.get<null>(failureKey);
|
|
336
336
|
if (failure && failure.expiresAt > now) {
|
|
337
|
-
return this.#deps.cache.
|
|
337
|
+
return this.#deps.cache.lastGood(cacheKey);
|
|
338
338
|
}
|
|
339
339
|
|
|
340
340
|
// Invalidation must not fork an already-running credential probe. Share it;
|
|
@@ -378,9 +378,7 @@ export class UsageService implements UsageApi {
|
|
|
378
378
|
// so an expired login does not display stale quota indefinitely.
|
|
379
379
|
const providerImpl = this.providerFor(request.provider);
|
|
380
380
|
const retainLastGood = !invalidated && providerImpl?.retainLastGoodOnFailure !== false;
|
|
381
|
-
const lastGood = retainLastGood
|
|
382
|
-
? (this.#deps.cache.getStale<UsageReport | null>(cacheKey)?.value ?? null)
|
|
383
|
-
: null;
|
|
381
|
+
const lastGood = retainLastGood ? this.#deps.cache.lastGood(cacheKey) : null;
|
|
384
382
|
const failureBackoffMs = providerImpl?.failureBackoffMs ?? USAGE_FAILURE_BACKOFF_MS;
|
|
385
383
|
const backoffJitter = failureBackoffMs * (Math.random() * 0.5 - 0.25);
|
|
386
384
|
const coolDown = Date.now() + failureBackoffMs + backoffJitter;
|
package/src/error/flags.ts
CHANGED
|
@@ -217,8 +217,14 @@ export function isCodexSteerRejection(message: { api?: Api; errorMessage?: strin
|
|
|
217
217
|
);
|
|
218
218
|
}
|
|
219
219
|
|
|
220
|
+
/**
|
|
221
|
+
* Transport-failure wording that marks an error transient. Covers
|
|
222
|
+
* diagnostics forwarded verbatim by gateways/proxies in other runtimes, e.g.
|
|
223
|
+
* undici's "other side closed" and Go's `net.ErrClosed` ("use of closed
|
|
224
|
+
* network connection", #15385).
|
|
225
|
+
*/
|
|
220
226
|
export const TRANSIENT_TRANSPORT_PATTERN =
|
|
221
|
-
/\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;
|
|
227
|
+
/\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|use of closed network connection|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;
|
|
222
228
|
const AUTH_FAILURE_PATTERN =
|
|
223
229
|
/\b(?:401|403|unauthorized|forbidden|authentication|auth[_ ]?unavailable|no auth available|(?:invalid|no)[_ ]?api[_ ]?key)\b/i;
|
|
224
230
|
const MALFORMED_FUNCTION_CALL_PATTERN = /\bmalformed.?function.?call\b/i;
|
|
@@ -237,6 +243,18 @@ export function isAnthropicAccountPolicyText(text: string, provider?: string, st
|
|
|
237
243
|
return ANTHROPIC_ACCOUNT_POLICY_PATTERN.test(text);
|
|
238
244
|
}
|
|
239
245
|
|
|
246
|
+
const IMMUTABLE_LATEST_THINKING_PATTERN =
|
|
247
|
+
/messages\.\d+\.content\.\d+.*\b(?:thinking|redacted_thinking)\b.*\blatest assistant message cannot be modified\b/is;
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Anthropic's rejection of a replayed latest assistant turn whose `thinking` or
|
|
251
|
+
* `redacted_thinking` blocks no longer verify. Resending the same turn fails the
|
|
252
|
+
* same way; only a request that changes that turn's reasoning can succeed.
|
|
253
|
+
*/
|
|
254
|
+
export function isImmutableLatestThinkingText(text: string): boolean {
|
|
255
|
+
return IMMUTABLE_LATEST_THINKING_PATTERN.test(text);
|
|
256
|
+
}
|
|
257
|
+
|
|
240
258
|
const CODEX_CHATGPT_ACCOUNT_MODEL_POLICY_PATTERN =
|
|
241
259
|
/\bThe ['"]([^'"\r\n]+)['"] model is not supported when using Codex with a ChatGPT account\./i;
|
|
242
260
|
const CODEX_CHATGPT_ACCOUNT_MODEL_MAX_LENGTH = 256;
|
|
@@ -18,8 +18,8 @@
|
|
|
18
18
|
* siblings are endpoint-scoped and stay: `strictToolsDisabled`
|
|
19
19
|
* (grammar-too-large 400 for the model's tool schema),
|
|
20
20
|
* `replayUnsignedThinkingDisabled` / `thinkingReplayDisabled` (the endpoint
|
|
21
|
-
* is a signing proxy), `prefixDroppedThinkingBlocks` (blocks a
|
|
22
|
-
*
|
|
21
|
+
* is a signing proxy), `prefixDroppedThinkingBlocks` (blocks a rejected
|
|
22
|
+
* replay made the client strip).
|
|
23
23
|
* - **OpenAI Responses** — the `previous_response_id` chain baselines are
|
|
24
24
|
* account-scoped: a stored response belongs to the account that created it.
|
|
25
25
|
* Strict-tools / reasoning-effort fallbacks, replay warmup and the chaining
|
|
@@ -538,9 +538,53 @@ function mapStopReasonOut(reason: StopReason, hasToolUse: boolean): "end_turn" |
|
|
|
538
538
|
}
|
|
539
539
|
}
|
|
540
540
|
|
|
541
|
+
/**
|
|
542
|
+
* Collapse a server-side-fallback turn for a bridge client that never opted
|
|
543
|
+
* into the beta: drop the `fallback` markers and keep reasoning from one
|
|
544
|
+
* model only. The downstream client never accepted a `fallbacks` request
|
|
545
|
+
* param and the bridge does not forward one, so the marker cannot go on the
|
|
546
|
+
* wire; without it, two models' signed reasoning replayed back-to-back reads
|
|
547
|
+
* as one modified chain and is the shape Anthropic rejects.
|
|
548
|
+
*
|
|
549
|
+
* The segment kept is the first that has any reasoning, not the last. The
|
|
550
|
+
* streaming encoder has already sent the first segment's blocks when the
|
|
551
|
+
* boundary arrives and cannot retract them, so first-segment is the only
|
|
552
|
+
* rule it can decide online; this path uses the same rule so streamed and
|
|
553
|
+
* non-streamed turns present the same reasoning. The offline converter
|
|
554
|
+
* (`collapseFallbackSegments` in `anthropic.ts`) can see the whole turn and
|
|
555
|
+
* keeps the last segment instead — both guarantee a single chain survives.
|
|
556
|
+
*/
|
|
557
|
+
function collapseFallbackSegmentsKeepFirst(content: AssistantMessage["content"]): AssistantMessage["content"] {
|
|
558
|
+
let firstReasoningSegment = -1;
|
|
559
|
+
let segment = 0;
|
|
560
|
+
for (const block of content) {
|
|
561
|
+
if (block.type === "fallback") segment++;
|
|
562
|
+
else if (block.type === "thinking" || block.type === "redactedThinking") {
|
|
563
|
+
firstReasoningSegment = segment;
|
|
564
|
+
break;
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
if (firstReasoningSegment === -1) {
|
|
568
|
+
return content.filter(block => block.type !== "fallback");
|
|
569
|
+
}
|
|
570
|
+
const collapsed: AssistantMessage["content"] = [];
|
|
571
|
+
segment = 0;
|
|
572
|
+
for (const block of content) {
|
|
573
|
+
if (block.type === "fallback") {
|
|
574
|
+
segment++;
|
|
575
|
+
continue;
|
|
576
|
+
}
|
|
577
|
+
if ((block.type === "thinking" || block.type === "redactedThinking") && segment !== firstReasoningSegment) {
|
|
578
|
+
continue;
|
|
579
|
+
}
|
|
580
|
+
collapsed.push(block);
|
|
581
|
+
}
|
|
582
|
+
return collapsed;
|
|
583
|
+
}
|
|
584
|
+
|
|
541
585
|
function encodeContentBlocks(message: AssistantMessage): Record<string, unknown>[] {
|
|
542
586
|
const blocks: Record<string, unknown>[] = [];
|
|
543
|
-
for (const c of message.content) {
|
|
587
|
+
for (const c of collapseFallbackSegmentsKeepFirst(message.content)) {
|
|
544
588
|
switch (c.type) {
|
|
545
589
|
case "text":
|
|
546
590
|
blocks.push({ type: "text", text: c.text });
|
|
@@ -668,6 +712,20 @@ export function encodeStream(
|
|
|
668
712
|
// preceding text/thinking block before it opens, so no block that is
|
|
669
713
|
// still open is ever renumbered.
|
|
670
714
|
const suppressed = new Set<number>();
|
|
715
|
+
// A persisted `fallback` marker occupies a content index but is
|
|
716
|
+
// never emitted — the bridge client never opted into the beta, so
|
|
717
|
+
// the marker cannot go on the wire. The walk below records each
|
|
718
|
+
// marker (and every other block this encoder cannot emit) in
|
|
719
|
+
// `suppressed` so `wireIndex` stays dense. Once a boundary has been
|
|
720
|
+
// passed with reasoning already sent, every later segment's
|
|
721
|
+
// reasoning is a second signed chain the client must not receive;
|
|
722
|
+
// the first chain is already on the wire and cannot be retracted,
|
|
723
|
+
// so it is the one that survives (see
|
|
724
|
+
// `collapseFallbackSegmentsKeepFirst`). `reasoningSeen` tracks what
|
|
725
|
+
// the client actually received, which is why redacted reasoning —
|
|
726
|
+
// which this encoder has no event for — does not set it.
|
|
727
|
+
let reasoningSeen = false;
|
|
728
|
+
let suppressLaterReasoning = false;
|
|
671
729
|
const wireIndex = (contentIndex: number): number => {
|
|
672
730
|
let shift = 0;
|
|
673
731
|
for (const index of suppressed) {
|
|
@@ -707,6 +765,20 @@ export function encodeStream(
|
|
|
707
765
|
while (nextContentIndexToInspect < limit) {
|
|
708
766
|
const contentIndex = nextContentIndexToInspect++;
|
|
709
767
|
const content = message.content[contentIndex];
|
|
768
|
+
if (content?.type === "fallback") {
|
|
769
|
+
// Never on the wire, so keep the numbering dense. Latch the
|
|
770
|
+
// collapse only when the client already has this turn's first
|
|
771
|
+
// chain: what follows would be a second one.
|
|
772
|
+
suppressed.add(contentIndex);
|
|
773
|
+
if (reasoningSeen) suppressLaterReasoning = true;
|
|
774
|
+
continue;
|
|
775
|
+
}
|
|
776
|
+
if (content?.type === "redactedThinking") {
|
|
777
|
+
// Redacted reasoning has no stream event, so it never reaches
|
|
778
|
+
// the client; account for its index like any other hole.
|
|
779
|
+
suppressed.add(contentIndex);
|
|
780
|
+
continue;
|
|
781
|
+
}
|
|
710
782
|
if (content?.type !== "anthropicServerTool") continue;
|
|
711
783
|
ensureStart(message);
|
|
712
784
|
const index = wireIndex(contentIndex);
|
|
@@ -781,6 +853,15 @@ export function encodeStream(
|
|
|
781
853
|
case "thinking_start": {
|
|
782
854
|
emitServerToolBlocksBefore(ev.partial, ev.contentIndex);
|
|
783
855
|
ensureStart(ev.partial);
|
|
856
|
+
if (suppressLaterReasoning) {
|
|
857
|
+
// A later segment's reasoning is a second signed
|
|
858
|
+
// chain; drop the whole block — start, deltas,
|
|
859
|
+
// signature and stop — and account for its index
|
|
860
|
+
// so later blocks stay densely numbered.
|
|
861
|
+
suppressed.add(ev.contentIndex);
|
|
862
|
+
break;
|
|
863
|
+
}
|
|
864
|
+
reasoningSeen = true;
|
|
784
865
|
const index = wireIndex(ev.contentIndex);
|
|
785
866
|
open.set(ev.contentIndex, { index, kind: "thinking" });
|
|
786
867
|
controller.enqueue(
|
|
@@ -793,6 +874,7 @@ export function encodeStream(
|
|
|
793
874
|
break;
|
|
794
875
|
}
|
|
795
876
|
case "thinking_delta":
|
|
877
|
+
if (suppressed.has(ev.contentIndex)) break;
|
|
796
878
|
controller.enqueue(
|
|
797
879
|
sseFrame("content_block_delta", {
|
|
798
880
|
type: "content_block_delta",
|
|
@@ -802,6 +884,7 @@ export function encodeStream(
|
|
|
802
884
|
);
|
|
803
885
|
break;
|
|
804
886
|
case "thinking_end": {
|
|
887
|
+
if (suppressed.has(ev.contentIndex)) break;
|
|
805
888
|
const c = ev.partial.content[ev.contentIndex];
|
|
806
889
|
if (c?.type === "thinking" && c.thinkingSignature) {
|
|
807
890
|
controller.enqueue(
|