@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.
- package/gui/dist/assets/{index-PZN0Edav.js → index-CG1hKRft.js} +1 -1
- package/gui/dist/index.html +1 -1
- package/package.json +1 -1
- package/src/adapters/anthropic.ts +11 -4
- package/src/adapters/client-fingerprint.ts +55 -0
- package/src/adapters/google-antigravity-wire.ts +8 -2
- package/src/adapters/google.ts +21 -5
- package/src/adapters/identity.ts +34 -0
- package/src/adapters/kiro-tools.ts +13 -4
- package/src/adapters/kiro-wire.ts +37 -0
- package/src/adapters/kiro.ts +22 -10
- package/src/adapters/openai-chat.ts +4 -5
- package/src/bridge.ts +114 -3
- package/src/codex-catalog.ts +5 -2
- package/src/types.ts +18 -0
- package/src/web-search/executor.ts +2 -1
- package/src/web-search/format-result.ts +36 -0
- package/src/web-search/index.ts +1 -1
- package/src/web-search/loop.ts +202 -54
- package/src/web-search/parse.ts +54 -2
- package/src/web-search/synthetic-tool.ts +7 -2
package/gui/dist/index.html
CHANGED
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
} catch (e) {}
|
|
17
17
|
})();
|
|
18
18
|
</script>
|
|
19
|
-
<script type="module" crossorigin src="/assets/index-
|
|
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
|
@@ -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?.
|
|
119
|
-
"
|
|
120
|
-
|
|
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
|
-
/**
|
|
5
|
-
|
|
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
|
package/src/adapters/google.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
|
242
|
-
//
|
|
243
|
-
|
|
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
|
|
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
|
|
139
|
-
//
|
|
140
|
-
|
|
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
|
}
|
package/src/adapters/kiro.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
491
|
-
|
|
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
|
-
|
|
27
|
-
|
|
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
|
|