@bitkyc08/opencodex 2.20.0 → 2.21.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-DSK3S5HY.js → index-BOFeam5a.js} +2 -2
- package/gui/dist/assets/{index-DF_UFrGS.css → index-Xq49CY8F.css} +1 -1
- package/gui/dist/index.html +2 -2
- package/package.json +1 -1
- package/src/adapters/anthropic.ts +2 -28
- package/src/adapters/google.ts +31 -3
- package/src/adapters/openai-chat.ts +25 -8
- package/src/adapters/responses-tool-schema.ts +67 -0
- package/src/bridge.ts +15 -2
- package/src/claude/gateway-cache.ts +41 -4
- package/src/cli/claude.ts +1 -1
- package/src/generated/compatibility-version.json +24 -16
- package/src/images/loop.ts +15 -5
- package/src/providers/registry.ts +7 -1
- package/src/responses/custom-tool-compat.ts +4 -1
- package/src/responses/parser.ts +7 -1
- package/src/responses/provider-opaque-metadata.ts +73 -0
- package/src/responses/schema.ts +6 -0
- package/src/server/auth-cors.ts +42 -6
- package/src/server/system-env.ts +1 -1
- package/src/types.ts +18 -1
- package/src/web-search/loop.ts +21 -5
package/gui/dist/index.html
CHANGED
|
@@ -16,8 +16,8 @@
|
|
|
16
16
|
} catch (e) {}
|
|
17
17
|
})();
|
|
18
18
|
</script>
|
|
19
|
-
<script type="module" crossorigin src="/assets/index-
|
|
20
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
19
|
+
<script type="module" crossorigin src="/assets/index-BOFeam5a.js"></script>
|
|
20
|
+
<link rel="stylesheet" crossorigin href="/assets/index-Xq49CY8F.css">
|
|
21
21
|
</head>
|
|
22
22
|
<body>
|
|
23
23
|
<div id="root"></div>
|
package/package.json
CHANGED
|
@@ -19,6 +19,7 @@ import { parseDataUrl } from "./image";
|
|
|
19
19
|
import { enforceAnthropicImageLimits } from "./anthropic-image-guard";
|
|
20
20
|
import { normalizeAnthropicImages } from "./anthropic-image-normalize";
|
|
21
21
|
import { normalizeAnthropicOutputSchema } from "./anthropic-output-schema";
|
|
22
|
+
import { stripResponsesOnlyEncryptedMarker } from "./responses-tool-schema";
|
|
22
23
|
import { identifyRoutedModel } from "./identity";
|
|
23
24
|
import { redactSecretString } from "../lib/redact";
|
|
24
25
|
import { CLAUDE_CODE_HEADERS, claudeCodeSessionId } from "./client-fingerprint";
|
|
@@ -721,35 +722,8 @@ function toolsToAnthropicFormat(parsed: OcxParsedRequest, toolNames: { toWire: (
|
|
|
721
722
|
return converted;
|
|
722
723
|
}
|
|
723
724
|
|
|
724
|
-
// Codex multi-agent v2 stamps a Responses-only `encrypted: true` marker on
|
|
725
|
-
// collaboration tool schemas (openai/codex 5f4d06ef; issue #85). It is an
|
|
726
|
-
// annotation for the ChatGPT backend only. Anthropic input_schema is strict
|
|
727
|
-
// JSON Schema; strip the marker defensively everywhere it can appear as a
|
|
728
|
-
// schema keyword, while preserving properties literally named "encrypted".
|
|
729
|
-
const ENCRYPTED_MARKER_NAME_BAG_KEYS = new Set(["properties", "patternProperties", "$defs", "definitions"]);
|
|
730
|
-
const ENCRYPTED_MARKER_LITERAL_VALUE_KEYS = new Set(["const", "default", "enum", "examples"]);
|
|
731
|
-
|
|
732
|
-
function stripEncryptedMarker(node: unknown, inNameBag = false): unknown {
|
|
733
|
-
if (Array.isArray(node)) return node.map(item => stripEncryptedMarker(item));
|
|
734
|
-
if (!node || typeof node !== "object") return node;
|
|
735
|
-
|
|
736
|
-
const out: Record<string, unknown> = {};
|
|
737
|
-
|
|
738
|
-
for (const [key, value] of Object.entries(node as Record<string, unknown>)) {
|
|
739
|
-
if (inNameBag) {
|
|
740
|
-
out[key] = stripEncryptedMarker(value);
|
|
741
|
-
} else if (key !== "encrypted") {
|
|
742
|
-
out[key] = ENCRYPTED_MARKER_LITERAL_VALUE_KEYS.has(key)
|
|
743
|
-
? value
|
|
744
|
-
: stripEncryptedMarker(value, ENCRYPTED_MARKER_NAME_BAG_KEYS.has(key));
|
|
745
|
-
}
|
|
746
|
-
}
|
|
747
|
-
|
|
748
|
-
return out;
|
|
749
|
-
}
|
|
750
|
-
|
|
751
725
|
function normalizeAnthropicInputSchema(schema: unknown): Record<string, unknown> {
|
|
752
|
-
const stripped =
|
|
726
|
+
const stripped = stripResponsesOnlyEncryptedMarker(schema);
|
|
753
727
|
const obj = stripped && typeof stripped === "object" && !Array.isArray(stripped)
|
|
754
728
|
? stripped as Record<string, unknown>
|
|
755
729
|
: {};
|
package/src/adapters/google.ts
CHANGED
|
@@ -8,6 +8,7 @@ import type {
|
|
|
8
8
|
OcxContentPart,
|
|
9
9
|
OcxParsedRequest,
|
|
10
10
|
OcxProviderConfig,
|
|
11
|
+
OcxProviderOpaqueToolCallMetadata,
|
|
11
12
|
OcxTextContent,
|
|
12
13
|
OcxToolCall,
|
|
13
14
|
OcxUsage,
|
|
@@ -209,7 +210,10 @@ function messagesToGeminiFormat(
|
|
|
209
210
|
// conversion 400s. Gemini accepts the optional id and pairs call/response by it.
|
|
210
211
|
if (callId !== undefined) functionCall.id = callId;
|
|
211
212
|
const part: Record<string, unknown> = { functionCall };
|
|
212
|
-
|
|
213
|
+
// Prefer the metadata that travelled with this exact call; fall back to the legacy
|
|
214
|
+
// field for callers that have not been migrated. Never merge or synthesize.
|
|
215
|
+
const signature = tc.providerMetadata?.google?.thoughtSignature ?? tc.thoughtSignature;
|
|
216
|
+
if (isLikelyRealThoughtSignature(signature)) part.thoughtSignature = signature;
|
|
213
217
|
parts.push(part);
|
|
214
218
|
}
|
|
215
219
|
}
|
|
@@ -326,9 +330,23 @@ function artifactMarkdownUrl(filePath: string): string {
|
|
|
326
330
|
interface GoogleResponsePart {
|
|
327
331
|
text?: string;
|
|
328
332
|
thought?: boolean;
|
|
333
|
+
thoughtSignature?: string;
|
|
329
334
|
functionCall?: { name: string; args: unknown };
|
|
330
335
|
}
|
|
331
336
|
|
|
337
|
+
/**
|
|
338
|
+
* Carry a Gemini thought signature with the exact function-call part that produced it. Google
|
|
339
|
+
* validates the signature against that specific part, so it must ride the individual tool call
|
|
340
|
+
* rather than be re-matched by name/arguments later (issue #1735).
|
|
341
|
+
*/
|
|
342
|
+
function googleToolCallMetadataFromPart(
|
|
343
|
+
part: GoogleResponsePart,
|
|
344
|
+
): { providerMetadata: OcxProviderOpaqueToolCallMetadata } | undefined {
|
|
345
|
+
const signature = part.thoughtSignature;
|
|
346
|
+
if (!isLikelyRealThoughtSignature(signature)) return undefined;
|
|
347
|
+
return { providerMetadata: { google: { thoughtSignature: signature } } };
|
|
348
|
+
}
|
|
349
|
+
|
|
332
350
|
/**
|
|
333
351
|
* Google marks model-internal reasoning as a normal text-bearing part plus `thought: true`.
|
|
334
352
|
* Keep that provider visibility bit authoritative here so the streaming and buffered parsers
|
|
@@ -674,7 +692,12 @@ export function createGoogleAdapter(provider: OcxProviderConfig): ProviderAdapte
|
|
|
674
692
|
const id = `call_${crypto.randomUUID().slice(0, 8)}`;
|
|
675
693
|
toolCallsStarted++;
|
|
676
694
|
emittedContentEvent = true;
|
|
677
|
-
yield {
|
|
695
|
+
yield {
|
|
696
|
+
type: "tool_call_start",
|
|
697
|
+
id,
|
|
698
|
+
name: restoreGoogleToolName(part.functionCall.name),
|
|
699
|
+
...googleToolCallMetadataFromPart(part),
|
|
700
|
+
};
|
|
678
701
|
yield { type: "tool_call_delta", arguments: JSON.stringify(part.functionCall.args ?? {}) };
|
|
679
702
|
yield { type: "tool_call_end" };
|
|
680
703
|
}
|
|
@@ -890,7 +913,12 @@ export function createGoogleAdapter(provider: OcxProviderConfig): ProviderAdapte
|
|
|
890
913
|
if (part.functionCall) {
|
|
891
914
|
const id = `call_${crypto.randomUUID().slice(0, 8)}`;
|
|
892
915
|
toolCallsStarted++;
|
|
893
|
-
events.push({
|
|
916
|
+
events.push({
|
|
917
|
+
type: "tool_call_start",
|
|
918
|
+
id,
|
|
919
|
+
name: restoreGoogleToolName(part.functionCall.name),
|
|
920
|
+
...googleToolCallMetadataFromPart(part),
|
|
921
|
+
});
|
|
894
922
|
events.push({ type: "tool_call_delta", arguments: JSON.stringify(part.functionCall.args ?? {}) });
|
|
895
923
|
events.push({ type: "tool_call_end" });
|
|
896
924
|
}
|
|
@@ -14,6 +14,7 @@ import { buildNonOpenAIToolCatalogNudgeForTools, shouldInjectNonOpenAIToolCatalo
|
|
|
14
14
|
import { openRouterProviderPayload, resolveOpenRouterRouting } from "../providers/openrouter-routing";
|
|
15
15
|
import { canSerializeServiceTierForChatModel } from "../providers/service-tier";
|
|
16
16
|
import { openaiChatCompletionsUrl } from "./openai-chat-url";
|
|
17
|
+
import { stripResponsesOnlyEncryptedMarker } from "./responses-tool-schema";
|
|
17
18
|
import {
|
|
18
19
|
isTranslatorBudgetExceededError,
|
|
19
20
|
retainTranslatedEventBatch,
|
|
@@ -332,6 +333,16 @@ type InvalidToolCallReason =
|
|
|
332
333
|
| "tool_call_function_name_blank"
|
|
333
334
|
| "tool_call_function_arguments_invalid";
|
|
334
335
|
|
|
336
|
+
/**
|
|
337
|
+
* Streamed string fields are absent when null or undefined (#1731): OpenAI-compatible
|
|
338
|
+
* streamers repeat already-sent `id`/`name`/`arguments` as null on continuation deltas.
|
|
339
|
+
* The accumulator and this diagnostic share this predicate so they cannot disagree about
|
|
340
|
+
* which delta was the invalid one.
|
|
341
|
+
*/
|
|
342
|
+
function isInvalidStreamStringField(value: unknown): boolean {
|
|
343
|
+
return value != null && typeof value !== "string";
|
|
344
|
+
}
|
|
345
|
+
|
|
335
346
|
/**
|
|
336
347
|
* Explain only the rejected wire shape, never its values. This diagnostic exists so provider
|
|
337
348
|
* compatibility can be tightened from evidence without retaining tool arguments or credentials.
|
|
@@ -358,6 +369,10 @@ function diagnoseInvalidToolCalls(
|
|
|
358
369
|
// Blank names are caught later at flush, not here, so they are not diagnosed on this
|
|
359
370
|
// branch. Describe exactly that boundary rather than tightening compatibility in a
|
|
360
371
|
// diagnostic change.
|
|
372
|
+
// #1731: "present" means the same thing here as in the accumulator — null and undefined
|
|
373
|
+
// are both absent, because some OpenAI-compatible streamers repeat already-sent fields
|
|
374
|
+
// as null on continuation deltas. A separate predicate here would diagnose accepted
|
|
375
|
+
// padding as the failure and point compatibility work at the wrong delta.
|
|
361
376
|
const streamFunction = (rawToolCall as { function?: unknown }).function;
|
|
362
377
|
if (streamFunction !== undefined && streamFunction !== null) {
|
|
363
378
|
if (!isRecord(streamFunction)) {
|
|
@@ -367,14 +382,14 @@ function diagnoseInvalidToolCalls(
|
|
|
367
382
|
valueType: Array.isArray(streamFunction) ? "array" : typeof streamFunction,
|
|
368
383
|
};
|
|
369
384
|
}
|
|
370
|
-
if (streamFunction.name
|
|
385
|
+
if (isInvalidStreamStringField(streamFunction.name)) {
|
|
371
386
|
return { reason: "tool_call_function_name_invalid", callIndex, valueType: typeof streamFunction.name };
|
|
372
387
|
}
|
|
373
|
-
if (streamFunction.arguments
|
|
388
|
+
if (isInvalidStreamStringField(streamFunction.arguments)) {
|
|
374
389
|
return { reason: "tool_call_function_arguments_invalid", callIndex, valueType: typeof streamFunction.arguments };
|
|
375
390
|
}
|
|
376
391
|
}
|
|
377
|
-
if (rawToolCall.id
|
|
392
|
+
if (isInvalidStreamStringField(rawToolCall.id)) {
|
|
378
393
|
return { reason: "tool_call_id_invalid", callIndex, valueType: typeof rawToolCall.id };
|
|
379
394
|
}
|
|
380
395
|
continue;
|
|
@@ -1077,9 +1092,9 @@ function toolsToChatFormat(parsed: OcxParsedRequest, provider: OcxProviderConfig
|
|
|
1077
1092
|
if (tools.length === 0) return undefined;
|
|
1078
1093
|
const xaiTarget = isXaiSchemaTarget(provider);
|
|
1079
1094
|
const formatted = tools.flatMap(t => {
|
|
1080
|
-
const parameters = xaiTarget
|
|
1095
|
+
const parameters = stripResponsesOnlyEncryptedMarker(xaiTarget
|
|
1081
1096
|
? normalizeXaiToolParameters(t.parameters)
|
|
1082
|
-
: ensureRootObjectType(t.parameters);
|
|
1097
|
+
: ensureRootObjectType(t.parameters));
|
|
1083
1098
|
|
|
1084
1099
|
if (parameters === undefined) return [];
|
|
1085
1100
|
return [{
|
|
@@ -1488,13 +1503,15 @@ export function createOpenAIChatAdapter(provider: OcxProviderConfig): ProviderAd
|
|
|
1488
1503
|
}
|
|
1489
1504
|
const rawName = rawFunction.name;
|
|
1490
1505
|
const rawArguments = rawFunction.arguments;
|
|
1491
|
-
|
|
1492
|
-
|
|
1506
|
+
// Some OpenAI-compatible streamers repeat already-sent fields as null on
|
|
1507
|
+
// continuation deltas. Treat only null/undefined as absent; every other
|
|
1508
|
+
// non-string value still fails closed before entering the accumulator.
|
|
1509
|
+
if (isInvalidStreamStringField(rawName) || isInvalidStreamStringField(rawArguments)) {
|
|
1493
1510
|
logInvalidToolCalls("stream", rawToolCalls);
|
|
1494
1511
|
return yield* terminateWithError(invalidToolCallsEvent(pendingUsage));
|
|
1495
1512
|
}
|
|
1496
1513
|
}
|
|
1497
|
-
if (tc.id
|
|
1514
|
+
if (isInvalidStreamStringField(tc.id)) {
|
|
1498
1515
|
logInvalidToolCalls("stream", rawToolCalls);
|
|
1499
1516
|
return yield* terminateWithError(invalidToolCallsEvent(pendingUsage));
|
|
1500
1517
|
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
// Codex multi-agent v2 stamps a Responses-only `encrypted: true` marker on
|
|
2
|
+
// collaboration tool schemas (openai/codex 5f4d06ef; issue #85). It is an
|
|
3
|
+
// annotation for the ChatGPT backend only, so translated provider schemas must
|
|
4
|
+
// drop it without removing properties or definitions literally named `encrypted`.
|
|
5
|
+
const ENCRYPTED_MARKER_NAME_BAG_KEYS = new Set([
|
|
6
|
+
"properties",
|
|
7
|
+
"patternProperties",
|
|
8
|
+
"$defs",
|
|
9
|
+
"definitions",
|
|
10
|
+
"dependencies",
|
|
11
|
+
"dependentSchemas",
|
|
12
|
+
"dependentRequired",
|
|
13
|
+
]);
|
|
14
|
+
const ENCRYPTED_MARKER_LITERAL_VALUE_KEYS = new Set(["const", "default", "enum", "examples"]);
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* The schema is caller-supplied, so its nesting depth is attacker-influenced. Native recursion
|
|
18
|
+
* would turn a deep schema into a stack overflow that takes down the request path, so this walks
|
|
19
|
+
* an explicit stack instead: depth costs heap, which is bounded and recoverable.
|
|
20
|
+
*/
|
|
21
|
+
export function stripResponsesOnlyEncryptedMarker(node: unknown, inNameBag = false): unknown {
|
|
22
|
+
type Assign = (value: unknown) => void;
|
|
23
|
+
interface Frame { node: unknown; inNameBag: boolean; assign: Assign }
|
|
24
|
+
|
|
25
|
+
let result: unknown;
|
|
26
|
+
const stack: Frame[] = [{ node, inNameBag, assign: value => { result = value; } }];
|
|
27
|
+
|
|
28
|
+
while (stack.length > 0) {
|
|
29
|
+
const frame = stack.pop()!;
|
|
30
|
+
const current = frame.node;
|
|
31
|
+
|
|
32
|
+
if (Array.isArray(current)) {
|
|
33
|
+
const out: unknown[] = new Array(current.length);
|
|
34
|
+
frame.assign(out);
|
|
35
|
+
// Array items are schemas in their own right, never a name bag.
|
|
36
|
+
for (let i = current.length - 1; i >= 0; i--) {
|
|
37
|
+
stack.push({ node: current[i], inNameBag: false, assign: value => { out[i] = value; } });
|
|
38
|
+
}
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
if (!current || typeof current !== "object") {
|
|
42
|
+
frame.assign(current);
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// A schema name may be `__proto__`; a null-prototype record keeps it as data.
|
|
47
|
+
const out: Record<string, unknown> = Object.create(null) as Record<string, unknown>;
|
|
48
|
+
frame.assign(out);
|
|
49
|
+
|
|
50
|
+
for (const [key, value] of Object.entries(current as Record<string, unknown>)) {
|
|
51
|
+
if (frame.inNameBag) {
|
|
52
|
+
// Inside a name bag every key is a caller-chosen name, so `encrypted` here is data.
|
|
53
|
+
stack.push({ node: value, inNameBag: false, assign: v => { out[key] = v; } });
|
|
54
|
+
} else if (key !== "encrypted") {
|
|
55
|
+
if (ENCRYPTED_MARKER_LITERAL_VALUE_KEYS.has(key)) {
|
|
56
|
+
// Literal payloads are values, not schemas: an `encrypted` key inside them is data.
|
|
57
|
+
out[key] = value;
|
|
58
|
+
} else {
|
|
59
|
+
const childInNameBag = ENCRYPTED_MARKER_NAME_BAG_KEYS.has(key);
|
|
60
|
+
stack.push({ node: value, inNameBag: childInNameBag, assign: v => { out[key] = v; } });
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return result;
|
|
67
|
+
}
|
package/src/bridge.ts
CHANGED
|
@@ -2,6 +2,7 @@ import type {
|
|
|
2
2
|
AdapterEvent,
|
|
3
3
|
OcxMessagePhase,
|
|
4
4
|
OcxProviderContinuationState,
|
|
5
|
+
OcxProviderOpaqueToolCallMetadata,
|
|
5
6
|
OcxReasoningReplayScopeRef,
|
|
6
7
|
OcxUsage,
|
|
7
8
|
} from "./types";
|
|
@@ -10,6 +11,7 @@ import { adapterFailureFromMessage, classifyError, CYBER_POLICY_ERROR_CODE, isCy
|
|
|
10
11
|
import { encodeCompactionSummary } from "./responses/compaction";
|
|
11
12
|
import { encodeReasoningEnvelope, type ReasoningEnvelope } from "./responses/reasoning-envelope";
|
|
12
13
|
import { rememberReasoningForCall } from "./responses/reasoning-replay-cache";
|
|
14
|
+
import { responsesExtraContentFromProviderMetadata } from "./responses/provider-opaque-metadata";
|
|
13
15
|
import { resolveStallTimeoutSec } from "./stall-timeout";
|
|
14
16
|
import { usageDisplayTotalTokens } from "./usage/totals";
|
|
15
17
|
import {
|
|
@@ -496,7 +498,7 @@ export function bridgeToResponsesSSE(
|
|
|
496
498
|
// synthetic compaction item's payload on done.
|
|
497
499
|
let compactionText = "";
|
|
498
500
|
let compactionTextBytes = 0;
|
|
499
|
-
let currentToolCall: { itemId: string; outputIndex: number; callId: string; name: string; args: string; argsBytes: number; namespace?: string; freeform?: boolean; toolSearch?: boolean; inputEmitted?: string } | null = null;
|
|
501
|
+
let currentToolCall: { itemId: string; outputIndex: number; callId: string; name: string; args: string; argsBytes: number; namespace?: string; freeform?: boolean; toolSearch?: boolean; inputEmitted?: string; providerMetadata?: OcxProviderOpaqueToolCallMetadata } | null = null;
|
|
500
502
|
// Open native web-search cell (between begin and end). Holds the output index allocated on
|
|
501
503
|
// begin so the matching done reuses it; closed as `failed` if the stream terminates early.
|
|
502
504
|
let currentWebSearch: { itemId: string; eventId: string; outputIndex: number } | null = null;
|
|
@@ -621,6 +623,9 @@ export function bridgeToResponsesSSE(
|
|
|
621
623
|
call_id: currentToolCall.callId, name: currentToolCall.name,
|
|
622
624
|
arguments: argsStr, status: "completed",
|
|
623
625
|
...(currentToolCall.namespace ? { namespace: currentToolCall.namespace } : {}),
|
|
626
|
+
// Provider-opaque metadata (issue #1735) rides the item so a client that replays
|
|
627
|
+
// this history can hand the signature back on the part it belongs to.
|
|
628
|
+
...(responsesExtraContentFromProviderMetadata(currentToolCall.providerMetadata) ?? {}),
|
|
624
629
|
};
|
|
625
630
|
emit("response.output_item.done", { output_index: currentToolCall.outputIndex, item });
|
|
626
631
|
retainFinishedItem(item as OutputItem);
|
|
@@ -655,6 +660,10 @@ export function bridgeToResponsesSSE(
|
|
|
655
660
|
call_id: currentToolCall.callId, name: currentToolCall.name,
|
|
656
661
|
arguments: argsStr, status: "incomplete",
|
|
657
662
|
...(currentToolCall.namespace ? { namespace: currentToolCall.namespace } : {}),
|
|
663
|
+
// An incomplete call can still be persisted and replayed (max_output_tokens), so it
|
|
664
|
+
// carries the same metadata as the completed item — otherwise SSE and buffered JSON
|
|
665
|
+
// would disagree about whether the signature survives.
|
|
666
|
+
...(responsesExtraContentFromProviderMetadata(currentToolCall.providerMetadata) ?? {}),
|
|
658
667
|
};
|
|
659
668
|
emit("response.output_item.done", { output_index: currentToolCall.outputIndex, item });
|
|
660
669
|
retainFinishedItem(item as OutputItem);
|
|
@@ -1013,7 +1022,7 @@ export function bridgeToResponsesSSE(
|
|
|
1013
1022
|
? { type: "custom_tool_call", id: itemId, call_id: event.id, name: realName, input: "", status: "in_progress" }
|
|
1014
1023
|
: { type: "function_call", id: itemId, call_id: event.id, name: realName, arguments: "", status: "in_progress", ...(ns ? { namespace: ns } : {}) };
|
|
1015
1024
|
emit("response.output_item.added", { output_index: outputIndex, item });
|
|
1016
|
-
currentToolCall = { itemId, outputIndex, callId: event.id, name: realName, args: "", argsBytes: 0, namespace: ns, freeform, toolSearch };
|
|
1025
|
+
currentToolCall = { itemId, outputIndex, callId: event.id, name: realName, args: "", argsBytes: 0, namespace: ns, freeform, toolSearch, providerMetadata: event.providerMetadata };
|
|
1017
1026
|
budget?.openCall(event.id);
|
|
1018
1027
|
break;
|
|
1019
1028
|
}
|
|
@@ -1476,6 +1485,7 @@ function buildResponseJSONWithBudget(
|
|
|
1476
1485
|
let currentToolCallId = "";
|
|
1477
1486
|
let currentToolCallName = "";
|
|
1478
1487
|
let currentToolCallArgs = "";
|
|
1488
|
+
let currentToolCallProviderMetadata: OcxProviderOpaqueToolCallMetadata | undefined;
|
|
1479
1489
|
let currentToolCallArgsBytes = 0;
|
|
1480
1490
|
// Web-search citations awaiting the next assistant message (attached as url_citation annotations).
|
|
1481
1491
|
let pendingWebSources: { url: string; title?: string }[] = [];
|
|
@@ -1584,11 +1594,13 @@ function buildResponseJSONWithBudget(
|
|
|
1584
1594
|
call_id: currentToolCallId, name: realName,
|
|
1585
1595
|
arguments: coercedArgs || "{}", status,
|
|
1586
1596
|
...(ns ? { namespace: ns } : {}),
|
|
1597
|
+
...(responsesExtraContentFromProviderMetadata(currentToolCallProviderMetadata) ?? {}),
|
|
1587
1598
|
});
|
|
1588
1599
|
}
|
|
1589
1600
|
budget?.closeCall(currentToolCallId);
|
|
1590
1601
|
currentToolCallId = "";
|
|
1591
1602
|
currentToolCallName = "";
|
|
1603
|
+
currentToolCallProviderMetadata = undefined;
|
|
1592
1604
|
currentToolCallArgs = "";
|
|
1593
1605
|
currentToolCallArgsBytes = 0;
|
|
1594
1606
|
};
|
|
@@ -1703,6 +1715,7 @@ function buildResponseJSONWithBudget(
|
|
|
1703
1715
|
currentToolCallName = e.name;
|
|
1704
1716
|
currentToolCallArgs = "";
|
|
1705
1717
|
currentToolCallArgsBytes = 0;
|
|
1718
|
+
currentToolCallProviderMetadata = e.providerMetadata;
|
|
1706
1719
|
break;
|
|
1707
1720
|
case "tool_call_delta":
|
|
1708
1721
|
{
|
|
@@ -13,12 +13,21 @@
|
|
|
13
13
|
import { mkdirSync, writeFileSync } from "node:fs";
|
|
14
14
|
import { homedir } from "node:os";
|
|
15
15
|
import { join } from "node:path";
|
|
16
|
+
import { loadServiceTokenFromFile, serviceApiTokenFilePath } from "../lib/service-secrets";
|
|
17
|
+
import type { OcxConfig } from "../types";
|
|
16
18
|
|
|
17
19
|
export interface GatewayModelRow {
|
|
18
20
|
id: string;
|
|
19
21
|
display_name?: string;
|
|
20
22
|
}
|
|
21
23
|
|
|
24
|
+
export interface GatewayModelCacheRefreshOptions {
|
|
25
|
+
timeoutMs?: number;
|
|
26
|
+
configDir?: string;
|
|
27
|
+
admissionConfig?: Pick<OcxConfig, "apiKeys">;
|
|
28
|
+
env?: NodeJS.ProcessEnv;
|
|
29
|
+
}
|
|
30
|
+
|
|
22
31
|
/** Claude Code config dir (CLAUDE_CONFIG_DIR override honored, like the CLI). */
|
|
23
32
|
export function claudeConfigDir(): string {
|
|
24
33
|
const custom = process.env.CLAUDE_CONFIG_DIR;
|
|
@@ -45,14 +54,42 @@ export function writeGatewayModelCache(baseUrl: string, models: readonly Gateway
|
|
|
45
54
|
}
|
|
46
55
|
}
|
|
47
56
|
|
|
57
|
+
/**
|
|
58
|
+
* Hardened service-token file, the same precedence `ocx opencode` uses. A service
|
|
59
|
+
* install writes the admission token to disk rather than the interactive environment,
|
|
60
|
+
* so an interactive `ocx claude` with neither env token nor configured key would
|
|
61
|
+
* otherwise still get a 401 and keep a stale picker list.
|
|
62
|
+
*/
|
|
63
|
+
function serviceFileToken(env: NodeJS.ProcessEnv): string | null {
|
|
64
|
+
const lookup = env.OCX_API_TOKEN_FILE?.trim()
|
|
65
|
+
? env
|
|
66
|
+
: { ...env, OCX_API_TOKEN_FILE: serviceApiTokenFilePath() };
|
|
67
|
+
return loadServiceTokenFromFile(lookup as Record<string, string | undefined>);
|
|
68
|
+
}
|
|
69
|
+
|
|
48
70
|
/** Fetch the anthropic-flavor /v1/models from the local proxy and write the cache. */
|
|
49
|
-
export async function refreshGatewayModelCacheFromProxy(
|
|
71
|
+
export async function refreshGatewayModelCacheFromProxy(
|
|
72
|
+
port: number,
|
|
73
|
+
options: GatewayModelCacheRefreshOptions = {},
|
|
74
|
+
): Promise<string | null> {
|
|
50
75
|
try {
|
|
76
|
+
const headers = new Headers({ "anthropic-version": "2023-06-01" });
|
|
77
|
+
// A wildcard/non-loopback listener requires data-plane admission even for a
|
|
78
|
+
// request sent to its local 127.0.0.1 address. Reuse the same dedicated
|
|
79
|
+
// credential domain as /v1/models admission; never place it in Authorization,
|
|
80
|
+
// which can belong to an upstream provider on other data-plane surfaces.
|
|
81
|
+
const envToken = (options.env ?? process.env).OPENCODEX_API_AUTH_TOKEN?.trim();
|
|
82
|
+
const configuredToken = options.admissionConfig?.apiKeys
|
|
83
|
+
?.find(entry => entry.key.trim().length > 0)
|
|
84
|
+
?.key.trim();
|
|
85
|
+
const admissionToken = envToken || serviceFileToken(options.env ?? process.env) || configuredToken;
|
|
86
|
+
if (admissionToken) headers.set("x-opencodex-api-key", admissionToken);
|
|
87
|
+
|
|
51
88
|
// ?ids=cli pins the readable claude-ocx id family deterministically (audit 051
|
|
52
89
|
// #5): the cache prewrite must not depend on UA sniffing.
|
|
53
90
|
const res = await fetch(`http://127.0.0.1:${port}/v1/models?limit=1000&ids=cli`, {
|
|
54
|
-
headers
|
|
55
|
-
signal: AbortSignal.timeout(timeoutMs),
|
|
91
|
+
headers,
|
|
92
|
+
signal: AbortSignal.timeout(options.timeoutMs ?? 3_000),
|
|
56
93
|
});
|
|
57
94
|
if (!res.ok) return null;
|
|
58
95
|
const body = await res.json() as { data?: unknown };
|
|
@@ -63,7 +100,7 @@ export async function refreshGatewayModelCacheFromProxy(port: number, timeoutMs
|
|
|
63
100
|
id: m.id as string,
|
|
64
101
|
display_name: typeof m.display_name === "string" ? m.display_name : undefined,
|
|
65
102
|
}));
|
|
66
|
-
return writeGatewayModelCache(`http://127.0.0.1:${port}`, models, configDir);
|
|
103
|
+
return writeGatewayModelCache(`http://127.0.0.1:${port}`, models, options.configDir);
|
|
67
104
|
} catch {
|
|
68
105
|
return null;
|
|
69
106
|
}
|
package/src/cli/claude.ts
CHANGED
|
@@ -317,7 +317,7 @@ export async function cmdClaude(args: string[]): Promise<number> {
|
|
|
317
317
|
// Pre-write the CLI's gateway-model cache (devlog 030): without a token the CLI
|
|
318
318
|
// never refreshes it, so the picker would keep showing yesterday's aliases.
|
|
319
319
|
try {
|
|
320
|
-
const cachePath = await refreshGatewayModelCacheFromProxy(port);
|
|
320
|
+
const cachePath = await refreshGatewayModelCacheFromProxy(port, { admissionConfig: config });
|
|
321
321
|
if (cachePath === null) {
|
|
322
322
|
console.error("⚠ Gateway model cache could not be refreshed; the model picker may be stale.");
|
|
323
323
|
}
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
},
|
|
11
11
|
{
|
|
12
12
|
"path": "package.json",
|
|
13
|
-
"sha256": "
|
|
13
|
+
"sha256": "4100af2a5b5f0eb057a3701bfa09decb431bfdd6933d2314370ff06abf1f8437"
|
|
14
14
|
},
|
|
15
15
|
{
|
|
16
16
|
"path": "scripts/model-metadata.source.json",
|
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
},
|
|
35
35
|
{
|
|
36
36
|
"path": "src/adapters/anthropic.ts",
|
|
37
|
-
"sha256": "
|
|
37
|
+
"sha256": "ab7791765594ed63fc57a170e2a7e760a882a17ae0d51c0f44a7fbbb789a0416"
|
|
38
38
|
},
|
|
39
39
|
{
|
|
40
40
|
"path": "src/adapters/azure.ts",
|
|
@@ -210,7 +210,7 @@
|
|
|
210
210
|
},
|
|
211
211
|
{
|
|
212
212
|
"path": "src/adapters/google.ts",
|
|
213
|
-
"sha256": "
|
|
213
|
+
"sha256": "2144cb4567aefe746a3b95eb78ac280fbd6ed7bf8dc0d05a88802d526abcc4f1"
|
|
214
214
|
},
|
|
215
215
|
{
|
|
216
216
|
"path": "src/adapters/identity.ts",
|
|
@@ -274,7 +274,7 @@
|
|
|
274
274
|
},
|
|
275
275
|
{
|
|
276
276
|
"path": "src/adapters/openai-chat.ts",
|
|
277
|
-
"sha256": "
|
|
277
|
+
"sha256": "b95eb2964332647ca72f83b1684394d2013d992287a626c04134a5897ddaa251"
|
|
278
278
|
},
|
|
279
279
|
{
|
|
280
280
|
"path": "src/adapters/openai-responses-url.ts",
|
|
@@ -288,6 +288,10 @@
|
|
|
288
288
|
"path": "src/adapters/registry.ts",
|
|
289
289
|
"sha256": "9a56ee08197bdd5b6ea12fcb847d2a89b60a600ee2536a010f4836784115d2d8"
|
|
290
290
|
},
|
|
291
|
+
{
|
|
292
|
+
"path": "src/adapters/responses-tool-schema.ts",
|
|
293
|
+
"sha256": "affeb179617d9abfb8cd18d47b132ff02d05c4438e5395b90103c7b03c184f55"
|
|
294
|
+
},
|
|
291
295
|
{
|
|
292
296
|
"path": "src/adapters/run-turn-queue.ts",
|
|
293
297
|
"sha256": "7599ccc46be13507bdec6e27095092efb0b6d84976bcffe0cca972ce08a6da62"
|
|
@@ -302,7 +306,7 @@
|
|
|
302
306
|
},
|
|
303
307
|
{
|
|
304
308
|
"path": "src/bridge.ts",
|
|
305
|
-
"sha256": "
|
|
309
|
+
"sha256": "3176bc017560ccec87b911a00e263aebee23dc8067b6c1d5405137e72a22e0d2"
|
|
306
310
|
},
|
|
307
311
|
{
|
|
308
312
|
"path": "src/chat/inbound.ts",
|
|
@@ -358,7 +362,7 @@
|
|
|
358
362
|
},
|
|
359
363
|
{
|
|
360
364
|
"path": "src/claude/gateway-cache.ts",
|
|
361
|
-
"sha256": "
|
|
365
|
+
"sha256": "cb8be3d8991acecd010adb7ac4351a2aa7b75a26feaf9e0ca3749085f05897ba"
|
|
362
366
|
},
|
|
363
367
|
{
|
|
364
368
|
"path": "src/claude/inbound-debug.ts",
|
|
@@ -426,7 +430,7 @@
|
|
|
426
430
|
},
|
|
427
431
|
{
|
|
428
432
|
"path": "src/cli/claude.ts",
|
|
429
|
-
"sha256": "
|
|
433
|
+
"sha256": "7b9b5ea1fbeab889f137a3f889e89350d7205ab77102f6b46357dd825be0e08d"
|
|
430
434
|
},
|
|
431
435
|
{
|
|
432
436
|
"path": "src/cli/codex-shim-autorestore.ts",
|
|
@@ -1022,7 +1026,7 @@
|
|
|
1022
1026
|
},
|
|
1023
1027
|
{
|
|
1024
1028
|
"path": "src/images/loop.ts",
|
|
1025
|
-
"sha256": "
|
|
1029
|
+
"sha256": "83036ac88b0a13eac64a0e4b170a89616daf5a1090eff83a499b891bd323ba18"
|
|
1026
1030
|
},
|
|
1027
1031
|
{
|
|
1028
1032
|
"path": "src/images/plan.ts",
|
|
@@ -2058,7 +2062,7 @@
|
|
|
2058
2062
|
},
|
|
2059
2063
|
{
|
|
2060
2064
|
"path": "src/providers/registry.ts",
|
|
2061
|
-
"sha256": "
|
|
2065
|
+
"sha256": "8e16a0051359debc48b59ade82901c64a0bfecf59edc69bd6536f6dd3fa66f1b"
|
|
2062
2066
|
},
|
|
2063
2067
|
{
|
|
2064
2068
|
"path": "src/providers/request-pacing.ts",
|
|
@@ -2090,7 +2094,7 @@
|
|
|
2090
2094
|
},
|
|
2091
2095
|
{
|
|
2092
2096
|
"path": "src/responses/custom-tool-compat.ts",
|
|
2093
|
-
"sha256": "
|
|
2097
|
+
"sha256": "332474e1d891bbc8a23a0bb76d99f96ae8f369ef325e324af0040e0b17cf6616"
|
|
2094
2098
|
},
|
|
2095
2099
|
{
|
|
2096
2100
|
"path": "src/responses/hosted-tool-policy.ts",
|
|
@@ -2098,7 +2102,11 @@
|
|
|
2098
2102
|
},
|
|
2099
2103
|
{
|
|
2100
2104
|
"path": "src/responses/parser.ts",
|
|
2101
|
-
"sha256": "
|
|
2105
|
+
"sha256": "bd1d2ad40ffc8b17453f657f9453d1a29c81d568a19826a0fb62e9af3175f7ae"
|
|
2106
|
+
},
|
|
2107
|
+
{
|
|
2108
|
+
"path": "src/responses/provider-opaque-metadata.ts",
|
|
2109
|
+
"sha256": "a452108841dcb07d21e43832aae70a66bbf26a8a6c9eb52c8c9ef11120dfb234"
|
|
2102
2110
|
},
|
|
2103
2111
|
{
|
|
2104
2112
|
"path": "src/responses/reasoning-envelope.ts",
|
|
@@ -2110,7 +2118,7 @@
|
|
|
2110
2118
|
},
|
|
2111
2119
|
{
|
|
2112
2120
|
"path": "src/responses/schema.ts",
|
|
2113
|
-
"sha256": "
|
|
2121
|
+
"sha256": "3cc771b346cc5577c182674b899356448a23b1dca7f681013982c71ae17791a1"
|
|
2114
2122
|
},
|
|
2115
2123
|
{
|
|
2116
2124
|
"path": "src/responses/spill-store.ts",
|
|
@@ -2230,7 +2238,7 @@
|
|
|
2230
2238
|
},
|
|
2231
2239
|
{
|
|
2232
2240
|
"path": "src/server/auth-cors.ts",
|
|
2233
|
-
"sha256": "
|
|
2241
|
+
"sha256": "412a504bd8c3df8fcc0b33de12f321f16a103168e278357de13cd479336073e9"
|
|
2234
2242
|
},
|
|
2235
2243
|
{
|
|
2236
2244
|
"path": "src/server/background-lifecycle.ts",
|
|
@@ -2570,7 +2578,7 @@
|
|
|
2570
2578
|
},
|
|
2571
2579
|
{
|
|
2572
2580
|
"path": "src/server/system-env.ts",
|
|
2573
|
-
"sha256": "
|
|
2581
|
+
"sha256": "4ba855172048295bd4933bcdade706a901fbf96b345c98e226c548bd32720a6e"
|
|
2574
2582
|
},
|
|
2575
2583
|
{
|
|
2576
2584
|
"path": "src/server/windows-tcp-drop.ts",
|
|
@@ -2666,7 +2674,7 @@
|
|
|
2666
2674
|
},
|
|
2667
2675
|
{
|
|
2668
2676
|
"path": "src/types.ts",
|
|
2669
|
-
"sha256": "
|
|
2677
|
+
"sha256": "59e811fabf37cd8b40621c6fcf7992c595ad645ddda3a6e6583ed6634fec6b84"
|
|
2670
2678
|
},
|
|
2671
2679
|
{
|
|
2672
2680
|
"path": "src/update/badge.ts",
|
|
@@ -2782,7 +2790,7 @@
|
|
|
2782
2790
|
},
|
|
2783
2791
|
{
|
|
2784
2792
|
"path": "src/web-search/loop.ts",
|
|
2785
|
-
"sha256": "
|
|
2793
|
+
"sha256": "a9c911108de03ec2ef1dce18d221b255f6015ce05a7949c38a33b1b6ac1fbff2"
|
|
2786
2794
|
},
|
|
2787
2795
|
{
|
|
2788
2796
|
"path": "src/web-search/parse.ts",
|