@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.
- package/gui/dist/assets/{index-Cs6p42GR.js → index-DaRQZAM0.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.ts +4 -1
- package/src/adapters/openai-chat.ts +4 -5
- package/src/codex-catalog.ts +5 -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-DaRQZAM0.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;
|
package/src/adapters/kiro.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
|
package/src/codex-catalog.ts
CHANGED
|
@@ -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
|
-
|
|
448
|
-
`You are a coding agent powered by the ${modelName} model
|
|
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);
|