@bitkyc08/opencodex 2.59.0 → 2.60.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/gui/dist/assets/index-BTuCbqQd.css +1 -0
- package/gui/dist/assets/index-DoBVdPHP.js +134 -0
- package/gui/dist/index.html +2 -2
- package/package.json +4 -1
- package/src/adapters/anthropic-image-codec.ts +16 -2
- package/src/adapters/anthropic-image-normalize.ts +49 -2
- package/src/adapters/anthropic.ts +4 -1
- package/src/adapters/base.ts +23 -0
- package/src/adapters/coding-agent/turn.ts +22 -2
- package/src/adapters/command-code.ts +50 -3
- package/src/adapters/cursor/checkpoint-store.ts +3 -0
- package/src/adapters/cursor/discovery.ts +11 -8
- package/src/adapters/cursor/live-transport.ts +26 -9
- package/src/adapters/cursor/request-builder.ts +7 -1
- package/src/adapters/cursor/transport.ts +19 -0
- package/src/adapters/cursor.ts +3 -1
- package/src/adapters/devin/cloud-direct/chat.ts +3 -1
- package/src/adapters/devin/cloud-direct/stated-reset-retry.ts +42 -5
- package/src/adapters/devin.ts +50 -13
- package/src/adapters/google-antigravity-replay.ts +1 -1
- package/src/adapters/google-antigravity-wire.ts +29 -5
- package/src/adapters/google-http.ts +49 -10
- package/src/adapters/google-tool-schema.ts +595 -31
- package/src/adapters/google-wire-compiler.ts +93 -10
- package/src/adapters/google-wire-shape.ts +461 -0
- package/src/adapters/google.ts +37 -6
- package/src/adapters/openai-chat-images.ts +3 -1
- package/src/adapters/openai-chat.ts +5 -1
- package/src/adapters/openai-responses/image-gen.ts +8 -6
- package/src/adapters/openai-responses/passthrough.ts +15 -3
- package/src/adapters/openai-responses/reasoning.ts +7 -0
- package/src/adapters/opencode-go-additional-tools.ts +12 -2
- package/src/bridge/sse.ts +3 -151
- package/src/cli/account-extended.ts +4 -4
- package/src/cli/dispatch.ts +3 -3
- package/src/cli/doctor.ts +28 -9
- package/src/cli/hub.ts +3 -2
- package/src/cli/index.ts +7 -2
- package/src/cli/opencode.ts +2 -2
- package/src/cli/provider.ts +13 -1
- package/src/client/machine-api.ts +2 -2
- package/src/client/machine-listener.ts +2 -2
- package/src/client/runtime.ts +26 -2
- package/src/codex/account-store.ts +65 -0
- package/src/codex/auth-api/account-list.ts +19 -11
- package/src/codex/auth-api/pool-quota-probe.ts +30 -7
- package/src/codex/catalog/gather-capture.ts +21 -2
- package/src/codex/catalog/model-hints.ts +29 -28
- package/src/codex/catalog/parsing.ts +7 -0
- package/src/codex/catalog/provider-models.ts +19 -2
- package/src/codex/catalog/retained-sync.ts +22 -26
- package/src/codex/catalog/routed-gather.ts +19 -0
- package/src/codex/context-compat.ts +5 -2
- package/src/codex/desired-state.ts +4 -1
- package/src/codex/history-job.ts +6 -6
- package/src/codex/history-provider.ts +20 -166
- package/src/codex/history-rollout-read.ts +174 -0
- package/src/codex/internal/catalog-writer.ts +33 -1
- package/src/codex/model-cache.ts +47 -0
- package/src/codex/model-entitlements.ts +29 -10
- package/src/codex/observed-model-denials.ts +101 -8
- package/src/codex/prompt-text-probe.ts +9 -6
- package/src/codex/routing.ts +7 -1
- package/src/codex/shim.ts +1 -1
- package/src/codex/subagent-model-fallback.ts +22 -4
- package/src/combos/failover.ts +3 -0
- package/src/config/admitted-identity.ts +222 -0
- package/src/config/diagnostics.ts +22 -1
- package/src/config/feature-flags.ts +5 -0
- package/src/config/load-degrade.ts +18 -0
- package/src/config/proxy-env.ts +8 -2
- package/src/config/schema/compaction-triggers.ts +11 -0
- package/src/config/schema/config-schema.ts +4 -0
- package/src/config/schema/leaf-validators.ts +12 -0
- package/src/config.ts +2 -2
- package/src/generated/compatibility-version.json +249 -169
- package/src/grok/reset-coupons.ts +38 -19
- package/src/images/loop.ts +6 -1
- package/src/integrations/aside-profile-context.ts +37 -3
- package/src/integrations/aside-profile-journal.ts +68 -3
- package/src/integrations/aside-profiles.ts +128 -3
- package/src/integrations/mutation-plan.ts +815 -0
- package/src/integrations/writer.ts +85 -99
- package/src/lab/live/transport.ts +4 -0
- package/src/lab/live/types.ts +5 -0
- package/src/lab/subject/behavior-fingerprint.ts +1 -1
- package/src/lib/admin-secrets.ts +9 -1
- package/src/lib/debug-log-buffer.ts +6 -1
- package/src/lib/debug.ts +23 -0
- package/src/lib/errors.ts +79 -0
- package/src/lib/http-response-semantics.ts +57 -0
- package/src/lib/lab-live-pinned-sender.ts +26 -12
- package/src/lib/pinned-http.ts +142 -2
- package/src/lib/plain-data.ts +103 -0
- package/src/lib/process-control.ts +13 -5
- package/src/lib/provider-outbound.ts +50 -2
- package/src/lib/socks5-fetch.ts +136 -26
- package/src/lib/spend-ledger-owner.ts +364 -0
- package/src/lib/spend-reservation-ledger.ts +218 -27
- package/src/lib/windows-system-proxy.ts +16 -11
- package/src/oauth/callback-server.ts +4 -3
- package/src/oauth/generic-account-failover.ts +1 -0
- package/src/oauth/health.ts +12 -1
- package/src/oauth/index.ts +3 -107
- package/src/oauth/login-flow-state.ts +127 -0
- package/src/providers/derive.ts +34 -17
- package/src/providers/devin-cli-authmode-migration.ts +14 -10
- package/src/providers/key-failover.ts +66 -18
- package/src/providers/model-rename-migration.ts +55 -1
- package/src/providers/model-rename-startup.ts +7 -5
- package/src/providers/openai-virtual-models.ts +42 -2
- package/src/providers/quota/antigravity.ts +22 -2
- package/src/providers/quota/vendor-probes-key.ts +1 -1
- package/src/providers/registry/entries-core.ts +39 -17
- package/src/providers/registry/entries-extended.ts +35 -1
- package/src/providers/registry/model-ids.ts +168 -0
- package/src/providers/registry/model-seeds.ts +9 -0
- package/src/providers/registry/types.ts +2 -0
- package/src/providers/resolved-model-policy-merge.ts +167 -0
- package/src/providers/resolved-model-policy.ts +406 -0
- package/src/providers/stale-vision-classification-migration.ts +137 -0
- package/src/responses/apply-patch-envelope.ts +0 -12
- package/src/responses/freeform-wrapper-scan.ts +279 -0
- package/src/responses/legacy-dotted-tool-name-repair.ts +134 -0
- package/src/responses/progressive-freeform-input.ts +130 -0
- package/src/responses/reasoning-envelope.ts +30 -0
- package/src/responses/state.ts +5 -12
- package/src/responses/tool-name-aliases.ts +15 -1
- package/src/router.ts +91 -115
- package/src/routing/compatibility/behavior.ts +9 -0
- package/src/routing/compatibility/subject.ts +16 -1
- package/src/server/adapter-resolve.ts +9 -0
- package/src/server/auth-cors.ts +3 -0
- package/src/server/chat-completions.ts +5 -2
- package/src/server/claude-messages.ts +6 -3
- package/src/server/effort-row.ts +11 -3
- package/src/server/grok-responses-control-frame.ts +160 -1
- package/src/server/index/serve-options.ts +86 -29
- package/src/server/index/spend-ledger-lifecycle.ts +66 -0
- package/src/server/index/websocket-handler.ts +6 -1
- package/src/server/index.ts +16 -13
- package/src/server/management/aside-profile-routes.ts +266 -7
- package/src/server/management/config-routes.ts +18 -2
- package/src/server/management/context.ts +3 -0
- package/src/server/management/integration-routes.ts +287 -5
- package/src/server/management/metrics-routes.ts +20 -0
- package/src/server/management/model-rows.ts +224 -12
- package/src/server/management/route-registry.ts +14 -0
- package/src/server/management/shared.ts +10 -3
- package/src/server/management/system-restart.ts +7 -2
- package/src/server/management/system-routes.ts +2 -0
- package/src/server/management/usage-aggregate-cache.ts +4 -0
- package/src/server/management-api.ts +2 -0
- package/src/server/management-auth.ts +15 -1
- package/src/server/readiness.ts +29 -10
- package/src/server/relay-eager.ts +24 -2
- package/src/server/relay.ts +119 -10
- package/src/server/request-log.ts +55 -2
- package/src/server/request-metrics.ts +236 -0
- package/src/server/responses/adapter-continuation.ts +3 -3
- package/src/server/responses/adapter-dispatch.ts +11 -6
- package/src/server/responses/compact.ts +32 -10
- package/src/server/responses/compaction-routing.ts +111 -0
- package/src/server/responses/core-codex-account.ts +8 -3
- package/src/server/responses/core-combo.ts +7 -7
- package/src/server/responses/core-normalize.ts +6 -12
- package/src/server/responses/core-opaque-recovery.ts +1 -0
- package/src/server/responses/core-options.ts +4 -0
- package/src/server/responses/encrypted-payload.ts +20 -2
- package/src/server/responses/passthrough-delivery.ts +31 -14
- package/src/server/responses/passthrough-dispatch.ts +36 -9
- package/src/server/responses/policy-fallback.ts +5 -13
- package/src/server/responses/request-prepare.ts +67 -17
- package/src/server/responses/request-send-budget.ts +5 -1
- package/src/server/responses/request-sidecar-auth.ts +1 -1
- package/src/server/responses/request-transport.ts +2 -2
- package/src/server/responses/run-turn-execution.ts +25 -3
- package/src/server/responses/sidecar-execution.ts +17 -2
- package/src/server/responses/ws-upstream.ts +14 -27
- package/src/server/responses-custom-tool-repair.ts +27 -54
- package/src/server/responses-undeclared-tool-guard.ts +31 -1
- package/src/server/sse-payload-rewrite.ts +1 -1
- package/src/tray/windows-tray.ps1 +155 -3
- package/src/types/config.ts +11 -3
- package/src/types/provider.ts +18 -0
- package/src/types/request.ts +2 -0
- package/src/types/tools.ts +14 -0
- package/src/types.ts +1 -0
- package/src/vision/eligibility.ts +88 -9
- package/src/vision/plan.ts +34 -10
- package/src/web-search/executor.ts +41 -2
- package/src/web-search/loop.ts +6 -1
- package/gui/dist/assets/index-C5IebErG.js +0 -136
- package/gui/dist/assets/index-OESInAjC.css +0 -1
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
import type { ResponsesTerminalStatus } from "../bridge";
|
|
2
|
+
import type { AttemptRecoveryKind } from "../usage/log";
|
|
3
|
+
|
|
4
|
+
export const REQUEST_METRICS_PROTOCOLS = Object.freeze(["responses", "chat", "messages", "unknown"] as const);
|
|
5
|
+
export const REQUEST_METRICS_RESULTS = Object.freeze(["completed", "failed", "incomplete", "aborted"] as const);
|
|
6
|
+
export const REQUEST_METRICS_RECOVERY_CLASSES = Object.freeze([
|
|
7
|
+
"transient",
|
|
8
|
+
"connection",
|
|
9
|
+
"credential",
|
|
10
|
+
"rate_limit",
|
|
11
|
+
"payload",
|
|
12
|
+
"empty_completion",
|
|
13
|
+
"effort_downgrade",
|
|
14
|
+
"other",
|
|
15
|
+
] as const);
|
|
16
|
+
|
|
17
|
+
export const REQUEST_DURATION_BUCKETS_SECONDS = Object.freeze([0.1, 0.25, 0.5, 1, 2.5, 5, 10, 30, 60] as const);
|
|
18
|
+
export const REQUEST_TTFT_BUCKETS_SECONDS = Object.freeze([0.05, 0.1, 0.25, 0.5, 1, 2.5, 5, 10, 30] as const);
|
|
19
|
+
|
|
20
|
+
export type RequestMetricsProtocol = typeof REQUEST_METRICS_PROTOCOLS[number];
|
|
21
|
+
export type RequestMetricsResult = typeof REQUEST_METRICS_RESULTS[number];
|
|
22
|
+
export type RequestMetricsRecoveryClass = typeof REQUEST_METRICS_RECOVERY_CLASSES[number];
|
|
23
|
+
|
|
24
|
+
export interface RequestMetricFinalFact {
|
|
25
|
+
protocol?: "responses" | "chat" | "messages";
|
|
26
|
+
status: number;
|
|
27
|
+
durationMs: number;
|
|
28
|
+
firstOutputMs?: number;
|
|
29
|
+
terminalStatus?: ResponsesTerminalStatus;
|
|
30
|
+
closeReason?: "terminal" | "client_cancel" | "non_stream" | "body_stall" | "body_overflow";
|
|
31
|
+
attempts?: ReadonlyArray<{
|
|
32
|
+
sendCount: number;
|
|
33
|
+
recoveryKinds: readonly AttemptRecoveryKind[];
|
|
34
|
+
}>;
|
|
35
|
+
spendSends?: number;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface RequestMetricsRecorder {
|
|
39
|
+
recordFinalRequest(fact: RequestMetricFinalFact): void;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface RequestMetricsSnapshotter {
|
|
43
|
+
snapshot(): string;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface RequestMetricsOwner extends RequestMetricsRecorder, RequestMetricsSnapshotter {
|
|
47
|
+
resetForTests(): void;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
interface HistogramCell {
|
|
51
|
+
buckets: number[];
|
|
52
|
+
count: number;
|
|
53
|
+
sum: number;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const protocolCell = (value: RequestMetricsProtocol): number => REQUEST_METRICS_PROTOCOLS.indexOf(value);
|
|
57
|
+
const resultCell = (value: RequestMetricsResult): number => REQUEST_METRICS_RESULTS.indexOf(value);
|
|
58
|
+
const recoveryCell = (value: RequestMetricsRecoveryClass): number => REQUEST_METRICS_RECOVERY_CLASSES.indexOf(value);
|
|
59
|
+
|
|
60
|
+
function matrix(rows: number, columns: number): number[][] {
|
|
61
|
+
return Array.from({ length: rows }, () => Array.from({ length: columns }, () => 0));
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function histograms(bounds: readonly number[]): HistogramCell[][] {
|
|
65
|
+
return Array.from({ length: REQUEST_METRICS_PROTOCOLS.length }, () => (
|
|
66
|
+
Array.from({ length: REQUEST_METRICS_RESULTS.length }, () => ({
|
|
67
|
+
buckets: Array.from({ length: bounds.length + 1 }, () => 0),
|
|
68
|
+
count: 0,
|
|
69
|
+
sum: 0,
|
|
70
|
+
}))
|
|
71
|
+
));
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function classifyResult(fact: RequestMetricFinalFact): RequestMetricsResult {
|
|
75
|
+
if (fact.closeReason === "client_cancel" || fact.status === 499) return "aborted";
|
|
76
|
+
if (fact.terminalStatus === "failed") return "failed";
|
|
77
|
+
if (fact.terminalStatus === "incomplete"
|
|
78
|
+
|| fact.closeReason === "body_stall"
|
|
79
|
+
|| fact.closeReason === "body_overflow") return "incomplete";
|
|
80
|
+
if (fact.terminalStatus === "completed") return "completed";
|
|
81
|
+
if (fact.terminalStatus === undefined
|
|
82
|
+
&& (fact.status === 101 || (fact.status >= 200 && fact.status < 400))) return "completed";
|
|
83
|
+
return "failed";
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function recoveryClass(kind: AttemptRecoveryKind): RequestMetricsRecoveryClass {
|
|
87
|
+
switch (kind) {
|
|
88
|
+
case "transient-5xx": return "transient";
|
|
89
|
+
case "connection-reset": return "connection";
|
|
90
|
+
case "oauth-401":
|
|
91
|
+
case "key-401": return "credential";
|
|
92
|
+
case "key-429":
|
|
93
|
+
case "rate-limit-429":
|
|
94
|
+
case "anthropic-oauth-429":
|
|
95
|
+
case "oauth-account-429": return "rate_limit";
|
|
96
|
+
case "image-413":
|
|
97
|
+
case "console-go-upload-retry":
|
|
98
|
+
case "opaque-blob-rejection": return "payload";
|
|
99
|
+
case "empty-completion": return "empty_completion";
|
|
100
|
+
case "reasoning-effort-downgrade": return "effort_downgrade";
|
|
101
|
+
default: return "other";
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function observeHistogram(cell: HistogramCell, bounds: readonly number[], value: number): void {
|
|
106
|
+
if (!Number.isFinite(value) || value < 0) return;
|
|
107
|
+
cell.count += 1;
|
|
108
|
+
cell.sum += value;
|
|
109
|
+
for (let index = 0; index < bounds.length; index += 1) {
|
|
110
|
+
if (value <= bounds[index]!) cell.buckets[index]! += 1;
|
|
111
|
+
}
|
|
112
|
+
cell.buckets[bounds.length]! += 1;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function sampleLabels(protocol: RequestMetricsProtocol, result?: RequestMetricsResult): string {
|
|
116
|
+
return result === undefined
|
|
117
|
+
? `{protocol="${protocol}"}`
|
|
118
|
+
: `{protocol="${protocol}",result="${result}"}`;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function appendHistogram(
|
|
122
|
+
lines: string[],
|
|
123
|
+
name: string,
|
|
124
|
+
help: string,
|
|
125
|
+
values: HistogramCell[][],
|
|
126
|
+
bounds: readonly number[],
|
|
127
|
+
): void {
|
|
128
|
+
lines.push(`# HELP ${name} ${help}`, `# TYPE ${name} histogram`);
|
|
129
|
+
for (const protocol of REQUEST_METRICS_PROTOCOLS) {
|
|
130
|
+
for (const result of REQUEST_METRICS_RESULTS) {
|
|
131
|
+
const cell = values[protocolCell(protocol)]![resultCell(result)]!;
|
|
132
|
+
for (let index = 0; index < bounds.length; index += 1) {
|
|
133
|
+
lines.push(`${name}_bucket{protocol="${protocol}",result="${result}",le="${bounds[index]}"} ${cell.buckets[index]}`);
|
|
134
|
+
}
|
|
135
|
+
lines.push(`${name}_bucket{protocol="${protocol}",result="${result}",le="+Inf"} ${cell.buckets[bounds.length]}`);
|
|
136
|
+
lines.push(`${name}_sum${sampleLabels(protocol, result)} ${cell.sum}`);
|
|
137
|
+
lines.push(`${name}_count${sampleLabels(protocol, result)} ${cell.count}`);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export function createRequestMetricsOwner(
|
|
143
|
+
processStartTimeSeconds = Date.now() / 1000,
|
|
144
|
+
): RequestMetricsOwner {
|
|
145
|
+
let logicalRequests = matrix(REQUEST_METRICS_PROTOCOLS.length, REQUEST_METRICS_RESULTS.length);
|
|
146
|
+
let physicalSends = Array.from({ length: REQUEST_METRICS_PROTOCOLS.length }, () => 0);
|
|
147
|
+
let recoveries = matrix(REQUEST_METRICS_PROTOCOLS.length, REQUEST_METRICS_RECOVERY_CLASSES.length);
|
|
148
|
+
let durations = histograms(REQUEST_DURATION_BUCKETS_SECONDS);
|
|
149
|
+
let ttft = histograms(REQUEST_TTFT_BUCKETS_SECONDS);
|
|
150
|
+
let missingTtft = matrix(REQUEST_METRICS_PROTOCOLS.length, REQUEST_METRICS_RESULTS.length);
|
|
151
|
+
|
|
152
|
+
return {
|
|
153
|
+
recordFinalRequest(fact): void {
|
|
154
|
+
const protocol: RequestMetricsProtocol = fact.protocol ?? "unknown";
|
|
155
|
+
const result = classifyResult(fact);
|
|
156
|
+
const protocolIndex = protocolCell(protocol);
|
|
157
|
+
const resultIndex = resultCell(result);
|
|
158
|
+
logicalRequests[protocolIndex]![resultIndex]! += 1;
|
|
159
|
+
|
|
160
|
+
const attempts = fact.attempts;
|
|
161
|
+
const sends = attempts === undefined
|
|
162
|
+
? (Number.isInteger(fact.spendSends) && fact.spendSends! >= 0 ? fact.spendSends! : 0)
|
|
163
|
+
: attempts.reduce((total, attempt) => (
|
|
164
|
+
Number.isInteger(attempt.sendCount) && attempt.sendCount >= 0 ? total + attempt.sendCount : total
|
|
165
|
+
), 0);
|
|
166
|
+
physicalSends[protocolIndex]! += sends;
|
|
167
|
+
|
|
168
|
+
for (const attempt of attempts ?? []) {
|
|
169
|
+
for (const kind of new Set(attempt.recoveryKinds)) {
|
|
170
|
+
recoveries[protocolIndex]![recoveryCell(recoveryClass(kind))]! += 1;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
observeHistogram(durations[protocolIndex]![resultIndex]!, REQUEST_DURATION_BUCKETS_SECONDS, fact.durationMs / 1000);
|
|
175
|
+
if (typeof fact.firstOutputMs === "number" && Number.isFinite(fact.firstOutputMs) && fact.firstOutputMs >= 0) {
|
|
176
|
+
observeHistogram(ttft[protocolIndex]![resultIndex]!, REQUEST_TTFT_BUCKETS_SECONDS, fact.firstOutputMs / 1000);
|
|
177
|
+
} else {
|
|
178
|
+
missingTtft[protocolIndex]![resultIndex]! += 1;
|
|
179
|
+
}
|
|
180
|
+
},
|
|
181
|
+
|
|
182
|
+
snapshot(): string {
|
|
183
|
+
const lines: string[] = [
|
|
184
|
+
"# HELP opencodex_logical_requests_total Finalized logical requests in this process.",
|
|
185
|
+
"# TYPE opencodex_logical_requests_total counter",
|
|
186
|
+
];
|
|
187
|
+
for (const protocol of REQUEST_METRICS_PROTOCOLS) {
|
|
188
|
+
for (const result of REQUEST_METRICS_RESULTS) {
|
|
189
|
+
lines.push(`opencodex_logical_requests_total${sampleLabels(protocol, result)} ${logicalRequests[protocolCell(protocol)]![resultCell(result)]}`);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
lines.push(
|
|
193
|
+
"# HELP opencodex_physical_sends_total Upstream sends made by finalized logical requests in this process.",
|
|
194
|
+
"# TYPE opencodex_physical_sends_total counter",
|
|
195
|
+
);
|
|
196
|
+
for (const protocol of REQUEST_METRICS_PROTOCOLS) {
|
|
197
|
+
lines.push(`opencodex_physical_sends_total${sampleLabels(protocol)} ${physicalSends[protocolCell(protocol)]}`);
|
|
198
|
+
}
|
|
199
|
+
lines.push(
|
|
200
|
+
"# HELP opencodex_recoveries_total Distinct recovery kinds observed per physical attempt in this process.",
|
|
201
|
+
"# TYPE opencodex_recoveries_total counter",
|
|
202
|
+
);
|
|
203
|
+
for (const protocol of REQUEST_METRICS_PROTOCOLS) {
|
|
204
|
+
for (const recovery of REQUEST_METRICS_RECOVERY_CLASSES) {
|
|
205
|
+
lines.push(`opencodex_recoveries_total{protocol="${protocol}",recovery="${recovery}"} ${recoveries[protocolCell(protocol)]![recoveryCell(recovery)]}`);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
appendHistogram(lines, "opencodex_request_duration_seconds", "Finalized logical request duration in seconds.", durations, REQUEST_DURATION_BUCKETS_SECONDS);
|
|
209
|
+
appendHistogram(lines, "opencodex_ttft_seconds", "Observed time to first output in seconds.", ttft, REQUEST_TTFT_BUCKETS_SECONDS);
|
|
210
|
+
lines.push(
|
|
211
|
+
"# HELP opencodex_ttft_missing_total Finalized logical requests without an observed time to first output.",
|
|
212
|
+
"# TYPE opencodex_ttft_missing_total counter",
|
|
213
|
+
);
|
|
214
|
+
for (const protocol of REQUEST_METRICS_PROTOCOLS) {
|
|
215
|
+
for (const result of REQUEST_METRICS_RESULTS) {
|
|
216
|
+
lines.push(`opencodex_ttft_missing_total${sampleLabels(protocol, result)} ${missingTtft[protocolCell(protocol)]![resultCell(result)]}`);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
lines.push(
|
|
220
|
+
"# HELP opencodex_metrics_process_start_time_seconds Unix time when this process metrics owner started.",
|
|
221
|
+
"# TYPE opencodex_metrics_process_start_time_seconds gauge",
|
|
222
|
+
`opencodex_metrics_process_start_time_seconds ${processStartTimeSeconds}`,
|
|
223
|
+
);
|
|
224
|
+
return `${lines.join("\n")}\n`;
|
|
225
|
+
},
|
|
226
|
+
|
|
227
|
+
resetForTests(): void {
|
|
228
|
+
logicalRequests = matrix(REQUEST_METRICS_PROTOCOLS.length, REQUEST_METRICS_RESULTS.length);
|
|
229
|
+
physicalSends = Array.from({ length: REQUEST_METRICS_PROTOCOLS.length }, () => 0);
|
|
230
|
+
recoveries = matrix(REQUEST_METRICS_PROTOCOLS.length, REQUEST_METRICS_RECOVERY_CLASSES.length);
|
|
231
|
+
durations = histograms(REQUEST_DURATION_BUCKETS_SECONDS);
|
|
232
|
+
ttft = histograms(REQUEST_TTFT_BUCKETS_SECONDS);
|
|
233
|
+
missingTtft = matrix(REQUEST_METRICS_PROTOCOLS.length, REQUEST_METRICS_RESULTS.length);
|
|
234
|
+
},
|
|
235
|
+
};
|
|
236
|
+
}
|
|
@@ -338,7 +338,7 @@ export function createAdapterContinuations(
|
|
|
338
338
|
route.provider = rotated;
|
|
339
339
|
invalidateSameTargetRequest();
|
|
340
340
|
transportState.activeAdapter = resolveSelectionAdapter(
|
|
341
|
-
resolveWireProtocolOverride(route.providerName, route.modelId, route.provider, inboundWire),
|
|
341
|
+
resolveWireProtocolOverride(route.providerName, route.modelId, route.provider, inboundWire, route.staticPolicy),
|
|
342
342
|
config.cacheRetention,
|
|
343
343
|
);
|
|
344
344
|
bindRouteReasoningReplayScope({
|
|
@@ -384,7 +384,7 @@ export function createAdapterContinuations(
|
|
|
384
384
|
invalidateSameTargetRequest();
|
|
385
385
|
logCtx.provider = formatAnthropicProviderForLog("anthropic", admitted.accountId, config);
|
|
386
386
|
transportState.activeAdapter = resolveSelectionAdapter(
|
|
387
|
-
resolveWireProtocolOverride(route.providerName, route.modelId, route.provider, inboundWire),
|
|
387
|
+
resolveWireProtocolOverride(route.providerName, route.modelId, route.provider, inboundWire, route.staticPolicy),
|
|
388
388
|
config.cacheRetention,
|
|
389
389
|
);
|
|
390
390
|
sealRequestAttemptIdentity(logCtx.activeAttempt, logCtx.provider, transportState.activeAdapter.name, logCtx.accountLogLabel);
|
|
@@ -449,7 +449,7 @@ export function createAdapterContinuations(
|
|
|
449
449
|
if (applied) {
|
|
450
450
|
invalidateSameTargetRequest();
|
|
451
451
|
transportState.activeAdapter = resolveSelectionAdapter(
|
|
452
|
-
resolveWireProtocolOverride(route.providerName, route.modelId, route.provider, inboundWire),
|
|
452
|
+
resolveWireProtocolOverride(route.providerName, route.modelId, route.provider, inboundWire, route.staticPolicy),
|
|
453
453
|
config.cacheRetention,
|
|
454
454
|
);
|
|
455
455
|
bindRouteReasoningReplayScope({
|
|
@@ -246,7 +246,11 @@ export async function prepareAdapterExchange(
|
|
|
246
246
|
return new Response(JSON.stringify(json), { headers: { "Content-Type": "application/json" } });
|
|
247
247
|
}
|
|
248
248
|
try {
|
|
249
|
-
initialRequest = await transportState.activeAdapter.buildRequest(parsed, {
|
|
249
|
+
initialRequest = await transportState.activeAdapter.buildRequest(parsed, {
|
|
250
|
+
headers: requestState.selectedForwardHeaders,
|
|
251
|
+
translatorBudget,
|
|
252
|
+
abortSignal: upstream.signal,
|
|
253
|
+
});
|
|
250
254
|
refreshRequestToolAliases(initialRequest);
|
|
251
255
|
recordAdapterReasoning(logCtx, initialRequest);
|
|
252
256
|
recordAdapterTier(logCtx, initialRequest);
|
|
@@ -405,6 +409,7 @@ export async function prepareAdapterExchange(
|
|
|
405
409
|
retryRequest = await transportState.activeAdapter.buildRequest(parsed, {
|
|
406
410
|
headers: requestState.selectedForwardHeaders,
|
|
407
411
|
translatorBudget,
|
|
412
|
+
abortSignal: upstream.signal,
|
|
408
413
|
...(transportState.imageTierBias > 0 ? { imageTierBias: transportState.imageTierBias } : {}),
|
|
409
414
|
});
|
|
410
415
|
recordAdapterReasoning(logCtx, retryRequest);
|
|
@@ -581,7 +586,7 @@ export async function prepareAdapterExchange(
|
|
|
581
586
|
route.provider = refreshedProvider;
|
|
582
587
|
invalidateSameTargetRequest();
|
|
583
588
|
transportState.activeAdapter = resolveSelectionAdapter(
|
|
584
|
-
resolveWireProtocolOverride(route.providerName, route.modelId, refreshedProvider, inboundWire),
|
|
589
|
+
resolveWireProtocolOverride(route.providerName, route.modelId, refreshedProvider, inboundWire, route.staticPolicy),
|
|
585
590
|
config.cacheRetention,
|
|
586
591
|
);
|
|
587
592
|
bindRouteReasoningReplayScope({
|
|
@@ -614,7 +619,7 @@ export async function prepareAdapterExchange(
|
|
|
614
619
|
route.provider = rotated;
|
|
615
620
|
invalidateSameTargetRequest();
|
|
616
621
|
transportState.activeAdapter = resolveSelectionAdapter(
|
|
617
|
-
resolveWireProtocolOverride(route.providerName, route.modelId, route.provider, inboundWire),
|
|
622
|
+
resolveWireProtocolOverride(route.providerName, route.modelId, route.provider, inboundWire, route.staticPolicy),
|
|
618
623
|
config.cacheRetention,
|
|
619
624
|
);
|
|
620
625
|
bindRouteReasoningReplayScope({
|
|
@@ -714,7 +719,7 @@ export async function prepareAdapterExchange(
|
|
|
714
719
|
route.provider = rotated;
|
|
715
720
|
invalidateSameTargetRequest();
|
|
716
721
|
transportState.activeAdapter = resolveSelectionAdapter(
|
|
717
|
-
resolveWireProtocolOverride(route.providerName, route.modelId, route.provider, inboundWire),
|
|
722
|
+
resolveWireProtocolOverride(route.providerName, route.modelId, route.provider, inboundWire, route.staticPolicy),
|
|
718
723
|
config.cacheRetention,
|
|
719
724
|
);
|
|
720
725
|
bindRouteReasoningReplayScope({
|
|
@@ -757,7 +762,7 @@ export async function prepareAdapterExchange(
|
|
|
757
762
|
invalidateSameTargetRequest();
|
|
758
763
|
logCtx.provider = formatAnthropicProviderForLog("anthropic", admitted.accountId, config);
|
|
759
764
|
transportState.activeAdapter = resolveSelectionAdapter(
|
|
760
|
-
resolveWireProtocolOverride(route.providerName, route.modelId, route.provider, inboundWire),
|
|
765
|
+
resolveWireProtocolOverride(route.providerName, route.modelId, route.provider, inboundWire, route.staticPolicy),
|
|
761
766
|
config.cacheRetention,
|
|
762
767
|
);
|
|
763
768
|
sealRequestAttemptIdentity(logCtx.activeAttempt, logCtx.provider, transportState.activeAdapter.name, logCtx.accountLogLabel);
|
|
@@ -832,7 +837,7 @@ export async function prepareAdapterExchange(
|
|
|
832
837
|
}
|
|
833
838
|
invalidateSameTargetRequest();
|
|
834
839
|
transportState.activeAdapter = resolveSelectionAdapter(
|
|
835
|
-
resolveWireProtocolOverride(route.providerName, route.modelId, route.provider, inboundWire),
|
|
840
|
+
resolveWireProtocolOverride(route.providerName, route.modelId, route.provider, inboundWire, route.staticPolicy),
|
|
836
841
|
config.cacheRetention,
|
|
837
842
|
);
|
|
838
843
|
sealRequestAttemptIdentity(logCtx.activeAttempt, logCtx.provider, transportState.activeAdapter.name, logCtx.accountLogLabel);
|
|
@@ -10,6 +10,7 @@ import { parseRequest } from "../../responses/parser";
|
|
|
10
10
|
import { buildCompactV1Output, COMPACT_PROMPT, decodeCompactionSummary, extractCompactUserMessages } from "../../responses/compaction";
|
|
11
11
|
import { FORWARD_HEADERS, sanitizeReasoningInputContent } from "../../adapters/openai-responses";
|
|
12
12
|
import { expandPreviousResponseInput, previousResponseProviderState, rememberResponseState } from "../../responses/state";
|
|
13
|
+
import { repairLegacyDottedToolCallNames } from "../../responses/legacy-dotted-tool-name-repair";
|
|
13
14
|
import { NoEligiblePolicyCandidateError, routeCompactionModel } from "../../router";
|
|
14
15
|
import { evidenceFromBody } from "../../routing/request-evidence";
|
|
15
16
|
import {
|
|
@@ -124,7 +125,12 @@ import { slugsEquivalent } from "../../providers/slug-codec";
|
|
|
124
125
|
import { decideTier, tierValueAfterDecision } from "../../providers/fastwire";
|
|
125
126
|
import { fastPolicyForModel } from "../../providers/service-tier";
|
|
126
127
|
import { parseFastOnlyRowId } from "../fast-row";
|
|
127
|
-
import {
|
|
128
|
+
import { captureOpenAiVirtualWirePolicy, resolveOpenAiCompactModel } from "../../providers/openai-virtual-models";
|
|
129
|
+
import {
|
|
130
|
+
applyCompactionRoutingOverride,
|
|
131
|
+
compactionRoutingKeepsProviderIdentity,
|
|
132
|
+
type CompactionRoutingOverride,
|
|
133
|
+
} from "./compaction-routing";
|
|
128
134
|
import { isUsageDebugEnabled } from "../../usage/debug";
|
|
129
135
|
import {
|
|
130
136
|
readJsonRequestBody,
|
|
@@ -271,6 +277,7 @@ function compactHandoffRoute(
|
|
|
271
277
|
}
|
|
272
278
|
|
|
273
279
|
export interface HandleResponsesCompactOptions {
|
|
280
|
+
compactionRoutingOverride?: CompactionRoutingOverride | null;
|
|
274
281
|
nativeMainRefreshDependencies?: NativeMainRefreshDependencies;
|
|
275
282
|
/** Release the listener's idle guard only after the complete request body is accepted. */
|
|
276
283
|
onRequestBodyRead?: () => void;
|
|
@@ -603,6 +610,9 @@ export async function handleResponsesCompact(
|
|
|
603
610
|
if (!body || typeof body !== "object" || Array.isArray(body)) {
|
|
604
611
|
return formatErrorResponse(400, "invalid_request_error", "Invalid compaction request body");
|
|
605
612
|
}
|
|
613
|
+
if (!options.compactionRoutingOverride) {
|
|
614
|
+
options = { ...options, compactionRoutingOverride: applyCompactionRoutingOverride(body, req.headers, config, { endpoint: "compact" }) };
|
|
615
|
+
}
|
|
606
616
|
const raw = body as { model?: unknown; input?: unknown };
|
|
607
617
|
if (typeof raw.model !== "string" || raw.model.length === 0) {
|
|
608
618
|
return formatErrorResponse(400, "invalid_request_error", "compaction request requires a model");
|
|
@@ -617,11 +627,12 @@ export async function handleResponsesCompact(
|
|
|
617
627
|
// The client's own selector, kept for the request log: `raw.model` is rewritten to the
|
|
618
628
|
// base id above, and logCtx.requestedModel is assigned from it further down, so without
|
|
619
629
|
// this the log would lose which id the client actually asked for.
|
|
620
|
-
const compactRequestedModel =
|
|
630
|
+
const compactRequestedModel = options.compactionRoutingOverride?.sourceModel
|
|
631
|
+
?? (compactFastRow ? compactFastRow.baseId + "--fast" : raw.model);
|
|
621
632
|
|
|
622
633
|
// Recall the last completed client-visible bare model after a combo switch (#3891).
|
|
623
634
|
// Configured selectors take precedence over this implicit session hint.
|
|
624
|
-
if (typeof compactModel === "string" && !compactModel.includes("/") && !compactFastRow
|
|
635
|
+
if (!options.compactionRoutingOverride && typeof compactModel === "string" && !compactModel.includes("/") && !compactFastRow
|
|
625
636
|
&& !resolveComboId(config, compactModel)) {
|
|
626
637
|
const recalledComboId = recallComboForLane(config, sessionLaneIdFromRequest(req.headers), compactModel);
|
|
627
638
|
if (recalledComboId) {
|
|
@@ -684,6 +695,7 @@ export async function handleResponsesCompact(
|
|
|
684
695
|
const virtual = resolveOpenAiCompactModel(route.providerName, selectedModelId);
|
|
685
696
|
if (virtual) {
|
|
686
697
|
route.modelId = virtual.wireModelId;
|
|
698
|
+
captureOpenAiVirtualWirePolicy(route, virtual);
|
|
687
699
|
logCtx.model = virtual.selectedModelId;
|
|
688
700
|
logCtx.resolvedModel = virtual.wireModelId;
|
|
689
701
|
} else {
|
|
@@ -747,7 +759,12 @@ export async function handleResponsesCompact(
|
|
|
747
759
|
// no budget at all, so `handleResponsesInner` minted a fresh four after the native attempt
|
|
748
760
|
// had already spent some of the first one.
|
|
749
761
|
const sendBudget: RequestExecutionBudget = options.sendBudget ?? createRequestExecutionBudget();
|
|
750
|
-
|
|
762
|
+
// A manual override onto another backend must not mint ciphertext the conversation model cannot replay.
|
|
763
|
+
const manualOverrideCrossesProvider = options.compactionRoutingOverride
|
|
764
|
+
? !compactionRoutingKeepsProviderIdentity(config, options.compactionRoutingOverride, route)
|
|
765
|
+
: false;
|
|
766
|
+
if (supportsNativeResponsesCompactEndpoint(route.providerName, route.provider) && !accountGatedCompactWireModel && !route.combo
|
|
767
|
+
&& !manualOverrideCrossesProvider) {
|
|
751
768
|
if (req.signal.aborted) {
|
|
752
769
|
return formatErrorResponse(499, "client_cancelled", "Client cancelled compact request");
|
|
753
770
|
}
|
|
@@ -845,7 +862,12 @@ export async function handleResponsesCompact(
|
|
|
845
862
|
// The regular /v1/responses path applies sanitizeReasoningInputContent via the adapter's
|
|
846
863
|
// buildRequest, but the compact endpoint forwards directly. Apply the same sanitizer here
|
|
847
864
|
// so routed-model reasoning items (reasoning_text content) don't 400 the ChatGPT backend.
|
|
848
|
-
|
|
865
|
+
// #5095: the compact endpoint forwards `raw` directly, so it needs the same legacy dotted
|
|
866
|
+
// call-name repair the adapter applies. A damaged item here refuses the compaction itself,
|
|
867
|
+
// which is the request a long task depends on to keep going.
|
|
868
|
+
const compactBody = repairLegacyDottedToolCallNames(
|
|
869
|
+
sanitizeReasoningInputContent(compactBodyRaw),
|
|
870
|
+
) as typeof compactBodyRaw;
|
|
849
871
|
{
|
|
850
872
|
const binding = conversationStateBindingFromAuth(authCtx, codexPoolAffinityKey(req.headers));
|
|
851
873
|
if (binding) {
|
|
@@ -1303,9 +1325,9 @@ export async function handleResponsesCompact(
|
|
|
1303
1325
|
// synthetic buffer errors are not upstream bodies and stay uninspected.
|
|
1304
1326
|
if (buffered.ok) {
|
|
1305
1327
|
inspectResponseLogJson(logCtx, await buffered.clone().text());
|
|
1306
|
-
forgetCompactHandoffRoute(req, admission);
|
|
1328
|
+
if (!options.compactionRoutingOverride) forgetCompactHandoffRoute(req, admission);
|
|
1307
1329
|
rememberServingConversationStateIssuer(outcomeCtx, codexPoolAffinityKey(req.headers));
|
|
1308
|
-
} else if (quotaFailure && !storedPool401ReplayAttempted) {
|
|
1330
|
+
} else if (!options.compactionRoutingOverride && quotaFailure && !storedPool401ReplayAttempted) {
|
|
1309
1331
|
const fallbackModel = compactHandoffRoute(req, admission, raw.model);
|
|
1310
1332
|
if (fallbackModel && !req.signal.aborted) {
|
|
1311
1333
|
const fallbackReq = new Request(req.url, {
|
|
@@ -1368,7 +1390,7 @@ export async function handleResponsesCompact(
|
|
|
1368
1390
|
// The routed compaction turn is a handoff inside the same logical request, so it draws the
|
|
1369
1391
|
// REMAINDER. Minting here is what let a native attempt spend three sends and the routed
|
|
1370
1392
|
// fallback spend four more.
|
|
1371
|
-
const response = await handleResponses(internalReq, config, logCtx, { abortSignal: req.signal, turnAdmissionLease, sendBudget, ...(admission ? { admission } : {}) });
|
|
1393
|
+
const response = await handleResponses(internalReq, config, logCtx, { abortSignal: req.signal, turnAdmissionLease, sendBudget, compactionRoutingOverride: options.compactionRoutingOverride, ...(admission ? { admission } : {}) });
|
|
1372
1394
|
if (!response.ok) return response;
|
|
1373
1395
|
let json: { output?: unknown[]; status?: unknown; error?: unknown };
|
|
1374
1396
|
if (response.headers.get("content-type")?.includes("text/event-stream")) {
|
|
@@ -1438,7 +1460,7 @@ export async function handleResponsesCompact(
|
|
|
1438
1460
|
const result = new Response(JSON.stringify({ output: compactionItems }), {
|
|
1439
1461
|
headers: { "Content-Type": "application/json" },
|
|
1440
1462
|
});
|
|
1441
|
-
rememberCompactHandoffRoute(req, admission, raw.model);
|
|
1463
|
+
if (!options.compactionRoutingOverride) rememberCompactHandoffRoute(req, admission, raw.model);
|
|
1442
1464
|
return result;
|
|
1443
1465
|
}
|
|
1444
1466
|
const encrypted = compactionItems[0]!.encrypted_content;
|
|
@@ -1449,6 +1471,6 @@ export async function handleResponsesCompact(
|
|
|
1449
1471
|
}
|
|
1450
1472
|
const summary = decoded;
|
|
1451
1473
|
const output = buildCompactV1Output(extractCompactUserMessages(inputItems), summary);
|
|
1452
|
-
rememberCompactHandoffRoute(req, admission, raw.model);
|
|
1474
|
+
if (!options.compactionRoutingOverride) rememberCompactHandoffRoute(req, admission, raw.model);
|
|
1453
1475
|
return new Response(JSON.stringify({ output }), { headers: { "Content-Type": "application/json" } });
|
|
1454
1476
|
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import type { OcxConfig } from "../../types";
|
|
2
|
+
import { isDeclaredReasoningEffort } from "../../reasoning-effort";
|
|
3
|
+
import { COMPACTION_TRIGGERS } from "../../config/schema/compaction-triggers";
|
|
4
|
+
import { routeConcreteModel, type RouteResult } from "../../router";
|
|
5
|
+
import { resolveComboId } from "../../combos/identifiers";
|
|
6
|
+
import { recallComboForLane } from "./combo-session-recall";
|
|
7
|
+
import { sessionLaneIdFromRequest } from "../request-log-conversation";
|
|
8
|
+
|
|
9
|
+
/** `sourceModel` is the conversation's own selector before the rewrite. */
|
|
10
|
+
export interface CompactionRoutingOverride {
|
|
11
|
+
sourceModel: string;
|
|
12
|
+
/** Combo the lane remembers for a bare `sourceModel` (#3891); the conversation resumes there, not on the bare route. */
|
|
13
|
+
sourceCombo?: string;
|
|
14
|
+
/** Combo the configured override resolves to; its children route concretely but stay portable. */
|
|
15
|
+
targetCombo?: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function record(value: unknown): Record<string, unknown> | undefined {
|
|
19
|
+
return value !== null && typeof value === "object" && !Array.isArray(value)
|
|
20
|
+
? value as Record<string, unknown>
|
|
21
|
+
: undefined;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Triggers this override covers. Absent means manual only, which is the narrowest
|
|
26
|
+
* reading of the setting and leaves automatic compaction exactly as it routes today.
|
|
27
|
+
* A hand-edited value the schema would have rejected disables the override rather
|
|
28
|
+
* than widening it, so a malformed edit can never route more than it names.
|
|
29
|
+
*/
|
|
30
|
+
function configuredTriggers(value: unknown): readonly string[] | null {
|
|
31
|
+
if (value === undefined) return ["manual"];
|
|
32
|
+
if (!Array.isArray(value) || value.length === 0) return null;
|
|
33
|
+
if (!value.every(entry => typeof entry === "string" && (COMPACTION_TRIGGERS as readonly string[]).includes(entry))) return null;
|
|
34
|
+
if (new Set(value).size !== value.length) return null;
|
|
35
|
+
return value as readonly string[];
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface CompactionRoutingOverrideOptions {
|
|
39
|
+
/** `responses` requires a `compaction_trigger` input item; the native compact endpoint carries none. */
|
|
40
|
+
endpoint?: "responses" | "compact";
|
|
41
|
+
transport?: "websocket";
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function applyCompactionRoutingOverride(
|
|
45
|
+
body: unknown,
|
|
46
|
+
headers: Headers,
|
|
47
|
+
config: OcxConfig,
|
|
48
|
+
options: CompactionRoutingOverrideOptions = {},
|
|
49
|
+
): CompactionRoutingOverride | null {
|
|
50
|
+
const override = config.compactionRouting;
|
|
51
|
+
const raw = record(body);
|
|
52
|
+
if (!raw || typeof raw.model !== "string" || !raw.model.trim()
|
|
53
|
+
|| typeof override?.model !== "string" || !override.model.trim()) return null;
|
|
54
|
+
if (override.reasoningEffort !== undefined
|
|
55
|
+
&& (typeof override.reasoningEffort !== "string" || !isDeclaredReasoningEffort(override.reasoningEffort))) return null;
|
|
56
|
+
const triggers = configuredTriggers(override.triggers);
|
|
57
|
+
if (!triggers) return null;
|
|
58
|
+
if (options.endpoint !== "compact"
|
|
59
|
+
&& !(Array.isArray(raw.input) && raw.input.some(item => record(item)?.type === "compaction_trigger"))) return null;
|
|
60
|
+
|
|
61
|
+
const metadata: unknown[] = [];
|
|
62
|
+
const header = headers.get("x-codex-turn-metadata");
|
|
63
|
+
if (options.transport !== "websocket" && header !== null) metadata.push(header);
|
|
64
|
+
const client = record(raw.client_metadata);
|
|
65
|
+
if (client && Object.hasOwn(client, "x-codex-turn-metadata")) metadata.push(client["x-codex-turn-metadata"]);
|
|
66
|
+
if (metadata.length === 0) return null;
|
|
67
|
+
let trigger: string | undefined;
|
|
68
|
+
for (const value of metadata) {
|
|
69
|
+
if (typeof value !== "string") return null;
|
|
70
|
+
try {
|
|
71
|
+
const parsed = record(JSON.parse(value));
|
|
72
|
+
if (parsed?.request_kind !== "compaction") return null;
|
|
73
|
+
const carried = record(parsed.compaction)?.trigger;
|
|
74
|
+
if (typeof carried !== "string" || !triggers.includes(carried)) return null;
|
|
75
|
+
// Copies that name different triggers are not agreement, and picking either one would
|
|
76
|
+
// let a caller widen an override by disagreeing with itself.
|
|
77
|
+
if (trigger !== undefined && trigger !== carried) return null;
|
|
78
|
+
trigger = carried;
|
|
79
|
+
} catch {
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
if (trigger === undefined) return null;
|
|
84
|
+
|
|
85
|
+
const sourceModel = raw.model;
|
|
86
|
+
const sourceCombo = recallComboForLane(config, sessionLaneIdFromRequest(headers), sourceModel);
|
|
87
|
+
const targetCombo = resolveComboId(config, override.model.trim()) ?? undefined;
|
|
88
|
+
raw.model = override.model.trim();
|
|
89
|
+
if (override.reasoningEffort !== undefined) {
|
|
90
|
+
raw.reasoning = { ...record(raw.reasoning), effort: override.reasoningEffort };
|
|
91
|
+
}
|
|
92
|
+
return { sourceModel, ...(sourceCombo ? { sourceCombo } : {}), ...(targetCombo ? { targetCombo } : {}) };
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** Same provider identity keeps caller auth and may use native compact; its ciphertext replays only there. */
|
|
96
|
+
export function compactionRoutingKeepsProviderIdentity(
|
|
97
|
+
config: OcxConfig,
|
|
98
|
+
override: CompactionRoutingOverride,
|
|
99
|
+
route: RouteResult,
|
|
100
|
+
): boolean {
|
|
101
|
+
if (route.combo || override.sourceCombo || override.targetCombo || resolveComboId(config, override.sourceModel)) return false;
|
|
102
|
+
let source: RouteResult;
|
|
103
|
+
try {
|
|
104
|
+
source = routeConcreteModel(config, override.sourceModel);
|
|
105
|
+
} catch {
|
|
106
|
+
return false;
|
|
107
|
+
}
|
|
108
|
+
return source.providerName === route.providerName
|
|
109
|
+
&& source.codexAccountMode === route.codexAccountMode
|
|
110
|
+
&& source.codexAccountNamespace === route.codexAccountNamespace;
|
|
111
|
+
}
|
|
@@ -328,7 +328,8 @@ export interface CodexPoolAccountRetryArgs {
|
|
|
328
328
|
/** Sanitized caller input, before any selected Pool credential was materialized. */
|
|
329
329
|
callerAuthHeaders: Headers;
|
|
330
330
|
config: OcxConfig;
|
|
331
|
-
|
|
331
|
+
/** Actual routed result narrowed to the fields this retry consumes. */
|
|
332
|
+
route: Pick<RouteResult, "providerName" | "modelId" | "provider" | "staticPolicy">;
|
|
332
333
|
parsed: OcxParsedRequest;
|
|
333
334
|
logCtx: RequestLogContext;
|
|
334
335
|
options: {
|
|
@@ -681,7 +682,7 @@ export async function retryCodexPoolOnAlternateAccount(
|
|
|
681
682
|
"pool",
|
|
682
683
|
);
|
|
683
684
|
const retryAdapter = resolveAdapter(
|
|
684
|
-
resolveWireProtocolOverride(route.providerName, route.modelId, retryProvider, inboundWire),
|
|
685
|
+
resolveWireProtocolOverride(route.providerName, route.modelId, retryProvider, inboundWire, route.staticPolicy),
|
|
685
686
|
config.cacheRetention,
|
|
686
687
|
route.providerName,
|
|
687
688
|
);
|
|
@@ -841,7 +842,11 @@ export async function retryCodexPoolOnAlternateAccount(
|
|
|
841
842
|
parsed.modelId,
|
|
842
843
|
);
|
|
843
844
|
if (retryModelDenial !== undefined) {
|
|
844
|
-
recordCodexModelDenialEvidence(
|
|
845
|
+
recordCodexModelDenialEvidence(
|
|
846
|
+
retryAuthCtx.accountId,
|
|
847
|
+
retryModelDenial,
|
|
848
|
+
retryAuthCtx.kind === "pool" ? retryAuthCtx.generation : undefined,
|
|
849
|
+
);
|
|
845
850
|
}
|
|
846
851
|
if (!retrySameConfirmedAccount || retrySendCount >= maxRetrySends) break;
|
|
847
852
|
// Caller-owned main is an alternate-account replay and can never enter the bounded
|
|
@@ -29,7 +29,6 @@ import {
|
|
|
29
29
|
import { formatErrorResponse } from "../../bridge";
|
|
30
30
|
import {
|
|
31
31
|
expandPreviousResponseInput,
|
|
32
|
-
previousResponseScopeMismatch,
|
|
33
32
|
previousResponseReplayFailure,
|
|
34
33
|
previousResponseProviderState,
|
|
35
34
|
} from "../../responses/state";
|
|
@@ -201,11 +200,12 @@ export async function executeComboResponses(
|
|
|
201
200
|
// imageInput is disabled (and so targets see the full replayed input).
|
|
202
201
|
const inboundClientThreadId = req.headers.get("x-codex-parent-thread-id")?.trim() || undefined;
|
|
203
202
|
const body = expandPreviousResponseInput(rawBody, inboundClientThreadId);
|
|
204
|
-
const
|
|
205
|
-
if (
|
|
206
|
-
console.warn("[opencodex]
|
|
203
|
+
const replayFailure = previousResponseReplayFailure(body);
|
|
204
|
+
if (replayFailure?.reason === "scope_mismatch") {
|
|
205
|
+
console.warn("[opencodex] refusing continuation because the client task scope does not match replay state");
|
|
207
206
|
}
|
|
208
|
-
|
|
207
|
+
// Local replay failures require full client replay.
|
|
208
|
+
if (replayFailure) {
|
|
209
209
|
return formatErrorResponse(
|
|
210
210
|
400,
|
|
211
211
|
"previous_response_not_found",
|
|
@@ -235,7 +235,7 @@ export async function executeComboResponses(
|
|
|
235
235
|
sourceBody: body,
|
|
236
236
|
previousResponseInputExpanded: body !== rawBody
|
|
237
237
|
&& typeof (body as { previous_response_id?: unknown }).previous_response_id === "string",
|
|
238
|
-
providerContinuation:
|
|
238
|
+
providerContinuation: body !== rawBody && requestedPreviousId
|
|
239
239
|
? previousResponseProviderState(requestedPreviousId)
|
|
240
240
|
: undefined,
|
|
241
241
|
recoveredPlaintext: false,
|
|
@@ -521,7 +521,7 @@ export async function executeComboResponses(
|
|
|
521
521
|
// The live config can change while the child is streaming. Never retain credentials.
|
|
522
522
|
const currentCombo = getCombo(config, comboId);
|
|
523
523
|
const provider = config.providers[completedTarget.provider];
|
|
524
|
-
if (Object.hasOwn(config.providers, completedTarget.provider)
|
|
524
|
+
if (!options.compactionRoutingOverride && Object.hasOwn(config.providers, completedTarget.provider)
|
|
525
525
|
&& provider && provider.disabled !== true
|
|
526
526
|
&& currentCombo?.targets.some(target => targetKey(target) === targetKey(completedTarget))) {
|
|
527
527
|
rememberComboForLane(sessionLaneIdFromRequest(req.headers), comboId, completedTarget, model, writerGeneration);
|