@iislee/opencodex 2.31.0 → 2.35.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 +3 -3
- package/bin/ocx.mjs +99 -70
- package/gui/dist/assets/index-Be1sDpWw.css +1 -0
- package/gui/dist/assets/index-D4OKgUV4.js +114 -0
- package/gui/dist/index.html +2 -2
- package/package.json +3 -1
- package/src/adapters/anthropic.ts +104 -15
- package/src/adapters/base.ts +29 -1
- package/src/adapters/command-code.ts +42 -6
- package/src/adapters/cursor/call-id.ts +44 -0
- package/src/adapters/cursor/catalog.ts +541 -0
- package/src/adapters/cursor/checkpoint-store.ts +15 -10
- package/src/adapters/cursor/cursor-errors.ts +15 -0
- package/src/adapters/cursor/discovery.ts +69 -18
- package/src/adapters/cursor/effort-map.ts +79 -1
- package/src/adapters/cursor/envelope-echo.ts +290 -0
- package/src/adapters/cursor/live-models.ts +7 -2
- package/src/adapters/cursor/live-transport.ts +14 -0
- package/src/adapters/cursor/message-mapper.ts +4 -1
- package/src/adapters/cursor/native-exec-desktop.ts +23 -0
- package/src/adapters/cursor/native-exec-fs.ts +2 -2
- package/src/adapters/cursor/native-exec-network.ts +1 -1
- package/src/adapters/cursor/native-exec-shell.ts +5 -2
- package/src/adapters/cursor/native-exec.ts +20 -0
- package/src/adapters/cursor/protobuf-request.ts +68 -14
- package/src/adapters/cursor/request-builder.ts +40 -10
- package/src/adapters/cursor/tool-definitions.ts +14 -4
- package/src/adapters/cursor/tool-result-normalize.ts +28 -5
- package/src/adapters/cursor/types.ts +20 -0
- package/src/adapters/cursor.ts +209 -38
- package/src/adapters/exec-tool-result-normalize.ts +99 -0
- package/src/adapters/google-antigravity-replay.ts +173 -25
- package/src/adapters/google-antigravity-wire.ts +5 -0
- package/src/adapters/google-errors.ts +44 -12
- package/src/adapters/google.ts +136 -24
- package/src/adapters/image.ts +1 -1
- package/src/adapters/kiro-constants.ts +27 -0
- package/src/adapters/kiro-tools.ts +55 -16
- package/src/adapters/kiro.ts +182 -23
- package/src/adapters/openai-chat.ts +300 -242
- package/src/adapters/openai-responses.ts +353 -27
- package/src/adapters/run-turn-queue.ts +36 -1
- package/src/adapters/tool-catalog-nudge.ts +4 -3
- package/src/adapters/xai-tool-schema.ts +436 -0
- package/src/adapters/xai-web-search.ts +17 -16
- package/src/bridge.ts +90 -37
- package/src/chat/inbound.ts +14 -1
- package/src/chat/outbound.ts +15 -7
- package/src/claude/agents-inject.ts +8 -1
- package/src/claude/context-windows.ts +16 -9
- package/src/claude/outbound.ts +24 -11
- package/src/cli/access.ts +46 -3
- package/src/cli/account-api.ts +116 -19
- package/src/cli/account-extended.ts +262 -29
- package/src/cli/account-main.ts +12 -12
- package/src/cli/account.ts +69 -15
- package/src/cli/agent.ts +8 -1
- package/src/cli/alias.ts +66 -0
- package/src/cli/capabilities-command.ts +94 -0
- package/src/cli/capabilities.ts +496 -0
- package/src/cli/claude-desktop.ts +31 -11
- package/src/cli/claude.ts +26 -1
- package/src/cli/dispatch.ts +256 -28
- package/src/cli/doctor.ts +126 -14
- package/src/cli/help.ts +17 -6
- package/src/cli/index.ts +39 -9
- package/src/cli/init.ts +1 -0
- package/src/cli/inspect.ts +230 -0
- package/src/cli/models-runtime.ts +95 -0
- package/src/cli/models.ts +26 -10
- package/src/cli/observe.ts +31 -8
- package/src/cli/provider-runtime.ts +16 -2
- package/src/cli/provider.ts +2 -1
- package/src/cli/registry.ts +49 -10
- package/src/cli/runtime-api.ts +51 -7
- package/src/cli/status.ts +41 -1
- package/src/cli/storage.ts +234 -0
- package/src/cli/system-command.ts +16 -0
- package/src/cli/system-restart-client.ts +1 -1
- package/src/cli/usage-report.ts +184 -0
- package/src/cli/v2.ts +34 -10
- package/src/cli/version-skew.ts +46 -0
- package/src/clients/config-export.ts +85 -3
- package/src/clients/effective-status.ts +30 -2
- package/src/clients/probes/cline.ts +81 -0
- package/src/codex/account-label.ts +21 -0
- package/src/codex/account-pause.ts +2 -1
- package/src/codex/account-priority.ts +3 -2
- package/src/codex/app-server-processes.ts +83 -7
- package/src/codex/auth-api.ts +50 -10
- package/src/codex/auth-context.ts +21 -18
- package/src/codex/autostart-health.ts +16 -0
- package/src/codex/catalog/aggregation.ts +18 -0
- package/src/codex/catalog/effort.ts +18 -3
- package/src/codex/catalog/metadata.ts +27 -1
- package/src/codex/catalog/native-models.ts +5 -2
- package/src/codex/catalog/parsing.ts +72 -27
- package/src/codex/catalog/provider-fetch.ts +187 -31
- package/src/codex/catalog/sync.ts +131 -5
- package/src/codex/catalog.ts +1 -1
- package/src/codex/codex-write-lock.ts +3 -1
- package/src/codex/convergence-types.ts +1 -1
- package/src/codex/convergence.ts +27 -2
- package/src/codex/desired-state.ts +2 -2
- package/src/codex/desktop-app-restart.ts +355 -0
- package/src/codex/history-job.ts +32 -3
- package/src/codex/history-manifest.ts +112 -0
- package/src/codex/history-migration-guardian.ts +5 -5
- package/src/codex/history-provider.ts +825 -247
- package/src/codex/history-worker.ts +8 -5
- package/src/codex/inject-coordination.ts +83 -0
- package/src/codex/inject.ts +63 -22
- package/src/codex/injected-marker.ts +1 -1
- package/src/codex/internal/history-writer.ts +4 -3
- package/src/codex/log-guard/inspect.ts +22 -4
- package/src/codex/model-entitlements.ts +9 -2
- package/src/codex/native-profile-startup.ts +157 -27
- package/src/codex/native-residue.ts +26 -33
- package/src/codex/prompt-layers.ts +371 -25
- package/src/codex/prompt-text-probe.ts +238 -0
- package/src/codex/quota.ts +91 -10
- package/src/codex/routing.ts +9 -0
- package/src/codex/shim.ts +56 -3
- package/src/codex/subagent-model-fallback.ts +104 -25
- package/src/codex/transition-state.ts +116 -8
- package/src/combos/failover.ts +27 -0
- package/src/combos/types.ts +10 -0
- package/src/compatibility/index.ts +26 -0
- package/src/compatibility/manifest.ts +253 -0
- package/src/compatibility/openai-responses.ts +121 -0
- package/src/config/atomic-write.ts +219 -0
- package/src/config/paths.ts +40 -0
- package/src/config/process-state.ts +309 -0
- package/src/config/provider-validation.ts +177 -0
- package/src/config/rebase-provenance.ts +68 -0
- package/src/config.ts +206 -824
- package/src/generated/compatibility-version.json +373 -213
- package/src/images/loop.ts +5 -4
- package/src/images/plan.ts +5 -4
- package/src/integrations/ownership-policy.ts +160 -0
- package/src/integrations/ownership.ts +46 -2
- package/src/integrations/registry.ts +7 -0
- package/src/integrations/state.ts +79 -7
- package/src/integrations/writer.ts +26 -2
- package/src/lab/conformance/fixtures/protocol-v1-cases.json +1 -1
- package/src/lab/fabric/producer-child.ts +1 -1
- package/src/lib/admin-secrets.ts +24 -0
- package/src/lib/bounded-body.ts +14 -2
- package/src/lib/config-ownership.ts +20 -0
- package/src/lib/errors.ts +36 -3
- package/src/lib/package-tree-integrity.ts +101 -0
- package/src/lib/process-control.ts +2 -1
- package/src/lib/service-secrets.ts +15 -0
- package/src/lib/tool-argument-integers.ts +56 -5
- package/src/oauth/callback-server.ts +22 -2
- package/src/oauth/cline.ts +203 -0
- package/src/oauth/generic-account-failover.ts +231 -0
- package/src/oauth/health.ts +1 -1
- package/src/oauth/index.ts +65 -6
- package/src/oauth/key-providers.ts +10 -1
- package/src/oauth/kimi.ts +9 -1
- package/src/oauth/kiro.ts +45 -0
- package/src/oauth/local-token-detect.ts +2 -0
- package/src/oauth/open-browser-choice.ts +26 -0
- package/src/oauth/store.ts +14 -5
- package/src/oauth/types.ts +15 -0
- package/src/providers/auto-compact-budget.ts +65 -0
- package/src/providers/command-code-efforts.ts +38 -6
- package/src/providers/context-cap.ts +4 -3
- package/src/providers/default-aliases.ts +65 -0
- package/src/providers/derive.ts +28 -0
- package/src/providers/fastwire.ts +7 -1
- package/src/providers/label.ts +34 -1
- package/src/providers/model-presets.ts +119 -0
- package/src/providers/new-model-policy.ts +146 -0
- package/src/providers/provider-id-rewrite.ts +2 -1
- package/src/providers/quota.ts +65 -7
- package/src/providers/registry.ts +184 -69
- package/src/providers/slug-codec.ts +52 -0
- package/src/providers/xai-transport.ts +21 -0
- package/src/reasoning-effort.ts +19 -2
- package/src/responses/apply-patch-envelope.ts +63 -0
- package/src/responses/code-mode-helper-compat.ts +50 -0
- package/src/responses/custom-tool-compat.ts +157 -39
- package/src/responses/namespace-tool-compat.ts +84 -4
- package/src/responses/parser.ts +7 -2
- package/src/responses/reasoning-replay-cache.ts +81 -3
- package/src/responses/schema.ts +5 -1
- package/src/responses/state.ts +94 -4
- package/src/responses/thought-signature-replay.ts +17 -0
- package/src/responses/turn-termination.ts +107 -0
- package/src/router.ts +40 -2
- package/src/routing/analytics.ts +1 -0
- package/src/routing/quota.ts +10 -0
- package/src/server/auth-cors.ts +27 -7
- package/src/server/chat-completions.ts +24 -16
- package/src/server/chat-native-sse.ts +3 -3
- package/src/server/chat-native.ts +30 -11
- package/src/server/claude-messages.ts +1 -1
- package/src/server/index.ts +179 -32
- package/src/server/lifecycle.ts +52 -1
- package/src/server/local-management-read-client.ts +1 -1
- package/src/server/local-provider-reload-client.ts +1 -1
- package/src/server/management/agent-settings-routes.ts +45 -23
- package/src/server/management/codex-prompt-routes.ts +570 -0
- package/src/server/management/combo-routes.ts +2 -1
- package/src/server/management/config-routes.ts +59 -12
- package/src/server/management/context.ts +10 -1
- package/src/server/management/logs-usage-routes.ts +30 -24
- package/src/server/management/model-routes.ts +274 -4
- package/src/server/management/model-rows.ts +4 -0
- package/src/server/management/native-integration-routes.ts +2 -1
- package/src/server/management/oauth-account-routes.ts +10 -4
- package/src/server/management/provider-capability-config.ts +1 -1
- package/src/server/management/provider-routes.ts +81 -11
- package/src/server/management/route-registry.ts +332 -0
- package/src/server/management/routing-profile-routes.ts +2 -2
- package/src/server/management/system-restart.ts +1 -1
- package/src/server/management-api.ts +18 -16
- package/src/server/port-reclaim.ts +1 -1
- package/src/server/proxy-liveness.ts +29 -5
- package/src/server/relay-eager.ts +147 -20
- package/src/server/relay.ts +251 -19
- package/src/server/request-log-conversation.ts +51 -0
- package/src/server/request-log.ts +70 -16
- package/src/server/responses/codex-auth-error.ts +55 -0
- package/src/server/responses/collaboration.ts +37 -3
- package/src/server/responses/combo-stream-preflight.ts +178 -0
- package/src/server/responses/compact.ts +6 -21
- package/src/server/responses/core.ts +810 -208
- package/src/server/responses/empty-completion-guard.ts +63 -6
- package/src/server/responses/fetch-helpers.ts +17 -102
- package/src/server/responses/input-admission.ts +3 -1
- package/src/server/responses/passthrough-error.ts +33 -9
- package/src/server/responses/responses-field-backfill.ts +105 -13
- package/src/server/responses/ws-upstream.ts +75 -2
- package/src/server/responses-custom-tool-repair.ts +87 -6
- package/src/server/responses-terminal-repair.ts +25 -4
- package/src/server/responses-undeclared-tool-guard.ts +241 -18
- package/src/server/sse-frame-buffer.ts +31 -4
- package/src/server/ws-bridge.ts +14 -2
- package/src/service.ts +42 -4
- package/src/storage/policy-job.ts +14 -4
- package/src/storage/policy.ts +88 -23
- package/src/types/config.ts +66 -0
- package/src/types/provider.ts +69 -0
- package/src/types/request.ts +17 -4
- package/src/types/tools.ts +116 -11
- package/src/types.ts +3 -1
- package/src/update/index.ts +5 -4
- package/src/update/job.ts +3 -1
- package/src/update/transactional-install.mjs +8 -1
- package/src/usage/expected-prices.ts +43 -24
- package/src/usage/log.ts +63 -12
- package/src/usage/summary.ts +211 -8
- package/src/vision/describe.ts +18 -13
- package/src/web-search/exa-executor.ts +40 -9
- package/src/web-search/executor.ts +10 -3
- package/src/web-search/loop.ts +5 -4
- package/gui/dist/assets/index-BF38heuV.js +0 -104
- package/gui/dist/assets/index-DMiI18Kv.css +0 -1
package/src/responses/parser.ts
CHANGED
|
@@ -11,7 +11,7 @@ import type {
|
|
|
11
11
|
OcxToolCall,
|
|
12
12
|
OcxReasoningReplayScopeRef,
|
|
13
13
|
} from "../types";
|
|
14
|
-
import {
|
|
14
|
+
import { createToolChoiceResolver, namespacedToolName } from "../types";
|
|
15
15
|
import { responsesRequestSchema } from "./schema";
|
|
16
16
|
import { providerMetadataFromResponsesFunctionCall } from "./provider-opaque-metadata";
|
|
17
17
|
import { lookupReplayThoughtSignature } from "./thought-signature-replay";
|
|
@@ -45,6 +45,7 @@ type InputBlock =
|
|
|
45
45
|
| { type: "input_text"; text: string }
|
|
46
46
|
| { type: "text"; text: string }
|
|
47
47
|
| { type: "input_image"; image_url?: string; file_id?: string; detail?: string }
|
|
48
|
+
| { type: "input_video"; video_url?: string }
|
|
48
49
|
| { type: "input_file"; file_id?: string; filename?: string; file_data?: string };
|
|
49
50
|
|
|
50
51
|
/** A usable reference string, or undefined. Empty strings and non-strings are not references. */
|
|
@@ -80,6 +81,9 @@ function inputContentParts(blocks: unknown): string | OcxContentPart[] {
|
|
|
80
81
|
}
|
|
81
82
|
// No usable reference: omit the block. A "[image: ?]" marker would claim an attachment
|
|
82
83
|
// the request never carried, which is worse than dropping malformed input.
|
|
84
|
+
} else if (block.type === "input_video") {
|
|
85
|
+
const videoUrl = nonEmptyString(block.video_url);
|
|
86
|
+
if (videoUrl) parts.push({ type: "video", videoUrl });
|
|
83
87
|
} else if (block.type === "input_file") {
|
|
84
88
|
const b = block as { file_id?: string; filename?: string; file_data?: string };
|
|
85
89
|
const fileId = nonEmptyString(b.file_id);
|
|
@@ -758,8 +762,9 @@ export function parseRequest(
|
|
|
758
762
|
const tc = mapToolChoice(data.tool_choice);
|
|
759
763
|
if (tc && typeof tc === "object") {
|
|
760
764
|
const selectors = "allowedTools" in tc ? tc.allowedTools : [tc.name];
|
|
765
|
+
const resolver = createToolChoiceResolver(mergedTools);
|
|
761
766
|
for (const selector of selectors) {
|
|
762
|
-
if (
|
|
767
|
+
if (resolver.candidateCount(selector) > 1) {
|
|
763
768
|
throw new Error(`ambiguous tool_choice name: ${selector}`);
|
|
764
769
|
}
|
|
765
770
|
}
|
|
@@ -29,6 +29,7 @@ import type {
|
|
|
29
29
|
const MAX_ENTRIES = 64;
|
|
30
30
|
const MAX_TOTAL_BYTES = 256 * 1024;
|
|
31
31
|
const TTL_MS = 60 * 60 * 1000;
|
|
32
|
+
const OPAQUE_BLOB_REJECTION_TTL_MS = 5 * 60 * 1000;
|
|
32
33
|
const replayIdentityKey = randomBytes(32);
|
|
33
34
|
const CREDENTIAL_HEADER_NAMES = new Set([
|
|
34
35
|
"authorization",
|
|
@@ -58,10 +59,17 @@ interface ServingIdentityEntry {
|
|
|
58
59
|
at: number;
|
|
59
60
|
}
|
|
60
61
|
|
|
62
|
+
interface OpaqueBlobRejectionEntry {
|
|
63
|
+
bytes: number;
|
|
64
|
+
at: number;
|
|
65
|
+
}
|
|
66
|
+
|
|
61
67
|
const entries = new Map<string, CacheEntry>();
|
|
62
68
|
const servingIdentities = new Map<string, ServingIdentityEntry>();
|
|
69
|
+
const opaqueBlobRejections = new Map<string, OpaqueBlobRejectionEntry>();
|
|
63
70
|
let totalBytes = 0;
|
|
64
71
|
let servingIdentityTotalBytes = 0;
|
|
72
|
+
let opaqueBlobRejectionTotalBytes = 0;
|
|
65
73
|
let clockForTests: (() => number) | null = null;
|
|
66
74
|
|
|
67
75
|
const now = (): number => clockForTests?.() ?? Date.now();
|
|
@@ -142,11 +150,31 @@ function servingIdentityFor(
|
|
|
142
150
|
return { threadId, identity: JSON.stringify(identityTuple) };
|
|
143
151
|
}
|
|
144
152
|
|
|
153
|
+
function opaqueBlobRejectionKeyFor(
|
|
154
|
+
scope: OcxReasoningReplayScopeRef | undefined,
|
|
155
|
+
): string | undefined {
|
|
156
|
+
const current = servingIdentityFor(scope);
|
|
157
|
+
return current ? JSON.stringify([current.threadId, current.identity]) : undefined;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function deleteOpaqueBlobRejection(key: string): void {
|
|
161
|
+
const entry = opaqueBlobRejections.get(key);
|
|
162
|
+
if (!entry) return;
|
|
163
|
+
opaqueBlobRejections.delete(key);
|
|
164
|
+
opaqueBlobRejectionTotalBytes -= entry.bytes;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function sweepExpiredOpaqueBlobRejections(at: number): void {
|
|
168
|
+
for (const [key, entry] of opaqueBlobRejections) {
|
|
169
|
+
if (at - entry.at >= OPAQUE_BLOB_REJECTION_TTL_MS) deleteOpaqueBlobRejection(key);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
145
173
|
/**
|
|
146
|
-
* Compare this request's route with the last successfully serving route for its
|
|
174
|
+
* Compare this request's route with the last successfully serving route for its conversation.
|
|
147
175
|
* A live mismatch means replayed opaque reasoning was minted by another backend and must not be
|
|
148
176
|
* forwarded to this one. Comparison deliberately does not refresh or replace the recorded route:
|
|
149
|
-
* a failed candidate request did not serve the
|
|
177
|
+
* a failed candidate request did not serve the conversation.
|
|
150
178
|
*
|
|
151
179
|
* Serving provenance uses restart-stable destination and credential dimensions so token
|
|
152
180
|
* generations and other volatile credential material cannot create false route changes. Missing
|
|
@@ -164,7 +192,7 @@ export function reasoningReplayServingIdentityChanged(
|
|
|
164
192
|
return previous !== undefined && previous.identity !== current.identity;
|
|
165
193
|
}
|
|
166
194
|
|
|
167
|
-
/** Record the route only after it has successfully served the
|
|
195
|
+
/** Record the route only after it has successfully served the conversation. */
|
|
168
196
|
export function commitReasoningReplayServingIdentity(
|
|
169
197
|
scope: OcxReasoningReplayScopeRef | undefined,
|
|
170
198
|
): void {
|
|
@@ -200,6 +228,54 @@ export function commitReasoningReplayServingIdentity(
|
|
|
200
228
|
}
|
|
201
229
|
}
|
|
202
230
|
|
|
231
|
+
/**
|
|
232
|
+
* Whether this exact conversation and durable serving identity previously rejected opaque replay.
|
|
233
|
+
*
|
|
234
|
+
* The five serving dimensions deliberately match the serving record. Missing durable destination
|
|
235
|
+
* or credential identity is unknown and never falls back to process-local dimensions. The five
|
|
236
|
+
* minute TTL is shorter than the serving record's hour: a stale memo silently degrades reasoning,
|
|
237
|
+
* while expiry costs one visible recovery round trip and can safely re-establish the memo.
|
|
238
|
+
*/
|
|
239
|
+
export function reasoningReplayOpaqueBlobRejectionMemoized(
|
|
240
|
+
scope: OcxReasoningReplayScopeRef | undefined,
|
|
241
|
+
): boolean {
|
|
242
|
+
const key = opaqueBlobRejectionKeyFor(scope);
|
|
243
|
+
if (!key) return false;
|
|
244
|
+
const at = now();
|
|
245
|
+
sweepExpiredOpaqueBlobRejections(at);
|
|
246
|
+
return opaqueBlobRejections.has(key);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/** Record only after a blobless retry succeeded for this durable serving identity. */
|
|
250
|
+
export function rememberReasoningReplayOpaqueBlobRejection(
|
|
251
|
+
scope: OcxReasoningReplayScopeRef | undefined,
|
|
252
|
+
): void {
|
|
253
|
+
const key = opaqueBlobRejectionKeyFor(scope);
|
|
254
|
+
if (!key) return;
|
|
255
|
+
const bytes = Buffer.byteLength(key, "utf8");
|
|
256
|
+
if (bytes > MAX_TOTAL_BYTES) return;
|
|
257
|
+
const at = now();
|
|
258
|
+
sweepExpiredOpaqueBlobRejections(at);
|
|
259
|
+
if (opaqueBlobRejections.has(key)) deleteOpaqueBlobRejection(key);
|
|
260
|
+
opaqueBlobRejections.set(key, { bytes, at });
|
|
261
|
+
opaqueBlobRejectionTotalBytes += bytes;
|
|
262
|
+
while (
|
|
263
|
+
(opaqueBlobRejectionTotalBytes > MAX_TOTAL_BYTES || opaqueBlobRejections.size > MAX_ENTRIES)
|
|
264
|
+
&& opaqueBlobRejections.size > 1
|
|
265
|
+
) {
|
|
266
|
+
let oldestKey: string | undefined;
|
|
267
|
+
let oldestAt = Infinity;
|
|
268
|
+
for (const [candidateKey, entry] of opaqueBlobRejections) {
|
|
269
|
+
if (entry.at < oldestAt) {
|
|
270
|
+
oldestAt = entry.at;
|
|
271
|
+
oldestKey = candidateKey;
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
if (oldestKey === undefined) break;
|
|
275
|
+
deleteOpaqueBlobRejection(oldestKey);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
203
279
|
function processLocalIdentity(domain: string, material: string): string {
|
|
204
280
|
return createHmac("sha256", replayIdentityKey)
|
|
205
281
|
.update(domain)
|
|
@@ -420,7 +496,9 @@ export function peekReasoningForCall(
|
|
|
420
496
|
export function clearReasoningReplayCacheForTests(clock?: (() => number) | null): void {
|
|
421
497
|
entries.clear();
|
|
422
498
|
servingIdentities.clear();
|
|
499
|
+
opaqueBlobRejections.clear();
|
|
423
500
|
totalBytes = 0;
|
|
424
501
|
servingIdentityTotalBytes = 0;
|
|
502
|
+
opaqueBlobRejectionTotalBytes = 0;
|
|
425
503
|
clockForTests = clock ?? null;
|
|
426
504
|
}
|
package/src/responses/schema.ts
CHANGED
|
@@ -11,6 +11,10 @@ const inputImageBlockSchema = z.object({
|
|
|
11
11
|
}).refine(v => typeof v.image_url === "string" || typeof v.file_id === "string", {
|
|
12
12
|
message: "input_image requires at least one of image_url or file_id",
|
|
13
13
|
});
|
|
14
|
+
const inputVideoBlockSchema = z.object({
|
|
15
|
+
type: z.literal("input_video"),
|
|
16
|
+
video_url: z.string().min(1),
|
|
17
|
+
});
|
|
14
18
|
const inputFileBlockSchema = z.object({
|
|
15
19
|
type: z.literal("input_file"),
|
|
16
20
|
file_id: z.string().optional(),
|
|
@@ -24,7 +28,7 @@ const reasoningTextSchema = z.object({ type: z.literal("reasoning_text"), text:
|
|
|
24
28
|
// codex-rs FunctionCallOutputContentItem (protocol/src/models.rs): input_text | input_image | encrypted_content.
|
|
25
29
|
const encryptedContentBlockSchema = z.object({ type: z.literal("encrypted_content"), encrypted_content: z.string() });
|
|
26
30
|
|
|
27
|
-
const inputContentBlockSchema = z.union([inputTextSchema, plainTextSchema, inputImageBlockSchema, inputFileBlockSchema]);
|
|
31
|
+
const inputContentBlockSchema = z.union([inputTextSchema, plainTextSchema, inputImageBlockSchema, inputVideoBlockSchema, inputFileBlockSchema]);
|
|
28
32
|
const outputContentBlockSchema = z.union([outputTextSchema, plainTextSchema, outputRefusalSchema]);
|
|
29
33
|
// Tool outputs on the wire mix codex-rs FunctionCallOutputContentItem with legacy output blocks.
|
|
30
34
|
const toolOutputContentBlockSchema = z.union([
|
package/src/responses/state.ts
CHANGED
|
@@ -18,6 +18,12 @@ import {
|
|
|
18
18
|
const MAX_STORED_RESPONSES = 1_000;
|
|
19
19
|
const RESPONSE_TTL_MS = 60 * 60 * 1_000;
|
|
20
20
|
const SNAPSHOT_DEBOUNCE_MS = 2_000;
|
|
21
|
+
/** Snapshot size below which the debounce stays at its base value. */
|
|
22
|
+
const SNAPSHOT_DEBOUNCE_SCALE_FROM_BYTES = 1 * 1024 * 1024;
|
|
23
|
+
/** Ceiling for the stretched debounce. Continuation state is only read after a
|
|
24
|
+
* restart, and a graceful shutdown flushes, so the exposure a longer debounce adds
|
|
25
|
+
* is bounded by a hard kill — paid against rewriting the whole snapshot every 2 s. */
|
|
26
|
+
const SNAPSHOT_DEBOUNCE_MAX_MS = 30_000;
|
|
21
27
|
/** In-memory high-water byte cap across all entries. Forced store:false retention (kiro/cursor
|
|
22
28
|
* continuation chains) stores the full expanded input each turn — ~quadratic bytes per chain —
|
|
23
29
|
* so a count cap alone cannot bound memory. Oldest-first eviction applies past this mark. */
|
|
@@ -90,6 +96,43 @@ let oldestResidentId: string | undefined;
|
|
|
90
96
|
let oldestResidentAt: number | null = null;
|
|
91
97
|
let byteCapOverride: number | null = null;
|
|
92
98
|
let stateRevision = 0;
|
|
99
|
+
/** Byte length and digest of the last snapshot actually written, for the
|
|
100
|
+
* identical-payload skip and the size-scaled debounce. The payload itself is not
|
|
101
|
+
* retained: at the 24 MiB bound that would double the snapshot's memory cost. */
|
|
102
|
+
let lastSnapshotBytes = 0;
|
|
103
|
+
let lastSnapshotDigest: string | null = null;
|
|
104
|
+
// The resolved file the digest above describes. Keeping it means a config-dir
|
|
105
|
+
// change or a retargeted symlink is a miss rather than a false "unchanged".
|
|
106
|
+
let lastSnapshotTarget: string | null = null;
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Is the snapshot on disk still byte-for-byte what we last wrote?
|
|
110
|
+
*
|
|
111
|
+
* The cached digest proves what this process wrote, not what is there now. Size is
|
|
112
|
+
* checked first so the common mismatch costs a `stat`, and the content comparison
|
|
113
|
+
* only runs when the size already agrees. Any read failure answers "no" and the
|
|
114
|
+
* caller rewrites — the safe direction.
|
|
115
|
+
*/
|
|
116
|
+
async function snapshotOnDiskMatches(path: string, payload: string, payloadBytes: number): Promise<boolean> {
|
|
117
|
+
try {
|
|
118
|
+
const file = Bun.file(path);
|
|
119
|
+
if (file.size !== payloadBytes) return false;
|
|
120
|
+
if (await file.text() !== payload) return false;
|
|
121
|
+
// Content matching is not the whole invariant. This file holds persisted request
|
|
122
|
+
// and response bodies, and `atomicWriteFileAsync` writes it owner-only; the
|
|
123
|
+
// unconditional rewrite used to restore that on every mutation. Skipping without
|
|
124
|
+
// checking would let a broadened mode persist indefinitely, so treat a widened
|
|
125
|
+
// file as "does not match" and let the caller rewrite it through the hardening
|
|
126
|
+
// path. POSIX only — Windows ACLs are re-applied by that same write path.
|
|
127
|
+
if (process.platform !== "win32") {
|
|
128
|
+
const mode = statSync(path).mode & 0o777;
|
|
129
|
+
if (mode !== 0o600) return false;
|
|
130
|
+
}
|
|
131
|
+
return true;
|
|
132
|
+
} catch {
|
|
133
|
+
return false;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
93
136
|
const spillCounters = { writes: 0, writeFailures: 0, readFailures: 0 };
|
|
94
137
|
/**
|
|
95
138
|
* Admission-boundary observability (test-visible). directSpills: oversized
|
|
@@ -788,9 +831,38 @@ async function writeBoundedSnapshot(path: string): Promise<SnapshotWriteOutcome>
|
|
|
788
831
|
entries.push(persistEntry);
|
|
789
832
|
}
|
|
790
833
|
entries.reverse();
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
834
|
+
const payload = JSON.stringify({ version: 2, states: entries });
|
|
835
|
+
const payloadBytes = Buffer.byteLength(payload, "utf8");
|
|
836
|
+
const payloadDigest = Bun.hash(payload).toString(36);
|
|
837
|
+
// A mutation does not always change what gets persisted: entries past the
|
|
838
|
+
// per-entry or total byte bound are dropped from the selection, and spill
|
|
839
|
+
// demotion moves bytes out of it. Re-writing a byte-identical 24 MiB file
|
|
840
|
+
// buys nothing, so compare first — but the cached digest describes what THIS
|
|
841
|
+
// process last wrote, which is not the same claim as "that is what is on disk
|
|
842
|
+
// now". A second proxy sharing the home, or anything that rewrites the file
|
|
843
|
+
// in place, leaves the digest describing bytes that are gone. Before every
|
|
844
|
+
// release-of-a-write, the previous behaviour rewrote unconditionally and so
|
|
845
|
+
// repaired that silently; skipping without checking would turn a repaired
|
|
846
|
+
// snapshot into a lost one at the next restart.
|
|
847
|
+
//
|
|
848
|
+
// Verify against the file itself, keyed to the resolved target so a retargeted
|
|
849
|
+
// symlink is also a miss. Reading back a matching-size file costs far less
|
|
850
|
+
// than the atomic replace it avoids, and only happens when the digest already
|
|
851
|
+
// matched — the amplification this fixes is the repeated WRITE, not the read.
|
|
852
|
+
const unchanged = lastSnapshotDigest !== null
|
|
853
|
+
&& payloadDigest === lastSnapshotDigest
|
|
854
|
+
&& payloadBytes === lastSnapshotBytes
|
|
855
|
+
&& lastSnapshotTarget === resolveWriteTarget(path)
|
|
856
|
+
&& existsSync(path)
|
|
857
|
+
&& await snapshotOnDiskMatches(path, payload, payloadBytes);
|
|
858
|
+
if (!unchanged) {
|
|
859
|
+
mkdirSync(dirname(path), { recursive: true, mode: 0o700 });
|
|
860
|
+
try { chmodSync(dirname(path), 0o700); } catch { /* best-effort (e.g. Windows) */ }
|
|
861
|
+
await atomicWriteFileAsync(path, payload);
|
|
862
|
+
lastSnapshotDigest = payloadDigest;
|
|
863
|
+
lastSnapshotBytes = payloadBytes;
|
|
864
|
+
lastSnapshotTarget = resolveWriteTarget(path);
|
|
865
|
+
}
|
|
794
866
|
persistAttemptHookForTests?.();
|
|
795
867
|
if (revision === stateRevision) return "stable";
|
|
796
868
|
}
|
|
@@ -809,11 +881,26 @@ function drainPendingSpillUnlinks(): void {
|
|
|
809
881
|
}
|
|
810
882
|
}
|
|
811
883
|
|
|
884
|
+
/**
|
|
885
|
+
* Debounce scaled by the size of the last snapshot written.
|
|
886
|
+
*
|
|
887
|
+
* The whole snapshot is re-serialized and atomically replaced on every flush, so at
|
|
888
|
+
* the 24 MiB bound a fixed 2 s debounce is up to ~12 MB/s of write amplification for
|
|
889
|
+
* state nothing reads until the next start (#2460). Small snapshots keep the base
|
|
890
|
+
* cadence; the stretch is linear in size and clamped, so the write rate is roughly
|
|
891
|
+
* flat instead of growing with the file.
|
|
892
|
+
*/
|
|
893
|
+
function snapshotDebounceMs(): number {
|
|
894
|
+
if (lastSnapshotBytes <= SNAPSHOT_DEBOUNCE_SCALE_FROM_BYTES) return SNAPSHOT_DEBOUNCE_MS;
|
|
895
|
+
const scaled = Math.round(SNAPSHOT_DEBOUNCE_MS * (lastSnapshotBytes / SNAPSHOT_DEBOUNCE_SCALE_FROM_BYTES));
|
|
896
|
+
return Math.min(scaled, SNAPSHOT_DEBOUNCE_MAX_MS);
|
|
897
|
+
}
|
|
898
|
+
|
|
812
899
|
function schedulePersistAt(path: string, replace = false): void {
|
|
813
900
|
if (persistTimer && !replace) return;
|
|
814
901
|
if (persistTimer) clearTimeout(persistTimer);
|
|
815
902
|
pendingPersistPath = path;
|
|
816
|
-
persistTimer = setTimeout(() => { void persistNow(path); },
|
|
903
|
+
persistTimer = setTimeout(() => { void persistNow(path); }, snapshotDebounceMs());
|
|
817
904
|
(persistTimer as { unref?: () => void }).unref?.();
|
|
818
905
|
}
|
|
819
906
|
|
|
@@ -1418,6 +1505,9 @@ export function clearResponseStateMemoryForTests(): void {
|
|
|
1418
1505
|
replayScopeMismatchDrops = 0;
|
|
1419
1506
|
replayOverlapSkips = 0;
|
|
1420
1507
|
persistAttemptHookForTests = null;
|
|
1508
|
+
lastSnapshotBytes = 0;
|
|
1509
|
+
lastSnapshotDigest = null;
|
|
1510
|
+
lastSnapshotTarget = null;
|
|
1421
1511
|
loaded = false;
|
|
1422
1512
|
}
|
|
1423
1513
|
|
|
@@ -308,6 +308,23 @@ export function lookupReplayThoughtSignature(
|
|
|
308
308
|
return entry.sig;
|
|
309
309
|
}
|
|
310
310
|
|
|
311
|
+
/** Drop a remembered signature for a specific callId and scope (e.g. when upstream rejects it). */
|
|
312
|
+
export function forgetThoughtSignatureForReplay(
|
|
313
|
+
callId: string,
|
|
314
|
+
scope: OcxReasoningReplayScopeRef | undefined,
|
|
315
|
+
): boolean {
|
|
316
|
+
const key = keyFor(callId, scope);
|
|
317
|
+
if (key === undefined) return false;
|
|
318
|
+
load();
|
|
319
|
+
const entry = entries.get(key);
|
|
320
|
+
if (!entry) return false;
|
|
321
|
+
entries.delete(key);
|
|
322
|
+
totalBytes -= entry.sig.length;
|
|
323
|
+
prune(Date.now());
|
|
324
|
+
void persist();
|
|
325
|
+
return true;
|
|
326
|
+
}
|
|
327
|
+
|
|
311
328
|
/** Test seams: clear in-memory state and the loaded flag without touching the file. */
|
|
312
329
|
export function resetThoughtSignatureReplayForTests(): void {
|
|
313
330
|
entries = new Map();
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import type { OcxAssistantMessage, OcxMessage, OcxParsedRequest } from "../types";
|
|
3
|
+
|
|
4
|
+
const DELIVERED_FINAL_ANSWER_TTL_MS = 60 * 60 * 1_000;
|
|
5
|
+
const DELIVERED_FINAL_ANSWER_MAX_ENTRIES = 1_024;
|
|
6
|
+
|
|
7
|
+
interface DeliveredFinalAnswerRecord {
|
|
8
|
+
fingerprint: string;
|
|
9
|
+
createdAt: number;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const scopesByRequest = new WeakMap<OcxParsedRequest, string>();
|
|
13
|
+
const deliveredFinalAnswers = new Map<string, DeliveredFinalAnswerRecord>();
|
|
14
|
+
|
|
15
|
+
function pruneDeliveredFinalAnswers(at = Date.now()): void {
|
|
16
|
+
for (const [scope, record] of deliveredFinalAnswers) {
|
|
17
|
+
if (at - record.createdAt > DELIVERED_FINAL_ANSWER_TTL_MS) deliveredFinalAnswers.delete(scope);
|
|
18
|
+
}
|
|
19
|
+
while (deliveredFinalAnswers.size > DELIVERED_FINAL_ANSWER_MAX_ENTRIES) {
|
|
20
|
+
const oldest = deliveredFinalAnswers.keys().next().value;
|
|
21
|
+
if (oldest === undefined) break;
|
|
22
|
+
deliveredFinalAnswers.delete(oldest);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function textFingerprint(text: string): string {
|
|
27
|
+
return createHash("sha256").update(text, "utf8").digest("hex");
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function assistantText(message: OcxAssistantMessage): string | undefined {
|
|
31
|
+
if (message.content.some(part => part.type === "toolCall")) return undefined;
|
|
32
|
+
const text = message.content
|
|
33
|
+
.filter((part): part is Extract<typeof part, { type: "text" }> => part.type === "text")
|
|
34
|
+
.map(part => part.text)
|
|
35
|
+
.join("");
|
|
36
|
+
return text.trim().length > 0 ? text : undefined;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function deliveredFinalAnswerText(response: unknown): string | undefined {
|
|
40
|
+
if (!response || typeof response !== "object" || Array.isArray(response)) return undefined;
|
|
41
|
+
const output = (response as { output?: unknown }).output;
|
|
42
|
+
if (!Array.isArray(output)) return undefined;
|
|
43
|
+
for (let i = output.length - 1; i >= 0; i--) {
|
|
44
|
+
const item = output[i];
|
|
45
|
+
if (!item || typeof item !== "object" || Array.isArray(item)) continue;
|
|
46
|
+
const message = item as { type?: unknown; role?: unknown; phase?: unknown; content?: unknown };
|
|
47
|
+
if (message.type !== "message" || message.role !== "assistant" || message.phase !== "final_answer") continue;
|
|
48
|
+
if (!Array.isArray(message.content)) return undefined;
|
|
49
|
+
const text = message.content
|
|
50
|
+
.filter(part => !!part && typeof part === "object" && !Array.isArray(part)
|
|
51
|
+
&& (part as { type?: unknown }).type === "output_text"
|
|
52
|
+
&& typeof (part as { text?: unknown }).text === "string")
|
|
53
|
+
.map(part => (part as { text: string }).text)
|
|
54
|
+
.join("");
|
|
55
|
+
return text.trim().length > 0 ? text : undefined;
|
|
56
|
+
}
|
|
57
|
+
return undefined;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Bind the normalized per-conversation digest without adding proxy-private fields to the wire body. */
|
|
61
|
+
export function bindTurnTerminationScope(parsed: OcxParsedRequest, scope: string | undefined): void {
|
|
62
|
+
// Only the normalized log-conversation digest may key this process-wide map. Refusing any raw
|
|
63
|
+
// fallback prevents a future caller from retaining a client header or account identifier here.
|
|
64
|
+
if (!scope || !/^[0-9a-f]{32}$/.test(scope)) return;
|
|
65
|
+
scopesByRequest.set(parsed, scope);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** Remember only a final-answer message the proxy actually emitted for this exact conversation. */
|
|
69
|
+
export function rememberDeliveredFinalAnswer(parsed: OcxParsedRequest, response: unknown): void {
|
|
70
|
+
const scope = scopesByRequest.get(parsed);
|
|
71
|
+
if (!scope) return;
|
|
72
|
+
const text = deliveredFinalAnswerText(response);
|
|
73
|
+
if (!text) return;
|
|
74
|
+
const at = Date.now();
|
|
75
|
+
pruneDeliveredFinalAnswers(at);
|
|
76
|
+
// Refresh insertion order so cap eviction removes the least recently delivered conversation.
|
|
77
|
+
deliveredFinalAnswers.delete(scope);
|
|
78
|
+
deliveredFinalAnswers.set(scope, { fingerprint: textFingerprint(text), createdAt: at });
|
|
79
|
+
pruneDeliveredFinalAnswers(at);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Match only when the delivered assistant answer is still the trailing content-bearing message.
|
|
84
|
+
* A later user/tool-result message is new work and must reach the provider, even though every
|
|
85
|
+
* legitimate next turn necessarily contains such a message somewhere in its history.
|
|
86
|
+
*/
|
|
87
|
+
export function hasRecordedTrailingDeliveredFinalAnswer(
|
|
88
|
+
parsed: OcxParsedRequest,
|
|
89
|
+
messages: readonly OcxMessage[],
|
|
90
|
+
): boolean {
|
|
91
|
+
const scope = scopesByRequest.get(parsed);
|
|
92
|
+
if (!scope) return false;
|
|
93
|
+
pruneDeliveredFinalAnswers();
|
|
94
|
+
const record = deliveredFinalAnswers.get(scope);
|
|
95
|
+
if (!record) return false;
|
|
96
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
97
|
+
const message = messages[i];
|
|
98
|
+
if (message.role !== "assistant") return false;
|
|
99
|
+
const text = assistantText(message as OcxAssistantMessage);
|
|
100
|
+
if (text === undefined) {
|
|
101
|
+
if ((message as OcxAssistantMessage).content.some(part => part.type === "toolCall")) return false;
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
return textFingerprint(text) === record.fingerprint;
|
|
105
|
+
}
|
|
106
|
+
return false;
|
|
107
|
+
}
|
package/src/router.ts
CHANGED
|
@@ -32,6 +32,7 @@ import {
|
|
|
32
32
|
OPENAI_CODEX_PROVIDER_ID,
|
|
33
33
|
} from "./providers/openai-tiers";
|
|
34
34
|
import { decodeRoutedModelIdOrThrow, encodeRoutedModelId } from "./providers/slug-codec";
|
|
35
|
+
import { resolveModelAlias } from "./providers/default-aliases";
|
|
35
36
|
import { getStaleCached } from "./codex/model-cache";
|
|
36
37
|
import { codexAccountNamespaceEntries } from "./codex/account-namespaces";
|
|
37
38
|
import {
|
|
@@ -116,6 +117,7 @@ export function knownModelIdsForProvider(
|
|
|
116
117
|
registry?.modelReasoningEffortMap,
|
|
117
118
|
registry?.modelMaxOutputTokens,
|
|
118
119
|
registry?.modelSupportsServiceTier,
|
|
120
|
+
registry?.modelSupportsVerbosity,
|
|
119
121
|
]) {
|
|
120
122
|
for (const id of Object.keys(map ?? {})) ids.add(id);
|
|
121
123
|
}
|
|
@@ -312,6 +314,10 @@ export function routedProviderConfig(providerName: string, provider: OcxProvider
|
|
|
312
314
|
: undefined,
|
|
313
315
|
provider.modelSupportsServiceTier,
|
|
314
316
|
);
|
|
317
|
+
const modelSupportsVerbosity = mergeRecordFill(
|
|
318
|
+
registryEntry.modelSupportsVerbosity,
|
|
319
|
+
provider.modelSupportsVerbosity,
|
|
320
|
+
);
|
|
315
321
|
const noVisionModels = mergeStringArray(registryEntry.noVisionModels, provider.noVisionModels);
|
|
316
322
|
const noReasoningModels = mergeStringArray(registryEntry.noReasoningModels, provider.noReasoningModels);
|
|
317
323
|
const noTemperatureModels = mergeStringArray(registryEntry.noTemperatureModels, provider.noTemperatureModels);
|
|
@@ -426,6 +432,7 @@ export function routedProviderConfig(providerName: string, provider: OcxProvider
|
|
|
426
432
|
...(modelMaxInputTokens ? { modelMaxInputTokens } : {}),
|
|
427
433
|
...(modelMaxOutputTokens ? { modelMaxOutputTokens } : {}),
|
|
428
434
|
...(modelSupportsServiceTier ? { modelSupportsServiceTier } : {}),
|
|
435
|
+
...(modelSupportsVerbosity ? { modelSupportsVerbosity } : {}),
|
|
429
436
|
...(modelReasoningEfforts ? { modelReasoningEfforts } : {}),
|
|
430
437
|
...(modelDefaultReasoningEfforts ? { modelDefaultReasoningEfforts } : {}),
|
|
431
438
|
...(reasoningEffortMap ? { reasoningEffortMap } : {}),
|
|
@@ -640,7 +647,14 @@ function routeModelInternal(
|
|
|
640
647
|
// slash-containing model ids (e.g. "anthropic/claude-...") fall through when
|
|
641
648
|
// no such provider exists.
|
|
642
649
|
if (slash > 0) {
|
|
643
|
-
const
|
|
650
|
+
const requestedProvider = modelId.slice(0, slash);
|
|
651
|
+
const provName = hasOwnProvider(config.providers, requestedProvider)
|
|
652
|
+
? requestedProvider
|
|
653
|
+
: Object.entries(config.providers).find(([, provider]) =>
|
|
654
|
+
typeof provider.alias === "string" && provider.alias.toLowerCase() === requestedProvider.toLowerCase())?.[0];
|
|
655
|
+
if (!provName) {
|
|
656
|
+
// A genuine slash-containing native model id still falls through unchanged.
|
|
657
|
+
} else {
|
|
644
658
|
if (provName === LEGACY_CHATGPT_PROVIDER_ID || provName === LEGACY_OPENAI_MULTI_PROVIDER_ID) {
|
|
645
659
|
throw new Error(`No provider configured for model: ${modelId}`);
|
|
646
660
|
}
|
|
@@ -656,14 +670,20 @@ function routeModelInternal(
|
|
|
656
670
|
}
|
|
657
671
|
// Codex-facing alias ids (`provider/vendor-model`) decode back to the native
|
|
658
672
|
// slash id via an exact known-id lookup; raw full-slash selectors keep working.
|
|
673
|
+
const requestedModel = modelId.slice(slash + 1);
|
|
674
|
+
const decoded = decodeRoutedModelIdOrThrow(requestedModel, known);
|
|
675
|
+
const nativeModel = known.includes(decoded)
|
|
676
|
+
? decoded
|
|
677
|
+
: resolveModelAlias(config, prov, known, requestedModel) ?? decoded;
|
|
659
678
|
return routeResult(
|
|
660
679
|
provName,
|
|
661
680
|
prov,
|
|
662
|
-
|
|
681
|
+
nativeModel,
|
|
663
682
|
"explicit-provider",
|
|
664
683
|
"explicit-provider-namespace",
|
|
665
684
|
);
|
|
666
685
|
}
|
|
686
|
+
}
|
|
667
687
|
}
|
|
668
688
|
|
|
669
689
|
if (isBareOpenAiFamilyModel(modelId)) {
|
|
@@ -693,6 +713,24 @@ function routeModelInternal(
|
|
|
693
713
|
}
|
|
694
714
|
}
|
|
695
715
|
|
|
716
|
+
const aliasMatches: Array<{ provider: string; model: string; qualified: string }> = [];
|
|
717
|
+
for (const [provName, prov] of activeProviderEntries(config)) {
|
|
718
|
+
const known = knownModelIdsForProvider(provName, prov, config);
|
|
719
|
+
const native = resolveModelAlias(config, prov, known, modelId);
|
|
720
|
+
if (native) aliasMatches.push({
|
|
721
|
+
provider: provName,
|
|
722
|
+
model: native,
|
|
723
|
+
qualified: `${prov.alias || provName}/${modelId}`,
|
|
724
|
+
});
|
|
725
|
+
}
|
|
726
|
+
if (aliasMatches.length > 1) {
|
|
727
|
+
throw new Error(`model alias '${modelId}' is ambiguous: ${aliasMatches.map(match => match.qualified).sort().join(", ")}`);
|
|
728
|
+
}
|
|
729
|
+
if (aliasMatches[0]) {
|
|
730
|
+
const match = aliasMatches[0];
|
|
731
|
+
return routeResult(match.provider, config.providers[match.provider], match.model, "explicit-provider", "model-alias");
|
|
732
|
+
}
|
|
733
|
+
|
|
696
734
|
if (config.defaultProvider === LEGACY_CHATGPT_PROVIDER_ID) {
|
|
697
735
|
throw new Error(`No provider configured for model: ${modelId}`);
|
|
698
736
|
}
|
package/src/routing/analytics.ts
CHANGED
package/src/routing/quota.ts
CHANGED
|
@@ -41,6 +41,12 @@ function codexAccountQuotaEvidence(accountId: string, plan?: string): RouteQuota
|
|
|
41
41
|
const percents = [
|
|
42
42
|
...(monthly ? [] : [quota.weeklyPercent]),
|
|
43
43
|
quota.monthlyPercent,
|
|
44
|
+
// The burst window is upstream-enforced independently of the governing window, so an
|
|
45
|
+
// account at 97% here has 3% headroom whatever its weekly figure says. This was invisible
|
|
46
|
+
// while the header parser misfiled 5h readings into weeklyPercent - routing saw the burst
|
|
47
|
+
// by accident. Once that is fixed, omitting it here reports 88% headroom for an account
|
|
48
|
+
// that is one request away from a 429. computeCodexUsageScore already folds it in.
|
|
49
|
+
quota.shortPercent,
|
|
44
50
|
].filter((value): value is number => typeof value === "number" && Number.isFinite(value));
|
|
45
51
|
const maxPercent = percents.length > 0 ? Math.max(...percents) : undefined;
|
|
46
52
|
// Credits-only snapshots prove neither usage nor exhaustion. Unknown must not
|
|
@@ -49,6 +55,10 @@ function codexAccountQuotaEvidence(accountId: string, plan?: string): RouteQuota
|
|
|
49
55
|
const resets = [
|
|
50
56
|
...(monthly ? [] : [quota.weeklyResetAt]),
|
|
51
57
|
quota.monthlyResetAt,
|
|
58
|
+
// Pair the reset with the window that can actually gate the next request: a burst-limited
|
|
59
|
+
// account recovers in hours, and reporting a distant weekly reset would defer a retry that
|
|
60
|
+
// is already safe.
|
|
61
|
+
quota.shortResetAt,
|
|
52
62
|
].filter((value): value is number => typeof value === "number" && Number.isFinite(value))
|
|
53
63
|
.filter(value => value > Date.now());
|
|
54
64
|
return {
|