@bitkyc08/opencodex 2.6.13 → 2.6.14
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-BTTqyZ-C.js → index-B64_jgH-.js} +1 -1
- package/gui/dist/index.html +1 -1
- package/package.json +1 -1
- package/src/adapters/anthropic.ts +8 -3
- package/src/adapters/client-fingerprint.ts +4 -0
- package/src/adapters/google.ts +0 -3
- package/src/adapters/kiro-wire.ts +2 -2
- package/src/adapters/kiro.ts +1 -1
- package/src/adapters/openai-chat.ts +104 -69
- package/src/adapters/openai-responses.ts +36 -1
- package/src/oauth/anthropic.ts +2 -2
- package/src/oauth/google-antigravity.ts +4 -3
- package/src/oauth/kimi.ts +2 -2
- package/src/server/adapter-resolve.ts +43 -0
- package/src/server/gui-static.ts +98 -0
- package/src/server.ts +8 -125
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-B64_jgH-.js"></script>
|
|
20
20
|
<link rel="stylesheet" crossorigin href="/assets/index-DIBiVVC0.css">
|
|
21
21
|
</head>
|
|
22
22
|
<body>
|
package/package.json
CHANGED
|
@@ -40,7 +40,7 @@ const MIN_THINKING_BUDGET = 1024;
|
|
|
40
40
|
const OUTPUT_HEADROOM = 8192;
|
|
41
41
|
/** Minimum visible-output room kept below `max_tokens` (so `max_tokens > budget_tokens` always holds). */
|
|
42
42
|
const OUTPUT_FLOOR = 4096;
|
|
43
|
-
const COMPAT_TOOL_PREFIX = "
|
|
43
|
+
const COMPAT_TOOL_PREFIX = "cx_";
|
|
44
44
|
|
|
45
45
|
/** Map a Responses reasoning effort to an Anthropic extended-thinking budget (tokens, >= 1024). */
|
|
46
46
|
function reasoningBudget(effort: string): number {
|
|
@@ -172,7 +172,7 @@ function messagesToAnthropicFormat(
|
|
|
172
172
|
resultBlocks.push({
|
|
173
173
|
type: "tool_result",
|
|
174
174
|
tool_use_id: id,
|
|
175
|
-
content: "[
|
|
175
|
+
content: "[missing tool_result for this tool_use in history]",
|
|
176
176
|
is_error: true,
|
|
177
177
|
});
|
|
178
178
|
}
|
|
@@ -240,7 +240,10 @@ export function createAnthropicAdapter(provider: OcxProviderConfig): ProviderAda
|
|
|
240
240
|
if (parsed.options.topP !== undefined) body.top_p = parsed.options.topP;
|
|
241
241
|
if (parsed.options.stopSequences) body.stop_sequences = parsed.options.stopSequences;
|
|
242
242
|
|
|
243
|
-
|
|
243
|
+
// `reasoning` is a Codex effort string; "none" is the disable sentinel (see parser.ts
|
|
244
|
+
// REASONING_EFFORTS). A bare truthy check would treat "none" as truthy and wrongly enable
|
|
245
|
+
// extended thinking (and strip temperature/top_p), so gate on a real, non-disable effort.
|
|
246
|
+
if (typeof parsed.options.reasoning === "string" && parsed.options.reasoning !== "none") {
|
|
244
247
|
// Anthropic requires max_tokens > thinking.budget_tokens (max_tokens caps thinking +
|
|
245
248
|
// visible output) and budget_tokens >= 1024. Codex sends the SAME value for both, which
|
|
246
249
|
// 400s ("max_tokens must be greater than thinking.budget_tokens"). Size them so max_tokens
|
|
@@ -270,6 +273,8 @@ export function createAnthropicAdapter(provider: OcxProviderConfig): ProviderAda
|
|
|
270
273
|
const headers: Record<string, string> = {
|
|
271
274
|
"Content-Type": "application/json",
|
|
272
275
|
"anthropic-version": "2023-06-01",
|
|
276
|
+
"Accept": parsed.stream ? "text/event-stream" : "application/json",
|
|
277
|
+
"User-Agent": "@anthropic-ai/sdk/0.74.0",
|
|
273
278
|
};
|
|
274
279
|
if (isOAuth) {
|
|
275
280
|
if (provider.apiKey) headers["Authorization"] = `Bearer ${provider.apiKey}`;
|
|
@@ -20,6 +20,10 @@ export const CLAUDE_CODE_HEADERS: Record<string, string> = {
|
|
|
20
20
|
"X-Stainless-Runtime": "node",
|
|
21
21
|
"X-Stainless-Lang": "js",
|
|
22
22
|
"X-Stainless-Timeout": "600",
|
|
23
|
+
"X-Stainless-Arch": process.arch,
|
|
24
|
+
"X-Stainless-OS": process.platform,
|
|
25
|
+
"X-Stainless-Package-Version": "0.74.0",
|
|
26
|
+
"X-Stainless-Runtime-Version": process.version.slice(1),
|
|
23
27
|
};
|
|
24
28
|
|
|
25
29
|
/**
|
package/src/adapters/google.ts
CHANGED
|
@@ -19,7 +19,6 @@ import { isVertexTruncationReason, vertexTruncationErrorMessage } from "./google
|
|
|
19
19
|
import { ANTIGRAVITY_REQUEST_UA, antigravitySessionId, isLikelyRealThoughtSignature, sanitizeAntigravityClaudeSignatures } from "./google-antigravity-wire";
|
|
20
20
|
import { sanitizeGeminiToolParameters } from "./google-tool-schema";
|
|
21
21
|
import { neutralizeIdentity } from "./identity";
|
|
22
|
-
import { ANTIGRAVITY_GOOG_API_CLIENT_UA } from "./client-fingerprint";
|
|
23
22
|
import { antigravityUsesReplayCache, applyAntigravityReplay, clearAntigravityReplay, observeAntigravityReplay } from "./google-antigravity-replay";
|
|
24
23
|
|
|
25
24
|
// Google-family models (Gemini/Vertex/Antigravity) tend to emit long running commentary between
|
|
@@ -264,8 +263,6 @@ export function createGoogleAdapter(provider: OcxProviderConfig): ProviderAdapte
|
|
|
264
263
|
request,
|
|
265
264
|
};
|
|
266
265
|
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;
|
|
269
266
|
if (provider.apiKey) headers["Authorization"] = `Bearer ${provider.apiKey}`;
|
|
270
267
|
return { url, method: "POST", headers, body: JSON.stringify(envelope) };
|
|
271
268
|
}
|
|
@@ -8,9 +8,9 @@ let cachedFp: string | undefined;
|
|
|
8
8
|
export function fingerprint(): string {
|
|
9
9
|
if (cachedFp) return cachedFp;
|
|
10
10
|
try {
|
|
11
|
-
cachedFp = createHash("sha256").update(`${hostname()}-${userInfo().username}-kiro
|
|
11
|
+
cachedFp = createHash("sha256").update(`${hostname()}-${userInfo().username}-kiro`).digest("hex");
|
|
12
12
|
} catch {
|
|
13
|
-
cachedFp = createHash("sha256").update("default-kiro
|
|
13
|
+
cachedFp = createHash("sha256").update("default-kiro").digest("hex");
|
|
14
14
|
}
|
|
15
15
|
return cachedFp;
|
|
16
16
|
}
|
package/src/adapters/kiro.ts
CHANGED
|
@@ -33,7 +33,7 @@ import { neutralizeIdentity } from "./identity";
|
|
|
33
33
|
const AMZ_TARGET = "AmazonCodeWhispererStreamingService.GenerateAssistantResponse";
|
|
34
34
|
const SDK_VERSION = "1.0.27";
|
|
35
35
|
const NODE_VERSION = "22.21.1";
|
|
36
|
-
const KIRO_IDE_VERSION = "1.
|
|
36
|
+
const KIRO_IDE_VERSION = "1.0.0";
|
|
37
37
|
|
|
38
38
|
// Payload construction (conversationState)
|
|
39
39
|
interface KiroToolUse {
|
|
@@ -223,92 +223,127 @@ export function createOpenAIChatAdapter(provider: OcxProviderConfig): ProviderAd
|
|
|
223
223
|
let currentToolCallId = "";
|
|
224
224
|
let currentToolCallName = "";
|
|
225
225
|
let pendingUsage: OcxUsage | undefined;
|
|
226
|
+
// Track terminal signals so a socket EOF without any terminator can fail closed instead of
|
|
227
|
+
// being reported as a clean completion (silent truncation). A graceful close is either an
|
|
228
|
+
// explicit `[DONE]` sentinel OR a chunk carrying a non-null `finish_reason` (some
|
|
229
|
+
// OpenAI-compatible providers omit `[DONE]` but do send finish_reason).
|
|
230
|
+
let sawFinish = false;
|
|
226
231
|
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
232
|
+
// Single per-line handler shared by the streaming loop and the EOF residual-frame flush, so
|
|
233
|
+
// a final frame is parsed identically wherever it lands (no duplicated, drift-prone parsing).
|
|
234
|
+
// Yields adapter events and returns "terminate" for a terminal frame ([DONE] / error) that
|
|
235
|
+
// must end the stream, or "continue" otherwise. Mutates the closure's terminal-signal state.
|
|
236
|
+
const handleDataLine = function* (line: string): Generator<AdapterEvent, "continue" | "terminate"> {
|
|
237
|
+
if (!line.startsWith("data: ")) return "continue";
|
|
238
|
+
const payload = line.slice(6).trim();
|
|
239
|
+
if (payload === "[DONE]") {
|
|
240
|
+
if (currentToolCallId) {
|
|
241
|
+
yield { type: "tool_call_end" };
|
|
242
|
+
currentToolCallId = "";
|
|
243
|
+
}
|
|
244
|
+
yield { type: "done", usage: pendingUsage };
|
|
245
|
+
return "terminate";
|
|
246
|
+
}
|
|
235
247
|
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
}
|
|
244
|
-
yield { type: "done", usage: pendingUsage };
|
|
245
|
-
return;
|
|
246
|
-
}
|
|
248
|
+
let chunk: Record<string, unknown>;
|
|
249
|
+
try {
|
|
250
|
+
chunk = JSON.parse(payload) as Record<string, unknown>;
|
|
251
|
+
} catch {
|
|
252
|
+
debugDroppedFrame("openai-chat", payload);
|
|
253
|
+
return "continue";
|
|
254
|
+
}
|
|
247
255
|
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
256
|
+
// A 200/OK chat-completions stream may carry an inline provider error envelope
|
|
257
|
+
// instead of a clean [DONE]. Surface it as a terminal error so the bridge emits a
|
|
258
|
+
// classified response.failed (bridge case "error") — never a truncated completion.
|
|
259
|
+
if (chunk.error) {
|
|
260
|
+
const err = chunk.error as { message?: string } | undefined;
|
|
261
|
+
if (currentToolCallId) yield { type: "tool_call_end" };
|
|
262
|
+
yield { type: "error", message: err?.message ?? "upstream error" };
|
|
263
|
+
return "terminate";
|
|
264
|
+
}
|
|
255
265
|
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
yield { type: "error", message: err?.message ?? "upstream error" };
|
|
263
|
-
return;
|
|
264
|
-
}
|
|
266
|
+
if (chunk.usage) {
|
|
267
|
+
// Record usage but keep parsing: some providers send usage and the final content
|
|
268
|
+
// delta in the SAME chunk; a bail here would drop that content. The choices
|
|
269
|
+
// guard below no-ops a usage-only chunk.
|
|
270
|
+
pendingUsage = usageFromOpenAIChat(chunk.usage as Record<string, unknown>);
|
|
271
|
+
}
|
|
265
272
|
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
273
|
+
const choices = chunk.choices as { delta?: Record<string, unknown>; finish_reason?: string }[] | undefined;
|
|
274
|
+
if (!choices || choices.length === 0) return "continue";
|
|
275
|
+
// Observe the terminator BEFORE the delta guard: a finish-only chunk (finish_reason set,
|
|
276
|
+
// no delta) is a graceful close and must mark sawFinish even though we skip it below.
|
|
277
|
+
if (typeof choices[0].finish_reason === "string" && choices[0].finish_reason) {
|
|
278
|
+
sawFinish = true;
|
|
279
|
+
}
|
|
280
|
+
const delta = choices[0].delta;
|
|
281
|
+
if (delta) {
|
|
282
|
+
if (typeof delta.content === "string" && delta.content.length > 0) {
|
|
283
|
+
yield { type: "text_delta", text: delta.content };
|
|
284
|
+
}
|
|
272
285
|
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
if (!delta) continue;
|
|
286
|
+
if (typeof delta.reasoning_content === "string" && delta.reasoning_content.length > 0) {
|
|
287
|
+
yield { type: "reasoning_raw_delta", text: delta.reasoning_content };
|
|
288
|
+
}
|
|
277
289
|
|
|
278
|
-
|
|
279
|
-
|
|
290
|
+
const toolCalls = delta.tool_calls as { index: number; id?: string; function?: { name?: string; arguments?: string } }[] | undefined;
|
|
291
|
+
if (toolCalls) {
|
|
292
|
+
for (const tc of toolCalls) {
|
|
293
|
+
if (tc.id && tc.id !== currentToolCallId) {
|
|
294
|
+
if (currentToolCallId) yield { type: "tool_call_end" };
|
|
295
|
+
currentToolCallId = tc.id;
|
|
296
|
+
currentToolCallName = tc.function?.name ?? "";
|
|
297
|
+
yield { type: "tool_call_start", id: tc.id, name: currentToolCallName };
|
|
298
|
+
}
|
|
299
|
+
if (tc.function?.arguments) {
|
|
300
|
+
yield { type: "tool_call_delta", arguments: tc.function.arguments };
|
|
301
|
+
}
|
|
280
302
|
}
|
|
303
|
+
}
|
|
304
|
+
}
|
|
281
305
|
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
306
|
+
if (choices[0].finish_reason === "tool_calls" && currentToolCallId) {
|
|
307
|
+
yield { type: "tool_call_end" };
|
|
308
|
+
currentToolCallId = "";
|
|
309
|
+
}
|
|
310
|
+
return "continue";
|
|
311
|
+
};
|
|
285
312
|
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
currentToolCallId = tc.id;
|
|
292
|
-
currentToolCallName = tc.function?.name ?? "";
|
|
293
|
-
yield { type: "tool_call_start", id: tc.id, name: currentToolCallName };
|
|
294
|
-
}
|
|
295
|
-
if (tc.function?.arguments) {
|
|
296
|
-
yield { type: "tool_call_delta", arguments: tc.function.arguments };
|
|
297
|
-
}
|
|
298
|
-
}
|
|
299
|
-
}
|
|
313
|
+
try {
|
|
314
|
+
while (true) {
|
|
315
|
+
const { done, value } = await reader.read();
|
|
316
|
+
if (done) break;
|
|
317
|
+
buffer += decoder.decode(value, { stream: true });
|
|
300
318
|
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
319
|
+
const lines = buffer.split("\n");
|
|
320
|
+
buffer = lines.pop() ?? "";
|
|
321
|
+
|
|
322
|
+
for (const line of lines) {
|
|
323
|
+
if ((yield* handleDataLine(line)) === "terminate") return;
|
|
305
324
|
}
|
|
306
325
|
}
|
|
307
326
|
|
|
327
|
+
// Some providers send the terminal `data:` frame (carrying the final delta, finish_reason,
|
|
328
|
+
// and/or usage) WITHOUT a trailing newline before closing the socket, so it never crosses
|
|
329
|
+
// the split("\n") boundary and stays in `buffer`. Run it through the SAME handler so its
|
|
330
|
+
// content/tool-calls are emitted and its terminal signal observed — otherwise a genuinely
|
|
331
|
+
// complete stream loses its last frame and may be falsely failed below.
|
|
332
|
+
if (buffer.length > 0) {
|
|
333
|
+
if ((yield* handleDataLine(buffer)) === "terminate") return;
|
|
334
|
+
}
|
|
308
335
|
if (currentToolCallId) {
|
|
309
336
|
yield { type: "tool_call_end" };
|
|
310
337
|
}
|
|
311
|
-
// EOF
|
|
338
|
+
// Reader EOF. A graceful close shows at least one terminal signal: `[DONE]` (returns above),
|
|
339
|
+
// a non-null finish_reason (sawFinish), or a trailing usage chunk (providers emit usage only
|
|
340
|
+
// at end-of-generation). If NONE of those were seen, the stream was cut mid-flight — fail
|
|
341
|
+
// closed so the bridge emits a classified response.failed rather than a silent truncation.
|
|
342
|
+
if (!sawFinish && pendingUsage === undefined) {
|
|
343
|
+
yield { type: "error", message: "upstream stream ended without a terminal signal ([DONE] or finish_reason) — possible truncation" };
|
|
344
|
+
return;
|
|
345
|
+
}
|
|
346
|
+
// Graceful close that omitted [DONE] but delivered finish_reason and/or final usage.
|
|
312
347
|
yield { type: "done", usage: pendingUsage };
|
|
313
348
|
} finally {
|
|
314
349
|
reader.releaseLock();
|
|
@@ -42,6 +42,41 @@ function sanitizeReasoningInputContent(body: unknown): unknown {
|
|
|
42
42
|
return changed ? { ...raw, input } : body;
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
+
/**
|
|
46
|
+
* Hosted (OpenAI-executed) tool types that specific native slugs reject at request time. Codex
|
|
47
|
+
* attaches these for app skills (e.g. `image_generation` for imagegen) regardless of the target
|
|
48
|
+
* model, and the passthrough path forwards the raw body untouched — so a slug that doesn't support
|
|
49
|
+
* the tool 400s (`Tool 'image_generation' is not supported with gpt-5.3-codex-spark.`). Each entry
|
|
50
|
+
* maps a model-slug matcher to the hosted tool types that must be stripped before forwarding.
|
|
51
|
+
* Extend this when another native slug rejects a hosted tool (e.g. `code_interpreter`).
|
|
52
|
+
*/
|
|
53
|
+
const UNSUPPORTED_HOSTED_TOOLS: ReadonlyArray<{ match: (model: string) => boolean; tools: ReadonlySet<string> }> = [
|
|
54
|
+
{ match: model => model.includes("codex-spark"), tools: new Set(["image_generation"]) },
|
|
55
|
+
];
|
|
56
|
+
|
|
57
|
+
function isPlainObject(v: unknown): v is Record<string, unknown> {
|
|
58
|
+
return !!v && typeof v === "object" && !Array.isArray(v);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Remove hosted tool entries the target native slug rejects, so the OAuth-passthrough body never
|
|
63
|
+
* carries a tool the upstream model 400s on. No-op (returns the original reference) when nothing
|
|
64
|
+
* matches, keeping the common path allocation-free.
|
|
65
|
+
*/
|
|
66
|
+
function stripUnsupportedHostedTools(body: unknown): unknown {
|
|
67
|
+
if (!isPlainObject(body) || !Array.isArray(body.tools)) return body;
|
|
68
|
+
const model = typeof body.model === "string" ? body.model : "";
|
|
69
|
+
const unsupported = UNSUPPORTED_HOSTED_TOOLS.filter(e => e.match(model));
|
|
70
|
+
if (unsupported.length === 0) return body;
|
|
71
|
+
|
|
72
|
+
const tools = body.tools.filter(t => {
|
|
73
|
+
const type = isPlainObject(t) && typeof t.type === "string" ? t.type : undefined;
|
|
74
|
+
if (!type) return true;
|
|
75
|
+
return !unsupported.some(e => e.tools.has(type));
|
|
76
|
+
});
|
|
77
|
+
return tools.length === body.tools.length ? body : { ...body, tools };
|
|
78
|
+
}
|
|
79
|
+
|
|
45
80
|
export function createResponsesPassthroughAdapter(provider: OcxProviderConfig): ProviderAdapter & { passthrough: true } {
|
|
46
81
|
return {
|
|
47
82
|
name: "openai-responses",
|
|
@@ -82,7 +117,7 @@ export function createResponsesPassthroughAdapter(provider: OcxProviderConfig):
|
|
|
82
117
|
url,
|
|
83
118
|
method: "POST",
|
|
84
119
|
headers,
|
|
85
|
-
body: JSON.stringify(sanitizeReasoningInputContent(parsed._rawBody)),
|
|
120
|
+
body: JSON.stringify(stripUnsupportedHostedTools(sanitizeReasoningInputContent(parsed._rawBody))),
|
|
86
121
|
};
|
|
87
122
|
},
|
|
88
123
|
|
package/src/oauth/anthropic.ts
CHANGED
|
@@ -13,7 +13,7 @@ const SCOPES = "org:create_api_key user:profile user:inference";
|
|
|
13
13
|
// ── OAuth-request requirements applied by the anthropic adapter when authMode==="oauth" ──
|
|
14
14
|
export const ANTHROPIC_OAUTH_BETA = "claude-code-20250219,oauth-2025-04-20";
|
|
15
15
|
export const CLAUDE_CODE_SYSTEM_INSTRUCTION = "You are a Claude agent, built on Anthropic's Claude Agent SDK.";
|
|
16
|
-
const CLAUDE_TOOL_PREFIX = "
|
|
16
|
+
const CLAUDE_TOOL_PREFIX = "custom_";
|
|
17
17
|
const ANTHROPIC_BUILTIN_TOOLS = new Set(["web_search", "code_execution", "text_editor", "computer"]);
|
|
18
18
|
|
|
19
19
|
/** OAuth tokens reject arbitrary tool names; prefix custom tools (Anthropic builtins are exempt). */
|
|
@@ -22,7 +22,7 @@ export function applyClaudeToolPrefix(name: string): string {
|
|
|
22
22
|
return CLAUDE_TOOL_PREFIX + name;
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
-
/** Strip the
|
|
25
|
+
/** Strip the custom_ prefix from a returned tool_use name so the caller (Codex) sees the original. */
|
|
26
26
|
export function stripClaudeToolPrefix(name: string): string {
|
|
27
27
|
return name.startsWith(CLAUDE_TOOL_PREFIX) ? name.slice(CLAUDE_TOOL_PREFIX.length) : name;
|
|
28
28
|
}
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
import { OAuthCallbackFlow, type OAuthCallbackFlowOptions } from "./callback-server";
|
|
13
13
|
import { generatePKCE } from "./pkce";
|
|
14
14
|
import type { OAuthController, OAuthCredentials } from "./types";
|
|
15
|
+
import { antigravityUserAgent, ANTIGRAVITY_GOOG_API_CLIENT_UA } from "../adapters/client-fingerprint";
|
|
15
16
|
|
|
16
17
|
const CLIENT_ID = process.env.GOOGLE_ANTIGRAVITY_CLIENT_ID
|
|
17
18
|
|| "1071006060591-tmhssin2h21lcre235vtolojh4g403ep.apps.googleusercontent.com";
|
|
@@ -94,7 +95,7 @@ function extractProjectId(data: Record<string, unknown> | undefined): string | u
|
|
|
94
95
|
async function loadCodeAssistProject(accessToken: string, signal?: AbortSignal): Promise<string | undefined> {
|
|
95
96
|
const response = await fetch(`${PROD_API}/${API_VERSION}:loadCodeAssist`, {
|
|
96
97
|
method: "POST",
|
|
97
|
-
headers: { Authorization: `Bearer ${accessToken}`, Accept: "*/*", "Content-Type": "application/json" },
|
|
98
|
+
headers: { Authorization: `Bearer ${accessToken}`, Accept: "*/*", "Content-Type": "application/json", "User-Agent": antigravityUserAgent() },
|
|
98
99
|
body: JSON.stringify({ metadata: { ideType: "ANTIGRAVITY" } }),
|
|
99
100
|
signal: requestSignal(signal),
|
|
100
101
|
});
|
|
@@ -107,8 +108,8 @@ async function onboardProject(accessToken: string, signal?: AbortSignal): Promis
|
|
|
107
108
|
if (signal?.aborted) throw signal.reason ?? new Error("Antigravity onboarding aborted");
|
|
108
109
|
const response = await fetch(`${DAILY_API}/${API_VERSION}:onboardUser`, {
|
|
109
110
|
method: "POST",
|
|
110
|
-
headers: { Authorization: `Bearer ${accessToken}`, Accept: "*/*", "Content-Type": "application/json" },
|
|
111
|
-
body: JSON.stringify({ tier_id: "free-tier", metadata: { ide_type: "ANTIGRAVITY", ide_name: "antigravity" } }),
|
|
111
|
+
headers: { Authorization: `Bearer ${accessToken}`, Accept: "*/*", "Content-Type": "application/json", "User-Agent": antigravityUserAgent(), "x-goog-api-client": ANTIGRAVITY_GOOG_API_CLIENT_UA },
|
|
112
|
+
body: JSON.stringify({ tier_id: "free-tier", metadata: { ide_type: "ANTIGRAVITY", ide_name: "antigravity", ide_version: antigravityUserAgent() } }),
|
|
112
113
|
signal: requestSignal(signal),
|
|
113
114
|
});
|
|
114
115
|
if (!response.ok) {
|
package/src/oauth/kimi.ts
CHANGED
|
@@ -12,7 +12,7 @@ const DEVICE_ID_FILENAME = "kimi-device-id";
|
|
|
12
12
|
const DEFAULT_POLL_INTERVAL_MS = 5000;
|
|
13
13
|
const DEFAULT_DEVICE_FLOW_TTL_MS = 15 * 60 * 1000;
|
|
14
14
|
const OAUTH_EXPIRY_SKEW_MS = 5 * 60 * 1000;
|
|
15
|
-
const KIMI_CLI_VERSION = "
|
|
15
|
+
const KIMI_CLI_VERSION = "0.14.0";
|
|
16
16
|
|
|
17
17
|
interface DeviceAuthorizationResponse {
|
|
18
18
|
user_code?: string;
|
|
@@ -70,7 +70,7 @@ function getDeviceId(): string {
|
|
|
70
70
|
function getKimiCommonHeaders(): Record<string, string> {
|
|
71
71
|
return {
|
|
72
72
|
"User-Agent": `KimiCLI/${KIMI_CLI_VERSION}`,
|
|
73
|
-
"X-Msh-Platform": "
|
|
73
|
+
"X-Msh-Platform": "kimi_code_cli",
|
|
74
74
|
"X-Msh-Version": KIMI_CLI_VERSION,
|
|
75
75
|
"X-Msh-Device-Name": os.hostname(),
|
|
76
76
|
"X-Msh-Device-Model": getDeviceModel(),
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { createAnthropicAdapter } from "../adapters/anthropic";
|
|
2
|
+
import { createAzureAdapter } from "../adapters/azure";
|
|
3
|
+
import { createGoogleAdapter } from "../adapters/google";
|
|
4
|
+
import { createKiroAdapter } from "../adapters/kiro";
|
|
5
|
+
import { createOpenAIChatAdapter } from "../adapters/openai-chat";
|
|
6
|
+
import { createResponsesPassthroughAdapter } from "../adapters/openai-responses";
|
|
7
|
+
import type { OcxProviderConfig } from "../types";
|
|
8
|
+
|
|
9
|
+
/** Providers whose listed model ids must be driven over the Anthropic wire even if the provider's
|
|
10
|
+
* configured adapter is something else (the upstream only speaks Anthropic for these models). */
|
|
11
|
+
const ANTHROPIC_WIRE_MODELS: Record<string, Set<string>> = {
|
|
12
|
+
"opencode-go": new Set(["minimax-m2.5", "minimax-m2.7", "minimax-m3", "qwen3.5-plus", "qwen3.6-plus", "qwen3.7-max", "qwen3.7-plus"]),
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
/** Return a provider config whose adapter is forced to "anthropic" when the model id is wire-pinned. */
|
|
16
|
+
export function resolveWireProtocolOverride(providerName: string, modelId: string, providerConfig: OcxProviderConfig): OcxProviderConfig {
|
|
17
|
+
const overrideSet = ANTHROPIC_WIRE_MODELS[providerName];
|
|
18
|
+
if (overrideSet?.has(modelId) && providerConfig.adapter !== "anthropic") {
|
|
19
|
+
return { ...providerConfig, adapter: "anthropic" };
|
|
20
|
+
}
|
|
21
|
+
return providerConfig;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** Build the provider adapter for a resolved provider config. */
|
|
25
|
+
export function resolveAdapter(providerConfig: OcxProviderConfig) {
|
|
26
|
+
switch (providerConfig.adapter) {
|
|
27
|
+
case "openai-chat":
|
|
28
|
+
return createOpenAIChatAdapter(providerConfig);
|
|
29
|
+
case "anthropic":
|
|
30
|
+
return createAnthropicAdapter(providerConfig);
|
|
31
|
+
case "openai-responses":
|
|
32
|
+
return createResponsesPassthroughAdapter(providerConfig);
|
|
33
|
+
case "google":
|
|
34
|
+
return createGoogleAdapter(providerConfig);
|
|
35
|
+
case "kiro":
|
|
36
|
+
return createKiroAdapter(providerConfig);
|
|
37
|
+
case "azure":
|
|
38
|
+
case "azure-openai":
|
|
39
|
+
return createAzureAdapter(providerConfig);
|
|
40
|
+
default:
|
|
41
|
+
throw new Error(`Unknown adapter: ${providerConfig.adapter}`);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { existsSync, readFileSync, statSync } from "node:fs";
|
|
2
|
+
import { extname, isAbsolute, join, relative, resolve } from "node:path";
|
|
3
|
+
|
|
4
|
+
/** opencodex version, read from the packaged package.json (same source as the server bootstrap). */
|
|
5
|
+
const VERSION = (() => {
|
|
6
|
+
try {
|
|
7
|
+
return JSON.parse(readFileSync(new URL("../../package.json", import.meta.url), "utf8")).version as string;
|
|
8
|
+
} catch {
|
|
9
|
+
return "0.0.0";
|
|
10
|
+
}
|
|
11
|
+
})();
|
|
12
|
+
|
|
13
|
+
const MIME_TYPES: Record<string, string> = {
|
|
14
|
+
".html": "text/html", ".js": "application/javascript", ".css": "text/css",
|
|
15
|
+
".json": "application/json", ".svg": "image/svg+xml", ".png": "image/png",
|
|
16
|
+
".ico": "image/x-icon",
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
function findGuiDist(): string | null {
|
|
20
|
+
const candidates = [
|
|
21
|
+
join(import.meta.dir, "..", "..", "gui", "dist"),
|
|
22
|
+
join(import.meta.dir, "..", "..", "..", "gui", "dist"),
|
|
23
|
+
];
|
|
24
|
+
for (const c of candidates) {
|
|
25
|
+
if (existsSync(join(c, "index.html"))) return c;
|
|
26
|
+
}
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function resolveGuiFilePath(guiDist: string, pathname: string): string | null {
|
|
31
|
+
let decodedPath: string;
|
|
32
|
+
try {
|
|
33
|
+
decodedPath = decodeURIComponent(pathname);
|
|
34
|
+
} catch {
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
if (decodedPath.includes("\0")) return null;
|
|
38
|
+
|
|
39
|
+
const relativePath = decodedPath === "/" || decodedPath === ""
|
|
40
|
+
? "index.html"
|
|
41
|
+
: decodedPath.replace(/\\/g, "/").replace(/^\/+/, "");
|
|
42
|
+
const root = resolve(guiDist);
|
|
43
|
+
const filePath = resolve(root, relativePath);
|
|
44
|
+
const rel = relative(root, filePath);
|
|
45
|
+
if (rel === "" || rel.startsWith("..") || isAbsolute(rel)) return null;
|
|
46
|
+
return filePath;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function isFile(path: string): boolean {
|
|
50
|
+
try {
|
|
51
|
+
return statSync(path).isFile();
|
|
52
|
+
} catch {
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function serveGuiFile(pathname: string): Response | null {
|
|
58
|
+
const guiDist = findGuiDist();
|
|
59
|
+
if (!guiDist) return null;
|
|
60
|
+
const filePath = resolveGuiFilePath(guiDist, pathname);
|
|
61
|
+
if (!filePath) return null;
|
|
62
|
+
|
|
63
|
+
if (!isFile(filePath)) {
|
|
64
|
+
if (!extname(pathname)) {
|
|
65
|
+
const indexPath = join(guiDist, "index.html");
|
|
66
|
+
if (isFile(indexPath)) {
|
|
67
|
+
return new Response(Bun.file(indexPath), {
|
|
68
|
+
headers: { "Content-Type": "text/html" },
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const ext = extname(filePath);
|
|
76
|
+
const contentType = MIME_TYPES[ext] || "application/octet-stream";
|
|
77
|
+
return new Response(Bun.file(filePath), {
|
|
78
|
+
headers: { "Content-Type": contentType },
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function rootFallbackPayload() {
|
|
83
|
+
return {
|
|
84
|
+
status: "ok",
|
|
85
|
+
service: "opencodex",
|
|
86
|
+
version: VERSION,
|
|
87
|
+
dashboard: {
|
|
88
|
+
available: false,
|
|
89
|
+
reason: "GUI build not found. Run `bun run build:gui` from the opencodex repo, or use `ocx gui` from a packaged install.",
|
|
90
|
+
},
|
|
91
|
+
endpoints: {
|
|
92
|
+
health: "/healthz",
|
|
93
|
+
models: "/v1/models",
|
|
94
|
+
responses: "/v1/responses",
|
|
95
|
+
management: "/api/*",
|
|
96
|
+
},
|
|
97
|
+
};
|
|
98
|
+
}
|