@bitkyc08/opencodex 2.11.0 → 2.11.1
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-Bk-PN-70.css → index-BA1dgo4q.css} +1 -1
- package/gui/dist/assets/index-DDZpgzKk.js +70 -0
- package/gui/dist/index.html +2 -2
- package/package.json +6 -4
- package/src/adapters/anthropic.ts +10 -2
- package/src/adapters/cursor/native-exec-common.ts +6 -2
- package/src/adapters/google-antigravity-replay.ts +3 -2
- package/src/adapters/google-antigravity-wire.ts +38 -6
- package/src/adapters/google.ts +57 -10
- package/src/adapters/openai-chat.ts +19 -2
- package/src/claude/model-info.ts +1 -1
- package/src/cli/account-auth.ts +3 -1
- package/src/cli/account-catalog-refresh.ts +14 -0
- package/src/cli/account-extended.ts +15 -1
- package/src/cli/codex-shim-readiness.ts +69 -0
- package/src/cli/combo.ts +5 -0
- package/src/cli/config-command.ts +33 -4
- package/src/cli/doctor.ts +21 -0
- package/src/cli/export-command.ts +6 -6
- package/src/cli/help.ts +6 -6
- package/src/cli/index.ts +22 -5
- package/src/cli/models-runtime.ts +13 -1
- package/src/cli/provider.ts +7 -0
- package/src/clients/config-export.ts +67 -5
- package/src/codex/account-lifecycle.ts +99 -10
- package/src/codex/auth-api.ts +222 -34
- package/src/codex/catalog/account-models.ts +9 -4
- package/src/codex/catalog/aggregation.ts +41 -8
- package/src/codex/catalog/bundled.ts +54 -22
- package/src/codex/catalog/effort.ts +47 -20
- package/src/codex/catalog/kinds.ts +2 -0
- package/src/codex/catalog/metadata.ts +70 -18
- package/src/codex/catalog/native-models.ts +7 -0
- package/src/codex/catalog/parsing.ts +29 -11
- package/src/codex/catalog/provider-fetch.ts +335 -50
- package/src/codex/catalog/sync.ts +588 -126
- package/src/codex/catalog-refresh-status.ts +87 -0
- package/src/codex/catalog-write-serialization.ts +2 -1
- package/src/codex/catalog.ts +4 -3
- package/src/codex/convergence-types.ts +1 -1
- package/src/codex/convergence.ts +190 -52
- package/src/codex/custom-model-catalog-migration.ts +176 -0
- package/src/codex/features.ts +79 -1
- package/src/codex/history-job.ts +81 -1
- package/src/codex/history-lock.ts +2 -1
- package/src/codex/history-provider.ts +4 -3
- package/src/codex/inject.ts +56 -12
- package/src/codex/model-cache.ts +50 -10
- package/src/codex/transition-state.ts +10 -2
- package/src/codex/user-identity.ts +110 -2
- package/src/combos/index.ts +3 -0
- package/src/combos/types.ts +75 -9
- package/src/config.ts +26 -12
- package/src/generated/{jawcode-model-metadata.ts → model-metadata.ts} +12 -10
- package/src/integrations/registry.ts +7 -0
- package/src/integrations/writer.ts +1 -1
- package/src/lib/pinned-http.ts +40 -9
- package/src/lib/process-control.ts +4 -1
- package/src/lib/provider-outbound.ts +42 -9
- package/src/oauth/cursor.ts +25 -5
- package/src/oauth/index.ts +59 -45
- package/src/providers/antigravity-models.ts +115 -3
- package/src/providers/context-cap.ts +13 -5
- package/src/providers/model-discovery-limits.ts +16 -0
- package/src/providers/model-discovery.ts +14 -15
- package/src/providers/provider-id-rewrite.ts +29 -0
- package/src/providers/quota.ts +93 -19
- package/src/providers/registry.ts +16 -5
- package/src/providers/slug-codec.ts +13 -6
- package/src/reasoning-effort.ts +22 -0
- package/src/router.ts +0 -20
- package/src/routing/history/indexer.ts +54 -39
- package/src/routing/quota.ts +77 -56
- package/src/server/index.ts +32 -7
- package/src/server/management/combo-routes.ts +43 -19
- package/src/server/management/config-routes.ts +115 -20
- package/src/server/management/model-routes.ts +10 -7
- package/src/server/management/model-rows.ts +6 -2
- package/src/server/management/oauth-account-routes.ts +12 -0
- package/src/server/management/provider-routes.ts +114 -40
- package/src/server/management/routing-profile-routes.ts +0 -22
- package/src/server/management-api.ts +7 -25
- package/src/server/responses/core.ts +3 -3
- package/src/server/responses/policy-fallback.ts +152 -0
- package/src/server/responses.ts +3 -2
- package/src/types.ts +15 -2
- package/src/usage/cost.ts +0 -0
- package/src/vision/describe.ts +3 -1
- package/src/vision/index.ts +17 -6
- package/src/vision/reasoning.ts +55 -0
- package/src/web-search/parse.ts +15 -3
- package/gui/dist/assets/index-BynIEIV-.js +0 -70
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-DDZpgzKk.js"></script>
|
|
20
|
+
<link rel="stylesheet" crossorigin href="/assets/index-BA1dgo4q.css">
|
|
21
21
|
</head>
|
|
22
22
|
<body>
|
|
23
23
|
<div id="root"></div>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bitkyc08/opencodex",
|
|
3
|
-
"version": "2.11.
|
|
3
|
+
"version": "2.11.1",
|
|
4
4
|
"description": "Universal provider proxy for OpenAI Codex & Claude Code — use any LLM with Codex CLI/App/SDK and Claude Code",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./bin/package-main.mjs",
|
|
@@ -42,7 +42,7 @@
|
|
|
42
42
|
"typecheck": "bun x tsc --noEmit",
|
|
43
43
|
"audit:high": "bun audit --audit-level=high && cd gui && bun audit --audit-level=high",
|
|
44
44
|
"privacy:scan": "bun scripts/privacy-scan.ts",
|
|
45
|
-
"generate:
|
|
45
|
+
"generate:model-metadata": "bun scripts/generate-model-metadata.ts",
|
|
46
46
|
"build:gui": "cd gui && bun install --frozen-lockfile && bun run build && cd .. && bun run prepare:package",
|
|
47
47
|
"prepare:package": "bun scripts/prepare-package.ts",
|
|
48
48
|
"prepack": "bun run prepare:package",
|
|
@@ -59,7 +59,7 @@
|
|
|
59
59
|
"setup:hooks": "bun scripts/setup-hooks.ts"
|
|
60
60
|
},
|
|
61
61
|
"dependencies": {
|
|
62
|
-
"@bufbuild/protobuf": "^2.
|
|
62
|
+
"@bufbuild/protobuf": "^2.13.0",
|
|
63
63
|
"@modelcontextprotocol/sdk": "^1.30.0",
|
|
64
64
|
"@napi-rs/keyring": "1.3.0",
|
|
65
65
|
"bun": "1.3.14",
|
|
@@ -67,10 +67,12 @@
|
|
|
67
67
|
},
|
|
68
68
|
"devDependencies": {
|
|
69
69
|
"@types/bun": "1.3.14",
|
|
70
|
-
"typescript": "
|
|
70
|
+
"typescript": "7.0.2"
|
|
71
71
|
},
|
|
72
72
|
"overrides": {
|
|
73
|
+
"@hono/node-server": "2.1.0",
|
|
73
74
|
"fast-uri": "^3.1.5",
|
|
75
|
+
"hono": "4.13.1",
|
|
74
76
|
"ip-address": "^10.4.0"
|
|
75
77
|
},
|
|
76
78
|
"keywords": [
|
|
@@ -984,13 +984,21 @@ export function createAnthropicAdapter(provider: OcxProviderConfig, cacheRetenti
|
|
|
984
984
|
const payload = record.data.trim();
|
|
985
985
|
if (!payload) continue;
|
|
986
986
|
|
|
987
|
-
let
|
|
987
|
+
let parsed: unknown;
|
|
988
988
|
try {
|
|
989
|
-
|
|
989
|
+
parsed = JSON.parse(payload);
|
|
990
990
|
} catch {
|
|
991
991
|
debugDroppedFrame("anthropic", payload);
|
|
992
992
|
continue;
|
|
993
993
|
}
|
|
994
|
+
// `JSON.parse("null")` returns null instead of throwing, so the catch above cannot cover
|
|
995
|
+
// it and the `data.type` read below crashed the stream. Drop a non-record frame the same
|
|
996
|
+
// way an unparseable one is dropped, so the message_stop check still governs the outcome.
|
|
997
|
+
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
998
|
+
debugDroppedFrame("anthropic", payload);
|
|
999
|
+
continue;
|
|
1000
|
+
}
|
|
1001
|
+
const data = parsed as Record<string, unknown>;
|
|
994
1002
|
|
|
995
1003
|
switch (record.event || data.type) {
|
|
996
1004
|
case "message_start": {
|
|
@@ -15,14 +15,18 @@ export function clientBytes(message: Parameters<typeof create<typeof AgentClient
|
|
|
15
15
|
return toBinary(AgentClientMessageSchema, create(AgentClientMessageSchema, message));
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
-
export function execBytes
|
|
18
|
+
export function execBytes<K extends ExecClientMessage["message"]["case"]>(
|
|
19
|
+
execMsg: ExecServerMessage,
|
|
20
|
+
messageCase: K,
|
|
21
|
+
value: Extract<ExecClientMessage["message"], { case: K }>["value"],
|
|
22
|
+
): Uint8Array {
|
|
19
23
|
return clientBytes({
|
|
20
24
|
message: {
|
|
21
25
|
case: "execClientMessage",
|
|
22
26
|
value: create(ExecClientMessageSchema, {
|
|
23
27
|
id: execMsg.id,
|
|
24
28
|
execId: execMsg.execId,
|
|
25
|
-
message: { case: messageCase, value
|
|
29
|
+
message: { case: messageCase, value } as ExecClientMessage["message"],
|
|
26
30
|
}),
|
|
27
31
|
},
|
|
28
32
|
});
|
|
@@ -2,14 +2,15 @@ import { createHash } from "node:crypto";
|
|
|
2
2
|
import { enforceAppOwnedMemoryBudget } from "../lib/app-owned-memory";
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
|
-
*
|
|
5
|
+
* Google-family thoughtSignature reasoning-replay cache.
|
|
6
6
|
*
|
|
7
7
|
* Gemini-3 interleaved thinking is stateless upstream: each model content part carries a
|
|
8
8
|
* `thoughtSignature` that MUST be echoed back on the matching part in the next request, or the
|
|
9
9
|
* upstream rejects the turn (HTTP 400). We observe signatures on the response stream, cache them
|
|
10
10
|
* per `model + session`, and re-inject them into the outgoing `request.contents` on the next turn.
|
|
11
11
|
*
|
|
12
|
-
* Mirrors CLIProxyAPI `internal/runtime/executor/antigravity_reasoning_replay.go
|
|
12
|
+
* Mirrors CLIProxyAPI `internal/runtime/executor/antigravity_reasoning_replay.go` and is also used
|
|
13
|
+
* by Vertex with a transport/project/location-prefixed model identity. Gemini-only;
|
|
13
14
|
* Claude-on-Antigravity uses inline signature sanitization instead (see google-antigravity-wire).
|
|
14
15
|
*/
|
|
15
16
|
|
|
@@ -51,18 +51,50 @@ function firstUserText(parsed: OcxParsedRequest): string | undefined {
|
|
|
51
51
|
* 0x7FFFFFFFFFFFFFFF, prefixed with "-". Falls back to a random "-<digits>" id when there is no text.
|
|
52
52
|
*/
|
|
53
53
|
export function antigravitySessionId(parsed: OcxParsedRequest): string {
|
|
54
|
-
//
|
|
55
|
-
// turn
|
|
56
|
-
//
|
|
57
|
-
//
|
|
58
|
-
//
|
|
59
|
-
|
|
54
|
+
// The id must be IDENTICAL on turn N and turn N+1: the replay cache observes thought signatures
|
|
55
|
+
// on turn N's response and re-injects them on turn N+1's request, so a changing id loses the
|
|
56
|
+
// association entirely. (A *shared* id does not — signatures are keyed on functionCall identity,
|
|
57
|
+
// name+args, so cross-conversation collisions stay harmless. Instability is the failure mode.)
|
|
58
|
+
//
|
|
59
|
+
// First-user text does not guarantee that. It is stable only while the first user message
|
|
60
|
+
// survives verbatim in what this adapter sees, and Codex compacts, summarises, and trims long
|
|
61
|
+
// histories — at which point the anchor changes mid-conversation (#1295's sibling, #1297).
|
|
62
|
+
//
|
|
63
|
+
// `_clientThreadId` is Codex's own thread identity from `x-codex-parent-thread-id`. It survives
|
|
64
|
+
// compaction because it does not depend on message content, which is the property first-user
|
|
65
|
+
// text fails to provide. Deliberately NOT `promptCacheKey`: that is arbitrary Responses input
|
|
66
|
+
// and is explicitly shared across conversations for some clients
|
|
67
|
+
// (`adapters/cursor/request-builder.ts`), so it identifies a cache cohort, not a conversation.
|
|
68
|
+
//
|
|
69
|
+
// What is NOT claimed: that Google treats this id as anything but opaque, or that upstream
|
|
70
|
+
// signatures are not session-bound. Only the local association is verified here.
|
|
71
|
+
//
|
|
72
|
+
// Clients that send no thread header keep the text anchor and its instability; this is a scoped
|
|
73
|
+
// repair, not a universal one.
|
|
74
|
+
const text = clientThreadAnchor(parsed) ?? firstUserText(parsed);
|
|
60
75
|
if (!text) return `-${Math.floor(Math.random() * 9e18).toString()}`;
|
|
61
76
|
const digest = createHash("sha256").update(text, "utf8").digest();
|
|
62
77
|
const masked = digest.readBigUInt64BE(0) & 0x7fffffffffffffffn;
|
|
63
78
|
return `-${masked.toString()}`;
|
|
64
79
|
}
|
|
65
80
|
|
|
81
|
+
/**
|
|
82
|
+
* Codex's stable client thread id, prefixed before hashing so it does not collide with a first
|
|
83
|
+
* user message equal to the bare thread id.
|
|
84
|
+
*
|
|
85
|
+
* This is a prefix, not full domain separation: a textless-anchor request whose first message is
|
|
86
|
+
* literally `codex-thread:<id>` still hashes the same preimage. Tagging BOTH anchor classes would
|
|
87
|
+
* separate them completely, but it would also change every existing text-derived id — a
|
|
88
|
+
* Google-visible wire value for live conversations — which is a larger blast radius than the
|
|
89
|
+
* collision it removes. A collision is harmless here anyway: the replay cache keys signatures on
|
|
90
|
+
* functionCall identity (name+args), so a shared id does not misattribute them. Instability, not
|
|
91
|
+
* collision, is the failure mode this function exists to prevent.
|
|
92
|
+
*/
|
|
93
|
+
function clientThreadAnchor(parsed: OcxParsedRequest): string | undefined {
|
|
94
|
+
const threadId = parsed._clientThreadId?.trim();
|
|
95
|
+
return threadId ? `codex-thread:${threadId}` : undefined;
|
|
96
|
+
}
|
|
97
|
+
|
|
66
98
|
/** A Gemini content part as it appears in an Antigravity request body. */
|
|
67
99
|
interface GeminiPart {
|
|
68
100
|
thought?: boolean;
|
package/src/adapters/google.ts
CHANGED
|
@@ -51,6 +51,13 @@ function resolveVertexApiKey(optKey?: string): string | undefined {
|
|
|
51
51
|
return realKey || process.env.GOOGLE_CLOUD_API_KEY;
|
|
52
52
|
}
|
|
53
53
|
|
|
54
|
+
/** Prefer Codex's stable opaque thread key; retain the existing deterministic fallback for clients
|
|
55
|
+
* that omit it. The replay store hashes this value and never retains the raw session identifier. */
|
|
56
|
+
function vertexReplaySessionId(parsed: OcxParsedRequest): string {
|
|
57
|
+
const promptCacheKey = parsed.options.promptCacheKey?.trim();
|
|
58
|
+
return promptCacheKey || antigravitySessionId(parsed);
|
|
59
|
+
}
|
|
60
|
+
|
|
54
61
|
/**
|
|
55
62
|
* Stable tool-call id for the Gemini wire `functionCall.id` / `functionResponse.id` fields.
|
|
56
63
|
*
|
|
@@ -302,6 +309,11 @@ export function createGoogleAdapter(provider: OcxProviderConfig): ProviderAdapte
|
|
|
302
309
|
// can stash the CCA model/session for parseStream's reasoning-replay observation.
|
|
303
310
|
let antigravityModel: string | undefined;
|
|
304
311
|
let antigravitySession: string | undefined;
|
|
312
|
+
// Vertex returns the same opaque Gemini thought signatures as CCA, but its replay namespace
|
|
313
|
+
// must stay transport-scoped: a signature minted by one Google backend must never be sent to
|
|
314
|
+
// another merely because the public model id and first prompt happen to match.
|
|
315
|
+
let vertexReplayModel: string | undefined;
|
|
316
|
+
let vertexReplaySession: string | undefined;
|
|
305
317
|
let restoreGoogleToolName = (name: string): string => name;
|
|
306
318
|
return {
|
|
307
319
|
name: "google",
|
|
@@ -436,6 +448,20 @@ export function createGoogleAdapter(provider: OcxProviderConfig): ProviderAdapte
|
|
|
436
448
|
if (provider.googleMode === "vertex") {
|
|
437
449
|
const compiled = compileGoogleWireBody(body);
|
|
438
450
|
restoreGoogleToolName = compiled.restoreToolName;
|
|
451
|
+
const vertexProject = provider.project || process.env.GOOGLE_CLOUD_PROJECT || process.env.GCLOUD_PROJECT || "api-key";
|
|
452
|
+
const vertexLocation = provider.location || process.env.GOOGLE_CLOUD_LOCATION || "global";
|
|
453
|
+
vertexReplayModel = `vertex:${vertexProject}:${vertexLocation}:${parsed.modelId}`;
|
|
454
|
+
vertexReplaySession = vertexReplaySessionId(parsed);
|
|
455
|
+
// Compile names before replay so the cache matches the exact provider-visible
|
|
456
|
+
// functionCall identity. This is the same bounded TTL/LRU store used by CCA, with the
|
|
457
|
+
// transport prefix above preventing cross-backend signature reuse (#1254).
|
|
458
|
+
if (Array.isArray((compiled.body as { contents?: unknown[] }).contents)) {
|
|
459
|
+
applyAntigravityReplay(
|
|
460
|
+
vertexReplayModel,
|
|
461
|
+
vertexReplaySession,
|
|
462
|
+
(compiled.body as { contents: unknown[] }).contents,
|
|
463
|
+
);
|
|
464
|
+
}
|
|
439
465
|
// Vertex AI: project/location endpoint with GCP ADC, or x-goog-api-key fast path.
|
|
440
466
|
const apiKey = resolveVertexApiKey(provider.apiKey);
|
|
441
467
|
if (apiKey) {
|
|
@@ -497,13 +523,23 @@ export function createGoogleAdapter(provider: OcxProviderConfig): ProviderAdapte
|
|
|
497
523
|
}
|
|
498
524
|
let emittedContentEvent = false;
|
|
499
525
|
|
|
500
|
-
let
|
|
526
|
+
let parsed: unknown;
|
|
501
527
|
try {
|
|
502
|
-
|
|
528
|
+
parsed = JSON.parse(payload);
|
|
503
529
|
} catch {
|
|
504
530
|
yield { type: "error", message: "malformed upstream SSE data frame" };
|
|
505
531
|
return "terminate";
|
|
506
532
|
}
|
|
533
|
+
// `JSON.parse("null")` returns null rather than throwing, so the catch above cannot cover
|
|
534
|
+
// it and the `chunk.error` read below crashed the stream (see openai-chat.ts). Skip such a
|
|
535
|
+
// frame rather than terminating, for the reason given there: it is padding between real
|
|
536
|
+
// frames, not a broken stream. Deliberately returns BEFORE `sawAnyFrame`, so a stream made
|
|
537
|
+
// only of non-record frames still fails the terminal-signal check below instead of
|
|
538
|
+
// completing empty. An unparseable frame stays terminal.
|
|
539
|
+
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
540
|
+
return "continue";
|
|
541
|
+
}
|
|
542
|
+
const chunk = parsed as Record<string, unknown>;
|
|
507
543
|
sawAnyFrame = true;
|
|
508
544
|
|
|
509
545
|
// Inline provider error inside a 200 stream → terminal error (see openai-chat.ts).
|
|
@@ -511,9 +547,12 @@ export function createGoogleAdapter(provider: OcxProviderConfig): ProviderAdapte
|
|
|
511
547
|
const err = chunk.error as { message?: string } | undefined;
|
|
512
548
|
// Clear-on-invalid: a signature rejection means our replayed thoughtSignatures are stale.
|
|
513
549
|
// Drop the cache entry so the next turn starts clean instead of re-injecting a bad sig.
|
|
514
|
-
|
|
550
|
+
const replayModel = provider.googleMode === "cloud-code-assist" ? antigravityModel : vertexReplayModel;
|
|
551
|
+
const replaySession = provider.googleMode === "cloud-code-assist" ? antigravitySession : vertexReplaySession;
|
|
552
|
+
if ((provider.googleMode === "cloud-code-assist" || provider.googleMode === "vertex")
|
|
553
|
+
&& replayModel && replaySession
|
|
515
554
|
&& /signature|invalid_argument|invalid argument/i.test(err?.message ?? "")) {
|
|
516
|
-
clearAntigravityReplay(
|
|
555
|
+
clearAntigravityReplay(replayModel, replaySession);
|
|
517
556
|
}
|
|
518
557
|
yield { type: "error", message: err?.message ?? "upstream error" };
|
|
519
558
|
return "terminate";
|
|
@@ -547,9 +586,13 @@ export function createGoogleAdapter(provider: OcxProviderConfig): ProviderAdapte
|
|
|
547
586
|
}
|
|
548
587
|
|
|
549
588
|
const parts = candidates[0].content?.parts as { text?: string; functionCall?: { name: string; args: unknown } }[] | undefined;
|
|
550
|
-
//
|
|
551
|
-
|
|
552
|
-
|
|
589
|
+
// Record Gemini thought signatures for the next stateless tool-result turn. Vertex and
|
|
590
|
+
// Antigravity use separate model namespaces so opaque provider state cannot cross routes.
|
|
591
|
+
const replayModel = provider.googleMode === "cloud-code-assist" ? antigravityModel : vertexReplayModel;
|
|
592
|
+
const replaySession = provider.googleMode === "cloud-code-assist" ? antigravitySession : vertexReplaySession;
|
|
593
|
+
if ((provider.googleMode === "cloud-code-assist" || provider.googleMode === "vertex")
|
|
594
|
+
&& parts && replayModel && replaySession) {
|
|
595
|
+
observeAntigravityReplay(replayModel, replaySession, parts as unknown[]);
|
|
553
596
|
}
|
|
554
597
|
if (parts) {
|
|
555
598
|
for (const part of parts) {
|
|
@@ -764,9 +807,13 @@ export function createGoogleAdapter(provider: OcxProviderConfig): ProviderAdapte
|
|
|
764
807
|
let toolCallsStarted = 0;
|
|
765
808
|
const imageBudget = createImageBudget();
|
|
766
809
|
if (candidates?.[0]?.content?.parts) {
|
|
767
|
-
// Non-streaming
|
|
768
|
-
|
|
769
|
-
|
|
810
|
+
// Non-streaming Google-family response: observe thought signatures for the next turn,
|
|
811
|
+
// using the same transport-scoped namespace as the streaming path.
|
|
812
|
+
const replayModel = provider.googleMode === "cloud-code-assist" ? antigravityModel : vertexReplayModel;
|
|
813
|
+
const replaySession = provider.googleMode === "cloud-code-assist" ? antigravitySession : vertexReplaySession;
|
|
814
|
+
if ((provider.googleMode === "cloud-code-assist" || provider.googleMode === "vertex")
|
|
815
|
+
&& replayModel && replaySession) {
|
|
816
|
+
observeAntigravityReplay(replayModel, replaySession, candidates[0].content.parts as unknown[]);
|
|
770
817
|
}
|
|
771
818
|
for (const part of candidates[0].content.parts) {
|
|
772
819
|
if (part.text) events.push({ type: "text_delta", text: part.text });
|
|
@@ -951,6 +951,9 @@ export function createOpenAIChatAdapter(provider: OcxProviderConfig): ProviderAd
|
|
|
951
951
|
const rawPayload = sseFieldValue(line, "data");
|
|
952
952
|
if (rawPayload === null) return "continue";
|
|
953
953
|
const payload = rawPayload.trim();
|
|
954
|
+
// A bare `data:` line carries nothing (heartbeat-style keep-alive on some gateways);
|
|
955
|
+
// it is not a malformed frame, just nothing to parse.
|
|
956
|
+
if (payload.length === 0) return "continue";
|
|
954
957
|
if (payload === "[DONE]") {
|
|
955
958
|
yield* flushToolCalls();
|
|
956
959
|
const stopReason = stopReasonFor(finishReason);
|
|
@@ -958,13 +961,27 @@ export function createOpenAIChatAdapter(provider: OcxProviderConfig): ProviderAd
|
|
|
958
961
|
return "terminate";
|
|
959
962
|
}
|
|
960
963
|
|
|
961
|
-
let
|
|
964
|
+
let parsed: unknown;
|
|
962
965
|
try {
|
|
963
|
-
|
|
966
|
+
parsed = JSON.parse(payload);
|
|
964
967
|
} catch {
|
|
965
968
|
yield { type: "error", message: "malformed upstream SSE data frame" };
|
|
966
969
|
return "terminate";
|
|
967
970
|
}
|
|
971
|
+
// Validate the shape instead of asserting it. `JSON.parse` yields a value, not necessarily
|
|
972
|
+
// an object — `JSON.parse("null")` returns null without throwing, so the catch above never
|
|
973
|
+
// sees it and the `chunk.error` read below crashed the stream mid-flight.
|
|
974
|
+
//
|
|
975
|
+
// Skip rather than terminate: `data: null` is emitted as a benign padding frame BETWEEN
|
|
976
|
+
// content deltas by real OpenAI-compatible routes (issue #1219), so failing here would
|
|
977
|
+
// discard the finish_reason chunk and [DONE] still in flight and turn a healthy response
|
|
978
|
+
// into a failed turn. Skipping cannot mask a genuinely broken stream — a stream carrying
|
|
979
|
+
// only such frames still sets neither finishReason nor sawUserFacingOutput and so trips
|
|
980
|
+
// the EOF truncation guard below. An unparseable frame stays terminal.
|
|
981
|
+
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
982
|
+
return "continue";
|
|
983
|
+
}
|
|
984
|
+
const chunk = parsed as Record<string, unknown>;
|
|
968
985
|
|
|
969
986
|
// A 200/OK chat-completions stream may carry an inline provider error envelope
|
|
970
987
|
// instead of a clean [DONE]. Surface it as a terminal error so the bridge emits a
|
package/src/claude/model-info.ts
CHANGED
|
@@ -134,7 +134,7 @@ export function buildAnthropicModelInfos(
|
|
|
134
134
|
const id = idStyle === "readable" ? claudeCodeNativeAlias(slug) : aliasForRoute("native", slug);
|
|
135
135
|
if (seen.has(id)) continue;
|
|
136
136
|
seen.add(id);
|
|
137
|
-
const info = modelInfo(id, `${slug} (native)`, nativeEffectiveLadder(slug), true);
|
|
137
|
+
const info = modelInfo(id, `${slug} (native)`, nativeEffectiveLadder(slug), true, nativeOpenAiContextWindow(slug));
|
|
138
138
|
out.push(info);
|
|
139
139
|
push1mVariant(info, nativeOpenAiContextWindow(slug));
|
|
140
140
|
}
|
package/src/cli/account-auth.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { writeSync } from "node:fs";
|
|
2
|
+
import { warnIfCodexCatalogRefreshPending } from "./account-catalog-refresh";
|
|
1
3
|
import {
|
|
2
4
|
CliUsageError,
|
|
3
5
|
printData,
|
|
@@ -11,7 +13,6 @@ import {
|
|
|
11
13
|
type CliStdin,
|
|
12
14
|
type RuntimeApiDeps,
|
|
13
15
|
} from "./runtime-api";
|
|
14
|
-
import { writeSync } from "node:fs";
|
|
15
16
|
|
|
16
17
|
/**
|
|
17
18
|
* Write the whole block to fd 1 synchronously (#1007). `console.log` can
|
|
@@ -127,6 +128,7 @@ async function login(argv: string[], deps: RuntimeApiDeps): Promise<void> {
|
|
|
127
128
|
);
|
|
128
129
|
if (state.status === "done") {
|
|
129
130
|
printData(state, wantsJson, [`Logged in${state.email ? ` as ${String(state.email)}` : ""}.`]);
|
|
131
|
+
if (!wantsJson) warnIfCodexCatalogRefreshPending(state);
|
|
130
132
|
return;
|
|
131
133
|
}
|
|
132
134
|
if (state.status === "error" || state.status === "expired") {
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export const CODEX_CATALOG_REFRESH_PENDING_WARNING =
|
|
2
|
+
"Warning: the account change was saved, but the Codex model catalog refresh is pending. Run 'ocx sync' to retry.";
|
|
3
|
+
|
|
4
|
+
/** Read only the public completion flag from an account mutation response. */
|
|
5
|
+
export function codexCatalogRefreshPending(value: unknown): boolean {
|
|
6
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) return false;
|
|
7
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, "catalogRefreshPending");
|
|
8
|
+
return descriptor !== undefined && "value" in descriptor && descriptor.value === true;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/** Print fixed recovery guidance without reflecting server or account details. */
|
|
12
|
+
export function warnIfCodexCatalogRefreshPending(value: unknown): void {
|
|
13
|
+
if (codexCatalogRefreshPending(value)) console.error(CODEX_CATALOG_REFRESH_PENDING_WARNING);
|
|
14
|
+
}
|
|
@@ -17,6 +17,10 @@ import {
|
|
|
17
17
|
type AccountDeps, type AccountStdin, type FamilyRows,
|
|
18
18
|
type ProviderQuotaDto, type ProviderQuotaReportDto,
|
|
19
19
|
} from "./account-api";
|
|
20
|
+
import {
|
|
21
|
+
codexCatalogRefreshPending,
|
|
22
|
+
warnIfCodexCatalogRefreshPending,
|
|
23
|
+
} from "./account-catalog-refresh";
|
|
20
24
|
|
|
21
25
|
const MAIN_ID = "__main__";
|
|
22
26
|
const AUTO_NOTE = "auto (no pin — lowest-usage account is selected per request)";
|
|
@@ -241,18 +245,28 @@ export async function cmdRemove(args: string[], deps: AccountDeps): Promise<numb
|
|
|
241
245
|
const response = await apiJson(deps, baseUrl, "DELETE", deletePath(classified.type, name, id));
|
|
242
246
|
if (response.status === 0) return fail("Proxy not reachable. Start it with 'ocx start' or 'ocx ensure'.");
|
|
243
247
|
if (response.status !== 200) return fail(errorText(response.json, `failed to remove ${requestedId}`));
|
|
248
|
+
const catalogRefreshPending = classified.type === "codex"
|
|
249
|
+
&& codexCatalogRefreshPending(response.json);
|
|
244
250
|
const after = await fetchRows(deps, baseUrl, name, classified.type);
|
|
245
251
|
if (after.networkDown || after.errorJson) {
|
|
246
252
|
const detail = after.networkDown ? "proxy not reachable" : typeof after.errorJson?.error === "string" ? after.errorJson.error : "unknown error";
|
|
247
253
|
return fail(`post-delete verification failed; delete may have succeeded: ${detail}`);
|
|
248
254
|
}
|
|
249
255
|
const removedActive = before.activeId === id;
|
|
250
|
-
const result = {
|
|
256
|
+
const result = {
|
|
257
|
+
ok: true,
|
|
258
|
+
provider: name,
|
|
259
|
+
id,
|
|
260
|
+
removedActive,
|
|
261
|
+
promotedActiveId: after.activeId,
|
|
262
|
+
...(classified.type === "codex" ? { catalogRefreshPending } : {}),
|
|
263
|
+
};
|
|
251
264
|
if (wantsJson) console.log(JSON.stringify(result, null, 2));
|
|
252
265
|
else if (classified.type === "codex" && removedActive && after.activeId === null) console.log(`openai: ${AUTO_NOTE}`);
|
|
253
266
|
else if (classified.type === "oauth") console.log(after.rows.length ? `${name}: active account is now ${after.activeId}` : `${name}: no accounts remaining`);
|
|
254
267
|
else if (classified.type === "api-key") console.log(after.rows.length ? `${name}: active key is now ${after.activeId}` : `${name}: no keys remaining`);
|
|
255
268
|
else console.log(`${name}: removed account ${requestedId}`);
|
|
269
|
+
if (!wantsJson && catalogRefreshPending) warnIfCodexCatalogRefreshPending(response.json);
|
|
256
270
|
return 0;
|
|
257
271
|
}
|
|
258
272
|
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import {
|
|
2
|
+
currentExternalCodexModelProvider,
|
|
3
|
+
getCodexRoutingKind,
|
|
4
|
+
type CodexRoutingKind,
|
|
5
|
+
} from "../codex/inject";
|
|
6
|
+
import { loadConfig, resolveEnvValue } from "../config";
|
|
7
|
+
|
|
8
|
+
const PROXY_ENV_KEYS = [
|
|
9
|
+
"HTTP_PROXY",
|
|
10
|
+
"HTTPS_PROXY",
|
|
11
|
+
"ALL_PROXY",
|
|
12
|
+
"http_proxy",
|
|
13
|
+
"https_proxy",
|
|
14
|
+
"all_proxy",
|
|
15
|
+
] as const;
|
|
16
|
+
|
|
17
|
+
export interface CodexShimReadinessInputs {
|
|
18
|
+
routingKind: CodexRoutingKind;
|
|
19
|
+
externalProvider: string | null;
|
|
20
|
+
processProxyEnvPresent: boolean;
|
|
21
|
+
configuredProxyResolved: boolean;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function externalProviderLabel(provider: string | null): string {
|
|
25
|
+
return provider
|
|
26
|
+
? `external model_provider ${JSON.stringify(provider)}`
|
|
27
|
+
: "the active Codex route";
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function codexShimReadinessWarnings(
|
|
31
|
+
inputs: CodexShimReadinessInputs,
|
|
32
|
+
): string[] {
|
|
33
|
+
const warnings: string[] = [];
|
|
34
|
+
const provider = externalProviderLabel(inputs.externalProvider);
|
|
35
|
+
|
|
36
|
+
if (inputs.routingKind === "unknown") {
|
|
37
|
+
warnings.push(
|
|
38
|
+
inputs.externalProvider
|
|
39
|
+
? `Codex still selects ${provider}. The shim can start OpenCodex, but it does not redirect that provider; point it at the live OpenCodex /v1 endpoint with wire_api = "responses", or switch to the built-in openai provider and run 'ocx sync'.`
|
|
40
|
+
: "Codex routing could not be verified. The shim can start OpenCodex, but it may not redirect Codex; run 'ocx doctor' before relying on autostart.",
|
|
41
|
+
);
|
|
42
|
+
} else if (inputs.routingKind === "custom-local") {
|
|
43
|
+
warnings.push(
|
|
44
|
+
`Codex uses ${provider} through a user-owned local gateway. The shim can start OpenCodex, but OpenCodex does not own that route; run 'ocx doctor' to verify its lifecycle.`,
|
|
45
|
+
);
|
|
46
|
+
} else if (inputs.routingKind === "custom-remote") {
|
|
47
|
+
warnings.push(
|
|
48
|
+
`Codex uses ${provider} through a remote gateway. The shim only starts a local OpenCodex proxy and will not affect those requests.`,
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (inputs.processProxyEnvPresent && !inputs.configuredProxyResolved) {
|
|
53
|
+
warnings.push(
|
|
54
|
+
"Proxy environment variables are present only in this process while config.proxy is unset or unresolved. Codex launchers and background services may not inherit them; persist config.proxy before relying on autostart.",
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return warnings;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function collectCodexShimReadinessWarnings(): string[] {
|
|
62
|
+
const config = loadConfig();
|
|
63
|
+
return codexShimReadinessWarnings({
|
|
64
|
+
routingKind: getCodexRoutingKind(),
|
|
65
|
+
externalProvider: currentExternalCodexModelProvider(),
|
|
66
|
+
processProxyEnvPresent: PROXY_ENV_KEYS.some(key => Boolean(process.env[key]?.trim())),
|
|
67
|
+
configuredProxyResolved: Boolean(resolveEnvValue(config.proxy)?.trim()),
|
|
68
|
+
});
|
|
69
|
+
}
|
package/src/cli/combo.ts
CHANGED
|
@@ -16,6 +16,7 @@ const USAGE = `Usage:
|
|
|
16
16
|
ocx combo set <id> --targets <provider/model[:weight],...>
|
|
17
17
|
[--strategy <failover|round-robin>] [--sticky <1-100>]
|
|
18
18
|
[--effort <low|medium|high|xhigh|max|ultra|->] [--alias <name|->]
|
|
19
|
+
[--native-alias] [--display-name <label|->]
|
|
19
20
|
[--rename-from <id>] [--json]
|
|
20
21
|
ocx combo remove <id> --yes [--json]`;
|
|
21
22
|
|
|
@@ -77,6 +78,8 @@ async function set(argv: string[], deps: RuntimeApiDeps): Promise<void> {
|
|
|
77
78
|
if (stickyLimit > 100) throw new CliUsageError("--sticky must be <= 100", USAGE);
|
|
78
79
|
const effort = takeOption(args, "--effort");
|
|
79
80
|
const alias = takeOption(args, "--alias");
|
|
81
|
+
const nativeAlias = takeFlag(args, "--native-alias");
|
|
82
|
+
const displayName = takeOption(args, "--display-name");
|
|
80
83
|
const renameFrom = takeOption(args, "--rename-from");
|
|
81
84
|
rejectArgs(args, USAGE);
|
|
82
85
|
const combo: Record<string, unknown> = {
|
|
@@ -86,6 +89,8 @@ async function set(argv: string[], deps: RuntimeApiDeps): Promise<void> {
|
|
|
86
89
|
};
|
|
87
90
|
if (effort !== undefined) combo.defaultEffort = effort === "-" ? null : effort;
|
|
88
91
|
if (alias !== undefined) combo.alias = alias === "-" ? "" : alias;
|
|
92
|
+
if (nativeAlias) combo.nativeAlias = true;
|
|
93
|
+
if (displayName !== undefined) combo.displayName = displayName === "-" ? "" : displayName;
|
|
89
94
|
const result = await runtimeRequest("/api/combos", {
|
|
90
95
|
method: "PUT",
|
|
91
96
|
body: JSON.stringify({ id, combo, ...(renameFrom ? { renameFrom } : {}) }),
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { readFileSync, writeFileSync } from "node:fs";
|
|
2
2
|
import { clearCodexAccountPin } from "../codex/account-priority";
|
|
3
3
|
import { getConfigPath, readConfigDiagnostics, saveConfig, validateConfigCandidate } from "../config";
|
|
4
|
+
import { VISION_REASONING_EFFORTS, isVisionReasoningEffort } from "../reasoning-effort";
|
|
4
5
|
import type { OcxConfig } from "../types";
|
|
6
|
+
import { normalizeVisionReasoningForModel } from "../vision/reasoning";
|
|
5
7
|
import { CliUsageError, printData, rejectArgs, runCliAction, takeFlag } from "./runtime-api";
|
|
6
8
|
|
|
7
9
|
const USAGE = `Usage:
|
|
@@ -65,10 +67,36 @@ function loadInput(path: string): unknown {
|
|
|
65
67
|
catch { throw new CliUsageError(`invalid JSON in ${path}`); }
|
|
66
68
|
}
|
|
67
69
|
|
|
70
|
+
function visionReasoningError(value: unknown): string | null {
|
|
71
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return null;
|
|
72
|
+
const vision = (value as Record<string, unknown>).visionSidecar;
|
|
73
|
+
if (!vision || typeof vision !== "object" || Array.isArray(vision)) return null;
|
|
74
|
+
const reasoning = (vision as Record<string, unknown>).reasoning;
|
|
75
|
+
if (reasoning === undefined || isVisionReasoningEffort(reasoning)) return null;
|
|
76
|
+
return `schema_invalid: visionSidecar.reasoning: must be one of ${VISION_REASONING_EFFORTS.join(", ")}`;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function validateCandidate(value: unknown): ReturnType<typeof validateConfigCandidate> {
|
|
80
|
+
const error = visionReasoningError(value);
|
|
81
|
+
return error ? { ok: false, error } : validateConfigCandidate(value);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function normalizeVisionConfig(config: OcxConfig): OcxConfig {
|
|
85
|
+
const vision = config.visionSidecar;
|
|
86
|
+
if (!vision || vision.reasoning === undefined) return config;
|
|
87
|
+
// Keep CLI import/set semantics aligned with the execution path: an omitted or blank model means
|
|
88
|
+
// the bounded OpenAI vision default, gpt-5.4-mini, not the Dashboard's web-search default.
|
|
89
|
+
const model = vision.model || "gpt-5.4-mini";
|
|
90
|
+
const normalized = normalizeVisionReasoningForModel(model, vision.reasoning);
|
|
91
|
+
if (normalized === undefined) delete vision.reasoning;
|
|
92
|
+
else vision.reasoning = normalized;
|
|
93
|
+
return config;
|
|
94
|
+
}
|
|
95
|
+
|
|
68
96
|
function validate(value: unknown): OcxConfig {
|
|
69
|
-
const result =
|
|
97
|
+
const result = validateCandidate(value);
|
|
70
98
|
if (!result.ok) throw new CliUsageError(result.error);
|
|
71
|
-
return result.config;
|
|
99
|
+
return normalizeVisionConfig(result.config);
|
|
72
100
|
}
|
|
73
101
|
|
|
74
102
|
export async function handleConfigCommand(argv: string[]): Promise<number> {
|
|
@@ -119,9 +147,10 @@ export async function handleConfigCommand(argv: string[]): Promise<number> {
|
|
|
119
147
|
if (action === "validate") {
|
|
120
148
|
const path = args.shift();
|
|
121
149
|
rejectArgs(args, USAGE);
|
|
122
|
-
const result = path ?
|
|
150
|
+
const result = path ? validateCandidate(loadInput(path)) : (() => {
|
|
123
151
|
const diagnostics = readConfigDiagnostics();
|
|
124
|
-
|
|
152
|
+
if (diagnostics.error) return { ok: false as const, error: diagnostics.error };
|
|
153
|
+
return validateCandidate(diagnostics.config);
|
|
125
154
|
})();
|
|
126
155
|
printData(result.ok ? { ok: true, source: path ?? getConfigPath() } : result, wantsJson,
|
|
127
156
|
[result.ok ? "Config is valid." : `Config is invalid: ${result.error}`]);
|
package/src/cli/doctor.ts
CHANGED
|
@@ -25,6 +25,11 @@ import { NativeProfileError } from "../codex/native-profile-types";
|
|
|
25
25
|
import { collectOrcaCodexHomeDiagnostic, resolveCodexHomeDir as resolveCodexHomeDirImpl, isWslRuntime, listWslWindowsCodexHomes, wslAutomountRoot, type CodexHomeDeps } from "../codex/home";
|
|
26
26
|
import { findCodexOnPath, isWindowsInteropDir } from "../codex/shim";
|
|
27
27
|
import { countPendingOpencodexHistory } from "../codex/history-provider";
|
|
28
|
+
import {
|
|
29
|
+
CodexUserIdentityRefusal,
|
|
30
|
+
probeCodexCoordinatorNamespace,
|
|
31
|
+
resolveEffectiveUserIdentity,
|
|
32
|
+
} from "../codex/user-identity";
|
|
28
33
|
import { collectProjectCodexConfigWarnings, formatProjectCodexConfigWarningsForDoctor } from "../codex/project-config-warnings";
|
|
29
34
|
import { collectStartupHealth, startupHealthSummary } from "../codex/autostart-health";
|
|
30
35
|
import {
|
|
@@ -902,6 +907,22 @@ export async function runDoctor(args: string[] = []): Promise<void> {
|
|
|
902
907
|
// Codex app until the one-time migration lands. Read-only probe (readonly sqlite, 100ms
|
|
903
908
|
// busy timeout) — reports state, never mutates.
|
|
904
909
|
console.log("\nCodex history migration");
|
|
910
|
+
// The history failure messages point here; make the visit worthwhile by
|
|
911
|
+
// probing the coordinator namespace the locks live in. The probe exercises
|
|
912
|
+
// identity, runtime-root, and permission checks without taking any lock or
|
|
913
|
+
// creating anything (a doctor run must observe, not initialize).
|
|
914
|
+
try {
|
|
915
|
+
const identity = resolveEffectiveUserIdentity();
|
|
916
|
+
const probe = probeCodexCoordinatorNamespace(identity);
|
|
917
|
+
if (probe.status === "missing") {
|
|
918
|
+
console.log(" ok history coordinator namespace not created yet (no history operation has run)");
|
|
919
|
+
} else {
|
|
920
|
+
console.log(" ok history coordinator namespace resolves");
|
|
921
|
+
}
|
|
922
|
+
} catch (cause) {
|
|
923
|
+
const reason = cause instanceof CodexUserIdentityRefusal ? cause.message : String(cause);
|
|
924
|
+
console.log(` -- history coordinator namespace refused: ${reason}`);
|
|
925
|
+
}
|
|
905
926
|
const pending = countPendingOpencodexHistory();
|
|
906
927
|
if (pending.failed) {
|
|
907
928
|
console.log(" -- state DB locked or unreadable (Codex app open?) — migration state unknown");
|