@bitkyc08/opencodex 2.6.9 → 2.6.11-preview.20260630

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,7 +16,7 @@
16
16
  } catch (e) {}
17
17
  })();
18
18
  </script>
19
- <script type="module" crossorigin src="/assets/index-PZN0Edav.js"></script>
19
+ <script type="module" crossorigin src="/assets/index-CG1hKRft.js"></script>
20
20
  <link rel="stylesheet" crossorigin href="/assets/index-DIBiVVC0.css">
21
21
  </head>
22
22
  <body>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bitkyc08/opencodex",
3
- "version": "2.6.9",
3
+ "version": "2.6.11-preview.20260630",
4
4
  "description": "Universal provider proxy for OpenAI Codex — use any LLM with Codex CLI/App/SDK",
5
5
  "type": "module",
6
6
  "main": "./bin/package-main.mjs",
@@ -16,6 +16,8 @@ import type {
16
16
  import { isAllowedToolChoice, namespacedToolName, resolveToolChoiceWireName, toolAllowedByChoice } from "../types";
17
17
  import { ANTHROPIC_OAUTH_BETA, CLAUDE_CODE_SYSTEM_INSTRUCTION, applyClaudeToolPrefix, stripClaudeToolPrefix } from "../oauth/anthropic";
18
18
  import { parseDataUrl } from "./image";
19
+ import { neutralizeIdentity } from "./identity";
20
+ import { CLAUDE_CODE_HEADERS, claudeCodeSessionId } from "./client-fingerprint";
19
21
 
20
22
  /** Map a user content part to an Anthropic content block (text or image source). */
21
23
  function toAnthropicContentPart(p: OcxContentPart): unknown {
@@ -115,10 +117,9 @@ function messagesToAnthropicFormat(
115
117
  parsed: OcxParsedRequest,
116
118
  toolNames: { toWire: (name: string) => string },
117
119
  ): { system: string | undefined; messages: unknown[] } {
118
- const system = parsed.context.systemPrompt?.join("\n\n").replace(
119
- "You are Codex, a coding agent based on GPT-5.",
120
- `You are a coding agent (underlying model: ${parsed.modelId}) running via the opencodex proxy. Do not claim to be GPT-5 or to be made by OpenAI.`,
121
- ) || undefined;
120
+ const system = parsed.context.systemPrompt?.length
121
+ ? neutralizeIdentity(parsed.context.systemPrompt.join("\n\n")) || undefined
122
+ : undefined;
122
123
  const messages: unknown[] = [];
123
124
 
124
125
  for (let i = 0; i < parsed.context.messages.length; i++) {
@@ -273,6 +274,12 @@ export function createAnthropicAdapter(provider: OcxProviderConfig): ProviderAda
273
274
  if (isOAuth) {
274
275
  if (provider.apiKey) headers["Authorization"] = `Bearer ${provider.apiKey}`;
275
276
  headers["anthropic-beta"] = ANTHROPIC_OAUTH_BETA;
277
+ // Match the real Claude Code CLI request fingerprint: a valid OAuth token with an empty
278
+ // header set is a non-first-party signature. (cch billing-header signing is intentionally
279
+ // out of scope — brittle and version-coupled.)
280
+ Object.assign(headers, CLAUDE_CODE_HEADERS);
281
+ headers["X-Claude-Code-Session-Id"] = claudeCodeSessionId(provider.apiKey);
282
+ headers["x-client-request-id"] = crypto.randomUUID();
276
283
  } else if (provider.apiKey) {
277
284
  headers["x-api-key"] = provider.apiKey;
278
285
  }
@@ -0,0 +1,55 @@
1
+ /**
2
+ * First-party client fingerprints.
3
+ *
4
+ * Routed OAuth providers reject — or quietly flag — requests whose header signature doesn't match
5
+ * the real first-party client that minted the token. Sending a valid OAuth token with an empty
6
+ * header set (or a giveaway literal UA like "antigravity") is a non-first-party signature. These
7
+ * constants mirror the headers the real Claude Code CLI and Antigravity CLI send, so the proxy's
8
+ * request fingerprint matches the credential.
9
+ *
10
+ * Pinned versions live HERE (single source) so they're trivial to bump. Values that need a live
11
+ * manifest fetch (Antigravity auto-updater) or a cryptographic billing signature (Claude cch) are
12
+ * intentionally NOT modeled — those are brittle and a wrong guess does more harm than the gap.
13
+ */
14
+ import { createHash } from "node:crypto";
15
+
16
+ // ── Claude Code CLI (matches Claude Code 2.1.63 / @anthropic-ai/sdk 0.74.0) ──
17
+ export const CLAUDE_CODE_HEADERS: Record<string, string> = {
18
+ "X-App": "cli",
19
+ "X-Stainless-Retry-Count": "0",
20
+ "X-Stainless-Runtime": "node",
21
+ "X-Stainless-Lang": "js",
22
+ "X-Stainless-Timeout": "600",
23
+ };
24
+
25
+ /**
26
+ * Stable per-credential session id, matching Claude Code's `X-Claude-Code-Session-Id`. Real Claude
27
+ * Code keeps one session id per CLI session; we derive a deterministic UUIDv4-shaped id from the
28
+ * OAuth token so it stays stable across a conversation's turns without persisting state. The token
29
+ * itself never leaves this function (only its hash drives the id).
30
+ */
31
+ export function claudeCodeSessionId(token: string | undefined): string {
32
+ const seed = token && token.length > 0 ? token : "opencodex-anon";
33
+ const h = createHash("sha256").update(`claude-code-session:${seed}`, "utf8").digest("hex");
34
+ // Shape the hash into a v4-looking UUID (version nibble 4, variant nibble 8-b).
35
+ const variant = ((parseInt(h[16], 16) & 0x3) | 0x8).toString(16);
36
+ return `${h.slice(0, 8)}-${h.slice(8, 12)}-4${h.slice(13, 16)}-${variant}${h.slice(17, 20)}-${h.slice(20, 32)}`;
37
+ }
38
+
39
+ // ── Antigravity CLI ──
40
+ /** Pinned fallback Antigravity CLI version (real client fetches a manifest; we pin to avoid the network dependency). */
41
+ export const ANTIGRAVITY_CLI_VERSION = "1.0.13";
42
+ const ANTIGRAVITY_CLI_CLIENT_NAME = "aidev_client";
43
+ const ANTIGRAVITY_CLI_PLATFORM = "darwin/arm64";
44
+ /** Secondary Google API client UA the Antigravity client library reports. */
45
+ export const ANTIGRAVITY_GOOG_API_CLIENT_UA = "google-api-nodejs-client/10.3.0";
46
+
47
+ /**
48
+ * The real Antigravity CLI User-Agent, e.g.
49
+ * `antigravity/cli/1.0.13 (aidev_client; os_type=darwin; arch=arm64)`.
50
+ * A `GOOGLE_ANTIGRAVITY_USER_AGENT` override (set by the caller) takes precedence upstream.
51
+ */
52
+ export function antigravityUserAgent(version = ANTIGRAVITY_CLI_VERSION): string {
53
+ const [osType, arch] = ANTIGRAVITY_CLI_PLATFORM.split("/");
54
+ return `antigravity/cli/${version} (${ANTIGRAVITY_CLI_CLIENT_NAME}; os_type=${osType}; arch=${arch})`;
55
+ }
@@ -1,8 +1,14 @@
1
1
  import { createHash } from "node:crypto";
2
2
  import type { OcxContentPart, OcxParsedRequest } from "../types";
3
+ import { antigravityUserAgent } from "./client-fingerprint";
3
4
 
4
- /** Antigravity request User-Agent (overridable). Mirrors the Antigravity desktop client UA. */
5
- export const ANTIGRAVITY_REQUEST_UA = process.env.GOOGLE_ANTIGRAVITY_USER_AGENT || "antigravity";
5
+ /**
6
+ * Antigravity request User-Agent. Mirrors the real Antigravity CLI UA
7
+ * (`antigravity/cli/{ver} (aidev_client; os_type=darwin; arch=arm64)`) so the request fingerprint
8
+ * matches the OAuth credential — the prior literal `"antigravity"` was a giveaway no real client
9
+ * sends. A `GOOGLE_ANTIGRAVITY_USER_AGENT` override still wins.
10
+ */
11
+ export const ANTIGRAVITY_REQUEST_UA = process.env.GOOGLE_ANTIGRAVITY_USER_AGENT || antigravityUserAgent();
6
12
 
7
13
  /**
8
14
  * Whether a stored `OcxToolCall.thoughtSignature` is a REAL upstream Gemini signature versus a
@@ -18,6 +18,8 @@ import { fetchAntigravityWithRetry, fetchVertexWithRetry } from "./google-http";
18
18
  import { isVertexTruncationReason, vertexTruncationErrorMessage } from "./google-truncation";
19
19
  import { ANTIGRAVITY_REQUEST_UA, antigravitySessionId, isLikelyRealThoughtSignature, sanitizeAntigravityClaudeSignatures } from "./google-antigravity-wire";
20
20
  import { sanitizeGeminiToolParameters } from "./google-tool-schema";
21
+ import { neutralizeIdentity } from "./identity";
22
+ import { ANTIGRAVITY_GOOG_API_CLIENT_UA } from "./client-fingerprint";
21
23
  import { antigravityUsesReplayCache, applyAntigravityReplay, clearAntigravityReplay, observeAntigravityReplay } from "./google-antigravity-replay";
22
24
 
23
25
  // Google-family models (Gemini/Vertex/Antigravity) tend to emit long running commentary between
@@ -83,7 +85,9 @@ function toolResultImageParts(content: string | OcxContentPart[]): unknown[] {
83
85
  }
84
86
 
85
87
  function messagesToGeminiFormat(parsed: OcxParsedRequest): { systemInstruction?: unknown; contents: unknown[] } {
86
- const systemText = [...(parsed.context.systemPrompt ?? []), GOOGLE_BREVITY_INSTRUCTION].join("\n\n");
88
+ // Neutralize Codex's GPT-5 identity line (Gemini/Antigravity share this path) so a routed model
89
+ // never misreports as GPT-5/OpenAI, and never leaks the proxy identity upstream.
90
+ const systemText = neutralizeIdentity([...(parsed.context.systemPrompt ?? []), GOOGLE_BREVITY_INSTRUCTION].join("\n\n"));
87
91
  const systemInstruction = { parts: [{ text: systemText }] };
88
92
 
89
93
  const contents: unknown[] = [];
@@ -238,18 +242,30 @@ export function createGoogleAdapter(provider: OcxProviderConfig): ProviderAdapte
238
242
  sanitizeAntigravityClaudeSignatures(contents);
239
243
  }
240
244
  }
241
- // The CCA client serializes `session_id` (snake_case); send both spellings so the
242
- // deterministic session id is honored regardless of which the backend accepts.
243
- const request: Record<string, unknown> = { ...body, sessionId, session_id: sessionId };
245
+ // The real Antigravity client puts the session id ONLY at `request.sessionId` (camelCase,
246
+ // nested) matching CLIProxyAPI `generateStableSessionID`. An extra top-level/snake_case
247
+ // spelling is a non-first-party key, so we send the single canonical location.
248
+ const request: Record<string, unknown> = { ...body, sessionId };
249
+ // Claude-on-Antigravity forces VALIDATED function calling (the real client always sets it).
250
+ if (/claude/i.test(parsed.modelId)) {
251
+ const existing = (request.toolConfig ?? {}) as Record<string, unknown>;
252
+ const fcc = (existing.functionCallingConfig ?? {}) as Record<string, unknown>;
253
+ request.toolConfig = { ...existing, functionCallingConfig: { ...fcc, mode: "VALIDATED" } };
254
+ }
244
255
  const envelope = {
245
256
  model: parsed.modelId,
246
- userAgent: ANTIGRAVITY_REQUEST_UA,
257
+ // The envelope's `userAgent` field is a protocol constant ("antigravity"), distinct from
258
+ // the HTTP `User-Agent` header (the real CLI UA). CLIProxyAPI `geminiToAntigravity` hardcodes
259
+ // the body field; only the header carries the versioned client string.
260
+ userAgent: "antigravity",
247
261
  requestType: "agent",
248
262
  project,
249
263
  requestId: `agent-${crypto.randomUUID()}`,
250
264
  request,
251
265
  };
252
266
  headers["User-Agent"] = ANTIGRAVITY_REQUEST_UA;
267
+ // The Antigravity client library reports a secondary Google API client UA alongside the CLI UA.
268
+ headers["x-goog-api-client"] = ANTIGRAVITY_GOOG_API_CLIENT_UA;
253
269
  if (provider.apiKey) headers["Authorization"] = `Bearer ${provider.apiKey}`;
254
270
  return { url, method: "POST", headers, body: JSON.stringify(envelope) };
255
271
  }
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Central identity neutralization.
3
+ *
4
+ * Codex sends the SAME GPT-5 identity line to EVERY model at request time (the per-model catalog
5
+ * `base_instructions` is ignored on the wire). For routed, non-OpenAI providers that line is both
6
+ * wrong (the model isn't GPT-5) and a liability: the previous fix replaced it with text that
7
+ * advertised "...served through / running via the opencodex proxy", which leaked our proxy identity
8
+ * into the upstream payload — a signature no first-party client (Claude Code, Gemini CLI, Kiro) ever
9
+ * sends, and a likely ToS trigger.
10
+ *
11
+ * The neutral replacement keeps ONLY the necessary instruction (don't misreport as GPT-5/OpenAI)
12
+ * and names no proxy. Provider-native identity blocks (e.g. the anthropic OAuth "You are a Claude
13
+ * agent..." prefix) are layered on TOP of this by the individual adapters; this module never claims
14
+ * to be a specific first-party client.
15
+ */
16
+
17
+ /** The exact identity line Codex injects for every model. */
18
+ export const CODEX_GPT5_IDENTITY_LINE = "You are Codex, a coding agent based on GPT-5.";
19
+
20
+ /** Proxy-neutral replacement: no "opencodex proxy" mention, just the GPT-5/OpenAI disclaimer. */
21
+ export const NEUTRAL_IDENTITY_LINE = "You are a coding agent. Do not claim to be GPT-5 or to be made by OpenAI.";
22
+
23
+ /**
24
+ * Replace Codex's hardcoded GPT-5 identity line with the proxy-neutral line. Safe to call on any
25
+ * system text: when the line is absent (already neutralized, or a provider that never received it)
26
+ * the input is returned unchanged. This is the single chokepoint every adapter routes through, so
27
+ * the leak can't reappear in one adapter while being fixed in another.
28
+ */
29
+ export function neutralizeIdentity(systemText: string): string {
30
+ return systemText.replace(CODEX_GPT5_IDENTITY_LINE, NEUTRAL_IDENTITY_LINE);
31
+ }
32
+
33
+ /** The catalog (static, on-disk) replacement for `base_instructions`. Same neutral wording. */
34
+ export const NEUTRAL_IDENTITY_CATALOG = NEUTRAL_IDENTITY_LINE;
@@ -1,5 +1,6 @@
1
1
  import type { OcxParsedRequest } from "../types";
2
2
  import { namespacedToolName } from "../types";
3
+ import { kiroToolName } from "./kiro-wire";
3
4
 
4
5
  const MAX_KIRO_TOOL_DESCRIPTION = 1024;
5
6
 
@@ -128,16 +129,23 @@ function ensureRootObjectType(schema: unknown): Record<string, unknown> {
128
129
  return merged;
129
130
  }
130
131
 
131
- export function convertKiroToolContext(parsed: OcxParsedRequest): { tools: unknown[]; systemAdditions: string[] } {
132
+ export function convertKiroToolContext(parsed: OcxParsedRequest): { tools: unknown[]; systemAdditions: string[]; nameMap: Map<string, string> } {
132
133
  const tools = parsed.context.tools ?? [];
133
134
  const systemAdditions: string[] = [];
135
+ // Maps the Kiro-safe toolSpecification.name back to the original wire name so the response parser
136
+ // can restore it (the bridge's toolNsMap is keyed by the original wire name). Only non-identity
137
+ // entries are stored.
138
+ const nameMap = new Map<string, string>();
134
139
  return {
135
140
  tools: tools.map(t => {
136
141
  const description = t.description || `Tool: ${t.name}`;
137
142
  // Send the full namespaced wire name (e.g. mcp__chrome-devtools__navigate_page) so Kiro echoes
138
- // it back unchanged; the bridge's toolNsMap is keyed by this name and restores the MCP namespace
139
- // Codex routes by. Truncating here breaks long MCP/computer-use round trips.
140
- const toolName = namespacedToolName(t.namespace, t.name);
143
+ // it back; the bridge's toolNsMap is keyed by this name and restores the MCP namespace Codex
144
+ // routes by. Kiro's runtimeservice rejects names with spaces or >64 chars, so normalize to a
145
+ // safe form and remember the mapping; the response parser restores the original wire name.
146
+ const wireName = namespacedToolName(t.namespace, t.name);
147
+ const toolName = kiroToolName(wireName);
148
+ if (toolName !== wireName) nameMap.set(toolName, wireName);
141
149
  const kiroDescription = description.length > MAX_KIRO_TOOL_DESCRIPTION
142
150
  ? `Tool documentation moved to the system prompt: ${toolName}.`
143
151
  : description;
@@ -153,6 +161,7 @@ export function convertKiroToolContext(parsed: OcxParsedRequest): { tools: unkno
153
161
  };
154
162
  }),
155
163
  systemAdditions,
164
+ nameMap,
156
165
  };
157
166
  }
158
167
 
@@ -33,6 +33,43 @@ export function normalizeToolId(id: string): string {
33
33
  return s.length > 64 ? s.slice(0, 64) : s;
34
34
  }
35
35
 
36
+ /**
37
+ * Kiro `runtimeservice` rejects a toolSpecification.name that is not `^[a-zA-Z0-9_-]{1,64}$`
38
+ * ("ValidationException: Invalid tool use format."). MCP wire names routinely break this: codex_apps
39
+ * tools carry spaces (e.g. `...__workspace agents_create_agent`) and the namespaced form often
40
+ * exceeds 64 chars. Normalize deterministically so the SAME input always maps to the SAME output —
41
+ * the toolSpecification, the replayed assistant toolUse, and the response-side restore all derive
42
+ * from the same wire name, so they stay in agreement without sharing state.
43
+ *
44
+ * Non-conforming chars become `_`. When the result would exceed 64 chars (or anything had to be
45
+ * rewritten and the tail would otherwise collide), the name is shortened to a 55-char prefix plus an
46
+ * 8-hex-char hash of the ORIGINAL wire name, keeping it unique and reversible via the per-request map.
47
+ */
48
+ export function kiroToolName(wireName: string, used?: Set<string>): string {
49
+ const cleaned = wireName.replace(/[^a-zA-Z0-9_-]/g, "_");
50
+ // Conforming, non-empty, short, and not already claimed: pass through unchanged (the common case;
51
+ // keeps names readable and round-trippable without a map lookup).
52
+ if (cleaned === wireName && cleaned.length >= 1 && cleaned.length <= 64 && !(used?.has(cleaned))) {
53
+ used?.add(cleaned);
54
+ return cleaned;
55
+ }
56
+ // Lossy (chars rewritten), too long, empty, or colliding: build `<=55-char prefix>_<8-hex>` where
57
+ // the hash covers the original wire name. A numeric salt is mixed in until the result is unclaimed,
58
+ // so two distinct wire names can never collapse to the same Kiro name within one request (the 8-hex
59
+ // suffix alone is only 32 bits, and a hashed name could otherwise equal a conforming one — the
60
+ // `used` check closes both gaps). Empty input falls back to a stable "tool" prefix.
61
+ const base = cleaned.slice(0, 55) || "tool";
62
+ for (let salt = 0; ; salt++) {
63
+ const hashInput = salt === 0 ? wireName : `${wireName}#${salt}`;
64
+ const suffix = createHash("sha256").update(hashInput).digest("hex").slice(0, 8);
65
+ const candidate = `${base}_${suffix}`;
66
+ if (!(used?.has(candidate))) {
67
+ used?.add(candidate);
68
+ return candidate;
69
+ }
70
+ }
71
+ }
72
+
36
73
  export function fallbackToolUseId(): string {
37
74
  return `toolu_${randomUUID().slice(0, 8)}`;
38
75
  }
@@ -9,7 +9,7 @@ import { safeKiroErrorMessage } from "./kiro-errors";
9
9
  import { appendFallbackText, toolCallFallbackText, toolResultFallbackText } from "./kiro-tool-fallback";
10
10
  import { KiroThinkingParser } from "./kiro-thinking";
11
11
  import { isCompleteKiroToolInput, kiroTruncationErrorMessage } from "./kiro-truncation";
12
- import { fallbackToolUseId, fingerprint, invocationId, mapModelId, normalizeToolId, osTag, stableConversationId } from "./kiro-wire";
12
+ import { fallbackToolUseId, fingerprint, invocationId, kiroToolName, mapModelId, normalizeToolId, osTag, stableConversationId } from "./kiro-wire";
13
13
  import { namespacedToolName } from "../types";
14
14
  import type {
15
15
  AdapterEvent,
@@ -28,6 +28,7 @@ import type { AdapterFetchContext, AdapterRequest } from "./base";
28
28
  import { extractKiroImages, type KiroImage } from "./kiro-images";
29
29
  import { fetchKiroWithRetry } from "./kiro-retry";
30
30
  import { convertKiroToolContext } from "./kiro-tools";
31
+ import { neutralizeIdentity } from "./identity";
31
32
 
32
33
  const AMZ_TARGET = "AmazonCodeWhispererStreamingService.GenerateAssistantResponse";
33
34
  const SDK_VERSION = "1.0.27";
@@ -192,12 +193,15 @@ function injectKiroThinkingTags(content: string, parsed: OcxParsedRequest): stri
192
193
  ].join("\n");
193
194
  }
194
195
 
195
- export function buildKiroPayload(parsed: OcxParsedRequest, profileArn: string | undefined): Record<string, unknown> {
196
+ export function buildKiroPayload(parsed: OcxParsedRequest, profileArn: string | undefined): { payload: Record<string, unknown>; nameMap: Map<string, string> } {
196
197
  const modelId = mapModelId(parsed.modelId);
197
198
  const toolContext = convertKiroToolContext(parsed);
198
199
  const kiroTools = toolContext.tools;
200
+ const nameMap = toolContext.nameMap;
199
201
  const systemParts: string[] = [];
200
- if (!parsed.previousResponseId && parsed.context.systemPrompt?.length) systemParts.push(parsed.context.systemPrompt.join("\n\n"));
202
+ // Neutralize Codex's GPT-5 identity line so a routed Kiro model never misreports as GPT-5/OpenAI
203
+ // and the proxy identity never leaks upstream.
204
+ if (!parsed.previousResponseId && parsed.context.systemPrompt?.length) systemParts.push(neutralizeIdentity(parsed.context.systemPrompt.join("\n\n")));
201
205
  if (toolContext.systemAdditions.length > 0) systemParts.push(...toolContext.systemAdditions);
202
206
  const systemPrefix = systemParts.length > 0 ? `${systemParts.join("\n\n")}\n\n` : "";
203
207
  const structuredToolIds = new Set<string>();
@@ -253,7 +257,9 @@ export function buildKiroPayload(parsed: OcxParsedRequest, profileArn: string |
253
257
  ? toolCalls.map(tc => {
254
258
  const toolUseId = normalizeToolId(tc.id);
255
259
  structuredToolIds.add(toolUseId);
256
- return { name: namespacedToolName(tc.namespace, tc.name), input: (tc.arguments ?? {}) as Record<string, unknown>, toolUseId };
260
+ // Same deterministic normalization as the toolSpecification so the replayed assistant
261
+ // toolUse name matches what Kiro was told the tool is called.
262
+ return { name: kiroToolName(namespacedToolName(tc.namespace, tc.name)), input: (tc.arguments ?? {}) as Record<string, unknown>, toolUseId };
257
263
  })
258
264
  : [];
259
265
  if (kiroTools.length === 0) {
@@ -317,7 +323,7 @@ export function buildKiroPayload(parsed: OcxParsedRequest, profileArn: string |
317
323
  },
318
324
  };
319
325
  if (profileArn) payload.profileArn = profileArn;
320
- return payload;
326
+ return { payload, nameMap };
321
327
  }
322
328
 
323
329
  // Stream parsing (shared by parseStream + parseResponse)
@@ -329,6 +335,7 @@ export async function* parseKiroStream(
329
335
  modelId?: string,
330
336
  inputTokens = 0,
331
337
  contextWindow?: number,
338
+ nameMap?: Map<string, string>,
332
339
  ): AsyncGenerator<AdapterEvent> {
333
340
  if (!response.body) {
334
341
  yield { type: "error", message: "Kiro response has no body" };
@@ -347,7 +354,10 @@ export async function* parseKiroStream(
347
354
  if (!open) return;
348
355
  const tool = open;
349
356
  open = null;
350
- yield { type: "tool_call_start", id: tool.id, name: tool.name };
357
+ // Restore the original wire name if it was normalized for Kiro (spaces/length), so the bridge's
358
+ // toolNsMap (keyed by the original wire name) can route the call back to its MCP namespace.
359
+ const restored = nameMap?.get(tool.name) ?? tool.name;
360
+ yield { type: "tool_call_start", id: tool.id, name: restored };
351
361
  for (const chunk of tool.chunks) if (chunk) yield { type: "tool_call_delta", arguments: chunk };
352
362
  yield { type: "tool_call_end" };
353
363
  }
@@ -467,6 +477,7 @@ export function createKiroAdapter(provider: OcxProviderConfig): ProviderAdapter
467
477
  let inputTokens = 0;
468
478
  let modelId: string | undefined;
469
479
  let contextWindow: number | undefined;
480
+ let toolNameMap: Map<string, string> | undefined;
470
481
  return {
471
482
  name: "kiro",
472
483
  buildRequest(parsed: OcxParsedRequest) {
@@ -487,8 +498,9 @@ export function createKiroAdapter(provider: OcxProviderConfig): ProviderAdapter
487
498
  if (profileArn) headers["x-amzn-kiro-profile-arn"] = profileArn;
488
499
  // CodeWhisperer GenerateAssistantResponse has no reasoning_effort field. Match kiro-gateway's
489
500
  // fake-reasoning contract by injecting effort-derived thinking tags into only the current user turn.
490
- const payload = buildKiroPayload(parsed, profileArn);
491
- const body = JSON.stringify(payload);
501
+ const built = buildKiroPayload(parsed, profileArn);
502
+ toolNameMap = built.nameMap;
503
+ const body = JSON.stringify(built.payload);
492
504
  debugProviderDiagnostic("kiro", "request", {
493
505
  region,
494
506
  requestedModel: parsed.modelId,
@@ -513,7 +525,7 @@ export function createKiroAdapter(provider: OcxProviderConfig): ProviderAdapter
513
525
  },
514
526
 
515
527
  parseStream(response: Response): AsyncGenerator<AdapterEvent> {
516
- return parseKiroStream(response, modelId, inputTokens, contextWindow);
528
+ return parseKiroStream(response, modelId, inputTokens, contextWindow, toolNameMap);
517
529
  },
518
530
 
519
531
  fetchResponse(request: AdapterRequest, ctx?: AdapterFetchContext): Promise<Response> {
@@ -526,7 +538,7 @@ export function createKiroAdapter(provider: OcxProviderConfig): ProviderAdapter
526
538
  // tool failed with "web-search sidecar requires a non-streaming adapter" (kiro-only).
527
539
  async parseResponse(response: Response): Promise<AdapterEvent[]> {
528
540
  const events: AdapterEvent[] = [];
529
- for await (const e of parseKiroStream(response, modelId, inputTokens, contextWindow)) events.push(e);
541
+ for await (const e of parseKiroStream(response, modelId, inputTokens, contextWindow, toolNameMap)) events.push(e);
530
542
  return events;
531
543
  },
532
544
  };
@@ -4,6 +4,7 @@ import type { AdapterEvent, OcxAssistantMessage, OcxContentPart, OcxMessage, Ocx
4
4
  import { isAllowedToolChoice, modelInList, namespacedToolName, resolveToolChoiceWireName, toolAllowedByChoice } from "../types";
5
5
  import { mapReasoningEffort } from "../reasoning-effort";
6
6
  import { contentPartsToText } from "./image";
7
+ import { neutralizeIdentity } from "./identity";
7
8
 
8
9
  // Z.AI's "glm-5.2[1m]" 1M-context id is a Claude-Code / Anthropic-endpoint-only
9
10
  // convention; OpenAI-compatible chat-completions endpoints reject the bracketed
@@ -22,11 +23,9 @@ function messagesToChatFormat(parsed: OcxParsedRequest, provider: OcxProviderCon
22
23
  if (context.systemPrompt && context.systemPrompt.length > 0) {
23
24
  // Codex sends its GPT-5 identity prompt for EVERY model (the per-model catalog
24
25
  // base_instructions is ignored at request time). Neutralize that one identity line
25
- // so routed, non-OpenAI models don't misreport themselves as GPT-5 / OpenAI.
26
- const sys = context.systemPrompt.join("\n\n").replace(
27
- "You are Codex, a coding agent based on GPT-5.",
28
- `You are a coding agent (underlying model: ${parsed.modelId}) running via the opencodex proxy. Do not claim to be GPT-5 or to be made by OpenAI.`,
29
- );
26
+ // so routed, non-OpenAI models don't misreport themselves as GPT-5 / OpenAI — without
27
+ // leaking the proxy identity into the payload.
28
+ const sys = neutralizeIdentity(context.systemPrompt.join("\n\n"));
30
29
  out.push({ role: "system", content: sys });
31
30
  }
32
31