@bitkyc08/opencodex 2.7.1 → 2.7.3
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/README.md +1 -2
- package/gui/dist/assets/index-D7o1qwy-.css +1 -0
- package/gui/dist/assets/index-DT4C-vKW.js +40 -0
- package/gui/dist/index.html +2 -2
- package/package.json +1 -1
- package/src/adapters/anthropic.ts +14 -3
- package/src/adapters/azure.ts +16 -11
- package/src/adapters/cursor/live-models.ts +36 -24
- package/src/adapters/cursor/live-transport.ts +5 -17
- package/src/adapters/cursor/native-exec.ts +11 -7
- package/src/adapters/cursor/transport-retry.ts +1 -4
- package/src/adapters/google.ts +32 -9
- package/src/adapters/kiro-retry.ts +6 -2
- package/src/adapters/kiro.ts +14 -9
- package/src/adapters/openai-chat.ts +38 -28
- package/src/codex/catalog.ts +67 -32
- package/src/config.ts +25 -2
- package/src/oauth/index.ts +4 -3
- package/src/oauth/key-providers.ts +1 -1
- package/src/oauth/kiro-credentials.ts +13 -3
- package/src/providers/derive.ts +4 -0
- package/src/providers/kiro-models.ts +1 -1
- package/src/providers/registry.ts +129 -40
- package/src/responses/parser.ts +12 -0
- package/src/router.ts +13 -1
- package/src/server/management-api.ts +10 -2
- package/src/server/request-log.ts +25 -1
- package/src/server/responses.ts +141 -43
- package/src/types.ts +12 -2
- package/src/update/index.ts +1 -1
- package/gui/dist/assets/index-BUAMcKFd.css +0 -1
- package/gui/dist/assets/index-CtHGtaW8.js +0 -34
package/gui/dist/index.html
CHANGED
|
@@ -16,8 +16,8 @@
|
|
|
16
16
|
} catch (e) {}
|
|
17
17
|
})();
|
|
18
18
|
</script>
|
|
19
|
-
<script type="module" crossorigin src="/assets/index-
|
|
20
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
19
|
+
<script type="module" crossorigin src="/assets/index-DT4C-vKW.js"></script>
|
|
20
|
+
<link rel="stylesheet" crossorigin href="/assets/index-D7o1qwy-.css">
|
|
21
21
|
</head>
|
|
22
22
|
<body>
|
|
23
23
|
<div id="root"></div>
|
package/package.json
CHANGED
|
@@ -241,7 +241,7 @@ function usesNativeAnthropicEndpoint(provider: OcxProviderConfig): boolean {
|
|
|
241
241
|
try {
|
|
242
242
|
return new URL(provider.baseUrl).hostname === "api.anthropic.com";
|
|
243
243
|
} catch {
|
|
244
|
-
|
|
244
|
+
throw new Error(`anthropic provider has malformed baseUrl: ${provider.baseUrl}`);
|
|
245
245
|
}
|
|
246
246
|
}
|
|
247
247
|
|
|
@@ -555,6 +555,13 @@ export function createAnthropicAdapter(provider: OcxProviderConfig, cacheRetenti
|
|
|
555
555
|
name: "anthropic",
|
|
556
556
|
|
|
557
557
|
buildRequest(parsed: OcxParsedRequest) {
|
|
558
|
+
if (typeof provider.apiKey !== "string" || provider.apiKey.trim() === "") {
|
|
559
|
+
if (isOAuth) {
|
|
560
|
+
throw new Error("anthropic oauth token missing — run ocx login anthropic");
|
|
561
|
+
}
|
|
562
|
+
throw new Error("anthropic provider requires a non-empty apiKey (authMode: key)");
|
|
563
|
+
}
|
|
564
|
+
|
|
558
565
|
const { system, messages } = messagesToAnthropicFormat(parsed, toolNames);
|
|
559
566
|
// Anthropic rejects many-image requests (>20 images) carrying any image over
|
|
560
567
|
// 2000px per side; see anthropic-image-guard.ts for the full limit policy.
|
|
@@ -618,6 +625,10 @@ export function createAnthropicAdapter(provider: OcxProviderConfig, cacheRetenti
|
|
|
618
625
|
|
|
619
626
|
const base = provider.baseUrl.replace(/\/v1\/?$/, "");
|
|
620
627
|
const url = `${base}/v1/messages`;
|
|
628
|
+
const unresolvedPlaceholder = url.match(/\{[^}]*\}/)?.[0];
|
|
629
|
+
if (unresolvedPlaceholder) {
|
|
630
|
+
throw new Error(`anthropic baseUrl contains unresolved ${unresolvedPlaceholder}`);
|
|
631
|
+
}
|
|
621
632
|
const headers: Record<string, string> = {
|
|
622
633
|
"Content-Type": "application/json",
|
|
623
634
|
"anthropic-version": "2023-06-01",
|
|
@@ -625,7 +636,7 @@ export function createAnthropicAdapter(provider: OcxProviderConfig, cacheRetenti
|
|
|
625
636
|
"User-Agent": "@anthropic-ai/sdk/0.74.0",
|
|
626
637
|
};
|
|
627
638
|
if (isOAuth) {
|
|
628
|
-
|
|
639
|
+
headers["Authorization"] = `Bearer ${provider.apiKey}`;
|
|
629
640
|
headers["anthropic-beta"] = ANTHROPIC_OAUTH_BETA;
|
|
630
641
|
// Match the real Claude Code CLI request fingerprint: a valid OAuth token with an empty
|
|
631
642
|
// header set is a non-first-party signature. (cch billing-header signing is intentionally
|
|
@@ -633,7 +644,7 @@ export function createAnthropicAdapter(provider: OcxProviderConfig, cacheRetenti
|
|
|
633
644
|
Object.assign(headers, CLAUDE_CODE_HEADERS);
|
|
634
645
|
headers["X-Claude-Code-Session-Id"] = claudeCodeSessionId(provider.apiKey);
|
|
635
646
|
headers["x-client-request-id"] = crypto.randomUUID();
|
|
636
|
-
} else
|
|
647
|
+
} else {
|
|
637
648
|
headers["x-api-key"] = provider.apiKey;
|
|
638
649
|
}
|
|
639
650
|
if (provider.headers) Object.assign(headers, provider.headers);
|
package/src/adapters/azure.ts
CHANGED
|
@@ -13,19 +13,24 @@ export function createAzureAdapter(provider: OcxProviderConfig): ProviderAdapter
|
|
|
13
13
|
name: "azure-openai",
|
|
14
14
|
|
|
15
15
|
async buildRequest(parsed: OcxParsedRequest) {
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
if (provider.apiKey) {
|
|
19
|
-
headers["api-key"] = provider.apiKey;
|
|
20
|
-
delete headers["Authorization"];
|
|
16
|
+
if (provider.authMode === "forward") {
|
|
17
|
+
throw new Error("azure-openai does not support forward auth mode");
|
|
21
18
|
}
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
const apiVersion = (provider.headers?.["api-version"]) ?? "2025-04-01-preview";
|
|
25
|
-
const separator = url.includes("?") ? "&" : "?";
|
|
26
|
-
url = `${url}${separator}api-version=${apiVersion}`;
|
|
19
|
+
if (typeof provider.apiKey !== "string" || provider.apiKey.trim() === "") {
|
|
20
|
+
throw new Error("azure-openai requires a non-empty apiKey");
|
|
27
21
|
}
|
|
28
|
-
|
|
22
|
+
|
|
23
|
+
const request = await inner.buildRequest(parsed);
|
|
24
|
+
const unresolvedPlaceholder = request.url.match(/\{[^}]*\}/)?.[0] ?? request.url.match(/[{}]/)?.[0];
|
|
25
|
+
if (unresolvedPlaceholder) {
|
|
26
|
+
throw new Error(`azure-openai baseUrl contains unresolved ${unresolvedPlaceholder} — set your real resource URL`);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const headers = { ...request.headers };
|
|
30
|
+
headers["api-key"] = provider.apiKey;
|
|
31
|
+
delete headers["Authorization"];
|
|
32
|
+
// The inner adapter always targets Azure's v1 API here, which needs no api-version query.
|
|
33
|
+
return { ...request, headers };
|
|
29
34
|
},
|
|
30
35
|
};
|
|
31
36
|
}
|
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
* Live Cursor model discovery via the `GetUsableModels` RPC (HTTP/2 + Connect-unary protobuf).
|
|
3
3
|
*
|
|
4
4
|
* Returns the account's actually-usable model ids (the full effort-suffixed variants Cursor offers
|
|
5
|
-
* for THIS plan), so the routed catalog reflects reality instead of a static superset.
|
|
6
|
-
*
|
|
5
|
+
* for THIS plan), so the routed catalog reflects reality instead of a static superset. Failures are
|
|
6
|
+
* classified so callers can surface the reason before applying their existing degradation policy.
|
|
7
7
|
*
|
|
8
8
|
* Protocol notes (hard-won, see devlog 350.110):
|
|
9
9
|
* - content-type `application/proto` + `connect-protocol-version: 1` (NOT `application/connect+proto`,
|
|
@@ -26,13 +26,17 @@ export interface CursorUsableModelsOptions {
|
|
|
26
26
|
timeoutMs?: number;
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
-
export
|
|
29
|
+
export type CursorUsableModelsResult =
|
|
30
|
+
| { ok: true; models: string[] }
|
|
31
|
+
| { ok: false; error: "auth" | "http" | "timeout" | "decode" | "empty"; detail?: string };
|
|
32
|
+
|
|
33
|
+
export async function fetchCursorUsableModels(opts: CursorUsableModelsOptions): Promise<CursorUsableModelsResult> {
|
|
30
34
|
const baseUrl = (opts.baseUrl ?? "https://api2.cursor.sh").replace(/\/+$/, "");
|
|
31
35
|
const timeoutMs = opts.timeoutMs ?? 8000;
|
|
32
36
|
|
|
33
|
-
return new Promise<
|
|
37
|
+
return new Promise<CursorUsableModelsResult>(resolve => {
|
|
34
38
|
let settled = false;
|
|
35
|
-
const finish = (value:
|
|
39
|
+
const finish = (value: CursorUsableModelsResult): void => {
|
|
36
40
|
if (settled) return;
|
|
37
41
|
settled = true;
|
|
38
42
|
resolve(value);
|
|
@@ -42,32 +46,37 @@ export async function fetchCursorUsableModels(opts: CursorUsableModelsOptions):
|
|
|
42
46
|
try {
|
|
43
47
|
client = http2.connect(baseUrl);
|
|
44
48
|
} catch {
|
|
45
|
-
return finish(
|
|
49
|
+
return finish({ ok: false, error: "http", detail: "HTTP/2 connection setup failed" });
|
|
46
50
|
}
|
|
47
51
|
|
|
48
52
|
const timer = setTimeout(() => {
|
|
53
|
+
finish({ ok: false, error: "timeout", detail: `No response within ${timeoutMs}ms` });
|
|
49
54
|
client.destroy();
|
|
50
|
-
finish(null);
|
|
51
55
|
}, timeoutMs);
|
|
52
|
-
const close = (value:
|
|
56
|
+
const close = (value: CursorUsableModelsResult): void => {
|
|
53
57
|
clearTimeout(timer);
|
|
54
58
|
client.close();
|
|
55
59
|
finish(value);
|
|
56
60
|
};
|
|
57
61
|
|
|
58
|
-
client.on("error", () => close(
|
|
62
|
+
client.on("error", () => close({ ok: false, error: "http", detail: "HTTP/2 session failed" }));
|
|
59
63
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
64
|
+
let req: http2.ClientHttp2Stream;
|
|
65
|
+
try {
|
|
66
|
+
req = client.request({
|
|
67
|
+
":method": "POST",
|
|
68
|
+
":path": CURSOR_GET_USABLE_MODELS_PATH,
|
|
69
|
+
"content-type": "application/proto",
|
|
70
|
+
"connect-protocol-version": "1",
|
|
71
|
+
authorization: `Bearer ${opts.apiKey}`,
|
|
72
|
+
"x-ghost-mode": "true",
|
|
73
|
+
"x-cursor-client-version": opts.clientVersion ?? CURSOR_DISCOVERY_CLIENT_VERSION,
|
|
74
|
+
"x-cursor-client-type": "cli",
|
|
75
|
+
"x-session-id": crypto.randomUUID(),
|
|
76
|
+
});
|
|
77
|
+
} catch {
|
|
78
|
+
return close({ ok: false, error: "http", detail: "HTTP/2 request setup failed" });
|
|
79
|
+
}
|
|
71
80
|
|
|
72
81
|
let status = 0;
|
|
73
82
|
const chunks: Buffer[] = [];
|
|
@@ -75,9 +84,12 @@ export async function fetchCursorUsableModels(opts: CursorUsableModelsOptions):
|
|
|
75
84
|
status = Number(headers[":status"] ?? 0);
|
|
76
85
|
});
|
|
77
86
|
req.on("data", (chunk: Buffer) => chunks.push(chunk));
|
|
78
|
-
req.on("error", () => close(
|
|
87
|
+
req.on("error", () => close({ ok: false, error: "http", detail: "HTTP/2 request failed" }));
|
|
79
88
|
req.on("end", () => {
|
|
80
|
-
if (status
|
|
89
|
+
if (status === 401 || status === 403) {
|
|
90
|
+
return close({ ok: false, error: "auth", detail: `HTTP ${status}` });
|
|
91
|
+
}
|
|
92
|
+
if (status !== 200) return close({ ok: false, error: "http", detail: `HTTP ${status || "unknown"}` });
|
|
81
93
|
try {
|
|
82
94
|
const response = fromBinary(GetUsableModelsResponseSchema, new Uint8Array(Buffer.concat(chunks)));
|
|
83
95
|
// Account filtering uses wire `model_id` values only. Aliases like `composer-2-5` must not
|
|
@@ -85,9 +97,9 @@ export async function fetchCursorUsableModels(opts: CursorUsableModelsOptions):
|
|
|
85
97
|
const ids = (response.models ?? [])
|
|
86
98
|
.map(model => (model as { modelId?: string }).modelId)
|
|
87
99
|
.filter((id): id is string => typeof id === "string" && id.length > 0);
|
|
88
|
-
close(ids.length > 0 ? ids :
|
|
100
|
+
close(ids.length > 0 ? { ok: true, models: ids } : { ok: false, error: "empty" });
|
|
89
101
|
} catch {
|
|
90
|
-
close(
|
|
102
|
+
close({ ok: false, error: "decode", detail: "Invalid GetUsableModels protobuf response" });
|
|
91
103
|
}
|
|
92
104
|
});
|
|
93
105
|
|
|
@@ -19,8 +19,6 @@ import {
|
|
|
19
19
|
ExaSearchRequestResponseSchema,
|
|
20
20
|
ExaSearchRequestResponse_RejectedSchema,
|
|
21
21
|
InteractionResponseSchema,
|
|
22
|
-
SetupVmEnvironmentResultSchema,
|
|
23
|
-
SetupVmEnvironmentSuccessSchema,
|
|
24
22
|
SwitchModeRequestResponseSchema,
|
|
25
23
|
SwitchModeRequestResponse_RejectedSchema,
|
|
26
24
|
WebSearchRequestResponseSchema,
|
|
@@ -270,18 +268,9 @@ export function planInteractionQueryReply(query: InteractionQuery): { response:
|
|
|
270
268
|
};
|
|
271
269
|
}
|
|
272
270
|
if (q.case === "setupVmEnvironmentArgs") {
|
|
273
|
-
|
|
274
|
-
response: respond({
|
|
275
|
-
case: "setupVmEnvironmentResult",
|
|
276
|
-
value: create(SetupVmEnvironmentResultSchema, {
|
|
277
|
-
result: { case: "success", value: create(SetupVmEnvironmentSuccessSchema, {}) },
|
|
278
|
-
}),
|
|
279
|
-
}),
|
|
280
|
-
replyCase: "setupVmEnvironmentResult:success",
|
|
281
|
-
};
|
|
271
|
+
throw new Error("Cursor setupVmEnvironment is not supported by opencodex");
|
|
282
272
|
}
|
|
283
|
-
|
|
284
|
-
return { response: respond({ case: undefined, value: undefined } as InteractionResponse["result"]), replyCase: "empty" };
|
|
273
|
+
throw new Error(`Unsupported Cursor interaction query case: ${q.case ?? "unknown"}`);
|
|
285
274
|
}
|
|
286
275
|
|
|
287
276
|
/**
|
|
@@ -354,8 +343,8 @@ class LiveCursorTransport implements CursorTransport {
|
|
|
354
343
|
/**
|
|
355
344
|
* Connect MCP servers and compute the tool definitions advertised to the Cursor server.
|
|
356
345
|
* MUST complete before the first `requestContextArgs` (the server only calls MCP tools it was
|
|
357
|
-
* told about), so `run()` awaits this before opening the stream.
|
|
358
|
-
*
|
|
346
|
+
* told about), so `run()` awaits this before opening the stream. Preparation failures reject the
|
|
347
|
+
* turn instead of silently running with MCP disabled.
|
|
359
348
|
*/
|
|
360
349
|
private prepareMcp(): Promise<void> {
|
|
361
350
|
if (!this.mcpManager) return Promise.resolve();
|
|
@@ -370,8 +359,7 @@ class LiveCursorTransport implements CursorTransport {
|
|
|
370
359
|
unsafeAllowNativeLocalExec: cursorUnsafeNativeLocalExecEnabled(this.input.provider),
|
|
371
360
|
};
|
|
372
361
|
} catch (err) {
|
|
373
|
-
|
|
374
|
-
this.execContext = { ...this.desktopDeps, unsafeAllowNativeLocalExec: cursorUnsafeNativeLocalExecEnabled(this.input.provider) };
|
|
362
|
+
throw new Error(`Cursor MCP preparation failed: ${err instanceof Error ? err.message : String(err)}`, { cause: err });
|
|
375
363
|
}
|
|
376
364
|
})();
|
|
377
365
|
}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
2
|
import { create } from "@bufbuild/protobuf";
|
|
3
3
|
import {
|
|
4
|
+
DiagnosticsErrorSchema,
|
|
4
5
|
DiagnosticsResultSchema,
|
|
5
|
-
DiagnosticsSuccessSchema,
|
|
6
6
|
GetBlobResultSchema,
|
|
7
7
|
KvClientMessageSchema,
|
|
8
8
|
McpErrorSchema,
|
|
@@ -63,14 +63,12 @@ export interface CursorNativeExecContext extends CursorNativeExecDeps {
|
|
|
63
63
|
clientToolDefs?: McpToolDefinition[];
|
|
64
64
|
/** Unsafe opt-in escape hatch for Cursor server-driven local fs/shell/fetch execution. */
|
|
65
65
|
unsafeAllowNativeLocalExec?: boolean;
|
|
66
|
-
/** @deprecated Use unsafeAllowNativeLocalExec. Kept as a transition alias for local experiments. */
|
|
67
|
-
allowNativeLocalExec?: boolean;
|
|
68
66
|
/** apply_patch is visible for this request; Cursor-native write/delete must not bypass Codex. */
|
|
69
67
|
rejectNativeFileMutations?: boolean;
|
|
70
68
|
}
|
|
71
69
|
|
|
72
|
-
export function cursorUnsafeNativeLocalExecEnabled(input: Pick<CursorNativeExecContext, "unsafeAllowNativeLocalExec"
|
|
73
|
-
return input.unsafeAllowNativeLocalExec === true
|
|
70
|
+
export function cursorUnsafeNativeLocalExecEnabled(input: Pick<CursorNativeExecContext, "unsafeAllowNativeLocalExec"> = {}): boolean {
|
|
71
|
+
return input.unsafeAllowNativeLocalExec === true;
|
|
74
72
|
}
|
|
75
73
|
|
|
76
74
|
/**
|
|
@@ -183,10 +181,16 @@ export async function handleCursorNativeExec(execMsg: ExecServerMessage, deps: C
|
|
|
183
181
|
if (execCase === "diagnosticsArgs") {
|
|
184
182
|
const path = execMsg.message.value.path;
|
|
185
183
|
return [execBytes(execMsg, "diagnosticsResult", create(DiagnosticsResultSchema, {
|
|
186
|
-
result: {
|
|
184
|
+
result: {
|
|
185
|
+
case: "error",
|
|
186
|
+
value: create(DiagnosticsErrorSchema, {
|
|
187
|
+
path,
|
|
188
|
+
error: "Diagnostics are not supported by the opencodex Cursor transport.",
|
|
189
|
+
}),
|
|
190
|
+
},
|
|
187
191
|
}))];
|
|
188
192
|
}
|
|
189
|
-
|
|
193
|
+
throw new Error(`Unsupported Cursor native exec case: ${execCase ?? "unknown"}`);
|
|
190
194
|
}
|
|
191
195
|
|
|
192
196
|
|
|
@@ -68,8 +68,7 @@ export async function runCursorTurnWithRetry(
|
|
|
68
68
|
signal: AbortSignal | undefined,
|
|
69
69
|
onEvent: (message: CursorServerMessage, transport: CursorTransport) => void,
|
|
70
70
|
): Promise<void> {
|
|
71
|
-
let
|
|
72
|
-
for (let attempt = 0; attempt < CURSOR_RETRY_ATTEMPTS; attempt++) {
|
|
71
|
+
for (let attempt = 0; ; attempt++) {
|
|
73
72
|
if (signal?.aborted) throw abortError(signal);
|
|
74
73
|
const transport = makeTransport(input);
|
|
75
74
|
let emittedAny = false;
|
|
@@ -80,7 +79,6 @@ export async function runCursorTurnWithRetry(
|
|
|
80
79
|
}
|
|
81
80
|
return;
|
|
82
81
|
} catch (err) {
|
|
83
|
-
lastError = err;
|
|
84
82
|
const canRetry =
|
|
85
83
|
!emittedAny &&
|
|
86
84
|
attempt < CURSOR_RETRY_ATTEMPTS - 1 &&
|
|
@@ -108,7 +106,6 @@ export async function runCursorTurnWithRetry(
|
|
|
108
106
|
await transport.close?.();
|
|
109
107
|
}
|
|
110
108
|
}
|
|
111
|
-
throw lastError ?? new Error("Cursor transport failed");
|
|
112
109
|
}
|
|
113
110
|
|
|
114
111
|
export type { CursorTransportFactory };
|
package/src/adapters/google.ts
CHANGED
|
@@ -231,7 +231,10 @@ export function createGoogleAdapter(provider: OcxProviderConfig): ProviderAdapte
|
|
|
231
231
|
|
|
232
232
|
if (provider.googleMode === "cloud-code-assist") {
|
|
233
233
|
// Google Antigravity (Cloud Code Assist): wrap the flat Gemini body in the CCA envelope.
|
|
234
|
-
const
|
|
234
|
+
const token = provider.apiKey?.trim();
|
|
235
|
+
if (!token) throw new Error("google-antigravity oauth token missing — run ocx login google-antigravity");
|
|
236
|
+
const base = provider.baseUrl?.trim();
|
|
237
|
+
if (!base) throw new Error("google-antigravity requires a non-empty baseUrl");
|
|
235
238
|
const url = `${base}/v1internal:${method}${streamParam}`;
|
|
236
239
|
const project = provider.project;
|
|
237
240
|
if (!project) throw new Error("Antigravity requires a discovered Cloud Code Assist project id (re-run `ocx login google-antigravity`).");
|
|
@@ -271,7 +274,7 @@ export function createGoogleAdapter(provider: OcxProviderConfig): ProviderAdapte
|
|
|
271
274
|
request,
|
|
272
275
|
};
|
|
273
276
|
headers["User-Agent"] = ANTIGRAVITY_REQUEST_UA;
|
|
274
|
-
|
|
277
|
+
headers["Authorization"] = `Bearer ${token}`;
|
|
275
278
|
return { url, method: "POST", headers, body: JSON.stringify(envelope) };
|
|
276
279
|
}
|
|
277
280
|
|
|
@@ -296,7 +299,9 @@ export function createGoogleAdapter(provider: OcxProviderConfig): ProviderAdapte
|
|
|
296
299
|
|
|
297
300
|
// ai-studio (default): Generative Language API + x-goog-api-key.
|
|
298
301
|
const url = `${provider.baseUrl}/v1beta/models/${parsed.modelId}:${method}${streamParam}`;
|
|
299
|
-
|
|
302
|
+
const apiKey = provider.apiKey?.trim();
|
|
303
|
+
if (!apiKey) throw new Error("google (AI Studio) requires a non-empty API key");
|
|
304
|
+
headers["x-goog-api-key"] = apiKey;
|
|
300
305
|
|
|
301
306
|
return { url, method: "POST", headers, body: JSON.stringify(body) };
|
|
302
307
|
},
|
|
@@ -345,9 +350,15 @@ export function createGoogleAdapter(provider: OcxProviderConfig): ProviderAdapte
|
|
|
345
350
|
}
|
|
346
351
|
|
|
347
352
|
// Antigravity (CCA) nests the standard Gemini payload under `response`.
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
353
|
+
let root = chunk;
|
|
354
|
+
if (provider.googleMode === "cloud-code-assist") {
|
|
355
|
+
const wrapped = chunk.response;
|
|
356
|
+
if (!wrapped || typeof wrapped !== "object" || Array.isArray(wrapped)) {
|
|
357
|
+
yield { type: "error", message: "google-antigravity response missing response wrapper" };
|
|
358
|
+
return;
|
|
359
|
+
}
|
|
360
|
+
root = wrapped as Record<string, unknown>;
|
|
361
|
+
}
|
|
351
362
|
// usageMetadata is a top-level field independent of candidates; read it BEFORE the
|
|
352
363
|
// candidates guard so a usage-only final chunk is not dropped.
|
|
353
364
|
const usageMeta = root.usageMetadata as Record<string, number> | undefined;
|
|
@@ -397,13 +408,25 @@ export function createGoogleAdapter(provider: OcxProviderConfig): ProviderAdapte
|
|
|
397
408
|
|
|
398
409
|
async parseResponse(response: Response): Promise<AdapterEvent[]> {
|
|
399
410
|
const raw = await response.json() as Record<string, unknown>;
|
|
411
|
+
if (raw.error) {
|
|
412
|
+
const err = raw.error as { message?: string };
|
|
413
|
+
return [{ type: "error", message: err.message ?? "upstream error" }];
|
|
414
|
+
}
|
|
400
415
|
// Antigravity (CCA) nests the standard Gemini payload under `response`; unwrap it.
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
416
|
+
let json = raw;
|
|
417
|
+
if (provider.googleMode === "cloud-code-assist") {
|
|
418
|
+
const wrapped = raw.response;
|
|
419
|
+
if (!wrapped || typeof wrapped !== "object" || Array.isArray(wrapped)) {
|
|
420
|
+
return [{ type: "error", message: "google-antigravity response missing response wrapper" }];
|
|
421
|
+
}
|
|
422
|
+
json = wrapped as Record<string, unknown>;
|
|
423
|
+
}
|
|
404
424
|
const events: AdapterEvent[] = [];
|
|
405
425
|
|
|
406
426
|
const candidates = json.candidates as { content?: { parts?: { text?: string; functionCall?: { name: string; args: unknown } }[] }; finishReason?: string }[] | undefined;
|
|
427
|
+
if (!candidates?.length) {
|
|
428
|
+
return [{ type: "error", message: "google response contained no candidates" }];
|
|
429
|
+
}
|
|
407
430
|
let toolCallsStarted = 0;
|
|
408
431
|
if (candidates?.[0]?.content?.parts) {
|
|
409
432
|
// Non-streaming CCA: observe thoughtSignatures for the next turn, same as the stream path.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { AdapterFetchContext, AdapterRequest } from "./base";
|
|
2
2
|
import { safeKiroHttpErrorMessage } from "./kiro-errors";
|
|
3
|
-
import { abortError, sleepWithAbort } from "../lib/upstream-retry";
|
|
3
|
+
import { abortError, isConnectionResetError, sleepWithAbort } from "../lib/upstream-retry";
|
|
4
4
|
|
|
5
5
|
const KIRO_RETRY_ATTEMPTS = 3;
|
|
6
6
|
const KIRO_RETRY_BASE_MS = 250;
|
|
@@ -32,6 +32,10 @@ function signalWithAttemptTimeout(parent: AbortSignal | undefined, timeoutMs: nu
|
|
|
32
32
|
return parent ? AbortSignal.any([parent, timeout]) : timeout;
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
+
function retryableKiroFetchError(err: unknown): boolean {
|
|
36
|
+
return isConnectionResetError(err) || (err instanceof Error && err.name === "TimeoutError");
|
|
37
|
+
}
|
|
38
|
+
|
|
35
39
|
async function normalizeFinalKiroHttpError(res: Response): Promise<Response> {
|
|
36
40
|
if (res.ok) return res;
|
|
37
41
|
const payloadText = await res.clone().text().catch(() => "");
|
|
@@ -62,8 +66,8 @@ export async function fetchKiroWithRetry(request: AdapterRequest, ctx: AdapterFe
|
|
|
62
66
|
await sleepWithAbort(retryDelayMs(attempt, res.headers), ctx.abortSignal);
|
|
63
67
|
} catch (err) {
|
|
64
68
|
if (ctx.abortSignal?.aborted) throw err;
|
|
69
|
+
if (!retryableKiroFetchError(err) || attempt === KIRO_RETRY_ATTEMPTS - 1) throw err;
|
|
65
70
|
lastError = err;
|
|
66
|
-
if (attempt === KIRO_RETRY_ATTEMPTS - 1) throw err;
|
|
67
71
|
await sleepWithAbort(retryDelayMs(attempt), ctx.abortSignal);
|
|
68
72
|
}
|
|
69
73
|
}
|
package/src/adapters/kiro.ts
CHANGED
|
@@ -445,15 +445,17 @@ export async function* parseKiroStream(
|
|
|
445
445
|
break;
|
|
446
446
|
}
|
|
447
447
|
case "tool_stop": {
|
|
448
|
-
if (open) {
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
open = null;
|
|
452
|
-
yield { type: "error", message: kiroTruncationErrorMessage("incomplete tool input JSON") };
|
|
453
|
-
return;
|
|
454
|
-
}
|
|
455
|
-
yield* flushTool();
|
|
448
|
+
if (!open) {
|
|
449
|
+
yield { type: "error", message: "Kiro response protocol error: tool stop received without an open tool call" };
|
|
450
|
+
return;
|
|
456
451
|
}
|
|
452
|
+
const input = open.chunks.join("");
|
|
453
|
+
if (!isCompleteKiroToolInput(input)) {
|
|
454
|
+
open = null;
|
|
455
|
+
yield { type: "error", message: kiroTruncationErrorMessage("incomplete tool input JSON") };
|
|
456
|
+
return;
|
|
457
|
+
}
|
|
458
|
+
yield* flushTool();
|
|
457
459
|
break;
|
|
458
460
|
}
|
|
459
461
|
case "truncation":
|
|
@@ -496,11 +498,14 @@ export function createKiroAdapter(provider: OcxProviderConfig): ProviderAdapter
|
|
|
496
498
|
return {
|
|
497
499
|
name: "kiro",
|
|
498
500
|
buildRequest(parsed: OcxParsedRequest) {
|
|
501
|
+
if (typeof provider.apiKey !== "string" || provider.apiKey.trim() === "") {
|
|
502
|
+
throw new Error("kiro token missing — run ocx login kiro");
|
|
503
|
+
}
|
|
499
504
|
const region = resolveKiroApiRegion();
|
|
500
505
|
const profileArn = resolveKiroProfileArn();
|
|
501
506
|
const fp = fingerprint().slice(0, 64);
|
|
502
507
|
const headers: Record<string, string> = {
|
|
503
|
-
authorization: `Bearer ${provider.apiKey
|
|
508
|
+
authorization: `Bearer ${provider.apiKey}`,
|
|
504
509
|
"content-type": "application/x-amz-json-1.0",
|
|
505
510
|
accept: "application/vnd.amazon.eventstream",
|
|
506
511
|
"x-amz-target": AMZ_TARGET,
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import type { ProviderAdapter } from "./base";
|
|
2
|
-
import { debugDroppedFrame } from "../lib/debug";
|
|
3
2
|
import type { AdapterEvent, OcxAssistantMessage, OcxContentPart, OcxMessage, OcxParsedRequest, OcxProviderConfig, OcxTextContent, OcxThinkingContent, OcxToolCall, OcxUsage } from "../types";
|
|
4
3
|
import { isAllowedToolChoice, modelInList, namespacedToolName, resolveToolChoiceWireName, toolAllowedByChoice } from "../types";
|
|
5
4
|
import { mapReasoningEffort } from "../reasoning-effort";
|
|
@@ -7,11 +6,9 @@ import { contentPartsToText } from "./image";
|
|
|
7
6
|
import { neutralizeIdentity } from "./identity";
|
|
8
7
|
import { buildNonOpenAIToolCatalogNudgeForTools, shouldInjectNonOpenAIToolCatalogNudge } from "./tool-catalog-nudge";
|
|
9
8
|
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
// group from the wire model id so the bare id is sent. Applies to the
|
|
14
|
-
// openai-chat path only — the anthropic adapter keeps the suffix verbatim.
|
|
9
|
+
// Providers may opt into stripping one trailing "[...]" group from the wire model id.
|
|
10
|
+
// Z.AI needs this because its OpenAI path rejects glm-5.2[1m] with 400 code 1211;
|
|
11
|
+
// unflagged OpenAI-compatible providers and the Anthropic adapter keep ids verbatim.
|
|
15
12
|
export function stripBracketedModelSuffix(modelId: string): string {
|
|
16
13
|
return modelId.replace(/\[[^\]]*\]\s*$/, "");
|
|
17
14
|
}
|
|
@@ -125,7 +122,7 @@ function messagesToChatFormat(parsed: OcxParsedRequest, provider: OcxProviderCon
|
|
|
125
122
|
function safeToolName(name: string | undefined): string {
|
|
126
123
|
const raw = name && name.trim().length > 0 ? name : "tool_result";
|
|
127
124
|
const sanitized = raw.replace(/[^A-Za-z0-9_-]/g, "_");
|
|
128
|
-
return sanitized
|
|
125
|
+
return sanitized;
|
|
129
126
|
}
|
|
130
127
|
|
|
131
128
|
function toolsToChatFormat(parsed: OcxParsedRequest): unknown[] | undefined {
|
|
@@ -187,12 +184,17 @@ export function createOpenAIChatAdapter(provider: OcxProviderConfig): ProviderAd
|
|
|
187
184
|
name: "openai-chat",
|
|
188
185
|
|
|
189
186
|
buildRequest(parsed: OcxParsedRequest) {
|
|
187
|
+
const hasCredential = typeof provider.apiKey === "string" && provider.apiKey.trim().length > 0;
|
|
188
|
+
if ((provider.authMode === "key" || provider.authMode === "oauth") && !provider.keyOptional && !hasCredential) {
|
|
189
|
+
throw new Error(`${provider.adapter} requires a non-empty credential (authMode: ${provider.authMode})`);
|
|
190
|
+
}
|
|
191
|
+
|
|
190
192
|
const messages = messagesToChatFormat(parsed, provider);
|
|
191
193
|
const tools = toolsToChatFormat(parsed);
|
|
192
194
|
const toolChoice = toolChoiceToChatFormat(parsed.options.toolChoice, parsed.context.tools);
|
|
193
195
|
|
|
194
196
|
const body: Record<string, unknown> = {
|
|
195
|
-
model: stripBracketedModelSuffix(parsed.modelId),
|
|
197
|
+
model: provider.modelSuffixBracketStrip ? stripBracketedModelSuffix(parsed.modelId) : parsed.modelId,
|
|
196
198
|
messages,
|
|
197
199
|
stream: parsed.stream,
|
|
198
200
|
};
|
|
@@ -247,7 +249,7 @@ export function createOpenAIChatAdapter(provider: OcxProviderConfig): ProviderAd
|
|
|
247
249
|
|
|
248
250
|
const url = `${provider.baseUrl}/chat/completions`;
|
|
249
251
|
const headers: Record<string, string> = { "Content-Type": "application/json" };
|
|
250
|
-
if (
|
|
252
|
+
if (hasCredential) headers["Authorization"] = `Bearer ${provider.apiKey}`;
|
|
251
253
|
if (provider.headers) Object.assign(headers, provider.headers);
|
|
252
254
|
|
|
253
255
|
return { url, method: "POST", headers, body: JSON.stringify(body) };
|
|
@@ -306,8 +308,8 @@ export function createOpenAIChatAdapter(provider: OcxProviderConfig): ProviderAd
|
|
|
306
308
|
try {
|
|
307
309
|
chunk = JSON.parse(payload) as Record<string, unknown>;
|
|
308
310
|
} catch {
|
|
309
|
-
|
|
310
|
-
return "
|
|
311
|
+
yield { type: "error", message: "malformed upstream SSE data frame" };
|
|
312
|
+
return "terminate";
|
|
311
313
|
}
|
|
312
314
|
|
|
313
315
|
// A 200/OK chat-completions stream may carry an inline provider error envelope
|
|
@@ -416,25 +418,33 @@ export function createOpenAIChatAdapter(provider: OcxProviderConfig): ProviderAd
|
|
|
416
418
|
|
|
417
419
|
async parseResponse(response: Response): Promise<AdapterEvent[]> {
|
|
418
420
|
const json = await response.json() as Record<string, unknown>;
|
|
421
|
+
if (json.error) {
|
|
422
|
+
const upstreamError = json.error as { message?: unknown };
|
|
423
|
+
return [{
|
|
424
|
+
type: "error",
|
|
425
|
+
message: typeof upstreamError.message === "string" ? upstreamError.message : "upstream error",
|
|
426
|
+
}];
|
|
427
|
+
}
|
|
428
|
+
|
|
419
429
|
const events: AdapterEvent[] = [];
|
|
420
430
|
const choices = json.choices as { message?: Record<string, unknown> }[] | undefined;
|
|
421
|
-
if (choices
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
}
|
|
431
|
+
if (!Array.isArray(choices) || choices.length === 0 || !choices[0].message) {
|
|
432
|
+
return [{ type: "error", message: "upstream response contained no choices" }];
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
const msg = choices[0].message;
|
|
436
|
+
if (typeof msg.content === "string") {
|
|
437
|
+
events.push({ type: "text_delta", text: msg.content });
|
|
438
|
+
}
|
|
439
|
+
if (typeof msg.reasoning_content === "string" && msg.reasoning_content.length > 0) {
|
|
440
|
+
events.push({ type: "reasoning_raw_delta", text: msg.reasoning_content });
|
|
441
|
+
}
|
|
442
|
+
const toolCalls = msg.tool_calls as { id: string; function: { name: string; arguments: string } }[] | undefined;
|
|
443
|
+
if (toolCalls) {
|
|
444
|
+
for (const tc of toolCalls) {
|
|
445
|
+
events.push({ type: "tool_call_start", id: tc.id, name: tc.function.name });
|
|
446
|
+
events.push({ type: "tool_call_delta", arguments: tc.function.arguments });
|
|
447
|
+
events.push({ type: "tool_call_end" });
|
|
438
448
|
}
|
|
439
449
|
}
|
|
440
450
|
const usage = json.usage as Record<string, unknown> | undefined;
|