@bitkyc08/opencodex 2.55.0 → 2.57.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/bin/ocx.mjs +10 -0
- package/gui/dist/assets/{index-BBOZWGB6.css → index-C5-RdDmD.css} +1 -1
- package/gui/dist/assets/{index-VuoiWj9J.js → index-Cz7CLdif.js} +21 -21
- package/gui/dist/index.html +2 -2
- package/package.json +4 -3
- package/src/adapters/base.ts +21 -0
- package/src/adapters/codebuddy/adapter.ts +2 -1
- package/src/adapters/codebuddy/scaffold-guard.ts +248 -0
- package/src/adapters/command-code.ts +1 -1
- package/src/adapters/cursor/envelope-echo.ts +8 -2
- package/src/adapters/cursor/transport-retry.ts +46 -1
- package/src/adapters/cursor.ts +4 -0
- package/src/adapters/google.ts +7 -7
- package/src/adapters/kiro/adapter.ts +42 -1
- package/src/adapters/kiro/payload.ts +17 -3
- package/src/adapters/kiro/reasoning.ts +70 -7
- package/src/adapters/kiro/stream.ts +8 -2
- package/src/adapters/kiro/wire.ts +2 -1
- package/src/adapters/kiro-events.ts +21 -13
- package/src/adapters/kiro-retry.ts +23 -4
- package/src/adapters/openai-chat/errors.ts +116 -0
- package/src/adapters/openai-chat/messages.ts +346 -0
- package/src/adapters/openai-chat/passthrough.ts +146 -0
- package/src/adapters/openai-chat/response-events.ts +117 -0
- package/src/adapters/openai-chat/tool-call-validation.ts +200 -0
- package/src/adapters/openai-chat/tool-name-registry.ts +166 -0
- package/src/adapters/openai-chat/tool-schema.ts +495 -0
- package/src/adapters/openai-chat/wire.ts +50 -0
- package/src/adapters/openai-chat.ts +40 -1452
- package/src/adapters/openai-responses/canonical-forward.ts +202 -0
- package/src/adapters/openai-responses/image-gen.ts +406 -0
- package/src/adapters/openai-responses/internal.ts +3 -0
- package/src/adapters/openai-responses/passthrough.ts +642 -0
- package/src/adapters/openai-responses/prompt-cache.ts +83 -0
- package/src/adapters/openai-responses/reasoning.ts +220 -0
- package/src/adapters/openai-responses/request-strips.ts +185 -0
- package/src/adapters/openai-responses/tool-output-recovery.ts +509 -0
- package/src/adapters/openai-responses/tool-schema.ts +293 -0
- package/src/adapters/openai-responses/web-search.ts +156 -0
- package/src/adapters/openai-responses.ts +4 -2625
- package/src/bridge/errors.ts +58 -0
- package/src/bridge/internal.ts +174 -0
- package/src/bridge/response-json.ts +630 -0
- package/src/bridge/sse.ts +1462 -0
- package/src/bridge.ts +5 -2204
- package/src/chat/inbound.ts +12 -1
- package/src/claude/desktop-profile.ts +66 -9
- package/src/claude/outbound.ts +18 -0
- package/src/cli/account-main.ts +1 -1
- package/src/cli/capabilities.ts +2 -2
- package/src/cli/combo.ts +10 -1
- package/src/cli/index.ts +48 -5
- package/src/cli/registry.ts +2 -1
- package/src/cli/system-command.ts +4 -4
- package/src/clients/config-export.ts +7 -3
- package/src/codex/account-label.ts +14 -3
- package/src/codex/account-lifecycle.ts +3 -0
- package/src/codex/account-store.ts +184 -35
- package/src/codex/account-usability.ts +21 -0
- package/src/codex/auth-api/account-list.ts +507 -0
- package/src/codex/auth-api/http.ts +32 -0
- package/src/codex/auth-api/login-flow.ts +566 -0
- package/src/codex/auth-api/login-state.ts +64 -0
- package/src/codex/auth-api/main-account-probe.ts +331 -0
- package/src/codex/auth-api/pool-mode-gate.ts +274 -0
- package/src/codex/auth-api/pool-quota-probe.ts +512 -0
- package/src/codex/auth-api/reset-credit-service.ts +431 -0
- package/src/codex/auth-api/routes.ts +425 -0
- package/src/codex/auth-api/runtime-config.ts +48 -0
- package/src/codex/auth-api.ts +27 -3118
- package/src/codex/auth-context.ts +252 -35
- package/src/codex/catalog/aggregation.ts +80 -1
- package/src/codex/catalog/auto-review.ts +507 -0
- package/src/codex/catalog/build-entries.ts +981 -0
- package/src/codex/catalog/combo-member.ts +375 -0
- package/src/codex/catalog/derive-entry.ts +229 -0
- package/src/codex/catalog/effort.ts +0 -1
- package/src/codex/catalog/gated-native-warn.ts +63 -0
- package/src/codex/catalog/gather-capture.ts +533 -0
- package/src/codex/catalog/model-hints.ts +691 -0
- package/src/codex/catalog/model-visibility.ts +305 -0
- package/src/codex/catalog/provider-fetch.ts +52 -2942
- package/src/codex/catalog/provider-models.ts +685 -0
- package/src/codex/catalog/remote.ts +30 -0
- package/src/codex/catalog/restore.ts +132 -0
- package/src/codex/catalog/retained-sync.ts +714 -0
- package/src/codex/catalog/routed-gather.ts +895 -0
- package/src/codex/catalog/subagent-roster.ts +176 -0
- package/src/codex/catalog/sync.ts +52 -2698
- package/src/codex/cli-install-provenance.ts +7 -1
- package/src/codex/convergence.ts +7 -2
- package/src/codex/desktop-app/types.ts +11 -2
- package/src/codex/desktop-app/windows.ts +5 -5
- package/src/codex/inject/config-toml.ts +563 -0
- package/src/codex/inject/remove.ts +192 -0
- package/src/codex/inject/restore.ts +567 -0
- package/src/codex/inject/routing-classify.ts +109 -0
- package/src/codex/inject/routing-target.ts +125 -0
- package/src/codex/inject.ts +89 -1444
- package/src/codex/lineage.ts +458 -0
- package/src/codex/model-entitlements.ts +152 -15
- package/src/codex/pool-refresh-backoff.ts +161 -0
- package/src/codex/quota-rejection.ts +104 -15
- package/src/codex/routing/active-account.ts +194 -0
- package/src/codex/routing/cache-affinity.ts +70 -0
- package/src/codex/routing/cooldown-math.ts +285 -0
- package/src/codex/routing/health-store.ts +402 -0
- package/src/codex/routing/probe-lease.ts +358 -0
- package/src/codex/routing/selection.ts +780 -0
- package/src/codex/routing/thread-affinity.ts +586 -0
- package/src/codex/routing/transient-hold-dispatch.ts +141 -0
- package/src/codex/routing.ts +370 -2271
- package/src/codex/shim-fingerprint.ts +223 -0
- package/src/codex/shim-inspect.ts +175 -0
- package/src/codex/shim-probe.ts +367 -0
- package/src/codex/shim-restore-lock.ts +169 -0
- package/src/codex/shim-state-file.ts +151 -0
- package/src/codex/shim-templates.ts +265 -0
- package/src/codex/shim.ts +48 -1268
- package/src/codex/warmup.ts +1 -1
- package/src/combos/failover.ts +85 -0
- package/src/combos/request.ts +17 -10
- package/src/combos/types.ts +23 -2
- package/src/config/diagnostics.ts +705 -0
- package/src/config/feature-flags.ts +55 -0
- package/src/config/live-reconcile.ts +403 -0
- package/src/config/load-degrade.ts +880 -0
- package/src/config/mutation-lock.ts +244 -0
- package/src/config/openai-tier-backup.ts +268 -0
- package/src/config/pending-teardown.ts +31 -0
- package/src/config/persist-unlocked.ts +92 -0
- package/src/config/proxy-env.ts +188 -0
- package/src/config/salvage.ts +244 -0
- package/src/config/schema/config-schema.ts +640 -0
- package/src/config/schema/leaf-validators.ts +855 -0
- package/src/config/warn-memo.ts +28 -0
- package/src/config.ts +234 -4481
- package/src/generated/compatibility-version.json +649 -121
- package/src/images/loop.ts +1 -1
- package/src/lib/errors.ts +17 -0
- package/src/lib/request-execution-budget.ts +198 -23
- package/src/lib/spend-reservation-ledger.ts +958 -0
- package/src/lib/state-store-registrations.ts +6 -2
- package/src/lib/test-home-guard.ts +85 -1
- package/src/lib/upstream-retry.ts +132 -21
- package/src/lib/windows-elevation.ts +76 -14
- package/src/lib/workflow-budget.ts +553 -30
- package/src/oauth/index.ts +2 -2
- package/src/oauth/key-providers.ts +2 -2
- package/src/providers/kiro-models.ts +4 -3
- package/src/providers/label.ts +19 -1
- package/src/providers/model-discovery.ts +16 -0
- package/src/providers/quota/account-cache.ts +441 -0
- package/src/providers/quota/antigravity.ts +295 -0
- package/src/providers/quota/report-cache.ts +320 -0
- package/src/providers/quota/vendor-probes-key.ts +1243 -0
- package/src/providers/quota/vendor-probes-oauth.ts +590 -0
- package/src/providers/quota.ts +324 -3079
- package/src/providers/registry/entries-core.ts +1228 -0
- package/src/providers/registry/entries-extended.ts +1213 -0
- package/src/providers/registry/model-seeds.ts +912 -0
- package/src/providers/registry/types.ts +352 -0
- package/src/providers/registry.ts +24 -3536
- package/src/responses/continuation-ownership.ts +29 -0
- package/src/responses/reasoning-envelope.ts +6 -3
- package/src/responses/state/replay-fingerprint.ts +80 -0
- package/src/responses/state/snapshot-codec.ts +104 -0
- package/src/responses/state/spill-failure.ts +118 -0
- package/src/responses/state/spill-queue.ts +665 -0
- package/src/responses/state/temp-recovery.ts +257 -0
- package/src/responses/state.ts +82 -1143
- package/src/routing/identity-domains.ts +456 -0
- package/src/routing/probe-lease.ts +613 -0
- package/src/server/chat-completions.ts +3 -1
- package/src/server/chat-native.ts +37 -9
- package/src/server/index/bounded-request.ts +88 -0
- package/src/server/index/live-sideband.ts +601 -0
- package/src/server/index/serve-options.ts +1766 -0
- package/src/server/index/startup-warnings.ts +213 -0
- package/src/server/index/websocket-handler.ts +339 -0
- package/src/server/index.ts +45 -2552
- package/src/server/inspection-tee.ts +107 -0
- package/src/server/live.ts +46 -1
- package/src/server/management/combo-routes.ts +10 -1
- package/src/server/management/route-registry.ts +26 -23
- package/src/server/management/shared.ts +8 -5
- package/src/server/management/workflow-budget-routes.ts +133 -0
- package/src/server/management-api.ts +12 -0
- package/src/server/relay-eager.ts +2 -0
- package/src/server/relay.ts +14 -19
- package/src/server/request-log-conversation.ts +9 -7
- package/src/server/request-log.ts +372 -4
- package/src/server/response-log-body.ts +153 -0
- package/src/server/responses/account-change-state.ts +307 -0
- package/src/server/responses/adapter-continuation.ts +540 -0
- package/src/server/responses/adapter-delivery.ts +208 -0
- package/src/server/responses/adapter-dispatch.ts +1042 -0
- package/src/server/responses/codex-ws-wire.ts +5 -0
- package/src/server/responses/collaboration.ts +74 -4
- package/src/server/responses/combo-session-recall.ts +68 -8
- package/src/server/responses/compact.ts +113 -17
- package/src/server/responses/completion-policy.ts +33 -0
- package/src/server/responses/core-auth.ts +529 -0
- package/src/server/responses/core-codex-account.ts +907 -0
- package/src/server/responses/core-combo-failure.ts +210 -0
- package/src/server/responses/core-combo.ts +787 -0
- package/src/server/responses/core-errors.ts +170 -0
- package/src/server/responses/core-lifetime.ts +95 -0
- package/src/server/responses/core-normalize.ts +350 -0
- package/src/server/responses/core-opaque-recovery.ts +380 -0
- package/src/server/responses/core-options.ts +159 -0
- package/src/server/responses/core-replay.ts +298 -0
- package/src/server/responses/core.ts +192 -8893
- package/src/server/responses/encrypted-payload.ts +0 -1
- package/src/server/responses/input-admission.ts +126 -6
- package/src/server/responses/passthrough-delivery.ts +869 -0
- package/src/server/responses/passthrough-dispatch.ts +1494 -0
- package/src/server/responses/passthrough-error.ts +38 -2
- package/src/server/responses/passthrough-execution.ts +54 -0
- package/src/server/responses/request-prepare.ts +1080 -0
- package/src/server/responses/request-send-budget.ts +259 -0
- package/src/server/responses/request-sidecar-auth.ts +149 -0
- package/src/server/responses/request-spend.ts +147 -0
- package/src/server/responses/request-transport.ts +803 -0
- package/src/server/responses/response-effects.ts +157 -0
- package/src/server/responses/run-turn-execution.ts +476 -0
- package/src/server/responses/sidecar-execution.ts +463 -0
- package/src/server/responses/terminal-guard.ts +65 -4
- package/src/server/responses-image-gen-repair.ts +1 -1
- package/src/server/responses-undeclared-tool-guard.ts +9 -5
- package/src/server/workflow-refusal.ts +84 -0
- package/src/service/windows-ops.ts +210 -16
- package/src/service/windows-scheduler.ts +28 -21
- package/src/service.ts +1 -1
- package/src/types/config.ts +34 -1
- package/src/types/request.ts +8 -5
- package/src/types/tools.ts +24 -0
- package/src/types.ts +2 -0
- package/src/update/index.ts +10 -0
- package/src/update/stop-contract.d.mts +1 -0
- package/src/update/stop-contract.mjs +19 -0
- package/src/update/stop-decision.d.mts +1 -1
- package/src/update/stop-decision.mjs +12 -3
- package/src/usage/log.ts +147 -1
- package/src/usage/summary.ts +171 -21
- package/src/vision/anthropic-describe.ts +1 -1
- package/src/vision/describe.ts +5 -5
- package/src/web-search/anthropic-executor.ts +1 -1
- package/src/web-search/exa-executor.ts +1 -1
- package/src/web-search/executor.ts +1 -1
- package/src/web-search/gemini-executor.ts +1 -1
- package/src/web-search/loop.ts +1 -1
- package/src/web-search/ollama-executor.ts +1 -1
- package/src/web-search/parse.ts +67 -14
- package/src/web-search/passthrough-bridge.ts +64 -31
- package/src/web-search/xai-executor.ts +1 -1
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
import { debugProviderDiagnostic } from "../../lib/debug";
|
|
2
|
+
import { isDebugEnabled } from "../../lib/debug-settings";
|
|
3
|
+
|
|
4
|
+
export function isRecord(value: unknown): value is Record<string, unknown> {
|
|
5
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
type InvalidToolCallReason =
|
|
9
|
+
| "tool_calls_not_array"
|
|
10
|
+
| "tool_call_not_object"
|
|
11
|
+
| "tool_call_id_invalid"
|
|
12
|
+
| "tool_call_function_not_object"
|
|
13
|
+
| "tool_call_function_name_invalid"
|
|
14
|
+
| "tool_call_function_name_blank"
|
|
15
|
+
| "tool_call_function_arguments_invalid";
|
|
16
|
+
|
|
17
|
+
export type InvalidToolCallDiagnostic = {
|
|
18
|
+
reason: InvalidToolCallReason;
|
|
19
|
+
callIndex?: number;
|
|
20
|
+
valueType: string;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
type InvalidFieldShape =
|
|
24
|
+
| {
|
|
25
|
+
kind: "object";
|
|
26
|
+
knownKeys: string[];
|
|
27
|
+
knownFieldTypes: Record<string, string>;
|
|
28
|
+
hasUnknownKeys: boolean;
|
|
29
|
+
}
|
|
30
|
+
| {
|
|
31
|
+
kind: "array";
|
|
32
|
+
length: number;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const SAFE_TOOL_CALL_SHAPE_KEYS = [
|
|
36
|
+
"name",
|
|
37
|
+
"type",
|
|
38
|
+
"value",
|
|
39
|
+
"function",
|
|
40
|
+
"arguments",
|
|
41
|
+
"id",
|
|
42
|
+
"index",
|
|
43
|
+
] as const;
|
|
44
|
+
const SAFE_TOOL_CALL_SHAPE_KEY_SET = new Set<string>(SAFE_TOOL_CALL_SHAPE_KEYS);
|
|
45
|
+
|
|
46
|
+
function structuralValueType(value: unknown): string {
|
|
47
|
+
return value === null ? "null" : Array.isArray(value) ? "array" : typeof value;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function invalidToolCallField(rawToolCalls: unknown, diagnostic: InvalidToolCallDiagnostic): unknown {
|
|
51
|
+
if (diagnostic.reason === "tool_calls_not_array") return rawToolCalls;
|
|
52
|
+
if (!Array.isArray(rawToolCalls) || diagnostic.callIndex === undefined) return undefined;
|
|
53
|
+
|
|
54
|
+
const rawToolCall = rawToolCalls[diagnostic.callIndex];
|
|
55
|
+
if (diagnostic.reason === "tool_call_not_object") return rawToolCall;
|
|
56
|
+
if (!isRecord(rawToolCall)) return undefined;
|
|
57
|
+
if (diagnostic.reason === "tool_call_function_not_object") return rawToolCall.function;
|
|
58
|
+
|
|
59
|
+
const rawFunction = rawToolCall.function;
|
|
60
|
+
switch (diagnostic.reason) {
|
|
61
|
+
case "tool_call_id_invalid":
|
|
62
|
+
return rawToolCall.id;
|
|
63
|
+
case "tool_call_function_name_invalid":
|
|
64
|
+
return isRecord(rawFunction) ? rawFunction.name : undefined;
|
|
65
|
+
case "tool_call_function_arguments_invalid":
|
|
66
|
+
return isRecord(rawFunction) ? rawFunction.arguments : undefined;
|
|
67
|
+
default:
|
|
68
|
+
return undefined;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function fingerprintInvalidField(value: unknown): InvalidFieldShape | undefined {
|
|
73
|
+
if (Array.isArray(value)) return { kind: "array", length: value.length };
|
|
74
|
+
if (!isRecord(value)) return undefined;
|
|
75
|
+
|
|
76
|
+
const knownKeys: string[] = [];
|
|
77
|
+
const knownFieldTypes: Record<string, string> = {};
|
|
78
|
+
for (const key of SAFE_TOOL_CALL_SHAPE_KEYS) {
|
|
79
|
+
if (!Object.hasOwn(value, key)) continue;
|
|
80
|
+
knownKeys.push(key);
|
|
81
|
+
knownFieldTypes[key] = structuralValueType(value[key]);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
let hasUnknownKeys = false;
|
|
85
|
+
for (const key of Object.keys(value)) {
|
|
86
|
+
if (!SAFE_TOOL_CALL_SHAPE_KEY_SET.has(key)) {
|
|
87
|
+
hasUnknownKeys = true;
|
|
88
|
+
break;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return { kind: "object", knownKeys, knownFieldTypes, hasUnknownKeys };
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Streamed string fields are absent when null or undefined (#1731): OpenAI-compatible
|
|
96
|
+
* streamers repeat already-sent `id`/`name`/`arguments` as null on continuation deltas.
|
|
97
|
+
* The accumulator and this diagnostic share this predicate so they cannot disagree about
|
|
98
|
+
* which delta was the invalid one.
|
|
99
|
+
*/
|
|
100
|
+
export function isInvalidStreamStringField(value: unknown): boolean {
|
|
101
|
+
return value != null && typeof value !== "string";
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Explain only the rejected wire shape, never its values. This diagnostic exists so provider
|
|
106
|
+
* compatibility can be tightened from evidence without retaining tool arguments or credentials.
|
|
107
|
+
*/
|
|
108
|
+
export function diagnoseInvalidToolCalls(
|
|
109
|
+
rawToolCalls: unknown,
|
|
110
|
+
mode: "stream" | "response",
|
|
111
|
+
): InvalidToolCallDiagnostic | undefined {
|
|
112
|
+
if (!Array.isArray(rawToolCalls)) {
|
|
113
|
+
return { reason: "tool_calls_not_array", valueType: rawToolCalls === null ? "null" : typeof rawToolCalls };
|
|
114
|
+
}
|
|
115
|
+
for (let callIndex = 0; callIndex < rawToolCalls.length; callIndex++) {
|
|
116
|
+
const rawToolCall = rawToolCalls[callIndex];
|
|
117
|
+
if (!isRecord(rawToolCall)) {
|
|
118
|
+
return {
|
|
119
|
+
reason: "tool_call_not_object",
|
|
120
|
+
callIndex,
|
|
121
|
+
valueType: rawToolCall === null ? "null" : Array.isArray(rawToolCall) ? "array" : typeof rawToolCall,
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
if (mode === "stream") {
|
|
125
|
+
// The streamed path validates the pieces it is about to store (#1531): a present
|
|
126
|
+
// `function` must be a record, and a present `name`/`arguments`/`id` must be a string.
|
|
127
|
+
// Blank names are caught later at flush, not here, so they are not diagnosed on this
|
|
128
|
+
// branch. Describe exactly that boundary rather than tightening compatibility in a
|
|
129
|
+
// diagnostic change.
|
|
130
|
+
// #1731: "present" means the same thing here as in the accumulator — null and undefined
|
|
131
|
+
// are both absent, because some OpenAI-compatible streamers repeat already-sent fields
|
|
132
|
+
// as null on continuation deltas. A separate predicate here would diagnose accepted
|
|
133
|
+
// padding as the failure and point compatibility work at the wrong delta.
|
|
134
|
+
const streamFunction = (rawToolCall as { function?: unknown }).function;
|
|
135
|
+
if (streamFunction !== undefined && streamFunction !== null) {
|
|
136
|
+
if (!isRecord(streamFunction)) {
|
|
137
|
+
return {
|
|
138
|
+
reason: "tool_call_function_not_object",
|
|
139
|
+
callIndex,
|
|
140
|
+
valueType: Array.isArray(streamFunction) ? "array" : typeof streamFunction,
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
if (isInvalidStreamStringField(streamFunction.name)) {
|
|
144
|
+
return { reason: "tool_call_function_name_invalid", callIndex, valueType: typeof streamFunction.name };
|
|
145
|
+
}
|
|
146
|
+
if (isInvalidStreamStringField(streamFunction.arguments)) {
|
|
147
|
+
return { reason: "tool_call_function_arguments_invalid", callIndex, valueType: typeof streamFunction.arguments };
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
if (isInvalidStreamStringField(rawToolCall.id)) {
|
|
151
|
+
return { reason: "tool_call_id_invalid", callIndex, valueType: typeof rawToolCall.id };
|
|
152
|
+
}
|
|
153
|
+
continue;
|
|
154
|
+
}
|
|
155
|
+
// Precedence must mirror the buffered validator below, or a payload with more than one
|
|
156
|
+
// problem is reported under the wrong reason and sends compatibility work after the wrong
|
|
157
|
+
// shape. That validator checks the `function` container first (`!isRecord(rawToolCall) ||
|
|
158
|
+
// !isRecord(rawToolCall.function)`), then id/name/arguments types together, and only then
|
|
159
|
+
// the blank name.
|
|
160
|
+
if (!isRecord(rawToolCall.function)) {
|
|
161
|
+
return {
|
|
162
|
+
reason: "tool_call_function_not_object",
|
|
163
|
+
callIndex,
|
|
164
|
+
valueType: rawToolCall.function === null ? "null" : Array.isArray(rawToolCall.function) ? "array" : typeof rawToolCall.function,
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
if (typeof rawToolCall.id !== "string") {
|
|
168
|
+
return { reason: "tool_call_id_invalid", callIndex, valueType: typeof rawToolCall.id };
|
|
169
|
+
}
|
|
170
|
+
if (typeof rawToolCall.function.name !== "string") {
|
|
171
|
+
return { reason: "tool_call_function_name_invalid", callIndex, valueType: typeof rawToolCall.function.name };
|
|
172
|
+
}
|
|
173
|
+
if (typeof rawToolCall.function.arguments !== "string") {
|
|
174
|
+
return { reason: "tool_call_function_arguments_invalid", callIndex, valueType: typeof rawToolCall.function.arguments };
|
|
175
|
+
}
|
|
176
|
+
// Last, matching the validator: #1531 also rejects a blank or whitespace-only name here,
|
|
177
|
+
// because such a call cannot select a dispatch target. Reporting it as `name_invalid`
|
|
178
|
+
// would claim a type problem for a correctly-typed value, so it gets its own code.
|
|
179
|
+
if (rawToolCall.function.name.trim().length === 0) {
|
|
180
|
+
return { reason: "tool_call_function_name_blank", callIndex, valueType: "string" };
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
return undefined;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
export function logInvalidToolCalls(
|
|
187
|
+
mode: "stream" | "response",
|
|
188
|
+
rawToolCalls: unknown,
|
|
189
|
+
diagnosticOverride?: InvalidToolCallDiagnostic,
|
|
190
|
+
): void {
|
|
191
|
+
if (!isDebugEnabled()) return;
|
|
192
|
+
const diagnostic = diagnosticOverride ?? diagnoseInvalidToolCalls(rawToolCalls, mode);
|
|
193
|
+
if (!diagnostic) return;
|
|
194
|
+
const fieldShape = fingerprintInvalidField(invalidToolCallField(rawToolCalls, diagnostic));
|
|
195
|
+
debugProviderDiagnostic("openai-chat", "invalid-tool-calls", {
|
|
196
|
+
mode,
|
|
197
|
+
...diagnostic,
|
|
198
|
+
...(fieldShape ? { fieldShape } : {}),
|
|
199
|
+
});
|
|
200
|
+
}
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { namespacedToolName, type OcxParsedRequest, type OcxTool } from "../../types";
|
|
3
|
+
import { frameAgentRouterMessages } from "../agentrouter";
|
|
4
|
+
|
|
5
|
+
const MAX_CHAT_TOOL_NAME_LENGTH = 64;
|
|
6
|
+
const ALIAS_HINT_CHARS = 16;
|
|
7
|
+
const RESERVED_ALIAS_PATTERN = /^ocx_[a-zA-Z0-9_-]{16}_[a-zA-Z0-9_-]{43}$/;
|
|
8
|
+
type ToolIdentity = Readonly<Pick<OcxTool, "namespace" | "name">>;
|
|
9
|
+
|
|
10
|
+
export interface OpenAIChatToolNameRegistry {
|
|
11
|
+
alias(tool: ToolIdentity): string;
|
|
12
|
+
aliasWireName(wireName: string): string;
|
|
13
|
+
restore(wireName: string): string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
interface OpenAIChatToolNameScope {
|
|
17
|
+
messages(parsed: OcxParsedRequest, baseUrl: string, messages: readonly unknown[]): unknown;
|
|
18
|
+
registry(): OpenAIChatToolNameRegistry;
|
|
19
|
+
restore(wireName: string): string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function identityKey(tool: ToolIdentity): string {
|
|
23
|
+
return JSON.stringify([tool.namespace ?? null, tool.name]);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function boundedAlias(tool: ToolIdentity, wireName: string): string {
|
|
27
|
+
const hint = wireName
|
|
28
|
+
.replace(/[^a-zA-Z0-9_-]/g, "_")
|
|
29
|
+
.slice(-ALIAS_HINT_CHARS)
|
|
30
|
+
.padStart(ALIAS_HINT_CHARS, "_");
|
|
31
|
+
const key = identityKey(tool);
|
|
32
|
+
const digest = createHash("sha256")
|
|
33
|
+
.update(key)
|
|
34
|
+
.digest("base64url");
|
|
35
|
+
return `ocx_${hint}_${digest}`;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Catalog declarations plus structured calls retained in replay history. */
|
|
39
|
+
export function openAIChatToolNameIdentities(parsed: OcxParsedRequest): ToolIdentity[] {
|
|
40
|
+
const identities: ToolIdentity[] = [...(parsed.context.tools ?? [])];
|
|
41
|
+
for (const message of parsed.context.messages) {
|
|
42
|
+
if (message.role !== "assistant" || !Array.isArray(message.content)) continue;
|
|
43
|
+
for (const part of message.content) {
|
|
44
|
+
if (part.type !== "toolCall") continue;
|
|
45
|
+
identities.push({
|
|
46
|
+
name: part.name,
|
|
47
|
+
...(part.namespace === undefined ? {} : { namespace: part.namespace }),
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return identities;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* One collision domain for a translated Chat Completions request.
|
|
56
|
+
*
|
|
57
|
+
* Namespaced names whose flattened spelling exceeds Chat Completions' 64-character function-name
|
|
58
|
+
* bound are rewritten. Ordinary names and bare names pass through byte-for-byte unless they occupy
|
|
59
|
+
* the reserved alias spelling; those are re-aliased so no declaration can shadow another identity's
|
|
60
|
+
* deterministic alias. Distinct identities sharing one flattened spelling each keep an identity
|
|
61
|
+
* alias, while replay rewriting leaves that ambiguous spelling untouched. Echoed aliases restore to
|
|
62
|
+
* the original flattened name consumed by the Responses bridge's existing namespace map.
|
|
63
|
+
*/
|
|
64
|
+
export function createOpenAIChatToolNameRegistry(
|
|
65
|
+
tools: readonly ToolIdentity[] | undefined,
|
|
66
|
+
): OpenAIChatToolNameRegistry {
|
|
67
|
+
const identities = new Map<string, ToolIdentity>();
|
|
68
|
+
for (const tool of tools ?? []) identities.set(identityKey(tool), tool);
|
|
69
|
+
const sortedIdentities = [...identities.entries()]
|
|
70
|
+
.sort(([left], [right]) => left < right ? -1 : left > right ? 1 : 0);
|
|
71
|
+
|
|
72
|
+
const aliasesByIdentity = new Map<string, string>();
|
|
73
|
+
const aliasesByWireName = new Map<string, string>();
|
|
74
|
+
const originalsByAlias = new Map<string, string>();
|
|
75
|
+
const wireOwners = new Map<string, string | null>();
|
|
76
|
+
for (const [key, tool] of sortedIdentities) {
|
|
77
|
+
const wireName = namespacedToolName(tool.namespace, tool.name);
|
|
78
|
+
const owner = wireOwners.get(wireName);
|
|
79
|
+
if (owner === undefined) wireOwners.set(wireName, key);
|
|
80
|
+
else if (owner !== key) wireOwners.set(wireName, null);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const aliasOwners = new Map<string, string>();
|
|
84
|
+
const wireClaims = new Map<string, { key: string; alias: string } | null>();
|
|
85
|
+
for (const [key, tool] of sortedIdentities) {
|
|
86
|
+
const wireName = namespacedToolName(tool.namespace, tool.name);
|
|
87
|
+
const candidate = (tool.namespace !== undefined && wireName.length > MAX_CHAT_TOOL_NAME_LENGTH)
|
|
88
|
+
|| RESERVED_ALIAS_PATTERN.test(wireName)
|
|
89
|
+
|| wireOwners.get(wireName) === null
|
|
90
|
+
? boundedAlias(tool, wireName)
|
|
91
|
+
: wireName;
|
|
92
|
+
// A full SHA-256 collision is not safely attributable. Keep the later identity's native
|
|
93
|
+
// spelling instead of failing the request or stealing the first identity's restore entry.
|
|
94
|
+
const alias = aliasOwners.has(candidate) ? wireName : candidate;
|
|
95
|
+
aliasesByIdentity.set(key, alias);
|
|
96
|
+
if (!aliasOwners.has(alias)) aliasOwners.set(alias, key);
|
|
97
|
+
if (alias !== wireName) originalsByAlias.set(alias, wireName);
|
|
98
|
+
|
|
99
|
+
const claim = wireClaims.get(wireName);
|
|
100
|
+
if (claim === undefined) wireClaims.set(wireName, { key, alias });
|
|
101
|
+
else if (claim !== null && claim.key !== key) wireClaims.set(wireName, null);
|
|
102
|
+
}
|
|
103
|
+
for (const [wireName, claim] of wireClaims) {
|
|
104
|
+
if (claim !== null) aliasesByWireName.set(wireName, claim.alias);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return {
|
|
108
|
+
alias(tool: ToolIdentity): string {
|
|
109
|
+
const key = identityKey(tool);
|
|
110
|
+
const known = aliasesByIdentity.get(key);
|
|
111
|
+
if (known !== undefined) return known;
|
|
112
|
+
return namespacedToolName(tool.namespace, tool.name);
|
|
113
|
+
},
|
|
114
|
+
aliasWireName(wireName: string): string {
|
|
115
|
+
return aliasesByWireName.get(wireName) ?? wireName;
|
|
116
|
+
},
|
|
117
|
+
restore(wireName: string): string {
|
|
118
|
+
return originalsByAlias.get(wireName) ?? wireName;
|
|
119
|
+
},
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export function restoreOpenAIChatToolName(
|
|
124
|
+
registry: OpenAIChatToolNameRegistry,
|
|
125
|
+
wireName: string,
|
|
126
|
+
): string {
|
|
127
|
+
return registry.restore(wireName);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
131
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** Rewrite replayed assistant tool calls after the ordinary message converter has flattened them. */
|
|
135
|
+
export function aliasOpenAIChatMessageToolNames(
|
|
136
|
+
messages: readonly unknown[],
|
|
137
|
+
registry: OpenAIChatToolNameRegistry,
|
|
138
|
+
): unknown[] {
|
|
139
|
+
return messages.map(message => {
|
|
140
|
+
if (!isRecord(message) || !Array.isArray(message.tool_calls)) return message;
|
|
141
|
+
let changed = false;
|
|
142
|
+
const toolCalls = message.tool_calls.map(toolCall => {
|
|
143
|
+
if (!isRecord(toolCall) || !isRecord(toolCall.function)
|
|
144
|
+
|| typeof toolCall.function.name !== "string") return toolCall;
|
|
145
|
+
const name = registry.aliasWireName(toolCall.function.name);
|
|
146
|
+
if (name === toolCall.function.name) return toolCall;
|
|
147
|
+
changed = true;
|
|
148
|
+
return { ...toolCall, function: { ...toolCall.function, name } };
|
|
149
|
+
});
|
|
150
|
+
return changed ? { ...message, tool_calls: toolCalls } : message;
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export function withOpenAIChatToolNames<T>(
|
|
155
|
+
build: (scope: OpenAIChatToolNameScope) => T,
|
|
156
|
+
): T {
|
|
157
|
+
let registry = createOpenAIChatToolNameRegistry(undefined);
|
|
158
|
+
return build({
|
|
159
|
+
messages(parsed, baseUrl, messages): unknown {
|
|
160
|
+
registry = createOpenAIChatToolNameRegistry(openAIChatToolNameIdentities(parsed));
|
|
161
|
+
return frameAgentRouterMessages(baseUrl, aliasOpenAIChatMessageToolNames(messages, registry));
|
|
162
|
+
},
|
|
163
|
+
registry: () => registry,
|
|
164
|
+
restore: wireName => restoreOpenAIChatToolName(registry, wireName),
|
|
165
|
+
});
|
|
166
|
+
}
|