@bitkyc08/opencodex 2.6.10 → 2.6.11

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-Cs6p42GR.js"></script>
19
+ <script type="module" crossorigin src="/assets/index-DaRQZAM0.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.10",
3
+ "version": "2.6.11",
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;
@@ -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";
@@ -198,7 +199,9 @@ export function buildKiroPayload(parsed: OcxParsedRequest, profileArn: string |
198
199
  const kiroTools = toolContext.tools;
199
200
  const nameMap = toolContext.nameMap;
200
201
  const systemParts: string[] = [];
201
- 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")));
202
205
  if (toolContext.systemAdditions.length > 0) systemParts.push(...toolContext.systemAdditions);
203
206
  const systemPrefix = systemParts.length > 0 ? `${systemParts.join("\n\n")}\n\n` : "";
204
207
  const structuredToolIds = new Set<string>();
@@ -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
 
@@ -11,6 +11,7 @@ import { CODEX_REASONING_LEVELS, configuredReasoningEfforts, modelRecordValue, s
11
11
  import { getJawcodeModelMetadata, getJawcodeModelMetadataCaseInsensitive, listJawcodeModelMetadata, resolveJawcodeProvider } from "./generated/jawcode-model-metadata";
12
12
  import { shouldCaseFoldMetadataModelId } from "./providers/derive";
13
13
  import { applyProviderContextCap, providerContextCap } from "./provider-context-cap";
14
+ import { CODEX_GPT5_IDENTITY_LINE } from "./adapters/identity";
14
15
 
15
16
  const BUNDLED_CATALOG_CACHE_MS = 60_000;
16
17
  let bundledCatalogCache: { expiresAt: number; value: RawCatalog | null } | null = null;
@@ -443,9 +444,11 @@ function deriveEntry(template: RawEntry | null, slug: string, desc: string, prio
443
444
  if (slug.includes("/")) {
444
445
  const modelName = slug.slice(slug.indexOf("/") + 1);
445
446
  if (typeof e.base_instructions === "string") {
447
+ // Proxy-neutral: keep the GPT-5/OpenAI disclaimer but never advertise the opencodex proxy
448
+ // (leaking that into base_instructions is a non-first-party signature → ToS risk).
446
449
  e.base_instructions = e.base_instructions.replace(
447
- "You are Codex, a coding agent based on GPT-5.",
448
- `You are a coding agent powered by the ${modelName} model, served through the opencodex proxy. Do not claim to be GPT-5 or made by OpenAI.`,
450
+ CODEX_GPT5_IDENTITY_LINE,
451
+ `You are a coding agent powered by the ${modelName} model. Do not claim to be GPT-5 or made by OpenAI.`,
449
452
  );
450
453
  }
451
454
  applyReasoningLevels(e, model?.reasoningEfforts);