@bitkyc08/opencodex 2.65.0 → 2.66.0-preview.20260925
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +14 -0
- package/gui/dist/assets/App-BVjA0T6O.js +51 -0
- package/gui/dist/assets/App-gkoJIEOV.css +1 -0
- package/gui/dist/assets/{Tray-D4bA1vff.js → Tray-BCFtiWwD.js} +1 -1
- package/gui/dist/assets/index-ComVsdSk.css +1 -0
- package/gui/dist/assets/index-vO4dgfiU.js +86 -0
- package/gui/dist/assets/{tray-data-BZXhB3qz.js → tray-data-C4FiNaxk.js} +1 -1
- package/gui/dist/index.html +2 -2
- package/package.json +1 -2
- package/src/adapters/anthropic/beta-allowlist.ts +80 -0
- package/src/adapters/anthropic/passthrough.ts +221 -0
- package/src/adapters/anthropic-image-codec.ts +56 -19
- package/src/adapters/anthropic-image-normalize.ts +20 -10
- package/src/adapters/anthropic.ts +55 -24
- package/src/adapters/coding-agent/protocol.ts +29 -19
- package/src/adapters/cursor/live-transport.ts +18 -3
- package/src/adapters/cursor/native-exec-shell.ts +18 -63
- package/src/adapters/cursor/native-exec.ts +5 -2
- package/src/adapters/cursor/native-foreground-shell.ts +44 -0
- package/src/adapters/devin/cloud-direct/chat.ts +7 -1
- package/src/adapters/exec-tool-result-normalize.ts +4 -1
- package/src/adapters/google.ts +12 -0
- package/src/adapters/ollama-native.ts +32 -1
- package/src/adapters/openai-chat/serialized-tool-call-content.ts +131 -34
- package/src/adapters/openai-chat.ts +6 -4
- package/src/adapters/openai-responses/passthrough.ts +1 -0
- package/src/adapters/openai-responses/reasoning.ts +45 -1
- package/src/bridge/response-json.ts +10 -2
- package/src/chat/outbound.ts +104 -52
- package/src/claude/claude-code-block.ts +16 -0
- package/src/claude/context-windows.ts +52 -4
- package/src/claude/desktop-first-party.ts +25 -8
- package/src/claude/inbound.ts +22 -0
- package/src/claude/intercept/connect-proxy.ts +37 -4
- package/src/claude/intercept/proxy-auth.ts +166 -0
- package/src/claude/intercept/runtime.ts +21 -0
- package/src/claude/intercept/settings.ts +33 -12
- package/src/claude/outbound.ts +45 -27
- package/src/cli/account-extended.ts +47 -15
- package/src/cli/account-target.ts +115 -0
- package/src/cli/account.ts +20 -28
- package/src/cli/agent.ts +10 -1
- package/src/cli/api-protocols.ts +206 -0
- package/src/cli/capabilities.ts +86 -0
- package/src/cli/combo.ts +2 -2
- package/src/cli/connect.ts +127 -3
- package/src/cli/dispatch.ts +8 -0
- package/src/cli/doctor.ts +33 -0
- package/src/cli/help.ts +2 -0
- package/src/cli/link.ts +288 -0
- package/src/cli/registry.ts +23 -0
- package/src/cli/runtime-api.ts +79 -0
- package/src/cli/system-command.ts +21 -2
- package/src/client/connect.ts +125 -23
- package/src/client/hub-relay.ts +12 -8
- package/src/client/link-join.ts +323 -0
- package/src/client/link-relay.ts +241 -0
- package/src/client/link-state.ts +110 -0
- package/src/client/link-teardown.ts +63 -0
- package/src/client/link-tunnel.ts +465 -0
- package/src/client/machine-listener.ts +14 -5
- package/src/client/runtime.ts +91 -42
- package/src/client/state.ts +4 -0
- package/src/codex/catalog/build-entries.ts +4 -0
- package/src/codex/catalog/derive-entry.ts +5 -0
- package/src/codex/catalog/parsing.ts +4 -0
- package/src/codex/catalog/retained-sync.ts +4 -1
- package/src/codex/desktop-switches.ts +88 -8
- package/src/codex/inject/plan.ts +16 -3
- package/src/codex/inject.ts +3 -0
- package/src/codex/internal/catalog-writer.ts +13 -2
- package/src/combos/failover.ts +3 -2
- package/src/combos/index.ts +12 -0
- package/src/combos/jev.ts +646 -0
- package/src/combos/request.ts +5 -0
- package/src/combos/resolve.ts +17 -15
- package/src/combos/types.ts +47 -3
- package/src/config/atomic-write.ts +3 -0
- package/src/config/live-reconcile.ts +20 -4
- package/src/config/persist-unlocked.ts +20 -7
- package/src/config/schema/config-schema.ts +16 -0
- package/src/config/schema/leaf-validators.ts +38 -1
- package/src/generated/compatibility-version.json +416 -128
- package/src/integrations/omp-yaml-source.ts +109 -4
- package/src/lab/automation/orchestrator.ts +60 -4
- package/src/lab/events/limits.ts +1 -1
- package/src/lib/bounded-body.ts +55 -38
- package/src/lib/config-ownership.ts +11 -22
- package/src/lib/local-desktop-snapshot-capability.ts +56 -0
- package/src/lib/local-management-capability.ts +25 -3
- package/src/lib/optional-shutdown-hooks.ts +17 -0
- package/src/lib/state-store-registrations.ts +2 -2
- package/src/lib/windows-elevation.ts +62 -25
- package/src/lib/windows-secret-acl.ts +114 -23
- package/src/link/admission-wait.ts +73 -0
- package/src/link/compensation.ts +100 -0
- package/src/link/fingerprint.ts +21 -0
- package/src/link/paths.ts +19 -0
- package/src/link/ports.ts +12 -0
- package/src/link/routes.ts +32 -0
- package/src/link/ssh-argv.ts +170 -0
- package/src/link/ssh-config.ts +152 -0
- package/src/link/ssh-runner.ts +161 -0
- package/src/link/status-projection.ts +96 -0
- package/src/link/store.ts +139 -0
- package/src/link/supervisor.ts +404 -0
- package/src/link/tunnel-state.ts +91 -0
- package/src/protocols/baseline.ts +88 -0
- package/src/protocols/codecs/chat.ts +17 -0
- package/src/protocols/codecs/messages.ts +18 -0
- package/src/protocols/codecs/responses.ts +17 -0
- package/src/protocols/contract.ts +185 -0
- package/src/protocols/dto.ts +267 -0
- package/src/protocols/encoders/adapter-events.ts +876 -0
- package/src/protocols/encoders/chat.ts +243 -0
- package/src/protocols/encoders/messages.ts +462 -0
- package/src/protocols/envelope.ts +43 -0
- package/src/protocols/features.ts +295 -0
- package/src/protocols/guard.ts +33 -0
- package/src/protocols/opaque-state.ts +132 -0
- package/src/protocols/path.ts +39 -0
- package/src/protocols/plan-snapshot.ts +226 -0
- package/src/protocols/plan.ts +192 -0
- package/src/protocols/provider-summary.ts +101 -0
- package/src/protocols/settings.ts +118 -0
- package/src/protocols/shadow-plan.ts +36 -0
- package/src/protocols/shadow.ts +62 -0
- package/src/protocols/trace.ts +335 -0
- package/src/providers/model-rename-migration.ts +3 -3
- package/src/providers/registry/entries-core.ts +6 -1
- package/src/providers/registry/entries-extended.ts +15 -0
- package/src/providers/registry/model-ids.ts +1 -0
- package/src/providers/registry/types.ts +6 -0
- package/src/providers/registry.ts +1 -1
- package/src/remote-control/workspace-hub.ts +5 -4
- package/src/responses/apply-patch-envelope.ts +6 -1
- package/src/responses/code-mode-shell-input.ts +4 -2
- package/src/responses/freeform-wrapper-scan.ts +51 -24
- package/src/responses/progressive-freeform-input.ts +14 -20
- package/src/responses/spill-store.ts +187 -33
- package/src/responses/state/snapshot-select.ts +49 -0
- package/src/responses/state/spill-inspect.ts +46 -0
- package/src/responses/state/spill-queue.ts +16 -0
- package/src/responses/state.ts +51 -27
- package/src/server/audio-client.ts +8 -3
- package/src/server/audio-upstream.ts +9 -4
- package/src/server/auth-cors.ts +25 -8
- package/src/server/chat-completions.ts +71 -11
- package/src/server/chat-native-eligibility.ts +74 -0
- package/src/server/chat-native.ts +139 -74
- package/src/server/claude-messages.ts +140 -22
- package/src/server/hub-usage.ts +4 -3
- package/src/server/index/link-listener.ts +195 -0
- package/src/server/index/optional-listeners.ts +115 -0
- package/src/server/index/serve-options.ts +44 -10
- package/src/server/index.ts +10 -9
- package/src/server/inference/attempt.ts +51 -0
- package/src/server/inference/client-encoder-delivery.ts +235 -0
- package/src/server/inference/client-wire-log.ts +49 -0
- package/src/server/inference/client-wire.ts +69 -0
- package/src/server/inference/context.ts +15 -0
- package/src/server/inference/final-log.ts +37 -0
- package/src/server/local-desktop-snapshot-auth.ts +57 -0
- package/src/server/management/agent-settings-routes.ts +15 -13
- package/src/server/management/api-access.ts +11 -1
- package/src/server/management/config-routes.ts +3 -6
- package/src/server/management/context.ts +25 -0
- package/src/server/management/link-routes.ts +535 -0
- package/src/server/management/logs-usage-routes.ts +31 -1
- package/src/server/management/native-integration-routes.ts +6 -11
- package/src/server/management/oauth-account-routes.ts +42 -16
- package/src/server/management/protocol-routes.ts +159 -0
- package/src/server/management/protocol-settings-patch.ts +145 -0
- package/src/server/management/provider-patch-transaction.ts +61 -0
- package/src/server/management/provider-routes.ts +39 -18
- package/src/server/management/route-registry.ts +12 -0
- package/src/server/management/sidebar-routes.ts +8 -3
- package/src/server/management/system-routes.ts +18 -1
- package/src/server/management/usage-aggregate-cache.ts +206 -6
- package/src/server/management-api.ts +26 -1
- package/src/server/management-auth.ts +19 -5
- package/src/server/messages-native-eligibility.ts +161 -0
- package/src/server/messages-native-oauth.ts +149 -0
- package/src/server/messages-native.ts +790 -0
- package/src/server/relay.ts +9 -0
- package/src/server/request-log-filter.ts +92 -0
- package/src/server/request-log.ts +31 -70
- package/src/server/responses/adapter-delivery.ts +43 -16
- package/src/server/responses/agent-task-recovery.ts +90 -22
- package/src/server/responses/codex-ws-exchange.ts +4 -0
- package/src/server/responses/codex-ws-wire.ts +10 -1
- package/src/server/responses/core-combo-native.ts +323 -0
- package/src/server/responses/core-combo.ts +224 -13
- package/src/server/responses/core-options.ts +22 -0
- package/src/server/responses/core.ts +14 -26
- package/src/server/responses/request-sidecar-auth.ts +8 -3
- package/src/server/responses/run-turn-execution.ts +257 -63
- package/src/server/responses/sidecar-execution.ts +7 -4
- package/src/server/system-env.ts +7 -0
- package/src/service/diagnostics.ts +7 -1
- package/src/service/launchd.ts +26 -37
- package/src/service/windows-ops.ts +24 -20
- package/src/types/config.ts +58 -1
- package/src/types.ts +2 -0
- package/src/update/transactional-install.d.mts +0 -1
- package/src/update/transactional-install.mjs +17 -17
- package/src/usage/jev-stats.ts +495 -0
- package/src/usage/log.ts +24 -3
- package/src/web-search/run-turn-loop.ts +568 -0
- package/gui/dist/assets/App-8NMiZxT0.css +0 -1
- package/gui/dist/assets/App-DGLte6IR.js +0 -50
- package/gui/dist/assets/index--EWgGQvZ.css +0 -1
- package/gui/dist/assets/index-Bi1K37Wl.js +0 -86
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { TranslatorBudget } from "../../lib/translator-budget";
|
|
2
2
|
import type { AdapterEvent } from "../../types";
|
|
3
|
+
import { repairFreeformToolInput } from "../../responses/apply-patch-envelope";
|
|
3
4
|
|
|
4
5
|
const OPEN_TAG = "<tool_call>";
|
|
5
6
|
const CLOSE_TAG = "</tool_call>";
|
|
@@ -27,9 +28,24 @@ export interface SerializedToolCall {
|
|
|
27
28
|
end: number;
|
|
28
29
|
}
|
|
29
30
|
|
|
31
|
+
/** A freeform tool's declared identity. Echoes are matched against it, never against the wire alias. */
|
|
32
|
+
export interface FreeformToolIdentity {
|
|
33
|
+
name: string;
|
|
34
|
+
namespace?: string;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** The request's freeform tools keyed by the wire name the provider sees for each of them. */
|
|
38
|
+
export function freeformToolsByWireName<T extends FreeformToolIdentity & { freeform?: boolean }>(
|
|
39
|
+
tools: readonly T[] | undefined,
|
|
40
|
+
wireName: (tool: T) => string,
|
|
41
|
+
): Map<string, FreeformToolIdentity> {
|
|
42
|
+
return new Map(tools?.filter(tool => tool.freeform).map(tool => [wireName(tool), { name: tool.name, namespace: tool.namespace }] as const) ?? []);
|
|
43
|
+
}
|
|
44
|
+
|
|
30
45
|
export interface StructuredToolCallReference {
|
|
31
46
|
names: ReadonlySet<string>;
|
|
32
47
|
argumentsText: string;
|
|
48
|
+
freeformTool?: FreeformToolIdentity;
|
|
33
49
|
}
|
|
34
50
|
|
|
35
51
|
const BLOCK_HEADER = /<tool_call>\s*<function=([^>\r\n]+)>/y;
|
|
@@ -176,6 +192,29 @@ function contextAfter(text: string, context: TextContext): TextContext {
|
|
|
176
192
|
return splitAtPossibleSerializedToolCall(text.replaceAll(OPEN_TAG, "<tool-call>"), context, true).context;
|
|
177
193
|
}
|
|
178
194
|
|
|
195
|
+
/**
|
|
196
|
+
* A deferred header/fence can itself be long. While its unresolved portion only grows, a
|
|
197
|
+
* chunk-local check suffices; the first resolving character goes back through the full splitter.
|
|
198
|
+
* Each unbounded phase is scanned once on entry and once on exit, preserving its Markdown rules.
|
|
199
|
+
*/
|
|
200
|
+
function deferredContinuationBreak(text: string, context: TextContext): RegExp | undefined {
|
|
201
|
+
if (text.startsWith(OPEN_TAG)) {
|
|
202
|
+
if (/^<tool_call>\s*$/.test(text)) return /\S/;
|
|
203
|
+
if (/^<tool_call>\s*<function=[^>\r\n]*$/.test(text)) return /[>\r\n]/;
|
|
204
|
+
}
|
|
205
|
+
if (!context.inlineTicks && context.lineStart) {
|
|
206
|
+
const fence = /^ {0,3}(`{3,}|~{3,})([^\n]*)$/.exec(text);
|
|
207
|
+
if (fence) {
|
|
208
|
+
if (!context.fence) return /\n/;
|
|
209
|
+
// Closing-fence ticks may grow only before its optional whitespace suffix.
|
|
210
|
+
if (!fence[2]) return fence[1]![0] === "`" ? /[^`]/ : /[^~]/;
|
|
211
|
+
return /[^ \t\r]/;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
if (/^`+$/.test(text)) return /[^`]/;
|
|
215
|
+
return undefined;
|
|
216
|
+
}
|
|
217
|
+
|
|
179
218
|
/**
|
|
180
219
|
* Holds possible duplicate text within the shared translator budget until the dispatch outcome is
|
|
181
220
|
* known. While a block candidate is open, any other event (reasoning) is queued at its position in
|
|
@@ -186,11 +225,30 @@ export class SerializedToolCallContentBuffer {
|
|
|
186
225
|
private text = "";
|
|
187
226
|
private bytes = 0;
|
|
188
227
|
private hasOpenTag = false;
|
|
228
|
+
private continuationBreak: RegExp | undefined;
|
|
229
|
+
private delimiterSuffix = "";
|
|
230
|
+
private sawCloser = false;
|
|
231
|
+
private openAfterCloser = false;
|
|
232
|
+
private trailingProseChars = 0;
|
|
189
233
|
private context: TextContext = { fence: null, lineStart: true };
|
|
190
234
|
private queued: { offset: number; event: AdapterEvent }[] = [];
|
|
191
235
|
|
|
192
236
|
constructor(private readonly budget: TranslatorBudget) {}
|
|
193
237
|
|
|
238
|
+
/** Measures an append exactly even when the runtime prices isolated UTF-16 surrogates differently. */
|
|
239
|
+
private appendedByteLength(delta: string): number {
|
|
240
|
+
let bytes = Buffer.byteLength(delta);
|
|
241
|
+
if (this.text.length === 0 || delta.length === 0) return bytes;
|
|
242
|
+
const tail = this.text[this.text.length - 1]!;
|
|
243
|
+
const head = delta[0]!;
|
|
244
|
+
const tailCode = tail.charCodeAt(0);
|
|
245
|
+
const headCode = head.charCodeAt(0);
|
|
246
|
+
if (tailCode >= 0xd800 && tailCode <= 0xdbff && headCode >= 0xdc00 && headCode <= 0xdfff) {
|
|
247
|
+
bytes += Buffer.byteLength(tail + head) - Buffer.byteLength(tail) - Buffer.byteLength(head);
|
|
248
|
+
}
|
|
249
|
+
return bytes;
|
|
250
|
+
}
|
|
251
|
+
|
|
194
252
|
/** Reserves the replacement before releasing the old text, preserving it if the budget rejects growth. */
|
|
195
253
|
private replace(next: string, hasOpenTag: boolean): void {
|
|
196
254
|
const nextBytes = Buffer.byteLength(next);
|
|
@@ -201,6 +259,12 @@ export class SerializedToolCallContentBuffer {
|
|
|
201
259
|
this.text = next;
|
|
202
260
|
this.bytes = nextBytes;
|
|
203
261
|
this.hasOpenTag = hasOpenTag;
|
|
262
|
+
this.continuationBreak = undefined;
|
|
263
|
+
this.delimiterSuffix = "";
|
|
264
|
+
this.sawCloser = false;
|
|
265
|
+
this.openAfterCloser = false;
|
|
266
|
+
this.trailingProseChars = 0;
|
|
267
|
+
if (hasOpenTag) this.observeDelimiters(next);
|
|
204
268
|
} catch (error) {
|
|
205
269
|
reservation.release();
|
|
206
270
|
throw error;
|
|
@@ -209,21 +273,41 @@ export class SerializedToolCallContentBuffer {
|
|
|
209
273
|
|
|
210
274
|
/** Charges only the appended bytes, so holding an open block never needs twice its retained size. */
|
|
211
275
|
private append(delta: string): void {
|
|
212
|
-
const deltaBytes =
|
|
276
|
+
const deltaBytes = this.appendedByteLength(delta);
|
|
213
277
|
this.budget.reserveTransient(deltaBytes, { kind: "live_transient" }).commitRetained();
|
|
278
|
+
if (this.hasOpenTag) this.observeDelimiters(delta);
|
|
214
279
|
this.text += delta;
|
|
215
280
|
this.bytes += deltaBytes;
|
|
216
281
|
}
|
|
217
282
|
|
|
283
|
+
/** Scan new text plus a fixed overlap, never the retained body (which may be a large rope). */
|
|
284
|
+
private observeDelimiters(delta: string): void {
|
|
285
|
+
const scan = this.delimiterSuffix + delta;
|
|
286
|
+
const closer = scan.lastIndexOf(CLOSE_TAG);
|
|
287
|
+
const afterCloser = closer + CLOSE_TAG.length;
|
|
288
|
+
if (closer >= 0) {
|
|
289
|
+
this.sawCloser = true;
|
|
290
|
+
this.openAfterCloser = false;
|
|
291
|
+
this.trailingProseChars = 0;
|
|
292
|
+
}
|
|
293
|
+
if (scan.lastIndexOf(OPEN_TAG) >= (closer >= 0 ? afterCloser : 0)) {
|
|
294
|
+
this.openAfterCloser = true;
|
|
295
|
+
}
|
|
296
|
+
const tail = closer >= 0 ? scan.slice(afterCloser) : delta;
|
|
297
|
+
if (this.sawCloser) this.trailingProseChars += tail.replace(/\s+/g, "").length;
|
|
298
|
+
this.delimiterSuffix = scan.slice(-(Math.max(OPEN_TAG.length, CLOSE_TAG.length) - 1));
|
|
299
|
+
}
|
|
300
|
+
|
|
218
301
|
/** Returns immediately safe text and retains only the suffix that still needs reconciliation. */
|
|
219
302
|
ingest(delta: string): string {
|
|
220
|
-
if (this.hasOpenTag) {
|
|
303
|
+
if (this.hasOpenTag || (this.continuationBreak && !this.continuationBreak.test(delta))) {
|
|
221
304
|
this.append(delta);
|
|
222
305
|
return "";
|
|
223
306
|
}
|
|
224
307
|
const split = splitAtPossibleSerializedToolCall(this.text + delta, this.context);
|
|
225
308
|
this.replace(split.defer, split.hasOpenTag);
|
|
226
309
|
this.context = split.context;
|
|
310
|
+
if (!split.hasOpenTag) this.continuationBreak = deferredContinuationBreak(split.defer, split.context);
|
|
227
311
|
return split.emit;
|
|
228
312
|
}
|
|
229
313
|
|
|
@@ -234,11 +318,11 @@ export class SerializedToolCallContentBuffer {
|
|
|
234
318
|
* context. The size bound is checked before the delta is retained.
|
|
235
319
|
*/
|
|
236
320
|
ingestStreaming(delta: string): AdapterEvent[] {
|
|
237
|
-
const deltaBytes =
|
|
321
|
+
const deltaBytes = this.appendedByteLength(delta);
|
|
238
322
|
if (this.hasOpenTag && this.bytes + deltaBytes > MAX_HELD_BYTES) {
|
|
239
323
|
const released = this.drain([]);
|
|
240
324
|
// A delta that alone passes the bound is delivered as text rather than retained.
|
|
241
|
-
if (
|
|
325
|
+
if (Buffer.byteLength(delta) > MAX_HELD_BYTES) {
|
|
242
326
|
this.context = contextAfter(delta, this.context);
|
|
243
327
|
return [...released, ...textEvents(delta)];
|
|
244
328
|
}
|
|
@@ -246,7 +330,7 @@ export class SerializedToolCallContentBuffer {
|
|
|
246
330
|
}
|
|
247
331
|
const text = this.ingest(delta);
|
|
248
332
|
// Checked after ingest too: one delta can open a block and already carry more than a bound.
|
|
249
|
-
if (this.hasOpenTag && (this.bytes > MAX_HELD_BYTES ||
|
|
333
|
+
if (this.hasOpenTag && (this.bytes > MAX_HELD_BYTES || (this.sawCloser && !this.openAfterCloser && this.trailingProseChars > MAX_TRAILING_CHARS))) {
|
|
250
334
|
return [...textEvents(text), ...this.drain([])];
|
|
251
335
|
}
|
|
252
336
|
return textEvents(text);
|
|
@@ -318,20 +402,30 @@ export class SerializedToolCallContentBuffer {
|
|
|
318
402
|
this.text = "";
|
|
319
403
|
this.bytes = 0;
|
|
320
404
|
this.hasOpenTag = false;
|
|
405
|
+
this.continuationBreak = undefined;
|
|
321
406
|
this.queued = [];
|
|
322
407
|
}
|
|
323
408
|
}
|
|
324
409
|
|
|
325
|
-
/** Reads
|
|
326
|
-
function inputFromArguments(
|
|
410
|
+
/** Reads a wrapped input or raw arguments from a declared freeform tool; neither path rewrites them. */
|
|
411
|
+
function inputFromArguments(
|
|
412
|
+
argumentsText: string,
|
|
413
|
+
freeformTool?: FreeformToolIdentity,
|
|
414
|
+
): string | undefined {
|
|
327
415
|
try {
|
|
328
416
|
const parsed = JSON.parse(argumentsText) as unknown;
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
417
|
+
const input = parsed !== null && typeof parsed === "object" && !Array.isArray(parsed)
|
|
418
|
+
? (parsed as Record<string, unknown>).input
|
|
419
|
+
: undefined;
|
|
420
|
+
if (typeof input === "string") return input;
|
|
332
421
|
} catch {
|
|
333
|
-
|
|
422
|
+
// Chat gateways can send custom-tool input as raw text.
|
|
334
423
|
}
|
|
424
|
+
if (!freeformTool) return undefined;
|
|
425
|
+
// Match raw input only when the Responses bridge dispatches these same bytes. It can unwrap
|
|
426
|
+
// alternate fields or repair freeform input using the declared tool's name and namespace.
|
|
427
|
+
return repairFreeformToolInput(argumentsText, freeformTool.name, freeformTool.namespace) === argumentsText
|
|
428
|
+
? argumentsText : undefined;
|
|
335
429
|
}
|
|
336
430
|
|
|
337
431
|
/** One wrapping newline after the function header is template layout, not input (vLLM `_trim_wrapping_newlines`). */
|
|
@@ -348,10 +442,20 @@ function agreesWithRepeatedBlock(
|
|
|
348
442
|
structured: StructuredToolCallReference,
|
|
349
443
|
repeated: SerializedToolCall,
|
|
350
444
|
): boolean {
|
|
351
|
-
const input = structured.names.has(repeated.name) ? inputFromArguments(structured.argumentsText) : undefined;
|
|
445
|
+
const input = structured.names.has(repeated.name) ? inputFromArguments(structured.argumentsText, structured.freeformTool) : undefined;
|
|
352
446
|
return input !== undefined && freeformBody(input) === freeformBody(repeated.body);
|
|
353
447
|
}
|
|
354
448
|
|
|
449
|
+
/** A second call can explain a repeated pair even when its arguments cannot safely be rewritten. */
|
|
450
|
+
function hasDoubledInput(structured: StructuredToolCallReference, repeated: SerializedToolCall): boolean {
|
|
451
|
+
if (!structured.names.has(repeated.name)) return false;
|
|
452
|
+
const input = inputFromArguments(structured.argumentsText, structured.freeformTool);
|
|
453
|
+
if (input === undefined) return false;
|
|
454
|
+
const body = freeformBody(repeated.body);
|
|
455
|
+
const normalized = freeformBody(input);
|
|
456
|
+
return normalized === body + body || normalized === body + "\n" + body;
|
|
457
|
+
}
|
|
458
|
+
|
|
355
459
|
/** The `[start, end)` ranges of blocks whose function identity and freeform input match a dispatched call. */
|
|
356
460
|
function duplicatedSerializedToolCallRanges(
|
|
357
461
|
text: string,
|
|
@@ -361,14 +465,16 @@ function duplicatedSerializedToolCallRanges(
|
|
|
361
465
|
if (structuredCalls.length === 0) return [];
|
|
362
466
|
const repeated = repeatedCallIn(text, context);
|
|
363
467
|
if (repeated) {
|
|
364
|
-
//
|
|
468
|
+
// A doubled call beside an agreeing call leaves the pair ambiguous. If reduction was
|
|
469
|
+
// refused, keep the markup too: otherwise the visible text and executable call disagree.
|
|
365
470
|
const matching = structuredCalls.filter(structured => agreesWithRepeatedBlock(structured, repeated));
|
|
366
|
-
|
|
471
|
+
const doubled = structuredCalls.some(structured => structured !== matching[0] && hasDoubledInput(structured, repeated));
|
|
472
|
+
return matching.length === 1 && !doubled ? [{ start: repeated.start, end: repeated.end }] : [];
|
|
367
473
|
}
|
|
368
474
|
return callsIn(text, context).filter(call => {
|
|
369
475
|
const body = freeformBody(call.body);
|
|
370
476
|
return structuredCalls.some(structured => {
|
|
371
|
-
const input = structured.names.has(call.name) ? inputFromArguments(structured.argumentsText) : undefined;
|
|
477
|
+
const input = structured.names.has(call.name) ? inputFromArguments(structured.argumentsText, structured.freeformTool) : undefined;
|
|
372
478
|
return input !== undefined && freeformBody(input) === body;
|
|
373
479
|
});
|
|
374
480
|
});
|
|
@@ -458,16 +564,16 @@ export interface StructuredToolCallInput {
|
|
|
458
564
|
wireName: string;
|
|
459
565
|
restoredName: string;
|
|
460
566
|
argumentsText: string;
|
|
567
|
+
freeformTool?: FreeformToolIdentity;
|
|
461
568
|
}
|
|
462
569
|
|
|
463
570
|
/**
|
|
464
571
|
* Repairs the arguments of every structured call in one response against the visible text that
|
|
465
572
|
* response carried, and returns them in input order. The per-call prefix repair stands alone,
|
|
466
573
|
* because the markup it proves is matched against that call's own repaired input. The
|
|
467
|
-
* doubled-input reduction is applied only when exactly
|
|
468
|
-
*
|
|
469
|
-
* arguments,
|
|
470
|
-
* to cover the whole batch rather than one call at a time.
|
|
574
|
+
* doubled-input reduction is applied only when exactly one call in the batch explains the repeated
|
|
575
|
+
* pair, including doubled shapes that cannot themselves be rewritten. It rewrites executable
|
|
576
|
+
* arguments, so a second explanatory call leaves the pair ambiguous.
|
|
471
577
|
*/
|
|
472
578
|
export function reconcileStructuredToolCalls(
|
|
473
579
|
calls: readonly StructuredToolCallInput[],
|
|
@@ -475,7 +581,7 @@ export function reconcileStructuredToolCalls(
|
|
|
475
581
|
): StructuredToolCallReference[] {
|
|
476
582
|
const references = calls.map(call => {
|
|
477
583
|
const names = new Set([call.wireName, call.restoredName]);
|
|
478
|
-
return { names, argumentsText: repairArgumentsDuplicatedBesideSerializedCall(call.argumentsText, names, serializedText) };
|
|
584
|
+
return { names, freeformTool: call.freeformTool, argumentsText: repairArgumentsDuplicatedBesideSerializedCall(call.argumentsText, names, serializedText) };
|
|
479
585
|
});
|
|
480
586
|
return reduceUnambiguousDoubledInput(references, serializedText);
|
|
481
587
|
}
|
|
@@ -486,9 +592,8 @@ export function reconcileStructuredToolCalls(
|
|
|
486
592
|
* alone. A call whose input already equals the repeated body is a competing explanation, not a
|
|
487
593
|
* bystander: both readings account for the pair and the response never picks one, so a batch with
|
|
488
594
|
* two qualifying calls keeps every argument exactly as sent. The reduction then rewrites nothing,
|
|
489
|
-
* and the
|
|
490
|
-
*
|
|
491
|
-
* already agrees leaves nothing to reduce.
|
|
595
|
+
* and the range matcher keeps the markup when a doubled call competes with an agreeing call.
|
|
596
|
+
* An already-agreeing call leaves nothing to reduce.
|
|
492
597
|
*/
|
|
493
598
|
function reduceUnambiguousDoubledInput(
|
|
494
599
|
references: readonly StructuredToolCallReference[],
|
|
@@ -499,13 +604,14 @@ function reduceUnambiguousDoubledInput(
|
|
|
499
604
|
const candidates = references.map(reference => ({
|
|
500
605
|
reduction: doubledInputReduction(reference.argumentsText, reference.names, repeated),
|
|
501
606
|
explains: agreesWithRepeatedBlock(reference, repeated),
|
|
607
|
+
doubled: hasDoubledInput(reference, repeated),
|
|
502
608
|
}));
|
|
503
|
-
if (candidates.filter(candidate => candidate.explains || candidate.
|
|
609
|
+
if (candidates.filter(candidate => candidate.explains || candidate.doubled).length !== 1) {
|
|
504
610
|
return [...references];
|
|
505
611
|
}
|
|
506
612
|
return references.map((reference, index) => {
|
|
507
613
|
const reduction = candidates[index]!.reduction;
|
|
508
|
-
return reduction === undefined ? reference : {
|
|
614
|
+
return reduction === undefined ? reference : { ...reference, argumentsText: reduction };
|
|
509
615
|
});
|
|
510
616
|
}
|
|
511
617
|
|
|
@@ -558,12 +664,3 @@ export function reconcileSerializedToolCallEvents(
|
|
|
558
664
|
function textEvents(text: string): AdapterEvent[] {
|
|
559
665
|
return text.length > 0 ? [{ type: "text_delta", text }] : [];
|
|
560
666
|
}
|
|
561
|
-
|
|
562
|
-
/** Non-whitespace characters after the last closed block, or 0 while a later block is still open. */
|
|
563
|
-
function proseAfterClosedBlock(text: string): number {
|
|
564
|
-
const closer = text.lastIndexOf(CLOSE_TAG);
|
|
565
|
-
if (closer < 0) return 0;
|
|
566
|
-
const tail = text.slice(closer + CLOSE_TAG.length);
|
|
567
|
-
if (tail.includes(OPEN_TAG)) return 0;
|
|
568
|
-
return tail.replace(/\s+/g, "").length;
|
|
569
|
-
}
|
|
@@ -45,7 +45,7 @@ import { messagesToChatFormat } from "./openai-chat/messages";
|
|
|
45
45
|
import { withOpenAIChatToolNames } from "./openai-chat/tool-name-registry";
|
|
46
46
|
import { openAIChatTransport, stripBracketedModelSuffix } from "./openai-chat/wire";
|
|
47
47
|
import { toolChoiceToChatFormat, toolsToChatFormatForProvider } from "./openai-chat/tool-schema";
|
|
48
|
-
import { reconcileSerializedToolCallEvents, reconcileStructuredToolCall, reconcileStructuredToolCalls, SerializedToolCallContentBuffer } from "./openai-chat/serialized-tool-call-content";
|
|
48
|
+
import { freeformToolsByWireName, type FreeformToolIdentity, reconcileSerializedToolCallEvents, reconcileStructuredToolCall, reconcileStructuredToolCalls, SerializedToolCallContentBuffer } from "./openai-chat/serialized-tool-call-content";
|
|
49
49
|
|
|
50
50
|
export { stripBracketedModelSuffix } from "./openai-chat/wire";
|
|
51
51
|
export { buildOpenAIChatPassthroughRequest } from "./openai-chat/passthrough";
|
|
@@ -93,6 +93,7 @@ function canSerializeOpenAIChatServiceTier(
|
|
|
93
93
|
|
|
94
94
|
export function createOpenAIChatAdapter(provider: OcxProviderConfig): ProviderAdapter {
|
|
95
95
|
let lastRequestedModelId: string | undefined;
|
|
96
|
+
let freeformTools = new Map<string, FreeformToolIdentity>();
|
|
96
97
|
return withOpenAIChatToolNames(toolNames => ({
|
|
97
98
|
name: "openai-chat",
|
|
98
99
|
|
|
@@ -102,6 +103,7 @@ export function createOpenAIChatAdapter(provider: OcxProviderConfig): ProviderAd
|
|
|
102
103
|
lastRequestedModelId = parsed.modelId;
|
|
103
104
|
const { url, headers, hasCredential } = openAIChatTransport(provider);
|
|
104
105
|
const messages = toolNames.messages(parsed, provider.baseUrl, messagesToChatFormat(parsed, provider));
|
|
106
|
+
freeformTools = freeformToolsByWireName(parsed.context.tools, tool => toolNames.registry().alias(tool));
|
|
105
107
|
const finish = (): AdapterRequest => {
|
|
106
108
|
const tools = toolsToChatFormatForProvider(parsed, provider, toolNames.registry());
|
|
107
109
|
const toolChoice = toolChoiceToChatFormat(parsed.options.toolChoice, parsed.context.tools, provider, toolNames.registry());
|
|
@@ -338,7 +340,7 @@ export function createOpenAIChatAdapter(provider: OcxProviderConfig): ProviderAd
|
|
|
338
340
|
}
|
|
339
341
|
}
|
|
340
342
|
// Held markup is released only now, as one batch per response: the doubled-input repair needs every call.
|
|
341
|
-
const references = reconcileStructuredToolCalls(calls.map(call => ({ wireName: call.name, restoredName: toolNames.restore(call.name), argumentsText: call.args })), toolCallContent.current());
|
|
343
|
+
const references = reconcileStructuredToolCalls(calls.map(call => ({ wireName: call.name, restoredName: toolNames.restore(call.name), argumentsText: call.args, freeformTool: freeformTools.get(call.name) })), toolCallContent.current());
|
|
342
344
|
calls.forEach((call, index) => { call.args = references[index]!.argumentsText; });
|
|
343
345
|
yield* toolCallContent.drain(references);
|
|
344
346
|
for (const call of calls) {
|
|
@@ -773,7 +775,7 @@ export function createOpenAIChatAdapter(provider: OcxProviderConfig): ProviderAd
|
|
|
773
775
|
const contentEnd = events.length;
|
|
774
776
|
const answerText = events.slice(contentStart).map(event => (event.type === "text_delta" ? event.text : "")).join("");
|
|
775
777
|
// Each call holds the delta event it emitted, so the batch repair sets its arguments later.
|
|
776
|
-
const structuredCalls: { wireName: string; restoredName: string; argumentsText: string; delta: Extract<AdapterEvent, { type: "tool_call_delta" }> }[] = [];
|
|
778
|
+
const structuredCalls: { wireName: string; restoredName: string; argumentsText: string; freeformTool?: FreeformToolIdentity; delta: Extract<AdapterEvent, { type: "tool_call_delta" }> }[] = [];
|
|
777
779
|
const rawToolCalls = msg.tool_calls;
|
|
778
780
|
if (rawToolCalls !== undefined && rawToolCalls !== null) {
|
|
779
781
|
if (!Array.isArray(rawToolCalls)) {
|
|
@@ -797,7 +799,7 @@ export function createOpenAIChatAdapter(provider: OcxProviderConfig): ProviderAd
|
|
|
797
799
|
return [invalidToolCallsEvent(rawToolCalls, "response", usage)];
|
|
798
800
|
}
|
|
799
801
|
const delta: Extract<AdapterEvent, { type: "tool_call_delta" }> = { type: "tool_call_delta", arguments: args };
|
|
800
|
-
structuredCalls.push({ wireName: name, restoredName: toolNames.restore(name), argumentsText: args, delta });
|
|
802
|
+
structuredCalls.push({ wireName: name, restoredName: toolNames.restore(name), argumentsText: args, freeformTool: freeformTools.get(name), delta });
|
|
801
803
|
events.push({ type: "tool_call_start", id, name: toolNames.restore(name) }, delta, { type: "tool_call_end" });
|
|
802
804
|
}
|
|
803
805
|
}
|
|
@@ -474,6 +474,7 @@ export function createResponsesPassthroughAdapter(provider: OcxProviderConfig):
|
|
|
474
474
|
dropNullContentChannel: !isOpenAiOperatedResponsesDestination(provider),
|
|
475
475
|
stripEncryptedContent: threadServingIdentityChanged || requiresPlaintextReasoningReplay(provider),
|
|
476
476
|
dropForeignItemId: parsed._dropForeignReasoningItemIds === true,
|
|
477
|
+
requirePlaintextReasoning: requiresPlaintextReasoningReplay(provider),
|
|
477
478
|
},
|
|
478
479
|
),
|
|
479
480
|
provider,
|
|
@@ -11,6 +11,37 @@ export function dropResponsesReasoningInputItems(body: unknown): unknown {
|
|
|
11
11
|
return input.length === body.input.length ? body : { ...body, input };
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
+
/** Whether the item already carries a non-empty `reasoning_text` content part. */
|
|
15
|
+
function hasPlaintextReasoningContent(content: unknown): boolean {
|
|
16
|
+
return Array.isArray(content) && content.some(part =>
|
|
17
|
+
isPlainObject(part)
|
|
18
|
+
&& part.type === "reasoning_text"
|
|
19
|
+
&& typeof part.text === "string"
|
|
20
|
+
&& part.text.length > 0
|
|
21
|
+
);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Backfill for a reasoning item that would reach a plaintext-required wire with no
|
|
26
|
+
* `reasoning_text`: its text lived only in ciphertext the provider cannot read and was
|
|
27
|
+
* stripped, so replay the item's `summary_text` as `reasoning_text` — real model output,
|
|
28
|
+
* unlike a fabricated chain — and fall back to the same minimal placeholder the chat
|
|
29
|
+
* adapter emits for `requiresReasoningPlaceholderModels` (#5421). DeepSeek's thinking
|
|
30
|
+
* mode answers a bare emptied item with `The reasoning_text in the thinking mode must
|
|
31
|
+
* be passed back to the API` and aborts the turn.
|
|
32
|
+
*/
|
|
33
|
+
function plaintextReasoningBackfill(rec: Record<string, unknown>): unknown[] {
|
|
34
|
+
const fromSummary = Array.isArray(rec.summary)
|
|
35
|
+
? rec.summary
|
|
36
|
+
.filter(part => isPlainObject(part)
|
|
37
|
+
&& part.type === "summary_text"
|
|
38
|
+
&& typeof part.text === "string"
|
|
39
|
+
&& part.text.length > 0)
|
|
40
|
+
.map(part => ({ type: "reasoning_text", text: part.text }))
|
|
41
|
+
: [];
|
|
42
|
+
return fromSummary.length > 0 ? fromSummary : [{ type: "reasoning_text", text: " " }];
|
|
43
|
+
}
|
|
44
|
+
|
|
14
45
|
/**
|
|
15
46
|
* Sanitize reasoning input by field policy, not by preserving each item's shape. Retaining a
|
|
16
47
|
* native `encrypted_content` guarantees only that blob value: `status` is always removed;
|
|
@@ -40,6 +71,13 @@ export function sanitizeReasoningInputContent(
|
|
|
40
71
|
* store this destination cannot read; see `OcxParsedRequest._dropForeignReasoningItemIds`.
|
|
41
72
|
*/
|
|
42
73
|
dropForeignItemId?: boolean;
|
|
74
|
+
/**
|
|
75
|
+
* The destination documents a strict plaintext-replay contract (DeepSeek's Responses
|
|
76
|
+
* thinking mode): every reasoning input item must carry `reasoning_text` content.
|
|
77
|
+
* An item left with none is backfilled from its summary, or with a minimal
|
|
78
|
+
* placeholder, rather than emitted in the shape the upstream 400s on.
|
|
79
|
+
*/
|
|
80
|
+
requirePlaintextReasoning?: boolean;
|
|
43
81
|
},
|
|
44
82
|
): unknown {
|
|
45
83
|
if (!body || typeof body !== "object" || Array.isArray(body)) return body;
|
|
@@ -85,9 +123,14 @@ export function sanitizeReasoningInputContent(
|
|
|
85
123
|
const blankContent = !dropNullContentChannel
|
|
86
124
|
&& !opts?.preserveRawReasoningContent
|
|
87
125
|
&& (hasRawContent || hasOcxEnvelope);
|
|
126
|
+
// A strict plaintext-replay destination cannot consume a reasoning item emptied of its
|
|
127
|
+
// `reasoning_text` — the resumed-subagent shape (#5421), where the text lived only in
|
|
128
|
+
// ciphertext the provider cannot read. Backfill after every other strip has run.
|
|
129
|
+
const missingPlaintextReasoning = opts?.requirePlaintextReasoning === true
|
|
130
|
+
&& !hasPlaintextReasoningContent(rec.content);
|
|
88
131
|
if (
|
|
89
132
|
!blankContent && !stripOutputStatus && !stripEncryptedContent && !dropNullContentChannel
|
|
90
|
-
&& !missingSummary && !dropItemId
|
|
133
|
+
&& !missingSummary && !dropItemId && !missingPlaintextReasoning
|
|
91
134
|
) {
|
|
92
135
|
return item;
|
|
93
136
|
}
|
|
@@ -106,6 +149,7 @@ export function sanitizeReasoningInputContent(
|
|
|
106
149
|
// `preserveResponsesReasoningContent` keep it — deleting valid replay content there breaks
|
|
107
150
|
// continuations after tool calls (issue #875 family).
|
|
108
151
|
if (blankContent) next.content = [];
|
|
152
|
+
if (missingPlaintextReasoning) next.content = plaintextReasoningBackfill(next);
|
|
109
153
|
return next;
|
|
110
154
|
});
|
|
111
155
|
|
|
@@ -49,7 +49,13 @@ import { bridgeToResponsesSSE } from "./sse";
|
|
|
49
49
|
export function buildResponseJSON(
|
|
50
50
|
events: AdapterEvent[],
|
|
51
51
|
modelId: string,
|
|
52
|
-
options?: Parameters<typeof buildResponseJSONWithBudget>[2]
|
|
52
|
+
options?: Parameters<typeof buildResponseJSONWithBudget>[2] & {
|
|
53
|
+
/**
|
|
54
|
+
* False when the body is not what the client receives: a direct client encoder counts its
|
|
55
|
+
* own relayed frames and folds the same events here only for the completion effects.
|
|
56
|
+
*/
|
|
57
|
+
recordBufferedDelivery?: boolean;
|
|
58
|
+
},
|
|
53
59
|
): Record<string, unknown> {
|
|
54
60
|
// Default-budget safety net: a caller that omits the budget gets a bounded
|
|
55
61
|
// default (disposed with the call), never the unbounded append path.
|
|
@@ -58,7 +64,9 @@ export function buildResponseJSON(
|
|
|
58
64
|
// A buffered turn delivers its whole answer as one body, so nothing calls the per-frame
|
|
59
65
|
// recorder on the SSE bridge. Without this the attempt would persist adapter events with
|
|
60
66
|
// zero relayed ones, which is the loss signal -- raised on every non-streaming request.
|
|
61
|
-
|
|
67
|
+
if (options.recordBufferedDelivery !== false) {
|
|
68
|
+
attemptDeliveryRecorder(options.translatorBudget)?.noteBufferedDelivery(body);
|
|
69
|
+
}
|
|
62
70
|
return body;
|
|
63
71
|
}
|
|
64
72
|
const budget = createTranslatorBudget();
|