@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,279 @@
|
|
|
1
|
+
// Bounded classification of a PARTIAL freeform tool-call wrapper.
|
|
2
|
+
//
|
|
3
|
+
// `unwrapFreeformToolInput` decides the completed input with `JSON.parse`, which does not care
|
|
4
|
+
// what order an object's properties arrive in or how their names are spelled. The streaming side
|
|
5
|
+
// has to reach the same answer from a prefix, and it used to do that by comparing the buffer
|
|
6
|
+
// against the literal `{"input":"`. Spellings `JSON.parse` calls identical therefore matched no
|
|
7
|
+
// wrapper at all, streamed as raw JSON, and then completed as the unwrapped body:
|
|
8
|
+
// `{"metadata":1,"input":"cmd"}` and `{"\u0069nput":"cmd"}` both previewed the whole object and
|
|
9
|
+
// completed as `cmd` (#5151). #5047 and #5129 closed the compact and whitespace spellings of the
|
|
10
|
+
// same disagreement; reordering and escaping are the two the literal matcher could never see,
|
|
11
|
+
// because RFC 8259 objects are unordered and their names are strings with escapes.
|
|
12
|
+
//
|
|
13
|
+
// So the prefix is scanned as JSON instead of matched as text. The scan answers one question —
|
|
14
|
+
// which wrapper, if any, the completed text will unwrap to — and it answers it three ways:
|
|
15
|
+
//
|
|
16
|
+
// `input` the canonical key is present with a string value, whatever preceded it and however
|
|
17
|
+
// its name was spelled. Its value can be decoded progressively, because
|
|
18
|
+
// `unwrapFreeformToolInput` gives an own `input` precedence over every other property.
|
|
19
|
+
// `raw` no wrapper can apply, because the text is not an object or because it is one that
|
|
20
|
+
// `JSON.parse` will reject. Completion returns the buffer, so streaming it agrees.
|
|
21
|
+
// `hold` undecided. A key that has not arrived yet can still change the answer, so nothing
|
|
22
|
+
// is published until the object parses and completion's own rule decides.
|
|
23
|
+
//
|
|
24
|
+
// The bound matters as much as the classification. A scan that walks the whole buffer on every
|
|
25
|
+
// delta is quadratic in the argument size, so classification gives up after
|
|
26
|
+
// `MAX_FREEFORM_WRAPPER_SCAN_CHARS` and holds. Giving up costs preview, never agreement: a held
|
|
27
|
+
// buffer is still resolved by the authoritative parse at completion.
|
|
28
|
+
|
|
29
|
+
/** The insignificant whitespace `JSON.parse` accepts between tokens. */
|
|
30
|
+
export const JSON_WHITESPACE = new Set([" ", "\t", "\n", "\r"]);
|
|
31
|
+
|
|
32
|
+
/** The two-character escapes JSON defines, and nothing else. */
|
|
33
|
+
export const JSON_ESCAPES = new Map<string, string>([
|
|
34
|
+
['"', '"'], ["\\", "\\"], ["/", "/"],
|
|
35
|
+
["b", "\b"], ["f", "\f"], ["n", "\n"], ["r", "\r"], ["t", "\t"],
|
|
36
|
+
]);
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* How far a classification scan walks before it gives up and holds.
|
|
40
|
+
*
|
|
41
|
+
* A wrapper's own structure — its property names and the small scalar siblings a model puts
|
|
42
|
+
* beside the body — lives at the front of the object. A buffer still undecided after this much
|
|
43
|
+
* scanning is one whose preceding values are large, and holding it is what the routed
|
|
44
|
+
* restoration path already does for every unrecognized object.
|
|
45
|
+
*/
|
|
46
|
+
export const MAX_FREEFORM_WRAPPER_SCAN_CHARS = 4096;
|
|
47
|
+
|
|
48
|
+
const CANONICAL_KEY = "input";
|
|
49
|
+
|
|
50
|
+
/** The buffer ends inside a token: what follows can still change the answer. */
|
|
51
|
+
const HOLD = -1;
|
|
52
|
+
/** `JSON.parse` will reject this text no matter what is appended to it. */
|
|
53
|
+
const NEVER = -2;
|
|
54
|
+
|
|
55
|
+
export type FreeformWrapperScan =
|
|
56
|
+
| { kind: "hold"; parse: boolean }
|
|
57
|
+
| { kind: "raw" }
|
|
58
|
+
| { kind: "input"; valueStart: number };
|
|
59
|
+
|
|
60
|
+
/** Index of the first non-whitespace character at or after `from`, or HOLD past the end. */
|
|
61
|
+
function skipWhitespace(text: string, from: number): number {
|
|
62
|
+
let i = from;
|
|
63
|
+
while (i < text.length && JSON_WHITESPACE.has(text[i]!)) i++;
|
|
64
|
+
return i < text.length ? i : HOLD;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Index just past the complete JSON string opening at `from`, or HOLD / NEVER.
|
|
69
|
+
*
|
|
70
|
+
* This reports a BOUNDARY where `decodeJsonStringPrefix` reports a decoded PREFIX: one has to
|
|
71
|
+
* fail on a truncated string and the other has to return what it decoded so far. They answer
|
|
72
|
+
* different questions about the same bytes and share one escape table so they cannot disagree
|
|
73
|
+
* about which escapes exist.
|
|
74
|
+
*/
|
|
75
|
+
function scanString(text: string, from: number): number {
|
|
76
|
+
let i = from + 1;
|
|
77
|
+
while (i < text.length) {
|
|
78
|
+
const c = text[i]!;
|
|
79
|
+
if (c === '"') return i + 1;
|
|
80
|
+
if (c === "\\") {
|
|
81
|
+
const n = text[i + 1];
|
|
82
|
+
if (n === undefined) return HOLD;
|
|
83
|
+
if (n === "u") {
|
|
84
|
+
const hex = text.slice(i + 2, i + 6);
|
|
85
|
+
if (hex.length < 4) return HOLD;
|
|
86
|
+
if (!/^[0-9a-fA-F]{4}$/.test(hex)) return NEVER;
|
|
87
|
+
i += 6;
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
if (!JSON_ESCAPES.has(n)) return NEVER;
|
|
91
|
+
i += 2;
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
// A literal control character is not legal inside a JSON string.
|
|
95
|
+
if (c.charCodeAt(0) <= 0x1f) return NEVER;
|
|
96
|
+
i++;
|
|
97
|
+
}
|
|
98
|
+
return HOLD;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const JSON_NUMBER_CHARS = /[-+0-9.eE]/;
|
|
102
|
+
const JSON_NUMBER = /^-?(?:0|[1-9][0-9]*)(?:\.[0-9]+)?(?:[eE][+-]?[0-9]+)?$/;
|
|
103
|
+
|
|
104
|
+
/** Index just past one complete scalar at `from`, or HOLD / NEVER. */
|
|
105
|
+
function scanScalar(text: string, from: number): number {
|
|
106
|
+
const c = text[from]!;
|
|
107
|
+
if (c === '"') return scanString(text, from);
|
|
108
|
+
if (c === "t" || c === "f" || c === "n") {
|
|
109
|
+
const literal = c === "t" ? "true" : c === "f" ? "false" : "null";
|
|
110
|
+
if (text.startsWith(literal, from)) return from + literal.length;
|
|
111
|
+
return literal.startsWith(text.slice(from)) ? HOLD : NEVER;
|
|
112
|
+
}
|
|
113
|
+
let end = from;
|
|
114
|
+
while (end < text.length && JSON_NUMBER_CHARS.test(text[end]!)) end++;
|
|
115
|
+
if (end === from) return NEVER;
|
|
116
|
+
// A number running to the end of the buffer can still grow another digit or exponent.
|
|
117
|
+
if (end >= text.length) return HOLD;
|
|
118
|
+
return JSON_NUMBER.test(text.slice(from, end)) ? end : NEVER;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** Index where the value of the member whose name opens at `from` begins, or HOLD / NEVER. */
|
|
122
|
+
function scanMemberKey(text: string, from: number): number {
|
|
123
|
+
if (text[from] !== '"') return NEVER;
|
|
124
|
+
const nameEnd = scanString(text, from);
|
|
125
|
+
if (nameEnd < 0) return nameEnd;
|
|
126
|
+
const colon = skipWhitespace(text, nameEnd);
|
|
127
|
+
if (colon === HOLD) return HOLD;
|
|
128
|
+
if (text[colon] !== ":") return NEVER;
|
|
129
|
+
return skipWhitespace(text, colon + 1);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Index just past one complete JSON value at `from`, or HOLD / NEVER.
|
|
134
|
+
*
|
|
135
|
+
* Containers are walked with an explicit stack rather than recursion: the value being skipped
|
|
136
|
+
* is provider-controlled, and a deeply nested one must not be able to exhaust the call stack.
|
|
137
|
+
*/
|
|
138
|
+
function scanValue(text: string, from: number): number {
|
|
139
|
+
const closers: string[] = [];
|
|
140
|
+
let i = from;
|
|
141
|
+
for (;;) {
|
|
142
|
+
const c = text[i]!;
|
|
143
|
+
if (c === "{" || c === "[") {
|
|
144
|
+
const closer = c === "{" ? "}" : "]";
|
|
145
|
+
closers.push(closer);
|
|
146
|
+
const first = skipWhitespace(text, i + 1);
|
|
147
|
+
if (first === HOLD) return HOLD;
|
|
148
|
+
if (text[first] === closer) {
|
|
149
|
+
closers.pop();
|
|
150
|
+
i = first + 1;
|
|
151
|
+
} else if (closer === "}") {
|
|
152
|
+
const value = scanMemberKey(text, first);
|
|
153
|
+
if (value < 0) return value;
|
|
154
|
+
i = value;
|
|
155
|
+
continue;
|
|
156
|
+
} else {
|
|
157
|
+
i = first;
|
|
158
|
+
continue;
|
|
159
|
+
}
|
|
160
|
+
} else {
|
|
161
|
+
const end = scanScalar(text, i);
|
|
162
|
+
if (end < 0) return end;
|
|
163
|
+
i = end;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// One value is complete: close whatever it finished and find the next value, if any.
|
|
167
|
+
for (;;) {
|
|
168
|
+
if (closers.length === 0) return i;
|
|
169
|
+
const at = skipWhitespace(text, i);
|
|
170
|
+
if (at === HOLD) return HOLD;
|
|
171
|
+
const closer = closers[closers.length - 1]!;
|
|
172
|
+
if (text[at] === closer) {
|
|
173
|
+
closers.pop();
|
|
174
|
+
i = at + 1;
|
|
175
|
+
continue;
|
|
176
|
+
}
|
|
177
|
+
if (text[at] !== ",") return NEVER;
|
|
178
|
+
const next = skipWhitespace(text, at + 1);
|
|
179
|
+
if (next === HOLD) return HOLD;
|
|
180
|
+
if (closer === "}") {
|
|
181
|
+
const value = scanMemberKey(text, next);
|
|
182
|
+
if (value < 0) return value;
|
|
183
|
+
i = value;
|
|
184
|
+
} else {
|
|
185
|
+
i = next;
|
|
186
|
+
}
|
|
187
|
+
break;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Which wrapper the completed text will unwrap to, as far as this prefix can say.
|
|
194
|
+
*
|
|
195
|
+
* Fallback keys are deliberately not recognized here. They only unwrap when exactly one of them
|
|
196
|
+
* carries a string, and a second one can still arrive, so no prefix decides them — which makes
|
|
197
|
+
* them indistinguishable from any other undecided object and lets one HOLD cover both.
|
|
198
|
+
*/
|
|
199
|
+
export function scanFreeformWrapper(text: string): FreeformWrapperScan {
|
|
200
|
+
// One clamp rather than a budget threaded through every helper. Every helper already holds
|
|
201
|
+
// when it runs off the end of what it can see, so a buffer whose classification needs more
|
|
202
|
+
// than this holds for exactly the right reason, and no scan can cost more than this many
|
|
203
|
+
// characters however large the arguments grow. Indices into the clamp are indices into the
|
|
204
|
+
// full text, because the clamp is a prefix of it.
|
|
205
|
+
const bounded = text.length > MAX_FREEFORM_WRAPPER_SCAN_CHARS
|
|
206
|
+
? text.slice(0, MAX_FREEFORM_WRAPPER_SCAN_CHARS)
|
|
207
|
+
: text;
|
|
208
|
+
// `parse` is true only where this scan actually SAW the object close. Every other hold ran
|
|
209
|
+
// out of buffer or out of budget, and in both cases asking `JSON.parse` is work with no
|
|
210
|
+
// possible payoff: the first is provably incomplete, and the second would re-read a growing
|
|
211
|
+
// buffer on every delta that happens to end in a brace — repeated braces inside a long
|
|
212
|
+
// unterminated string are enough to make that quadratic. Holding a budget-exhausted prefix
|
|
213
|
+
// costs nothing that matters, because the value it would release arrives in the same instant
|
|
214
|
+
// as the authoritative completion that follows it.
|
|
215
|
+
const hold = (): FreeformWrapperScan => ({ kind: "hold", parse: false });
|
|
216
|
+
const open = skipWhitespace(bounded, 0);
|
|
217
|
+
if (open === HOLD) return hold();
|
|
218
|
+
// Not an object, so no wrapper rule reaches it: arrays, scalars and ordinary bodies all
|
|
219
|
+
// complete as themselves.
|
|
220
|
+
if (bounded[open] !== "{") return { kind: "raw" };
|
|
221
|
+
|
|
222
|
+
let i = open + 1;
|
|
223
|
+
for (;;) {
|
|
224
|
+
const at = skipWhitespace(bounded, i);
|
|
225
|
+
if (at === HOLD) return hold();
|
|
226
|
+
if (bounded[at] === "}") return afterTopLevelClose(bounded, at + 1);
|
|
227
|
+
if (bounded[at] !== '"') return { kind: "raw" };
|
|
228
|
+
|
|
229
|
+
const nameEnd = scanString(bounded, at);
|
|
230
|
+
if (nameEnd === HOLD) return hold();
|
|
231
|
+
if (nameEnd === NEVER) return { kind: "raw" };
|
|
232
|
+
let name: string;
|
|
233
|
+
try {
|
|
234
|
+
// The authoritative decoder for the name, so `{"\u0069nput":...}` resolves to the same
|
|
235
|
+
// key completion sees. Decoding it by hand here is the second implementation that would
|
|
236
|
+
// drift from `JSON.parse`, which is how this defect existed in the first place.
|
|
237
|
+
name = JSON.parse(bounded.slice(at, nameEnd)) as string;
|
|
238
|
+
} catch {
|
|
239
|
+
return { kind: "raw" };
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
const colon = skipWhitespace(bounded, nameEnd);
|
|
243
|
+
if (colon === HOLD) return hold();
|
|
244
|
+
if (bounded[colon] !== ":") return { kind: "raw" };
|
|
245
|
+
const valueAt = skipWhitespace(bounded, colon + 1);
|
|
246
|
+
if (valueAt === HOLD) return hold();
|
|
247
|
+
|
|
248
|
+
if (name === CANONICAL_KEY) {
|
|
249
|
+
// An own `input` wins over everything else in the object, so this is decidable now. A
|
|
250
|
+
// non-string value is decidable too, in the other direction: completion hands back the
|
|
251
|
+
// argument text unchanged rather than unwrapping a value it cannot use.
|
|
252
|
+
return bounded[valueAt] === '"'
|
|
253
|
+
? { kind: "input", valueStart: valueAt + 1 }
|
|
254
|
+
: { kind: "raw" };
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
const valueEnd = scanValue(bounded, valueAt);
|
|
258
|
+
if (valueEnd === HOLD) return hold();
|
|
259
|
+
if (valueEnd === NEVER) return { kind: "raw" };
|
|
260
|
+
|
|
261
|
+
const next = skipWhitespace(bounded, valueEnd);
|
|
262
|
+
if (next === HOLD) return hold();
|
|
263
|
+
if (bounded[next] === ",") {
|
|
264
|
+
i = next + 1;
|
|
265
|
+
continue;
|
|
266
|
+
}
|
|
267
|
+
if (bounded[next] === "}") return afterTopLevelClose(bounded, next + 1);
|
|
268
|
+
return { kind: "raw" };
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* The object closed without a canonical key. Only whitespace may follow one that parses, so
|
|
274
|
+
* anything else makes the text raw; otherwise the completed parse decides between a fallback
|
|
275
|
+
* wrapper and no wrapper at all.
|
|
276
|
+
*/
|
|
277
|
+
function afterTopLevelClose(text: string, from: number): FreeformWrapperScan {
|
|
278
|
+
return skipWhitespace(text, from) === HOLD ? { kind: "hold", parse: true } : { kind: "raw" };
|
|
279
|
+
}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { collectResponsesToolGroups } from "./tool-groups";
|
|
2
|
+
import { isSchemaValidResponsesToolName, wireToolInnerName } from "./tool-name-aliases";
|
|
3
|
+
import { CODE_MODE_HELPER_WIRE_NAMES, dottedToolName, namespacedToolName } from "../types";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* The one invented namespace this proxy is known to have emitted. Routed providers flatten a
|
|
7
|
+
* namespaced call as `ns.name`, and a provider that invents the grouping picks `default`; #3402,
|
|
8
|
+
* #4176 and #4412 all repaired that spelling on the way IN, each on its own path.
|
|
9
|
+
*
|
|
10
|
+
* This is deliberately one literal prefix rather than a rule. "Strip everything before the first
|
|
11
|
+
* dot" would silently rewrite a legitimate tool name that happens to contain a dot in some other
|
|
12
|
+
* provider's vocabulary, and a replayed history item is the worst possible place to guess: it names
|
|
13
|
+
* a call that already happened.
|
|
14
|
+
*/
|
|
15
|
+
const LEGACY_INVENTED_NAMESPACE_PREFIX = "default.";
|
|
16
|
+
|
|
17
|
+
/** Item types whose `name` the upstream validates against the tool-name pattern. */
|
|
18
|
+
const NAMED_CALL_ITEM_TYPES = new Set(["function_call", "custom_tool_call"]);
|
|
19
|
+
|
|
20
|
+
function isPlainObject(value: unknown): value is Record<string, unknown> {
|
|
21
|
+
return !!value && typeof value === "object" && !Array.isArray(value);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
type DeclarationView = {
|
|
25
|
+
/** Wire spellings the caller's own catalog declares, including flattened namespace forms. */
|
|
26
|
+
readonly spellings: ReadonlySet<string>;
|
|
27
|
+
/** How many declared identities answer to a given inner name. */
|
|
28
|
+
readonly ownersByInnerName: ReadonlyMap<string, number>;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Read the request's own catalog. Namespace children contribute their inner name and both
|
|
33
|
+
* flattened spellings, which is what makes a declared `default` namespace distinguishable from an
|
|
34
|
+
* invented one.
|
|
35
|
+
*/
|
|
36
|
+
function readDeclarations(body: unknown): DeclarationView {
|
|
37
|
+
const spellings = new Set<string>();
|
|
38
|
+
const owners = new Map<string, number>();
|
|
39
|
+
const identities = new Map<string, Set<string>>();
|
|
40
|
+
const claim = (innerName: string, identity: string): void => {
|
|
41
|
+
const seen = identities.get(innerName);
|
|
42
|
+
if (seen === undefined) identities.set(innerName, new Set([identity]));
|
|
43
|
+
else seen.add(identity);
|
|
44
|
+
};
|
|
45
|
+
for (const group of collectResponsesToolGroups(body)) {
|
|
46
|
+
for (const spec of group) {
|
|
47
|
+
if (!isPlainObject(spec)) continue;
|
|
48
|
+
if (spec.type === "namespace" && Array.isArray(spec.tools)) {
|
|
49
|
+
const namespace = typeof spec.name === "string" ? spec.name : undefined;
|
|
50
|
+
if (namespace === undefined) continue;
|
|
51
|
+
for (const child of spec.tools) {
|
|
52
|
+
const inner = wireToolInnerName(child);
|
|
53
|
+
if (!inner) continue;
|
|
54
|
+
spellings.add(namespacedToolName(namespace, inner));
|
|
55
|
+
spellings.add(dottedToolName(namespace, inner));
|
|
56
|
+
claim(inner, JSON.stringify([namespace, inner]));
|
|
57
|
+
}
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
const name = wireToolInnerName(spec);
|
|
61
|
+
if (!name) continue;
|
|
62
|
+
spellings.add(name);
|
|
63
|
+
claim(name, JSON.stringify([null, name]));
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
for (const [inner, owning] of identities) owners.set(inner, owning.size);
|
|
67
|
+
return { spellings, ownersByInnerName: owners };
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Canonical name a legacy dotted alias should be replayed under, or undefined to leave it alone.
|
|
72
|
+
*
|
|
73
|
+
* Fail-closed in both directions that matter. A dotted spelling the caller's catalog actually
|
|
74
|
+
* declares is a real tool identity, so it is never stripped — rewriting it would change which tool
|
|
75
|
+
* the history says was called. A suffix claimed by two declared identities is ambiguous and is
|
|
76
|
+
* left as it is. Only two things resolve: a suffix that names exactly one declared tool, and the
|
|
77
|
+
* bounded set of code-mode helper spellings, which a code-mode catalog never declares because they
|
|
78
|
+
* exist only as nested `tools.*` helpers inside `exec`.
|
|
79
|
+
*/
|
|
80
|
+
function repairedCallName(name: string, declarations: DeclarationView): string | undefined {
|
|
81
|
+
if (isSchemaValidResponsesToolName(name)) return undefined;
|
|
82
|
+
if (!name.startsWith(LEGACY_INVENTED_NAMESPACE_PREFIX)) return undefined;
|
|
83
|
+
if (declarations.spellings.has(name)) return undefined;
|
|
84
|
+
const suffix = name.slice(LEGACY_INVENTED_NAMESPACE_PREFIX.length);
|
|
85
|
+
if (!isSchemaValidResponsesToolName(suffix)) return undefined;
|
|
86
|
+
const owners = declarations.ownersByInnerName.get(suffix);
|
|
87
|
+
if (owners === 1) return suffix;
|
|
88
|
+
if (owners !== undefined) return undefined;
|
|
89
|
+
return CODE_MODE_HELPER_WIRE_NAMES.has(suffix) ? suffix : undefined;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function repairedItem(item: unknown, declarations: DeclarationView): unknown {
|
|
93
|
+
if (!isPlainObject(item)) return item;
|
|
94
|
+
if (typeof item.type !== "string" || !NAMED_CALL_ITEM_TYPES.has(item.type)) return item;
|
|
95
|
+
// An item carrying an explicit namespace is a namespaced identity whose `name` is the inner
|
|
96
|
+
// name; it is not the flattened shape this repairs, and its own field is the authority.
|
|
97
|
+
if (item.namespace !== undefined) return item;
|
|
98
|
+
if (typeof item.name !== "string") return item;
|
|
99
|
+
const repaired = repairedCallName(item.name, declarations);
|
|
100
|
+
return repaired === undefined ? item : { ...item, name: repaired };
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Make an already-damaged conversation replayable.
|
|
105
|
+
*
|
|
106
|
+
* A `function_call` whose name violates `^[a-zA-Z0-9_-]+$` is not a one-turn failure: Codex stores
|
|
107
|
+
* the item, and the upstream then refuses EVERY later request that replays it
|
|
108
|
+
* (`Invalid 'input[N].name': ...`) — including the automatic compaction a long task or a side chat
|
|
109
|
+
* triggers. The emit-side fix stops new items from being created; without this the conversations
|
|
110
|
+
* that already contain one stay permanently unusable, which is the part of #5095 that actually
|
|
111
|
+
* hurts.
|
|
112
|
+
*
|
|
113
|
+
* Only replayed input items are touched. The caller's tool catalog is never rewritten, and a body
|
|
114
|
+
* with nothing to repair is returned by reference.
|
|
115
|
+
*/
|
|
116
|
+
export function repairLegacyDottedToolCallNames(body: unknown): unknown {
|
|
117
|
+
if (!isPlainObject(body) || !Array.isArray(body.input)) return body;
|
|
118
|
+
if (!body.input.some(item =>
|
|
119
|
+
isPlainObject(item)
|
|
120
|
+
&& typeof item.type === "string"
|
|
121
|
+
&& NAMED_CALL_ITEM_TYPES.has(item.type)
|
|
122
|
+
&& typeof item.name === "string"
|
|
123
|
+
&& item.name.startsWith(LEGACY_INVENTED_NAMESPACE_PREFIX)
|
|
124
|
+
)) return body;
|
|
125
|
+
|
|
126
|
+
const declarations = readDeclarations(body);
|
|
127
|
+
let changed = false;
|
|
128
|
+
const input = body.input.map(item => {
|
|
129
|
+
const repaired = repairedItem(item, declarations);
|
|
130
|
+
if (repaired !== item) changed = true;
|
|
131
|
+
return repaired;
|
|
132
|
+
});
|
|
133
|
+
return changed ? { ...body, input } : body;
|
|
134
|
+
}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import { unwrapFreeformToolInput } from "./apply-patch-envelope";
|
|
2
|
+
import { JSON_ESCAPES, scanFreeformWrapper } from "./freeform-wrapper-scan";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Whether a body could still grow into one complete outer Markdown fence.
|
|
6
|
+
*
|
|
7
|
+
* `stripMarkdownCodeFence` removes such a fence at completion for exactly the two tools that
|
|
8
|
+
* own the grammar, so a fenced body's streamed bytes and its completed input disagree unless
|
|
9
|
+
* the stream holds. A buffer that does not open with a fence can never acquire one, so
|
|
10
|
+
* ordinary bodies are unaffected; a buffer that does keeps its preview suppressed for the
|
|
11
|
+
* whole call, because a closing fence can still be followed by more text that withdraws it.
|
|
12
|
+
*/
|
|
13
|
+
function mayBecomeFencedBody(text: string, toolName: string): boolean {
|
|
14
|
+
if (toolName !== "exec" && toolName !== "apply_patch") return false;
|
|
15
|
+
const head = text.trimStart();
|
|
16
|
+
if (head === "") return true;
|
|
17
|
+
return head.startsWith("```") || "```".startsWith(head);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const LOW_SURROGATE_ESCAPE = /^\\u[dD][c-fC-F][0-9a-fA-F]{2}$/;
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* The decoded prefix of a JSON string body, stopping at the first byte it cannot resolve.
|
|
24
|
+
*
|
|
25
|
+
* Every stop is a hold rather than a guess, because `JSON.parse` decides the completed value
|
|
26
|
+
* and anything invented here would be retracted. Three of them are not obvious:
|
|
27
|
+
*
|
|
28
|
+
* An escape JSON does not define makes the whole wrapper unparseable no matter what arrives
|
|
29
|
+
* next, so completion falls back to the raw text. Returning `null` for that case stops the
|
|
30
|
+
* preview rather than continuing to decode a value the completed item will never carry.
|
|
31
|
+
* `\\b` and `\\f` are defined, and were previously decoded to the letters b and f.
|
|
32
|
+
*
|
|
33
|
+
* A lone high surrogate is not a character. Emitting one alone puts an unpaired code unit in a
|
|
34
|
+
* delta the client has to decode by itself, so the pair is emitted together or not at all.
|
|
35
|
+
*/
|
|
36
|
+
function decodeJsonStringPrefix(body: string): string | null {
|
|
37
|
+
let out = "";
|
|
38
|
+
for (let i = 0; i < body.length; i++) {
|
|
39
|
+
const c = body[i];
|
|
40
|
+
if (c === '"') break; // unescaped closing quote: value complete
|
|
41
|
+
if (c !== "\\") {
|
|
42
|
+
// A literal control character is not legal inside a JSON string, so `JSON.parse` will
|
|
43
|
+
// reject this wrapper and completion will return the raw text. Emitting the decoded value
|
|
44
|
+
// first is the disagreement this decoder exists to prevent, so stop instead.
|
|
45
|
+
if (c!.charCodeAt(0) <= 0x1f) return null;
|
|
46
|
+
out += c;
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
const n = body[i + 1];
|
|
50
|
+
if (n === undefined) break; // escape split across chunks: wait for more
|
|
51
|
+
if (n === "u") {
|
|
52
|
+
const hex = body.slice(i + 2, i + 6);
|
|
53
|
+
if (hex.length < 4) break; // split across chunks: wait for more
|
|
54
|
+
if (!/^[0-9a-fA-F]{4}$/.test(hex)) return null; // never parses
|
|
55
|
+
const code = parseInt(hex, 16);
|
|
56
|
+
if (code >= 0xd800 && code <= 0xdbff) {
|
|
57
|
+
const low = body.slice(i + 6, i + 12);
|
|
58
|
+
if (!LOW_SURROGATE_ESCAPE.test(low)) break;
|
|
59
|
+
out += String.fromCharCode(code, parseInt(low.slice(2), 16));
|
|
60
|
+
i += 11;
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
out += String.fromCharCode(code);
|
|
64
|
+
i += 5;
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
const escaped = JSON_ESCAPES.get(n);
|
|
68
|
+
if (escaped === undefined) return null; // never parses
|
|
69
|
+
out += escaped;
|
|
70
|
+
i += 1;
|
|
71
|
+
}
|
|
72
|
+
return out;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* The value to stream so far, or `null` to HOLD because nothing can be decided yet.
|
|
77
|
+
*
|
|
78
|
+
* `input` is decidable from its prefix: `unwrapFreeformToolInput` returns it whenever the
|
|
79
|
+
* key is present, whatever else the object carries, so its value can be unescaped
|
|
80
|
+
* progressively and never retracted.
|
|
81
|
+
*
|
|
82
|
+
* One case escapes that claim and is accepted rather than fixed: a duplicate `input` key.
|
|
83
|
+
* `JSON.parse` keeps the last one, so `{"input":"a","input":"b"}` streams a and completes with
|
|
84
|
+
* b. Closing it means holding every canonical wrapper until its object parses, which is the
|
|
85
|
+
* progressive streaming this path exists to provide. The completed item stays authoritative,
|
|
86
|
+
* and no model emits a duplicate key in practice.
|
|
87
|
+
*
|
|
88
|
+
* A wrapper that turns invalid AFTER streaming has committed has the same shape and the same
|
|
89
|
+
* answer. `{"input":"a` followed by `\\qb"}` has already published a when the undefined escape
|
|
90
|
+
* arrives, and completion returns the raw text because nothing parses. The preview stops
|
|
91
|
+
* there: no rewind, and no decoded text the completed item does not contain. Bounding the
|
|
92
|
+
* damage is what is available without giving up progressive streaming, and the args are
|
|
93
|
+
* unusable in that case whichever representation wins.
|
|
94
|
+
*
|
|
95
|
+
* A fallback key is not decidable. It only unwraps when it is the SINGLE string field, and a
|
|
96
|
+
* second key can still arrive — so a value emitted early would have to be taken back. That is
|
|
97
|
+
* the rewind this holds instead: stream nothing until the object closes, then publish the one
|
|
98
|
+
* repaired body. The routed passthrough in `responses-custom-tool-repair.ts` already holds
|
|
99
|
+
* any object prefix for the same reason (#5047).
|
|
100
|
+
*
|
|
101
|
+
* An object that has not reached a canonical key YET is in exactly that position, and used to
|
|
102
|
+
* be treated as raw because it did not match the literal `{"input":"`. It holds now: `input`
|
|
103
|
+
* can still arrive after other properties, or wearing an escaped spelling, and completion would
|
|
104
|
+
* then unwrap a body whose wrapper syntax had already been published as deltas (#5151). The
|
|
105
|
+
* cost of holding is preview on an object body that turns out not to be a wrapper; the cost of
|
|
106
|
+
* not holding was publishing bytes the completed item removes.
|
|
107
|
+
*/
|
|
108
|
+
export function progressiveFreeformInput(args: string, toolName: string): string | null {
|
|
109
|
+
const scan = scanFreeformWrapper(args);
|
|
110
|
+
if (scan.kind === "input") {
|
|
111
|
+
const decoded = decodeJsonStringPrefix(args.slice(scan.valueStart));
|
|
112
|
+
if (decoded === null) return null;
|
|
113
|
+
return mayBecomeFencedBody(decoded, toolName) ? null : decoded;
|
|
114
|
+
}
|
|
115
|
+
if (scan.kind === "raw") return mayBecomeFencedBody(args, toolName) ? null : args;
|
|
116
|
+
|
|
117
|
+
// Undecided: some wrapper may still apply, and only the completed object says which one.
|
|
118
|
+
// The parse runs exactly where the scan SAW the object close, so it reads a buffer the scan
|
|
119
|
+
// already walked and its cost is bounded by the same clamp. A hold from either limit stays
|
|
120
|
+
// held: an incomplete object has nothing to parse, and re-reading a budget-exhausted buffer
|
|
121
|
+
// on every delta is quadratic work for a delta that would arrive in the same instant as the
|
|
122
|
+
// authoritative completion behind it.
|
|
123
|
+
if (!scan.parse) return null;
|
|
124
|
+
try {
|
|
125
|
+
JSON.parse(args);
|
|
126
|
+
} catch {
|
|
127
|
+
return null;
|
|
128
|
+
}
|
|
129
|
+
return unwrapFreeformToolInput(args, toolName);
|
|
130
|
+
}
|
|
@@ -63,6 +63,36 @@ export function encodeReasoningEnvelope(envelope: ReasoningEnvelope, budget?: Tr
|
|
|
63
63
|
}
|
|
64
64
|
}
|
|
65
65
|
|
|
66
|
+
/**
|
|
67
|
+
* Whether a thinking part's `signature` is an attestation some provider issued, rather than
|
|
68
|
+
* the serialized reasoning item the parser parks in the same field.
|
|
69
|
+
*
|
|
70
|
+
* `src/responses/parser.ts` stores `JSON.stringify(reasoningItem)` on an UNSIGNED thinking
|
|
71
|
+
* part so the opaque item survives a same-provider round trip. Every adapter that forwards a
|
|
72
|
+
* signature upstream is forwarding an opaque attestation, and a provider asked to verify our
|
|
73
|
+
* own parser state cannot recognize it. The predicate lives beside the envelope code that owns
|
|
74
|
+
* this field's representation rather than in one adapter, because a private second copy is how
|
|
75
|
+
* the side that writes the field and a side that reads it come to disagree about what it holds.
|
|
76
|
+
*
|
|
77
|
+
* This is a deny-list for exactly that one shape, not a guess at what an attestation looks
|
|
78
|
+
* like. `src/adapters/anthropic.ts` applies a stricter allow-list on top of it, because an
|
|
79
|
+
* Anthropic signature has a known base64 spelling; that shape is a fact about Anthropic's wire
|
|
80
|
+
* and is not assumed of any other provider's token here.
|
|
81
|
+
*/
|
|
82
|
+
export function isProviderIssuedThinkingSignature(
|
|
83
|
+
signature: string | undefined,
|
|
84
|
+
): signature is string {
|
|
85
|
+
if (typeof signature !== "string" || signature.length === 0) return false;
|
|
86
|
+
if (!signature.startsWith("{")) return true;
|
|
87
|
+
try {
|
|
88
|
+
const parsed: unknown = JSON.parse(signature);
|
|
89
|
+
return !parsed || typeof parsed !== "object" || Array.isArray(parsed)
|
|
90
|
+
|| (parsed as { type?: unknown }).type !== "reasoning";
|
|
91
|
+
} catch {
|
|
92
|
+
return true;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
66
96
|
/** Decode an ocxr1 envelope; returns null for native (OpenAI-encrypted) blobs or garbage. */
|
|
67
97
|
export function decodeReasoningEnvelope(encryptedContent: string, budget?: TranslatorBudget): ReasoningEnvelope | null {
|
|
68
98
|
if (!encryptedContent.startsWith(OCX_REASONING_PREFIX)) return null;
|
package/src/responses/state.ts
CHANGED
|
@@ -135,11 +135,10 @@ export type ResidentInput = Omit<ResidentResponseState, "kind" | "sizeBytes">;
|
|
|
135
135
|
|
|
136
136
|
export type PreviousResponseReplayFailure = {
|
|
137
137
|
code: "previous_response_not_found";
|
|
138
|
-
reason: "spill_missing" | "spill_corrupt" | "spill_failed" | "spill_too_large";
|
|
138
|
+
reason: "spill_missing" | "spill_corrupt" | "spill_failed" | "spill_too_large" | "scope_mismatch";
|
|
139
139
|
};
|
|
140
140
|
|
|
141
141
|
const states = new Map<string, StoredResponseState>();
|
|
142
|
-
const replayScopeMismatches = new WeakSet<object>();
|
|
143
142
|
let storedResponseBytes = 0;
|
|
144
143
|
let residentResponseBytes = 0;
|
|
145
144
|
let oldestResidentId: string | undefined;
|
|
@@ -1044,11 +1043,6 @@ function normalizedClientThreadId(value: unknown): string | undefined {
|
|
|
1044
1043
|
return typeof value === "string" && value.trim().length > 0 ? value.trim() : undefined;
|
|
1045
1044
|
}
|
|
1046
1045
|
|
|
1047
|
-
function withoutPreviousResponseId(request: Record<string, unknown>): Record<string, unknown> {
|
|
1048
|
-
const { previous_response_id: _previousResponseId, ...freshRequest } = request;
|
|
1049
|
-
return freshRequest;
|
|
1050
|
-
}
|
|
1051
|
-
|
|
1052
1046
|
export function expandPreviousResponseInput(body: unknown, clientThreadId?: string): unknown {
|
|
1053
1047
|
if (!body || typeof body !== "object" || Array.isArray(body)) return body;
|
|
1054
1048
|
const request = body as Record<string, unknown>;
|
|
@@ -1068,10 +1062,9 @@ export function expandPreviousResponseInput(body: unknown, clientThreadId?: stri
|
|
|
1068
1062
|
// A Codex task must never inherit another task's continuation, nor a legacy unscoped entry.
|
|
1069
1063
|
// Unscoped callers retain backward-compatible replay only with other unscoped entries.
|
|
1070
1064
|
if (requestThreadId !== storedThreadId) {
|
|
1071
|
-
|
|
1072
|
-
replayScopeMismatches.add(freshRequest);
|
|
1065
|
+
replayFailures.set(request, { code: "previous_response_not_found", reason: "scope_mismatch" });
|
|
1073
1066
|
replayScopeMismatchDrops += 1;
|
|
1074
|
-
return
|
|
1067
|
+
return body;
|
|
1075
1068
|
}
|
|
1076
1069
|
// The client already replayed this history verbatim. Prepending the stored copy would
|
|
1077
1070
|
// double it, and the doubled turn is stored again, so the next turn triples (#1412 saw
|
|
@@ -1136,9 +1129,9 @@ export function copyPreviousResponseReplayProvenance(source: unknown, target: un
|
|
|
1136
1129
|
replayedInputPrefixLengths.set(target, prefixLength);
|
|
1137
1130
|
}
|
|
1138
1131
|
|
|
1139
|
-
/** True when
|
|
1132
|
+
/** True when this exact request could not replay because its task scope did not match. */
|
|
1140
1133
|
export function previousResponseScopeMismatch(body: unknown): boolean {
|
|
1141
|
-
return
|
|
1134
|
+
return previousResponseReplayFailure(body)?.reason === "scope_mismatch";
|
|
1142
1135
|
}
|
|
1143
1136
|
|
|
1144
1137
|
export function previousResponseConversationId(responseId: string | undefined): string | undefined {
|
|
@@ -19,6 +19,21 @@ export function dottedAliasIsUnambiguous(namespace: string, name: string): boole
|
|
|
19
19
|
return !namespace.includes("__") && !name.includes("__");
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
+
/**
|
|
23
|
+
* Charset the Responses API enforces on a tool-call `name`.
|
|
24
|
+
*
|
|
25
|
+
* A name outside it is not merely rejected for the turn that carries it: Codex stores the item in
|
|
26
|
+
* the conversation, and every later request that replays that history is refused before it runs
|
|
27
|
+
* (`Invalid 'input[N].name': string does not match pattern '^[a-zA-Z0-9_-]+$'`). One relayed item
|
|
28
|
+
* therefore ends the conversation permanently, which is why the emit boundary treats an invalid
|
|
29
|
+
* name as a defect to repair rather than as a name to forward. See #5095.
|
|
30
|
+
*/
|
|
31
|
+
const RESPONSES_TOOL_NAME_PATTERN = /^[a-zA-Z0-9_-]+$/;
|
|
32
|
+
|
|
33
|
+
export function isSchemaValidResponsesToolName(name: string): boolean {
|
|
34
|
+
return RESPONSES_TOOL_NAME_PATTERN.test(name);
|
|
35
|
+
}
|
|
36
|
+
|
|
22
37
|
export function wireToolInnerName(tool: unknown): string | undefined {
|
|
23
38
|
if (!isPlainObject(tool)) return undefined;
|
|
24
39
|
const nestedFunction = tool.type === "function" && isPlainObject(tool.function)
|
|
@@ -76,4 +91,3 @@ export function collectAmbiguousDottedAliases(specGroups: readonly unknown[]): S
|
|
|
76
91
|
for (const [alias, owner] of owners) if (owner === null) ambiguous.add(alias);
|
|
77
92
|
return ambiguous;
|
|
78
93
|
}
|
|
79
|
-
|