@bitkyc08/opencodex 2.88.0 → 2.90.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/README.md +11 -1
- package/bin/ocx.mjs +15 -4
- package/gui/dist/assets/App-DgxkE6Md.js +58 -0
- package/gui/dist/assets/App-dnkJ85te.css +1 -0
- package/gui/dist/assets/{Tray-B5bC7AWL.js → Tray-DbTNc_88.js} +1 -1
- package/gui/dist/assets/{index-B5bsvxor.js → index-TmaHa54u.js} +15 -15
- package/gui/dist/assets/quota-summary-B16x5DZZ.js +1 -0
- package/gui/dist/index.html +1 -1
- package/package.json +2 -2
- package/src/adapters/anthropic/cache-retention.ts +117 -0
- package/src/adapters/anthropic/passthrough.ts +7 -4
- package/src/adapters/anthropic.ts +10 -3
- package/src/adapters/base.ts +4 -0
- package/src/adapters/claude-cli/adapter.ts +13 -7
- package/src/adapters/claude-cli/usage-admission.ts +14 -2
- package/src/adapters/coding-agent/protocol.ts +6 -0
- package/src/adapters/coding-agent/turn.ts +14 -4
- package/src/adapters/cursor/protobuf-request.ts +3 -0
- package/src/adapters/google-http.ts +3 -2
- package/src/adapters/google-progress-history.ts +19 -1
- package/src/adapters/google-vertex-endpoint.ts +67 -0
- package/src/adapters/google.ts +62 -37
- package/src/adapters/image.ts +5 -3
- package/src/adapters/input-media-guard.ts +5 -2
- package/src/adapters/kiro-refusal.ts +41 -1
- package/src/adapters/ollama-native.ts +1 -0
- package/src/adapters/openai-chat/messages.ts +34 -4
- package/src/adapters/openai-chat/passthrough.ts +37 -4
- package/src/adapters/openai-chat/reasoning-replay.ts +98 -13
- package/src/adapters/openai-chat/serialized-tool-call-content.ts +95 -134
- package/src/adapters/openai-chat/serialized-tool-call-markup.ts +108 -0
- package/src/adapters/openai-chat/standalone-bare-tool-calls.ts +136 -0
- package/src/adapters/openai-chat.ts +7 -6
- package/src/adapters/openai-responses/compaction-search-history.ts +27 -0
- package/src/adapters/openai-responses/opengateway-input.ts +139 -0
- package/src/adapters/openai-responses/passthrough.ts +22 -13
- package/src/adapters/openai-responses/reasoning.ts +68 -1
- package/src/adapters/openai-responses/transport.ts +13 -1
- package/src/adapters/registry.ts +1 -1
- package/src/adapters/upstream-http-error.ts +3 -2
- package/src/adapters/zed.ts +7 -4
- package/src/bridge/internal.ts +11 -1
- package/src/bridge/response-json.ts +2 -2
- package/src/bridge/sse.ts +10 -4
- package/src/claude/desktop-3p-library.ts +43 -3
- package/src/claude/desktop-3p.ts +20 -10
- package/src/claude/desktop-picker-profile.ts +18 -9
- package/src/claude/desktop-remote-store-io.ts +2 -2
- package/src/claude/inbound.ts +12 -26
- package/src/claude/outbound.ts +5 -0
- package/src/cli/account-api.ts +13 -1
- package/src/cli/account-extended.ts +17 -5
- package/src/cli/account-key-quota.ts +4 -1
- package/src/cli/account.ts +1 -1
- package/src/cli/capabilities-agents-routing.ts +45 -8
- package/src/cli/capabilities-base.ts +5 -5
- package/src/cli/capabilities-provider-models.ts +5 -4
- package/src/cli/combo-result.ts +8 -2
- package/src/cli/combo.ts +17 -0
- package/src/cli/ensure-desktop-cold-start.ts +2 -2
- package/src/cli/help.ts +1 -1
- package/src/cli/message-command.ts +4 -1
- package/src/cli/message-remote-args.ts +69 -0
- package/src/cli/message-remote-command.ts +77 -0
- package/src/cli/provider-runtime.ts +69 -1
- package/src/cli/registry.ts +3 -3
- package/src/codex/account-lifecycle.ts +19 -6
- package/src/codex/account-pause.ts +6 -1
- package/src/codex/account-runtime-state.ts +5 -0
- package/src/codex/account-store.ts +55 -9
- package/src/codex/account-usability.ts +4 -1
- package/src/codex/auth-api/account-list.ts +13 -5
- package/src/codex/auth-api/account-pause-group.ts +2 -22
- package/src/codex/auth-api/login-collision.ts +59 -0
- package/src/codex/auth-api/login-flow.ts +12 -7
- package/src/codex/auth-api/main-account-probe.ts +3 -2
- package/src/codex/auth-api/pool-mode-gate.ts +81 -13
- package/src/codex/auth-collision.ts +31 -4
- package/src/codex/auth-context.ts +57 -10
- package/src/codex/catalog/aggregation.ts +15 -0
- package/src/codex/catalog/auto-review.ts +2 -1
- package/src/codex/catalog/build-entries.ts +17 -7
- package/src/codex/catalog/derive-entry.ts +3 -1
- package/src/codex/catalog/metadata.ts +2 -0
- package/src/codex/catalog/model-visibility.ts +0 -2
- package/src/codex/catalog/ownership.ts +71 -0
- package/src/codex/catalog/parsing.ts +12 -3
- package/src/codex/catalog/retained-sync.ts +2 -1
- package/src/codex/catalog/routed-removal.ts +4 -4
- package/src/codex/cli-update-plan.ts +18 -3
- package/src/codex/collision-identity.ts +78 -0
- package/src/codex/convergence.ts +2 -1
- package/src/codex/low-quota-desktop-notifier.ts +125 -0
- package/src/codex/low-quota-protection.ts +57 -31
- package/src/codex/main-account-cache.ts +77 -3
- package/src/codex/main-account-hard-lock.ts +14 -0
- package/src/codex/main-account.ts +6 -2
- package/src/codex/native-residue.ts +2 -3
- package/src/codex/quota.ts +26 -2
- package/src/codex/routing/cooldown-math.ts +5 -0
- package/src/codex/routing/failure-window.ts +61 -9
- package/src/codex/routing.ts +4 -3
- package/src/codex/shim-diagnostics.ts +2 -1
- package/src/codex/shim-fingerprint.ts +11 -2
- package/src/codex/shim-templates.ts +16 -5
- package/src/codex/shim-windows-refresh.ts +224 -0
- package/src/codex/shim.ts +45 -21
- package/src/combos/failover.ts +10 -2
- package/src/combos/jev-model-backend.ts +30 -7
- package/src/combos/jev-model-request.ts +20 -0
- package/src/combos/jev-quota-activation.ts +153 -0
- package/src/combos/jev-quota-config.ts +62 -0
- package/src/combos/jev-quota-route.ts +26 -0
- package/src/combos/jev-quota.ts +88 -0
- package/src/combos/jev.ts +25 -12
- package/src/combos/types.ts +27 -0
- package/src/compatibility/index.ts +3 -0
- package/src/compatibility/openai-responses.ts +8 -1
- package/src/compatibility/opengateway-responses.ts +61 -0
- package/src/config/account-auto-pause-revisions.ts +136 -0
- package/src/config/diagnostics.ts +13 -0
- package/src/config/live-reconcile.ts +9 -0
- package/src/config/persist-unlocked.ts +10 -1
- package/src/config/provider-validation.ts +7 -2
- package/src/config/removed-opencode-free.ts +135 -0
- package/src/config/schema/account-auto-pause.ts +14 -0
- package/src/config/schema/config-schema.ts +15 -0
- package/src/config/schema/leaf-validators.ts +7 -0
- package/src/config.ts +10 -3
- package/src/generated/compatibility-version.json +476 -216
- package/src/generated/model-metadata.ts +1 -1
- package/src/images/loop.ts +34 -11
- package/src/lib/account-auto-pause-response.ts +16 -0
- package/src/lib/bun-runtime.ts +18 -1
- package/src/lib/bun-transpiler-cache-scope.d.mts +8 -0
- package/src/lib/bun-transpiler-cache-scope.mjs +82 -0
- package/src/lib/decision-quota-sink.ts +76 -0
- package/src/lib/provider-admission-wait.ts +14 -0
- package/src/lib/service-secrets.ts +37 -2
- package/src/lib/state-store-registrations.ts +25 -0
- package/src/lib/translator-budget.ts +1 -1
- package/src/lib/upstream-retry.ts +15 -0
- package/src/link/ssh-argv.ts +3 -0
- package/src/messaging/envelope.ts +6 -3
- package/src/messaging/remote-auth.ts +83 -0
- package/src/messaging/remote-bridge.ts +270 -0
- package/src/messaging/remote-contract.ts +108 -0
- package/src/messaging/remote-enrollment-recovery.ts +59 -0
- package/src/messaging/remote-enrollment.ts +244 -0
- package/src/messaging/remote-envelope.ts +40 -0
- package/src/messaging/remote-files.ts +117 -0
- package/src/messaging/remote-owner.ts +169 -0
- package/src/messaging/remote-ports.ts +56 -0
- package/src/messaging/remote-process.ts +131 -0
- package/src/messaging/remote-replies.ts +98 -0
- package/src/messaging/remote-rpc-admission.ts +32 -0
- package/src/messaging/remote-send.ts +72 -0
- package/src/messaging/remote-store.ts +111 -0
- package/src/messaging/remote-tunnels.ts +65 -0
- package/src/messaging/rpc.ts +13 -1
- package/src/oauth/account-auto-pause-policy.ts +66 -0
- package/src/oauth/account-auto-pause.ts +171 -0
- package/src/oauth/account-import/google-antigravity-adapter.ts +38 -2
- package/src/oauth/account-import/parser.ts +15 -2
- package/src/oauth/account-import/types.ts +7 -0
- package/src/oauth/account-quota-rank.ts +2 -1
- package/src/oauth/anthropic-account-refusal.ts +6 -0
- package/src/oauth/anthropic-model-family.ts +8 -0
- package/src/oauth/anthropic-model-quota.ts +2 -6
- package/src/oauth/api-key-pool-preserve.ts +37 -0
- package/src/oauth/chatgpt-shared.ts +10 -1
- package/src/oauth/dsh.ts +45 -2
- package/src/oauth/index.ts +5 -35
- package/src/oauth/kiro.ts +44 -1
- package/src/oauth/login-flow-state.ts +11 -2
- package/src/oauth/store.ts +35 -8
- package/src/oauth/types.ts +2 -0
- package/src/plugins/upstream-hooks.ts +14 -1
- package/src/providers/api-key-pause.ts +32 -0
- package/src/providers/api-key-resolve.ts +10 -4
- package/src/providers/api-key-selection.ts +6 -1
- package/src/providers/api-keys.ts +8 -4
- package/src/providers/claude-cli-usage.ts +25 -5
- package/src/providers/initial-model-selection.ts +2 -2
- package/src/providers/key-failover.ts +21 -13
- package/src/providers/key-store.ts +8 -3
- package/src/providers/mistral-transport.ts +207 -0
- package/src/providers/model-rename-fields.ts +1 -0
- package/src/providers/openai-sidecar.ts +5 -0
- package/src/providers/opencode-zen-rate-limit.ts +6 -72
- package/src/providers/quota/account-cache.ts +15 -1
- package/src/providers/quota/anthropic-account-quota.ts +20 -7
- package/src/providers/quota/anthropic-cooldown-recovery.ts +22 -4
- package/src/providers/quota/anthropic-decision-windows.ts +47 -0
- package/src/providers/quota/antigravity.ts +45 -8
- package/src/providers/quota/failure-codes.ts +18 -0
- package/src/providers/quota/report-cache.ts +14 -0
- package/src/providers/quota/vendor-probes-oauth.ts +15 -6
- package/src/providers/quota-decision-publication.ts +77 -0
- package/src/providers/quota-decision-snapshot.ts +145 -0
- package/src/providers/quota-routing-cache.ts +4 -1
- package/src/providers/quota-types.ts +6 -1
- package/src/providers/quota.ts +8 -3
- package/src/providers/registry/entries-extended.ts +14 -53
- package/src/providers/registry/model-seeds.ts +1 -1
- package/src/providers/registry.ts +1 -2
- package/src/providers/routed-config.ts +1 -1
- package/src/providers/stale-vision-classification-migration.ts +1 -1
- package/src/providers/xai-transport.ts +2 -1
- package/src/responses/audio.ts +46 -0
- package/src/responses/input-media.ts +37 -15
- package/src/responses/parser-content.ts +22 -27
- package/src/responses/parser.ts +18 -10
- package/src/responses/reasoning-retention.ts +218 -11
- package/src/responses/schema.ts +3 -4
- package/src/responses/web-search-replay.ts +147 -0
- package/src/routing/compatibility/behavior.ts +4 -1
- package/src/server/auth-cors.ts +7 -0
- package/src/server/background-lifecycle.ts +2 -1
- package/src/server/chat-native.ts +36 -20
- package/src/server/claude-messages.ts +2 -1
- package/src/server/index/optional-listeners.ts +6 -0
- package/src/server/index/serve-options.ts +8 -2
- package/src/server/index/websocket-handler.ts +10 -0
- package/src/server/lifecycle.ts +17 -1
- package/src/server/management/combo-routes.ts +17 -2
- package/src/server/management/oauth-account-routes.ts +12 -2
- package/src/server/management/provider-completion-test.ts +289 -0
- package/src/server/management/provider-routes.ts +9 -1
- package/src/server/management/route-registry.ts +2 -0
- package/src/server/messages-native.ts +29 -4
- package/src/server/messages-shared.ts +4 -1
- package/src/server/request-log.ts +23 -9
- package/src/server/responses/account-auto-pause-recovery.ts +26 -0
- package/src/server/responses/adapter-continuation.ts +2 -2
- package/src/server/responses/adapter-dispatch.ts +79 -30
- package/src/server/responses/codex-ws-exchange.ts +3 -0
- package/src/server/responses/codex-ws-metadata.ts +44 -0
- package/src/server/responses/codex-ws-session.ts +21 -1
- package/src/server/responses/combo-session-recall.ts +25 -0
- package/src/server/responses/combo-stream-preflight.ts +10 -3
- package/src/server/responses/combo-tool-loop-pin.ts +70 -0
- package/src/server/responses/compact.ts +10 -5
- package/src/server/responses/context-overflow.ts +141 -2
- package/src/server/responses/core-auth.ts +20 -0
- package/src/server/responses/core-codex-account.ts +28 -15
- package/src/server/responses/core-combo.ts +34 -5
- package/src/server/responses/core-opaque-recovery.ts +20 -11
- package/src/server/responses/core.ts +12 -9
- package/src/server/responses/deepseek-image-files.ts +121 -0
- package/src/server/responses/empty-completion-guard.ts +10 -0
- package/src/server/responses/fetch-helpers.ts +65 -14
- package/src/server/responses/input-admission.ts +6 -2
- package/src/server/responses/jev-model-invoke.ts +13 -14
- package/src/server/responses/main-hard-lock-routing.ts +33 -8
- package/src/server/responses/native-steering-log.ts +2 -2
- package/src/server/responses/opengateway-completion-guard.ts +232 -0
- package/src/server/responses/passthrough-completion.ts +41 -0
- package/src/server/responses/passthrough-delivery.ts +5 -2
- package/src/server/responses/passthrough-dispatch.ts +42 -45
- package/src/server/responses/passthrough-key-rotation.ts +40 -0
- package/src/server/responses/passthrough-observations.ts +38 -0
- package/src/server/responses/reasoning-archive-error.ts +21 -0
- package/src/server/responses/request-compression.ts +98 -0
- package/src/server/responses/request-prepare.ts +11 -1
- package/src/server/responses/request-transport.ts +92 -5
- package/src/server/responses/run-turn-execution.ts +2 -0
- package/src/server/responses/sidecar-execution.ts +16 -3
- package/src/server/responses/terminal-guard.ts +2 -0
- package/src/server/startup-action-control.ts +2 -2
- package/src/server/startup-health-cache.ts +2 -2
- package/src/server/windows-tray-control.ts +2 -2
- package/src/service/definition.ts +9 -1
- package/src/service/orchestration.ts +17 -0
- package/src/types/account-auto-pause.ts +15 -0
- package/src/types/config.ts +23 -1
- package/src/types/provider.ts +11 -5
- package/src/types/request.ts +36 -3
- package/src/types.ts +2 -0
- package/src/usage/cost.ts +17 -1
- package/src/usage/expected-prices.ts +3 -1
- package/src/usage/jev-stats.ts +4 -0
- package/src/usage/log.ts +2 -0
- package/src/usage/summary.ts +7 -0
- package/src/web-search/loop.ts +39 -13
- package/src/web-search/progress-stream.ts +4 -1
- package/src/web-search/run-turn-loop.ts +9 -3
- package/src/web-search/sidecar-search-replay.ts +96 -0
- package/gui/dist/assets/App-CHC89EXQ.js +0 -58
- package/gui/dist/assets/App-wT3mXqFz.css +0 -1
- package/gui/dist/assets/quota-summary-D8y2wTsB.js +0 -1
|
@@ -4,14 +4,21 @@
|
|
|
4
4
|
* Provider ciphertext and signatures are never included in the portable retained items.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
+
import { Database } from "bun:sqlite";
|
|
7
8
|
import { expandRetainedCompactionInput } from "./retained-compaction";
|
|
8
|
-
import { closeSync, mkdirSync, openSync, rmSync, writeFileSync } from "node:fs";
|
|
9
|
-
import { join } from "node:path";
|
|
9
|
+
import { chmodSync, closeSync, lstatSync, mkdirSync, openSync, opendirSync, rmSync, writeFileSync } from "node:fs";
|
|
10
|
+
import { basename, dirname, join } from "node:path";
|
|
11
|
+
import { getConfigDir } from "../config/paths";
|
|
12
|
+
import { recordOwnedConfigPath } from "../lib/config-ownership";
|
|
13
|
+
import { assertNotRealHomeUnderTest } from "../lib/test-home-guard";
|
|
10
14
|
import { estimateTokens } from "../lib/token-estimate";
|
|
11
15
|
import { decodeReasoningEnvelope } from "./reasoning-envelope";
|
|
12
16
|
|
|
13
17
|
export const DEFAULT_REASONING_RETENTION_PERCENT = 20;
|
|
14
18
|
export const DEFAULT_MAX_REASONING_TOKENS = 100_000;
|
|
19
|
+
/** Compiled-in aggregate ceiling for the on-disk reasoning archive directory. */
|
|
20
|
+
export const DEFAULT_ARCHIVE_MAX_BYTES = 512 * 1024 * 1024;
|
|
21
|
+
export const DEFAULT_ARCHIVE_MAX_FILES = 256;
|
|
15
22
|
|
|
16
23
|
export interface ReasoningRetentionOptions {
|
|
17
24
|
/** Directory that receives archived reasoning files. */
|
|
@@ -24,6 +31,10 @@ export interface ReasoningRetentionOptions {
|
|
|
24
31
|
maxTokens?: number;
|
|
25
32
|
/** Clock injection for tests. */
|
|
26
33
|
now?: number;
|
|
34
|
+
/** Aggregate archive byte ceiling. Defaults to 512 MiB; smaller values are a test seam. */
|
|
35
|
+
archiveMaxBytes?: number;
|
|
36
|
+
/** Aggregate archive file-count ceiling. Defaults to 256; smaller values are a test seam. */
|
|
37
|
+
archiveMaxFiles?: number;
|
|
27
38
|
}
|
|
28
39
|
|
|
29
40
|
export interface ReasoningArchive {
|
|
@@ -100,6 +111,183 @@ function retentionTokenCap(value: number | undefined): number {
|
|
|
100
111
|
: DEFAULT_MAX_REASONING_TOKENS;
|
|
101
112
|
}
|
|
102
113
|
|
|
114
|
+
/** Aggregate-capacity refusal: the archive directory is at or over its compiled-in ceiling. */
|
|
115
|
+
export class ReasoningArchiveQuotaError extends Error {
|
|
116
|
+
constructor(message: string, options?: { cause?: unknown }) {
|
|
117
|
+
super(message, options);
|
|
118
|
+
this.name = "ReasoningArchiveQuotaError";
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** The sibling archive lock is held by another cooperating writer. */
|
|
123
|
+
export class ReasoningArchiveBusyError extends Error {
|
|
124
|
+
constructor(message: string, options?: { cause?: unknown }) {
|
|
125
|
+
super(message, options);
|
|
126
|
+
this.name = "ReasoningArchiveBusyError";
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** Unsafe or uninspectable archive state; the attempt is refused without touching anything. */
|
|
131
|
+
export class ReasoningArchiveError extends Error {
|
|
132
|
+
constructor(message: string, options?: { cause?: unknown }) {
|
|
133
|
+
super(message, options);
|
|
134
|
+
this.name = "ReasoningArchiveError";
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const ARCHIVE_SEPARATOR = "\n\n---\n\n";
|
|
139
|
+
const ARCHIVE_SEPARATOR_BYTES = Buffer.byteLength(ARCHIVE_SEPARATOR, "utf8");
|
|
140
|
+
const ARCHIVE_LOCK_SIDECARS = ["-journal", "-wal", "-shm"] as const;
|
|
141
|
+
|
|
142
|
+
function archiveCap(value: number | undefined, fallback: number): number {
|
|
143
|
+
return typeof value === "number" && Number.isSafeInteger(value) && value > 0 ? value : fallback;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/** lstat that treats a vanished path as absent and refuses anything it cannot classify. */
|
|
147
|
+
function archiveLstat(path: string): ReturnType<typeof lstatSync> | undefined {
|
|
148
|
+
try {
|
|
149
|
+
return lstatSync(path);
|
|
150
|
+
} catch (error) {
|
|
151
|
+
if ((error as NodeJS.ErrnoException).code === "ENOENT") return undefined;
|
|
152
|
+
throw new ReasoningArchiveError(`reasoning archive path is not inspectable: ${path}`, { cause: error });
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function assertRegularArchiveDirectory(archiveDir: string): void {
|
|
157
|
+
const entry = archiveLstat(archiveDir);
|
|
158
|
+
if (!entry?.isDirectory() || entry.isSymbolicLink()) {
|
|
159
|
+
throw new ReasoningArchiveError(`reasoning archive directory is not a regular directory: ${archiveDir}`);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function assertRegularArchiveLockPath(lockPath: string): void {
|
|
164
|
+
const entry = archiveLstat(lockPath);
|
|
165
|
+
if (entry !== undefined && (!entry.isFile() || entry.isSymbolicLink())) {
|
|
166
|
+
throw new ReasoningArchiveError(`reasoning archive lock path is not a regular file: ${lockPath}`);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/** Exact UTF-8 byte total of the would-be archive payload, computed before any joined string exists. */
|
|
171
|
+
function archivePayloadBytes(texts: string[]): number {
|
|
172
|
+
let bytes = texts.length > 0 ? (texts.length - 1) * ARCHIVE_SEPARATOR_BYTES : 0;
|
|
173
|
+
for (const text of texts) {
|
|
174
|
+
const next = bytes + Buffer.byteLength(text, "utf8");
|
|
175
|
+
if (!Number.isSafeInteger(next)) {
|
|
176
|
+
throw new ReasoningArchiveError("reasoning archive payload byte size is not representable");
|
|
177
|
+
}
|
|
178
|
+
bytes = next;
|
|
179
|
+
}
|
|
180
|
+
return bytes;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
interface ArchiveUsage {
|
|
184
|
+
files: number;
|
|
185
|
+
bytes: number;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Non-recursive accounting of every committed, remnant and foreign file. Anything that is not a
|
|
190
|
+
* plain regular file refuses closed; an entry vanishing mid-scan is already gone and skipped.
|
|
191
|
+
*/
|
|
192
|
+
function scanArchiveUsage(archiveDir: string, maxBytes: number, maxFiles: number): ArchiveUsage {
|
|
193
|
+
let directory: ReturnType<typeof opendirSync> | undefined;
|
|
194
|
+
let files = 0;
|
|
195
|
+
let bytes = 0;
|
|
196
|
+
let inspected = 0;
|
|
197
|
+
let failure: ReasoningArchiveError | undefined;
|
|
198
|
+
try {
|
|
199
|
+
// One entry per read: a legacy over-capacity directory must not first become an array
|
|
200
|
+
// or an unbounded read-ahead buffer on this synchronous request path.
|
|
201
|
+
directory = opendirSync(archiveDir, { bufferSize: 1 });
|
|
202
|
+
let entry;
|
|
203
|
+
while ((entry = directory.readSync()) !== null) {
|
|
204
|
+
inspected += 1;
|
|
205
|
+
if (!entry.isFile()) {
|
|
206
|
+
throw new ReasoningArchiveError(`reasoning archive entry is not a regular file: ${entry.name}`);
|
|
207
|
+
}
|
|
208
|
+
const stat = archiveLstat(join(archiveDir, entry.name));
|
|
209
|
+
if (stat === undefined) {
|
|
210
|
+
if (inspected >= maxFiles) throw new ReasoningArchiveError("reasoning archive scan changed beyond its inspection limit");
|
|
211
|
+
continue;
|
|
212
|
+
}
|
|
213
|
+
if (!stat.isFile() || stat.isSymbolicLink()) {
|
|
214
|
+
throw new ReasoningArchiveError(`reasoning archive entry is not a regular file: ${entry.name}`);
|
|
215
|
+
}
|
|
216
|
+
files += 1;
|
|
217
|
+
bytes += Number(stat.size);
|
|
218
|
+
if (files >= maxFiles || bytes > maxBytes) break; // refusal is already certain
|
|
219
|
+
if (inspected >= maxFiles) throw new ReasoningArchiveError("reasoning archive scan changed beyond its inspection limit");
|
|
220
|
+
}
|
|
221
|
+
} catch (error) {
|
|
222
|
+
failure = error instanceof ReasoningArchiveError ? error
|
|
223
|
+
: new ReasoningArchiveError("reasoning archive directory cannot be scanned", { cause: error });
|
|
224
|
+
} finally {
|
|
225
|
+
try { directory?.closeSync(); } catch (error) {
|
|
226
|
+
failure ??= new ReasoningArchiveError("reasoning archive directory could not be closed", { cause: error });
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
if (failure !== undefined) throw failure;
|
|
230
|
+
return { files, bytes };
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function isSqliteBusy(error: unknown): boolean {
|
|
234
|
+
const code = error && typeof error === "object" && "code" in error
|
|
235
|
+
? String((error as { code?: unknown }).code)
|
|
236
|
+
: "";
|
|
237
|
+
const message = error instanceof Error ? error.message : "";
|
|
238
|
+
return code === "SQLITE_BUSY" || code === "SQLITE_LOCKED" || /database (?:is|table is) locked/i.test(message);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Serialize usage scan, exclusive create, write and close for one physical archive directory
|
|
243
|
+
* across cooperating processes. The sibling SQLite write transaction is the lock: the OS
|
|
244
|
+
* releases it on process exit, so no stale-owner reclamation is needed. busy_timeout=0 keeps
|
|
245
|
+
* the synchronous request path fail-fast instead of freezing the event loop.
|
|
246
|
+
*/
|
|
247
|
+
function withArchiveQuotaLockSync<T>(archiveDir: string, fn: () => T): T {
|
|
248
|
+
const normalized = archiveDir.replace(/[/\\]+$/, "");
|
|
249
|
+
const base = basename(normalized);
|
|
250
|
+
if (!base || base === "." || base === "..") {
|
|
251
|
+
throw new ReasoningArchiveError(`reasoning archive directory is not a usable path: ${archiveDir}`);
|
|
252
|
+
}
|
|
253
|
+
const parent = dirname(normalized);
|
|
254
|
+
assertNotRealHomeUnderTest(parent);
|
|
255
|
+
const lockPath = `${normalized}.lock`;
|
|
256
|
+
// The lock lives beside the archive inside the config home; foreign parents need no manifest entry.
|
|
257
|
+
if (parent === getConfigDir()) {
|
|
258
|
+
recordOwnedConfigPath(parent, lockPath);
|
|
259
|
+
for (const suffix of ARCHIVE_LOCK_SIDECARS) recordOwnedConfigPath(parent, `${lockPath}${suffix}`);
|
|
260
|
+
}
|
|
261
|
+
assertRegularArchiveLockPath(lockPath);
|
|
262
|
+
let database: Database;
|
|
263
|
+
try {
|
|
264
|
+
database = new Database(lockPath, { create: true });
|
|
265
|
+
} catch (error) {
|
|
266
|
+
throw new ReasoningArchiveError(`reasoning archive lock could not be opened: ${lockPath}`, { cause: error });
|
|
267
|
+
}
|
|
268
|
+
try {
|
|
269
|
+
try { chmodSync(lockPath, 0o600); } catch { /* platform may ignore chmod */ }
|
|
270
|
+
assertRegularArchiveLockPath(lockPath);
|
|
271
|
+
database.exec("PRAGMA locking_mode = NORMAL; PRAGMA busy_timeout = 0; BEGIN IMMEDIATE");
|
|
272
|
+
} catch (error) {
|
|
273
|
+
try { database.close(); } catch { /* acquisition already failed */ }
|
|
274
|
+
if (error instanceof ReasoningArchiveError) throw error;
|
|
275
|
+
if (isSqliteBusy(error)) {
|
|
276
|
+
throw new ReasoningArchiveBusyError(
|
|
277
|
+
`reasoning archive lock is held by another writer: ${lockPath}`,
|
|
278
|
+
{ cause: error },
|
|
279
|
+
);
|
|
280
|
+
}
|
|
281
|
+
throw new ReasoningArchiveError(`reasoning archive lock could not be acquired: ${lockPath}`, { cause: error });
|
|
282
|
+
}
|
|
283
|
+
try {
|
|
284
|
+
return fn();
|
|
285
|
+
} finally {
|
|
286
|
+
try { database.exec("ROLLBACK"); } catch { /* close below still releases the OS lock */ }
|
|
287
|
+
try { database.close(); } catch { /* the OS lock is released with the handle */ }
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
103
291
|
/** Separate reasoning locally before dispatch; unknown windows enforce the absolute cap. */
|
|
104
292
|
export function applyReasoningRetention(
|
|
105
293
|
input: unknown,
|
|
@@ -130,17 +318,36 @@ export function applyReasoningRetention(
|
|
|
130
318
|
content: [{ type: "input_text", text }] })),
|
|
131
319
|
};
|
|
132
320
|
|
|
133
|
-
|
|
321
|
+
const archiveBytes = archiveCap(options.archiveMaxBytes, DEFAULT_ARCHIVE_MAX_BYTES);
|
|
322
|
+
const archiveFiles = archiveCap(options.archiveMaxFiles, DEFAULT_ARCHIVE_MAX_FILES);
|
|
323
|
+
const incomingBytes = archivePayloadBytes(texts);
|
|
134
324
|
const stamp = new Date(options.now ?? Date.now()).toISOString().replace(/[:.]/g, "-");
|
|
135
325
|
const path = join(options.archiveDir, "reasoning-" + stamp + "-" + Math.random().toString(36).slice(2, 8) + ".md");
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
326
|
+
try {
|
|
327
|
+
mkdirSync(options.archiveDir, { recursive: true, mode: 0o700 });
|
|
328
|
+
assertRegularArchiveDirectory(options.archiveDir);
|
|
329
|
+
withArchiveQuotaLockSync(options.archiveDir, () => {
|
|
330
|
+
assertRegularArchiveDirectory(options.archiveDir);
|
|
331
|
+
const usage = scanArchiveUsage(options.archiveDir, archiveBytes, archiveFiles);
|
|
332
|
+
if (usage.files >= archiveFiles || incomingBytes > archiveBytes - usage.bytes) {
|
|
333
|
+
throw new ReasoningArchiveQuotaError(
|
|
334
|
+
`reasoning archive is at capacity: ${usage.files} file(s), ${usage.bytes} byte(s) under ${options.archiveDir}; delete older archives to archive more`,
|
|
335
|
+
);
|
|
336
|
+
}
|
|
337
|
+
// Only a successful exclusive open grants ownership; an occupied path is never removed.
|
|
338
|
+
const fd = openSync(path, "wx", 0o600);
|
|
339
|
+
let failure: unknown;
|
|
340
|
+
try { writeFileSync(fd, texts.join(ARCHIVE_SEPARATOR), "utf-8"); } catch (error) { failure = error; }
|
|
341
|
+
try { closeSync(fd); } catch (error) { failure ??= error; }
|
|
342
|
+
if (failure !== undefined) {
|
|
343
|
+
try { rmSync(path); } catch { /* preserve the original I/O error; cleanup is best-effort */ }
|
|
344
|
+
throw failure;
|
|
345
|
+
}
|
|
346
|
+
});
|
|
347
|
+
} catch (error) {
|
|
348
|
+
if (error instanceof ReasoningArchiveQuotaError || error instanceof ReasoningArchiveBusyError
|
|
349
|
+
|| error instanceof ReasoningArchiveError) throw error;
|
|
350
|
+
throw new ReasoningArchiveError("reasoning archive could not be written", { cause: error });
|
|
144
351
|
}
|
|
145
352
|
|
|
146
353
|
const limitDescription = windowKnown
|
package/src/responses/schema.ts
CHANGED
|
@@ -27,10 +27,9 @@ const inputFileBlockSchema = z.object({
|
|
|
27
27
|
file_data: z.string().optional(),
|
|
28
28
|
});
|
|
29
29
|
// codex-rs protocol/src/models.rs sends audio as input_audio with an audio_url, in
|
|
30
|
-
// both user content and tool output.
|
|
31
|
-
//
|
|
32
|
-
//
|
|
33
|
-
// signal reaching final adapter dispatch remains a recorded residual.
|
|
30
|
+
// both user content and tool output. The translated IR preserves supported inline
|
|
31
|
+
// WAV/MP3 bytes and the Chat adapter sends them as input_audio; other wires and
|
|
32
|
+
// unsupported formats are refused at the final translated-adapter boundary.
|
|
34
33
|
const inputAudioBlockSchema = z.object({
|
|
35
34
|
type: z.literal("input_audio"),
|
|
36
35
|
audio_url: z.string().min(1),
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Parse-time rebuild of replayed hosted `web_search_call` cells for routed providers (#7269).
|
|
3
|
+
*
|
|
4
|
+
* Codex replays the cell it was shown, which carries the query and at most a source list, never the
|
|
5
|
+
* result text. Dropping the cell made a routed model forget it had searched: after a client tool
|
|
6
|
+
* call ended the turn, the next request carried no trace of the search, so the model searched and
|
|
7
|
+
* ran its tools again in a loop.
|
|
8
|
+
*
|
|
9
|
+
* When search is declared, the parser rewrites each cell into the `web_search` function call and
|
|
10
|
+
* output the routed model made, at the raw-input level, so the ordinary call/output handling owns
|
|
11
|
+
* turn batching and reasoning attachment. The call takes the cell's place; its output is deferred to
|
|
12
|
+
* the end of the contiguous run of call/output items that follows, so a cell inside a parallel
|
|
13
|
+
* client batch never splits it. The output is a fallback built from the cell alone; the web-search
|
|
14
|
+
* sidecar swaps in the result it recorded for the cell, and removes the pairs again when it does not
|
|
15
|
+
* run, because the synthetic tool is then undeclared (src/web-search/sidecar-search-replay.ts).
|
|
16
|
+
*/
|
|
17
|
+
import { isObj } from "./parser-content";
|
|
18
|
+
import { WEB_SEARCH_TOOL_NAME } from "../web-search/synthetic-tool";
|
|
19
|
+
import type { OcxReplayedWebSearchCell } from "../types";
|
|
20
|
+
|
|
21
|
+
const MAX_FIELD_CHARS = 2048;
|
|
22
|
+
const MAX_LIST_ENTRIES = 20;
|
|
23
|
+
const CELL_ID = /^[A-Za-z0-9_-]{1,128}$/;
|
|
24
|
+
|
|
25
|
+
/** Call items of a batch; a call after the batch's first output starts the next assistant turn. */
|
|
26
|
+
const CALL_TYPES = new Set(["function_call", "custom_tool_call", "local_shell_call", "tool_search_call"]);
|
|
27
|
+
/** Output items of a batch. Anything that is neither a call nor an output ends the batch. */
|
|
28
|
+
const OUTPUT_TYPES = new Set(["function_call_output", "custom_tool_call_output", "local_shell_call_output", "tool_search_output"]);
|
|
29
|
+
|
|
30
|
+
function capped(value: string): string {
|
|
31
|
+
return value.length <= MAX_FIELD_CHARS ? value : `${value.slice(0, MAX_FIELD_CHARS)}…`;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function queriesOf(action: Record<string, unknown>): string[] {
|
|
35
|
+
const queries = Array.isArray(action.queries)
|
|
36
|
+
? action.queries.filter((query): query is string => typeof query === "string" && query.trim() !== "")
|
|
37
|
+
: [];
|
|
38
|
+
if (queries.length === 0 && typeof action.query === "string" && action.query.trim() !== "") queries.push(action.query);
|
|
39
|
+
return queries.slice(0, MAX_LIST_ENTRIES).map(capped);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function sourcesOf(item: Record<string, unknown>, action: Record<string, unknown>): string[] {
|
|
43
|
+
const lines: string[] = [];
|
|
44
|
+
const seen = new Set<string>();
|
|
45
|
+
for (const list of [item.sources, action.sources]) {
|
|
46
|
+
if (!Array.isArray(list)) continue;
|
|
47
|
+
for (const source of list) {
|
|
48
|
+
if (lines.length >= MAX_LIST_ENTRIES) return lines;
|
|
49
|
+
if (!isObj(source) || typeof source.url !== "string" || seen.has(source.url)) continue;
|
|
50
|
+
seen.add(source.url);
|
|
51
|
+
lines.push(typeof source.title === "string" && source.title.trim() !== ""
|
|
52
|
+
? `- ${capped(source.title)} <${capped(source.url)}>`
|
|
53
|
+
: `- <${capped(source.url)}>`);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return lines;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** The call arguments and fallback result text for one replayed cell. */
|
|
60
|
+
export function replayedWebSearchContent(item: Record<string, unknown>): { arguments: Record<string, unknown>; result: string; failed: boolean } {
|
|
61
|
+
const action = isObj(item.action) ? item.action : {};
|
|
62
|
+
const failed = item.status === "failed";
|
|
63
|
+
const queries = queriesOf(action);
|
|
64
|
+
const lines: string[] = [];
|
|
65
|
+
if (action.type === "open_page" && typeof action.url === "string") {
|
|
66
|
+
lines.push(`Earlier page open: <${capped(action.url)}>.`);
|
|
67
|
+
} else if (action.type === "find_in_page" && typeof action.url === "string") {
|
|
68
|
+
lines.push(`Earlier find in page <${capped(action.url)}>${typeof action.pattern === "string" ? ` for "${capped(action.pattern)}"` : ""}.`);
|
|
69
|
+
} else {
|
|
70
|
+
lines.push(queries.length > 0 ? `Earlier web search for: ${queries.map(query => `"${query}"`).join(", ")}.` : "Earlier web search.");
|
|
71
|
+
}
|
|
72
|
+
if (failed) {
|
|
73
|
+
lines.push("This search failed and returned no results.");
|
|
74
|
+
} else {
|
|
75
|
+
lines.push("The result text of this search is not available in the replayed conversation history.");
|
|
76
|
+
const sources = sourcesOf(item, action);
|
|
77
|
+
if (sources.length > 0) lines.push("Sources it returned (titles and URLs only, untrusted):", ...sources);
|
|
78
|
+
}
|
|
79
|
+
return { arguments: queries.length > 1 ? { queries } : { query: queries[0] ?? "" }, result: lines.join("\n"), failed };
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export interface RewrittenWebSearchInput {
|
|
83
|
+
items: unknown[];
|
|
84
|
+
/** Original input index per rewritten item, for boundaries measured on the caller's input. */
|
|
85
|
+
sourceIndexes: number[];
|
|
86
|
+
cells: OcxReplayedWebSearchCell[];
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** Rewrite replayed cells into web_search call/output pairs; undefined when the input has none. */
|
|
90
|
+
export function rewriteReplayedWebSearchCells(input: readonly unknown[]): RewrittenWebSearchInput | undefined {
|
|
91
|
+
if (!input.some(item => isObj(item) && item.type === "web_search_call")) return undefined;
|
|
92
|
+
// A synthetic call id must not collide with any client call id, nor with another cell's.
|
|
93
|
+
const taken = new Set(input.flatMap(item => isObj(item) && typeof item.call_id === "string" ? [item.call_id] : []));
|
|
94
|
+
// First suffix each base has not settled yet; taken stays the collision check on every candidate,
|
|
95
|
+
// so seeded ids and ids handed out under another base still block the smallest free suffix.
|
|
96
|
+
const nextSuffix = new Map<string, number>();
|
|
97
|
+
const allocate = (base: string): string => {
|
|
98
|
+
if (!taken.has(base)) {
|
|
99
|
+
taken.add(base);
|
|
100
|
+
return base;
|
|
101
|
+
}
|
|
102
|
+
let n = nextSuffix.get(base) ?? 1;
|
|
103
|
+
let id = `${base}_${n}`;
|
|
104
|
+
while (taken.has(id)) id = `${base}_${++n}`;
|
|
105
|
+
nextSuffix.set(base, n + 1);
|
|
106
|
+
taken.add(id);
|
|
107
|
+
return id;
|
|
108
|
+
};
|
|
109
|
+
const items: unknown[] = [];
|
|
110
|
+
const sourceIndexes: number[] = [];
|
|
111
|
+
const cells: OcxReplayedWebSearchCell[] = [];
|
|
112
|
+
const seenCells = new Set<string>();
|
|
113
|
+
let pending: Array<{ item: unknown; sourceIndex: number }> = [];
|
|
114
|
+
// True once the current batch reached its outputs: the next call opens a new assistant turn.
|
|
115
|
+
let outputPhase = false;
|
|
116
|
+
const flush = () => {
|
|
117
|
+
for (const entry of pending) { items.push(entry.item); sourceIndexes.push(entry.sourceIndex); }
|
|
118
|
+
pending = [];
|
|
119
|
+
outputPhase = false;
|
|
120
|
+
};
|
|
121
|
+
input.forEach((item, sourceIndex) => {
|
|
122
|
+
const type = isObj(item) ? item.type : undefined;
|
|
123
|
+
if (isObj(item) && type === "web_search_call") {
|
|
124
|
+
const cellId = typeof item.id === "string" && CELL_ID.test(item.id) ? item.id : undefined;
|
|
125
|
+
// The same cell replayed twice is one search.
|
|
126
|
+
if (cellId && seenCells.has(cellId)) return;
|
|
127
|
+
if (cellId) seenCells.add(cellId);
|
|
128
|
+
if (outputPhase) flush();
|
|
129
|
+
const content = replayedWebSearchContent(item);
|
|
130
|
+
const callId = allocate(cellId ?? "ws_replay");
|
|
131
|
+
cells.push({ ...(cellId ? { cellId } : {}), callId, failed: content.failed });
|
|
132
|
+
items.push({ type: "function_call", call_id: callId, name: WEB_SEARCH_TOOL_NAME, arguments: JSON.stringify(content.arguments) });
|
|
133
|
+
sourceIndexes.push(sourceIndex);
|
|
134
|
+
pending.push({ item: { type: "function_call_output", call_id: callId, output: content.result }, sourceIndex });
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
if (typeof type === "string" && OUTPUT_TYPES.has(type)) {
|
|
138
|
+
outputPhase = true;
|
|
139
|
+
} else if (!(typeof type === "string" && CALL_TYPES.has(type)) || outputPhase) {
|
|
140
|
+
flush();
|
|
141
|
+
}
|
|
142
|
+
items.push(item);
|
|
143
|
+
sourceIndexes.push(sourceIndex);
|
|
144
|
+
});
|
|
145
|
+
flush();
|
|
146
|
+
return { items, sourceIndexes, cells };
|
|
147
|
+
}
|
|
@@ -4,6 +4,7 @@ import type { OcxConfig, OcxProviderConfig } from "../../types";
|
|
|
4
4
|
import { PROVIDER_REGISTRY } from "../../providers/registry";
|
|
5
5
|
import { fastPolicyForModel, serviceTierSupportFromPolicy } from "../../providers/service-tier";
|
|
6
6
|
import { resolveProviderAuthTransport } from "../../providers/fastwire";
|
|
7
|
+
import { defaultAnthropicCacheRetention } from "../../adapters/anthropic/cache-retention";
|
|
7
8
|
import { localFingerprint } from "../../lab/digest";
|
|
8
9
|
import type { LabBehaviorSource, LabBehaviorValues } from "../../lab/live/types";
|
|
9
10
|
|
|
@@ -237,7 +238,9 @@ export function resolveProductionBehaviorValues(
|
|
|
237
238
|
}),
|
|
238
239
|
"tools.builtinNameEscaping": behaviorRow("provider_config", effective.escapeBuiltinToolNames === true),
|
|
239
240
|
"cache.forwarding": behaviorRow("provider_config", effective.promptCacheKey === true),
|
|
240
|
-
|
|
241
|
+
// The wire TTL, not just the setting: an omitted value means 1h on first-party Anthropic.
|
|
242
|
+
"cache.retention": behaviorRow("global_config", config.cacheRetention
|
|
243
|
+
?? (adapter === "anthropic" ? defaultAnthropicCacheRetention(effective) : "short")),
|
|
241
244
|
"anthropic.eofPolicy": behaviorRow("provider_config", effective.anthropicEofTolerance === true ? "tolerant" : "strict"),
|
|
242
245
|
"openai-chat.eofPolicy": behaviorRow("provider_config", effective.openaiChatEofTolerance === true ? "tolerant" : "strict"),
|
|
243
246
|
"google.mode": behaviorRow("provider_config", effective.googleMode ?? null),
|
package/src/server/auth-cors.ts
CHANGED
|
@@ -40,6 +40,7 @@ import { DECLARABLE_HOSTED_TOOL_TYPES } from "../responses/hosted-tool-policy";
|
|
|
40
40
|
import { effectiveGoogleMode, getProviderRegistryEntry, providerCodexAccountMode, providerMatchesRegistryTransport, registryEntryForProviderDestination } from "../providers/registry";
|
|
41
41
|
import { providerConfigSeed } from "../providers/derive";
|
|
42
42
|
import { anthropicOAuthInstanceConfigError } from "../config/schema/anthropic-account-pool";
|
|
43
|
+
import { accountAutoPausePolicySchema } from "../config/schema/account-auto-pause";
|
|
43
44
|
import type { OcxConfig, OcxProviderConfig } from "../types";
|
|
44
45
|
import { openRouterRoutingConfigError } from "../providers/openrouter-routing";
|
|
45
46
|
import { modelAutoCompactTokenLimitsConfigError } from "../providers/auto-compact-budget";
|
|
@@ -740,6 +741,9 @@ export function providerManagementConfigError(
|
|
|
740
741
|
return "provider must be a plain object";
|
|
741
742
|
}
|
|
742
743
|
const raw = provider as Record<string, unknown>;
|
|
744
|
+
if (raw.accountAutoPause !== undefined && !accountAutoPausePolicySchema.safeParse(raw.accountAutoPause).success) {
|
|
745
|
+
return "accountAutoPause must be an object with optional boolean enabled and integer consecutiveFailures (1-100)";
|
|
746
|
+
}
|
|
743
747
|
const contextTiersError = contextTierRecordConfigError(raw.modelContextTiers);
|
|
744
748
|
if (contextTiersError) return contextTiersError;
|
|
745
749
|
const capabilitiesError = modelCapabilitiesConfigError(raw.modelCapabilities);
|
|
@@ -796,6 +800,8 @@ export function providerManagementConfigError(
|
|
|
796
800
|
// sends recover, not what the canonical forward seed is. Validated below
|
|
797
801
|
// (retryOnResetPolicyConfigError).
|
|
798
802
|
delete canonicalCandidate.retryOnReset;
|
|
803
|
+
// Validated account eligibility policy is independent of the canonical transport.
|
|
804
|
+
delete canonicalCandidate.accountAutoPause;
|
|
799
805
|
// Context windows are the same kind of user-owned overlay as requestPacing: the operator
|
|
800
806
|
// narrowing what their own native rows advertise. They can only ever LOWER the measured
|
|
801
807
|
// window (see nativeOpenAiContextWindow), so admitting them cannot widen what the proxy
|
|
@@ -1087,6 +1093,7 @@ const PROVIDER_CONFIG_FIELD_POLICY = {
|
|
|
1087
1093
|
apiKeyPool: "redacted",
|
|
1088
1094
|
// Ordering preference only; it names no key material, so an editor may read and set it.
|
|
1089
1095
|
apiKeyPoolStrategy: "editor",
|
|
1096
|
+
accountAutoPause: "editor",
|
|
1090
1097
|
apiKeySelectionRevision: "runtime",
|
|
1091
1098
|
_apiKeyAttempt: "runtime",
|
|
1092
1099
|
defaultModel: "editor",
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { OcxConfig, StorageCleanupPolicy } from "../types";
|
|
2
2
|
import { registerCodexLowQuotaProtection, type LowQuotaRegistration } from "../codex/low-quota-protection";
|
|
3
|
+
import { resolveLowQuotaDesktopSink } from "../codex/low-quota-desktop-notifier";
|
|
3
4
|
import type { LowQuotaEvent } from "../codex/low-quota-events";
|
|
4
5
|
import { startStateStoreSweeper } from "../lib/state-store-sweeper";
|
|
5
6
|
import {
|
|
@@ -209,7 +210,7 @@ export function acquireServerBackgroundLifecycle(
|
|
|
209
210
|
fanoutRevoked: false,
|
|
210
211
|
applyPolicy,
|
|
211
212
|
resources: acquireServerResourceOwner(),
|
|
212
|
-
lowQuota: registerCodexLowQuotaProtection(config),
|
|
213
|
+
lowQuota: registerCodexLowQuotaProtection(config, { desktopSink: () => resolveLowQuotaDesktopSink() }),
|
|
213
214
|
};
|
|
214
215
|
try {
|
|
215
216
|
if (!processLoops) {
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { captureAccountAutoPauseAttempt, wasAccountAutoPaused } from "../oauth/account-auto-pause";
|
|
1
2
|
import { createPhysicalSendReporter } from "../lib/request-execution-budget";
|
|
2
3
|
import { createInferenceSendBudget } from "./inference/context";
|
|
3
4
|
import { observeAdvisorNativeReasoning } from "./inference/advisor-diagnostics";
|
|
@@ -30,7 +31,7 @@ import {
|
|
|
30
31
|
applyUpstreamRecoveryInit,
|
|
31
32
|
fetchWithResetRetry,
|
|
32
33
|
fetchWithTransientRetry,
|
|
33
|
-
isNonReplayableResponse,
|
|
34
|
+
isNonReplayableResponse, isSameTargetRetryHold,
|
|
34
35
|
isReplayRefusalCode,
|
|
35
36
|
isReplayRefusalResponse,
|
|
36
37
|
prepareSameTarget429Wait,
|
|
@@ -44,10 +45,7 @@ import {
|
|
|
44
45
|
UPSTREAM_RESET_REPLAY_REFUSED_CODE,
|
|
45
46
|
wrapWithZeroOutputRefetch,
|
|
46
47
|
} from "../lib/upstream-retry";
|
|
47
|
-
import {
|
|
48
|
-
isTranslatorBudgetExceededError,
|
|
49
|
-
type TranslatorBudget,
|
|
50
|
-
} from "../lib/translator-budget";
|
|
48
|
+
import { isTranslatorBudgetExceededError, type TranslatorBudget } from "../lib/translator-budget";
|
|
51
49
|
import { authorizeResendForRecovery } from "../lib/request-resend-gate";
|
|
52
50
|
import { ambiguousResendAllowanceFor, selfContainedChatBody } from "./responses/reset-replay";
|
|
53
51
|
import {
|
|
@@ -64,11 +62,10 @@ import { RequestPacingQueueOverloadError } from "../providers/request-pacing";
|
|
|
64
62
|
import { stampApiKeyAccountLabel } from "../providers/label";
|
|
65
63
|
import { distributedKeyPhysicalFetch, distributedKeyResponseOwner } from "../providers/api-key-response-ownership";
|
|
66
64
|
import { providerApiKeySelectionIsCurrent, resolveCurrentProviderApiKeyTransport } from "../providers/api-key-selection";
|
|
67
|
-
import { enrichOpenCodeZenFreeTierMessage } from "../providers/opencode-zen-rate-limit";
|
|
68
65
|
import type { OcxProviderTransport } from "../providers/xai-transport";
|
|
69
66
|
import type { RouteResult } from "../router";
|
|
70
67
|
import type { OcxConfig, OcxProviderConfig } from "../types";
|
|
71
|
-
import { fetchWithHeaderTimeout, providerFetch, safeHostLabel, sendWithConnectionPolicy } from "./responses/fetch-helpers";
|
|
68
|
+
import { fetchWithHeaderTimeout, physicalSendObserverInit, providerFetch, safeHostLabel, sendWithConnectionPolicy } from "./responses/fetch-helpers";
|
|
72
69
|
import { isLocalUpstream } from "../lib/local-upstream";
|
|
73
70
|
import { linkAbortSignal } from "./responses";
|
|
74
71
|
import {
|
|
@@ -88,6 +85,7 @@ import { workflowRefusalResponse } from "./workflow-refusal";
|
|
|
88
85
|
import type { ComboProtocolSource } from "./responses/core-combo-native";
|
|
89
86
|
import type { ProtocolEnvelope } from "../protocols/envelope";
|
|
90
87
|
import { nativeChatReplayScope, bindNativeChatReplayScope, nativeChatReasoningCapture } from "./chat-native-reasoning";
|
|
88
|
+
import { resolveMistralTransport } from "../providers/mistral-transport";
|
|
91
89
|
|
|
92
90
|
export { isNativeChatRouteEligible, nativeChatDeclineReason } from "./chat-native-eligibility";
|
|
93
91
|
|
|
@@ -335,7 +333,8 @@ export async function runNativeChatAttempt(
|
|
|
335
333
|
refusal.headers.set("Retry-After", String(error.retryAfterSeconds));
|
|
336
334
|
return refusal;
|
|
337
335
|
}
|
|
338
|
-
|
|
336
|
+
// Key selection re-resolves the transport; a single-key route still needs canonical Mistral pacing.
|
|
337
|
+
let activeProvider: OcxProviderConfig = resolveMistralTransport(route.provider);
|
|
339
338
|
stampApiKeyAccountLabel(logCtx, route.providerName, activeProvider);
|
|
340
339
|
const spendTracker = sendBudget ? undefined : attachRequestSpendTracker(req, logCtx);
|
|
341
340
|
const physicalBudget = sendBudget ?? createInferenceSendBudget(req, logCtx);
|
|
@@ -365,6 +364,8 @@ export async function runNativeChatAttempt(
|
|
|
365
364
|
config.fastMode,
|
|
366
365
|
route.providerName,
|
|
367
366
|
replayScope,
|
|
367
|
+
undefined,
|
|
368
|
+
translatorBudget,
|
|
368
369
|
);
|
|
369
370
|
observeAdvisorNativeReasoning(req.headers, logCtx, request.body);
|
|
370
371
|
return request;
|
|
@@ -449,7 +450,7 @@ export async function runNativeChatAttempt(
|
|
|
449
450
|
let responseOwnsDistributedKey = true;
|
|
450
451
|
const send = async (
|
|
451
452
|
request: AdapterRequest,
|
|
452
|
-
recovery?: "rate-limit-429" | "key-
|
|
453
|
+
recovery?: "rate-limit-429" | "key-401" | "key-429" | UpstreamSendRecovery,
|
|
453
454
|
singleShot = false,
|
|
454
455
|
): Promise<Response> => {
|
|
455
456
|
try {
|
|
@@ -516,6 +517,7 @@ export async function runNativeChatAttempt(
|
|
|
516
517
|
const spendReport = createPhysicalSendReporter(physicalBudget, () => ({
|
|
517
518
|
poolId: logCtx.spendPoolId ?? route.providerName, identityId: logCtx.accountLogLabel,
|
|
518
519
|
}), execution.comboDispatchPermit);
|
|
520
|
+
const observeAutoPause = captureAccountAutoPauseAttempt(route.providerName, activeProvider, { config, oauth: undefined }, headers);
|
|
519
521
|
let dispatched: Response;
|
|
520
522
|
try {
|
|
521
523
|
// Capture this request's spend policy at its first physical boundary, including
|
|
@@ -538,6 +540,7 @@ export async function runNativeChatAttempt(
|
|
|
538
540
|
request.url,
|
|
539
541
|
applyUpstreamRecoveryInit({
|
|
540
542
|
...init, method: request.method, headers, body: request.body,
|
|
543
|
+
...physicalSendObserverInit(observeAutoPause, request.url),
|
|
541
544
|
}, transportRecovery),
|
|
542
545
|
// Reselection can replace the provider transport and the wire shape, so the
|
|
543
546
|
// egress route is bound to the provider this send actually uses. Omitting it
|
|
@@ -552,7 +555,7 @@ export async function runNativeChatAttempt(
|
|
|
552
555
|
responseOwnsDistributedKey = !activeProvider._apiKeyAttempt?.distributed
|
|
553
556
|
|| distributedKeyResponseOwner(dispatched, config, route.providerName) !== null;
|
|
554
557
|
if (!dispatched.ok) await recordKeyAttemptFailure(logCtx, dispatched, init.signal ?? upstream.signal);
|
|
555
|
-
return dispatched;
|
|
558
|
+
return observeAutoPause ? observeAutoPause(dispatched) : dispatched;
|
|
556
559
|
},
|
|
557
560
|
}),
|
|
558
561
|
);
|
|
@@ -585,13 +588,30 @@ export async function runNativeChatAttempt(
|
|
|
585
588
|
response = await send(activeRequest);
|
|
586
589
|
const retryPolicy = rateLimitRetryPolicyFor(activeProvider);
|
|
587
590
|
let retries = 0;
|
|
591
|
+
let autoPauseMoves = 0;
|
|
592
|
+
for (;;) {
|
|
593
|
+
const priorResponse = response;
|
|
594
|
+
for (; wasAccountAutoPaused(response) && config.providers[route.providerName]?.apiKey && autoPauseMoves < 3 && transientSendAvailable(); autoPauseMoves++) {
|
|
595
|
+
const fresh = resolveCurrentProviderApiKeyTransport(config, route.providerName, { ...activeProvider, _apiKeyAttempt: undefined });
|
|
596
|
+
const next = fresh && (selectProactiveApiKeyTransport(config, route.providerName, fresh) ?? fresh);
|
|
597
|
+
if (!next) break;
|
|
598
|
+
try { void response.body?.cancel().catch(() => {}); } catch { /* closed */ }
|
|
599
|
+
activeProvider = next;
|
|
600
|
+
stampApiKeyAccountLabel(logCtx, route.providerName, activeProvider);
|
|
601
|
+
activeAdapter = createOpenAIChatAdapter(activeProvider);
|
|
602
|
+
releaseRetainedRequest();
|
|
603
|
+
activeRequest = buildActiveRequest();
|
|
604
|
+
retainRequest(activeRequest);
|
|
605
|
+
response = await send(activeRequest, "key-401");
|
|
606
|
+
}
|
|
607
|
+
|
|
588
608
|
while (
|
|
589
609
|
response.status === 429
|
|
590
610
|
// A 429 this proxy synthesized for a refused reset replay is not a provider rate
|
|
591
611
|
// limit: waiting and re-sending here is exactly the duplicate inference the refusal
|
|
592
612
|
// exists to stop. It kept the same shape under the old 502 only because 502 never
|
|
593
613
|
// matched this branch.
|
|
594
|
-
&& !isNonReplayableResponse(response)
|
|
614
|
+
&& !isNonReplayableResponse(response) && !isSameTargetRetryHold(response)
|
|
595
615
|
&& retryPolicy
|
|
596
616
|
&& retries < retryPolicy.attempts
|
|
597
617
|
&& transientSendAvailable()
|
|
@@ -632,6 +652,10 @@ export async function runNativeChatAttempt(
|
|
|
632
652
|
retainRequest(activeRequest);
|
|
633
653
|
response = await send(activeRequest, failureStatus === 401 ? "key-401" : "key-429");
|
|
634
654
|
}
|
|
655
|
+
if (response !== priorResponse && wasAccountAutoPaused(response)) continue;
|
|
656
|
+
break;
|
|
657
|
+
}
|
|
658
|
+
|
|
635
659
|
} catch (error) {
|
|
636
660
|
releaseRetainedRequest();
|
|
637
661
|
cleanupAbort();
|
|
@@ -694,20 +718,12 @@ export async function runNativeChatAttempt(
|
|
|
694
718
|
&& (isCyberPolicyCode(upstreamCode) || isCyberPolicyMessage(upstreamMessage))
|
|
695
719
|
? upstreamMessage
|
|
696
720
|
: detail ? `Provider error ${response.status}: ${detail}` : `Provider error ${response.status}`;
|
|
697
|
-
// Zen's keyless free tier refuses the request outright rather than rate-limiting it, and
|
|
698
|
-
// the raw `MissingSessionID` tells a user nothing about why or what to do (#4121).
|
|
699
|
-
const clientMessage = enrichOpenCodeZenFreeTierMessage(message, {
|
|
700
|
-
providerName: route.providerName,
|
|
701
|
-
baseUrl: route.provider.baseUrl,
|
|
702
|
-
adapter: route.provider.adapter,
|
|
703
|
-
upstreamErrorType: upstreamType,
|
|
704
|
-
});
|
|
705
721
|
const classified = classifyError(
|
|
706
722
|
response.status,
|
|
707
723
|
upstreamType ?? (response.status === 401 ? "authentication_error"
|
|
708
724
|
: response.status === 429 ? "rate_limit_error"
|
|
709
725
|
: response.status >= 500 ? "server_error" : "invalid_request_error"),
|
|
710
|
-
|
|
726
|
+
message,
|
|
711
727
|
);
|
|
712
728
|
// The verdict is read once, from the response that carries it and from the code a
|
|
713
729
|
// re-wrapped body kept -- never from the status, which a real rate limit shares.
|