@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.
@@ -16,8 +16,8 @@
16
16
  } catch (e) {}
17
17
  })();
18
18
  </script>
19
- <script type="module" crossorigin src="/assets/index-DSK3S5HY.js"></script>
20
- <link rel="stylesheet" crossorigin href="/assets/index-DF_UFrGS.css">
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bitkyc08/opencodex",
3
- "version": "2.20.0",
3
+ "version": "2.21.0",
4
4
  "description": "Universal provider proxy for OpenAI Codex & Claude Code — use any LLM with Codex CLI/App/SDK and Claude Code",
5
5
  "type": "module",
6
6
  "main": "./bin/package-main.mjs",
@@ -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 = stripEncryptedMarker(schema);
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
  : {};
@@ -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
- if (isLikelyRealThoughtSignature(tc.thoughtSignature)) part.thoughtSignature = tc.thoughtSignature;
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 { type: "tool_call_start", id, name: restoreGoogleToolName(part.functionCall.name) };
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({ type: "tool_call_start", id, name: restoreGoogleToolName(part.functionCall.name) });
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 !== undefined && typeof streamFunction.name !== "string") {
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 !== undefined && typeof streamFunction.arguments !== "string") {
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 !== undefined && typeof rawToolCall.id !== "string") {
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
- if ((rawName !== undefined && typeof rawName !== "string")
1492
- || (rawArguments !== undefined && typeof rawArguments !== "string")) {
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 !== undefined && typeof tc.id !== "string") {
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(port: number, timeoutMs = 3_000, configDir?: string): Promise<string | null> {
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: { "anthropic-version": "2023-06-01" },
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": "c05ac96358e4e2454545e35034a0235dd8de7f50e16f92b464f978ede80564c3"
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": "7245f5ae5b5b32444fcd50144d5f752f6fde39c471b30b355d44c27531a8059a"
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": "7905d7c039bebc63a2fa968fc934570d5dedf882c474087bcf4d55e317641e63"
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": "d087da38fd9706f22c1f8e1513cdebd0ecd890146d7f9c09f8abe759d6aa2548"
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": "47ccd936f3abd32d98ea9f044d35acd353b16ee1c9f0f0542f4b519f63f52221"
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": "134e7b5b18045a1909fbfcf26d952475df4057d4a6538f659dcc3d3c28be6968"
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": "5a225032c2b306dd00d5106ea215f98c3182cd61ccbf465196eb97fe8220c459"
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": "172baea637ae1314e3e45886abc906a2308497d6eca913557255d3f14a92e8cc"
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": "3b29779cc4495c35097d941a40f915cb98af1813423cab8da5039daf166e857b"
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": "8e082bf45123416643f7071211e36a8ad4b3e21d19a44e01c1fe5aa3dc2aa213"
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": "255aab86fab5d6b2beedfcae1aced4cb69406b2c1d1e72bb6c0ca56337bfb0f1"
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": "25b063cae8ed65348ab6f5fe2df67d16b63f7432c7c6db154b2a9fbe8b467653"
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": "baca73b0b188683eed9b4708d954d9089e6eaeae4ef88487c1435582988baf58"
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": "d9d569ea71f74a84f8618dd2e43a21ea5917141aafdfe0def47eca17ce95e7f4"
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": "7e20fa8bd3052a6869623060cfa67c1654115046daf65a42087d07465785b38a"
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": "7934594b240697a96d081114d0922655f3e3d4d4b8a4f6518bf5d339e35d353a"
2793
+ "sha256": "a9c911108de03ec2ef1dce18d221b255f6015ce05a7949c38a33b1b6ac1fbff2"
2786
2794
  },
2787
2795
  {
2788
2796
  "path": "src/web-search/parse.ts",