@bitkyc08/opencodex 2.7.27 → 2.7.28
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.ko.md +1 -1
- package/README.md +1 -1
- package/README.zh-CN.md +1 -1
- package/gui/dist/assets/index-DyBPh28A.css +1 -0
- package/gui/dist/assets/index-TZysP4q4.js +40 -0
- package/gui/dist/index.html +2 -2
- package/package.json +3 -2
- package/src/adapters/google-antigravity-wire.ts +15 -7
- package/src/bridge.ts +19 -1
- package/src/cli/provider.ts +11 -9
- package/src/codex/auth-api.ts +68 -5
- package/src/codex/auth-collision.ts +2 -0
- package/src/generated/jawcode-model-metadata.ts +40 -13
- package/src/oauth/index.ts +16 -2
- package/src/providers/base-url-choices.ts +36 -0
- package/src/providers/derive.ts +6 -0
- package/src/providers/registry.ts +21 -2
- package/src/responses/parser.ts +4 -2
- package/src/server/claude-messages.ts +2 -1
- package/src/server/index.ts +3 -0
- package/src/server/management-api.ts +144 -13
- package/src/server/relay.ts +41 -0
- package/src/server/request-log.ts +26 -0
- package/src/server/responses.ts +22 -1
- package/src/stall-timeout.ts +20 -0
- package/src/storage/scanner.ts +223 -0
- package/src/types.ts +4 -1
- package/src/usage/cost.ts +0 -0
- package/src/usage/expected-prices.ts +112 -0
- package/src/usage/log.ts +12 -0
- package/src/usage/summary.ts +35 -0
- package/src/web-search/index.ts +2 -3
- package/src/web-search/loop.ts +3 -0
- package/gui/dist/assets/index-BnrJO9Wz.css +0 -1
- package/gui/dist/assets/index-Vcr0pzdO.js +0 -40
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-TZysP4q4.js"></script>
|
|
20
|
+
<link rel="stylesheet" crossorigin href="/assets/index-DyBPh28A.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.7.
|
|
3
|
+
"version": "2.7.28",
|
|
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",
|
|
@@ -46,7 +46,8 @@
|
|
|
46
46
|
"prepublishOnly": "bun run typecheck && bun run build:gui",
|
|
47
47
|
"release": "bun scripts/release.ts",
|
|
48
48
|
"release:watch": "bun scripts/release.ts watch",
|
|
49
|
-
"prepush": "bun run typecheck && bun run test && bun run privacy:scan && bun run doctor:gui:if-changed",
|
|
49
|
+
"prepush": "bun run typecheck && bun run lint:gui && bun run test && bun run privacy:scan && bun run doctor:gui:if-changed",
|
|
50
|
+
"lint:gui": "cd gui && bun run lint",
|
|
50
51
|
"doctor:gui": "cd gui && bun run doctor",
|
|
51
52
|
"doctor:gui:full": "cd gui && bun run doctor:full",
|
|
52
53
|
"doctor:gui:if-changed": "bun scripts/doctor-gui-if-changed.ts",
|
|
@@ -12,16 +12,24 @@ export const ANTIGRAVITY_REQUEST_UA = process.env.GOOGLE_ANTIGRAVITY_USER_AGENT
|
|
|
12
12
|
|
|
13
13
|
/**
|
|
14
14
|
* Whether a stored `OcxToolCall.thoughtSignature` is a REAL upstream Gemini signature versus a
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
15
|
+
* foreign id that must not be forwarded to Gemini/Antigravity.
|
|
16
|
+
*
|
|
17
|
+
* Foreign ids that have 400'd Antigravity (`TYPE_BYTES` / Base64 decoding failed) include:
|
|
18
|
+
* - Responses/bridge item ids: `fc_...`, `ctc_...` (custom_tool_call), `tsc_...` (tool_search_call),
|
|
19
|
+
* `call_...`, `rs_...`, …
|
|
20
|
+
* - Anthropic tool-use ids: `toolu_...`
|
|
21
|
+
*
|
|
22
|
+
* Only real signatures may be forwarded — sending a foreign id as `thoughtSignature` breaks
|
|
23
|
+
* multi-turn reasoning continuity. Real signatures are opaque base64-ish blobs with no
|
|
24
|
+
* Responses/Anthropic id prefix.
|
|
25
|
+
*
|
|
26
|
+
* Note: do NOT reject a bare `sig_` / `sig-` prefix — existing Gemini replay fixtures and some
|
|
27
|
+
* upstream blobs use that shape; a deny-list entry for `sig` would drop valid continuity tokens.
|
|
19
28
|
*/
|
|
20
29
|
export function isLikelyRealThoughtSignature(sig: string | undefined): boolean {
|
|
21
30
|
if (typeof sig !== "string" || sig.length < 16) return false;
|
|
22
|
-
// Reject synthetic Responses/tool-call ids
|
|
23
|
-
|
|
24
|
-
if (/^(fc|call|msg|rs|resp|reasoning|item|ws|tool|func|function)[-_]/i.test(sig)) return false;
|
|
31
|
+
// Reject synthetic Responses/tool-call ids and Anthropic tool-use ids (`_` or `-` separators).
|
|
32
|
+
if (/^(fc|ctc|tsc|call|msg|rs|resp|reasoning|item|ws|toolu|tool|func|function)[-_]/i.test(sig)) return false;
|
|
25
33
|
// Real Gemini thought signatures are opaque base64/base64url blobs: only [A-Za-z0-9+/_=-].
|
|
26
34
|
// Anything containing other characters (or whitespace) is not a real signature.
|
|
27
35
|
return /^[A-Za-z0-9+/_=-]+$/.test(sig);
|
package/src/bridge.ts
CHANGED
|
@@ -2,6 +2,7 @@ import type { AdapterEvent, OcxUsage } from "./types";
|
|
|
2
2
|
import { adapterFailureFromMessage, classifyError, type OcxErrorPayload } from "./lib/errors";
|
|
3
3
|
import { encodeCompactionSummary } from "./responses/compaction";
|
|
4
4
|
import { encodeReasoningEnvelope, type ReasoningEnvelope } from "./responses/reasoning-envelope";
|
|
5
|
+
import { resolveStallTimeoutSec } from "./stall-timeout";
|
|
5
6
|
import { usageDisplayTotalTokens } from "./usage/totals";
|
|
6
7
|
|
|
7
8
|
function uuid(): string {
|
|
@@ -81,6 +82,8 @@ export function bridgeToResponsesSSE(
|
|
|
81
82
|
* response.completed — codex-rs collect_compaction_output requires exactly one.
|
|
82
83
|
*/
|
|
83
84
|
compaction?: boolean;
|
|
85
|
+
/** One-shot: first non-empty text/thinking/raw-reasoning delta observed (WP4 TTFT). */
|
|
86
|
+
onFirstOutput?: () => void;
|
|
84
87
|
onTerminal?: (status: ResponsesTerminalStatus) => void;
|
|
85
88
|
onCompletedResponse?: (response: Record<string, unknown>) => void;
|
|
86
89
|
},
|
|
@@ -181,7 +184,7 @@ export function bridgeToResponsesSSE(
|
|
|
181
184
|
// whenever a real event was emitted since the last tick, so it only fires on a genuine stall.
|
|
182
185
|
const heartbeatFrame = encoder.encode('event: response.heartbeat\ndata: {"type":"response.heartbeat"}\n\n');
|
|
183
186
|
let stallTicks = 0;
|
|
184
|
-
const stallSec =
|
|
187
|
+
const stallSec = resolveStallTimeoutSec(options?.stallTimeoutSec);
|
|
185
188
|
const maxStallTicks = Math.ceil((stallSec * 1000) / heartbeatMs);
|
|
186
189
|
beat = setInterval(() => {
|
|
187
190
|
if (closed) return;
|
|
@@ -401,6 +404,20 @@ export function bridgeToResponsesSSE(
|
|
|
401
404
|
// we synthesize response.completed below, so Codex never hits the parser's
|
|
402
405
|
// "stream closed before response.completed" (responses.rs) -> ApiError::Stream.
|
|
403
406
|
let terminated = false;
|
|
407
|
+
let firstOutputReported = false;
|
|
408
|
+
const reportFirstOutput = (event: AdapterEvent): void => {
|
|
409
|
+
if (firstOutputReported) return;
|
|
410
|
+
const nonEmpty = event.type === "text_delta"
|
|
411
|
+
? event.text.length > 0
|
|
412
|
+
: event.type === "thinking_delta"
|
|
413
|
+
? event.thinking.length > 0
|
|
414
|
+
: event.type === "reasoning_raw_delta"
|
|
415
|
+
? event.text.length > 0
|
|
416
|
+
: false;
|
|
417
|
+
if (!nonEmpty) return;
|
|
418
|
+
firstOutputReported = true;
|
|
419
|
+
try { options?.onFirstOutput?.(); } catch { /* metrics must not break the stream */ }
|
|
420
|
+
};
|
|
404
421
|
let macrotaskFired = true;
|
|
405
422
|
let macrotaskTimer: ReturnType<typeof setTimeout> | undefined;
|
|
406
423
|
|
|
@@ -416,6 +433,7 @@ export function bridgeToResponsesSSE(
|
|
|
416
433
|
macrotaskTimer = setTimeout(() => { macrotaskFired = true; macrotaskTimer = undefined; }, 0);
|
|
417
434
|
activity = true;
|
|
418
435
|
stallTicks = 0;
|
|
436
|
+
reportFirstOutput(event);
|
|
419
437
|
// Compaction turns emit ONLY the synthetic compaction item + response.completed. The
|
|
420
438
|
// summary text is accumulated silently: emitting it as a normal assistant message would
|
|
421
439
|
// duplicate the summary if this response is ever replayed via previous_response_id
|
package/src/cli/provider.ts
CHANGED
|
@@ -124,7 +124,7 @@ function handleList(args: string[]): void {
|
|
|
124
124
|
// provider add
|
|
125
125
|
// ---------------------------------------------------------------------------
|
|
126
126
|
|
|
127
|
-
const ADD_USAGE = "Usage: ocx provider add <name> [--adapter <adapter>] [--base-url <url>] [--api-key <key>] [--default-model <model>] [--set-default] [--force] [--json] [--sync]";
|
|
127
|
+
const ADD_USAGE = "Usage: ocx provider add <name> [--adapter <adapter>] [--base-url <url>] [--api-key <key>] [--default-model <model>] [--allow-private-network] [--set-default] [--force] [--json] [--sync]";
|
|
128
128
|
|
|
129
129
|
async function handleAdd(args: string[]): Promise<void> {
|
|
130
130
|
const name = args[0];
|
|
@@ -138,12 +138,13 @@ async function handleAdd(args: string[]): Promise<void> {
|
|
|
138
138
|
process.exit(1);
|
|
139
139
|
}
|
|
140
140
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
const
|
|
141
|
+
const restArgs = args.slice(1);
|
|
142
|
+
const force = consumeFlag(restArgs, "--force");
|
|
143
|
+
const setDefault = consumeFlag(restArgs, "--set-default");
|
|
144
|
+
const wantsJson = consumeFlag(restArgs, "--json");
|
|
145
|
+
const wantsSync = consumeFlag(restArgs, "--sync");
|
|
146
|
+
const allowPrivateNetwork = consumeFlag(restArgs, "--allow-private-network");
|
|
147
|
+
const apiKey = consumeFlagValue(restArgs, "--api-key");
|
|
147
148
|
const adapter = consumeFlagValue(restArgs, "--adapter");
|
|
148
149
|
const baseUrl = consumeFlagValue(restArgs, "--base-url");
|
|
149
150
|
const defaultModel = consumeFlagValue(restArgs, "--default-model");
|
|
@@ -187,8 +188,9 @@ async function handleAdd(args: string[]): Promise<void> {
|
|
|
187
188
|
};
|
|
188
189
|
}
|
|
189
190
|
|
|
190
|
-
|
|
191
|
-
if (
|
|
191
|
+
config.providers[name] = provConfig;
|
|
192
|
+
if (allowPrivateNetwork) provConfig.allowPrivateNetwork = true;
|
|
193
|
+
if (setDefault) config.defaultProvider = name;
|
|
192
194
|
|
|
193
195
|
validateAndSave(config);
|
|
194
196
|
|
package/src/codex/auth-api.ts
CHANGED
|
@@ -564,16 +564,24 @@ export async function handleCodexAuthAPI(
|
|
|
564
564
|
}
|
|
565
565
|
|
|
566
566
|
if (url.pathname === "/api/codex-auth/login" && req.method === "POST") {
|
|
567
|
-
const body = (await req.json().catch(() => ({}))) as { id?: string };
|
|
567
|
+
const body = (await req.json().catch(() => ({}))) as { id?: string; reauth?: boolean };
|
|
568
568
|
const requestedAccountId = body.id?.trim();
|
|
569
|
+
const reauth = body.reauth === true;
|
|
569
570
|
if (requestedAccountId && !ACCOUNT_ID_RE.test(requestedAccountId)) {
|
|
570
571
|
return jsonResponse({ error: "Invalid account id format" }, 400);
|
|
571
572
|
}
|
|
572
573
|
const accountId = requestedAccountId || `chatgpt-${Date.now()}`;
|
|
573
574
|
const runtimeConfig = getRuntimeConfig(config);
|
|
574
|
-
|
|
575
|
+
const exists = (runtimeConfig.codexAccounts ?? []).some(a => a.id === accountId) || Boolean(getCodexAccountCredential(accountId));
|
|
576
|
+
if (exists && !reauth) {
|
|
575
577
|
return jsonResponse({ error: `Account id already exists: ${accountId}` }, 400);
|
|
576
578
|
}
|
|
579
|
+
if (reauth) {
|
|
580
|
+
if (!requestedAccountId) return jsonResponse({ error: "id required for reauth" }, 400);
|
|
581
|
+
if (!configuredPoolAccount(runtimeConfig, accountId)) {
|
|
582
|
+
return jsonResponse({ error: "Unknown pool account for reauth" }, 404);
|
|
583
|
+
}
|
|
584
|
+
}
|
|
577
585
|
const flowId = `flow-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
578
586
|
try {
|
|
579
587
|
const { startLoginFlow, getLoginStatus } = await import("../oauth");
|
|
@@ -622,8 +630,49 @@ export async function handleCodexAuthAPI(
|
|
|
622
630
|
quota = parseUsageQuota(data);
|
|
623
631
|
}
|
|
624
632
|
} catch { /* wham fetch is non-blocking */ }
|
|
633
|
+
// Reauth must refresh the same ChatGPT identity already bound to this pool slot.
|
|
634
|
+
// Otherwise a different login would silently overwrite credentials under a trusted id.
|
|
635
|
+
if (reauth) {
|
|
636
|
+
const existingCred = getCodexAccountCredential(accountId);
|
|
637
|
+
const poolAccount = configuredPoolAccount(getRuntimeConfig(config), accountId);
|
|
638
|
+
const expectedChatgptId = existingCred?.chatgptAccountId?.trim();
|
|
639
|
+
const expectedEmail = poolAccount?.email?.trim().toLowerCase();
|
|
640
|
+
const gotEmail = email.trim().toLowerCase();
|
|
641
|
+
if (expectedChatgptId) {
|
|
642
|
+
if (expectedChatgptId !== oauthAccountId) {
|
|
643
|
+
codexAuthLoginState.set(flowId, {
|
|
644
|
+
status: "error",
|
|
645
|
+
error: "Signed-in ChatGPT account does not match this pool account. Sign in with the same account, or remove it and add a new one.",
|
|
646
|
+
doneAt: Date.now(),
|
|
647
|
+
});
|
|
648
|
+
completed = true;
|
|
649
|
+
break;
|
|
650
|
+
}
|
|
651
|
+
} else if (expectedEmail) {
|
|
652
|
+
if (!gotEmail || gotEmail !== expectedEmail) {
|
|
653
|
+
codexAuthLoginState.set(flowId, {
|
|
654
|
+
status: "error",
|
|
655
|
+
error: "Signed-in ChatGPT account does not match this pool account. Sign in with the same account, or remove it and add a new one.",
|
|
656
|
+
doneAt: Date.now(),
|
|
657
|
+
});
|
|
658
|
+
completed = true;
|
|
659
|
+
break;
|
|
660
|
+
}
|
|
661
|
+
} else {
|
|
662
|
+
// No chatgptAccountId and no pool email — refuse silent identity replacement
|
|
663
|
+
// (including empty credential slots that still have a pool row).
|
|
664
|
+
codexAuthLoginState.set(flowId, {
|
|
665
|
+
status: "error",
|
|
666
|
+
error: "Cannot verify account identity for reauth. Remove this account and add it again.",
|
|
667
|
+
doneAt: Date.now(),
|
|
668
|
+
});
|
|
669
|
+
completed = true;
|
|
670
|
+
break;
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
|
|
625
674
|
// 1.2: Duplicate check is scoped by personal vs workspace plan bucket.
|
|
626
|
-
const collision = checkAccountIdCollision(oauthAccountId, email, plan);
|
|
675
|
+
const collision = checkAccountIdCollision(oauthAccountId, email, plan, reauth ? accountId : undefined);
|
|
627
676
|
if (collision.collision) {
|
|
628
677
|
codexAuthLoginState.set(flowId, {
|
|
629
678
|
status: "error", error: collision.reason, doneAt: Date.now(),
|
|
@@ -665,7 +714,18 @@ export async function handleCodexAuthAPI(
|
|
|
665
714
|
|
|
666
715
|
const latestConfig = getRuntimeConfig(config);
|
|
667
716
|
const accounts = latestConfig.codexAccounts ?? [];
|
|
668
|
-
|
|
717
|
+
const existingIdx = accounts.findIndex(a => a.id === accountId);
|
|
718
|
+
if (existingIdx >= 0) {
|
|
719
|
+
// Keep the pool id stable; refresh display metadata after a successful login/reauth.
|
|
720
|
+
accounts[existingIdx] = withCodexAccountLogLabel({
|
|
721
|
+
...accounts[existingIdx],
|
|
722
|
+
email,
|
|
723
|
+
plan,
|
|
724
|
+
isMain: false,
|
|
725
|
+
}, accounts);
|
|
726
|
+
latestConfig.codexAccounts = accounts;
|
|
727
|
+
saveRuntimeConfig(config, latestConfig);
|
|
728
|
+
} else {
|
|
669
729
|
accounts.push(withCodexAccountLogLabel({ id: accountId, email, plan, isMain: false }, accounts));
|
|
670
730
|
latestConfig.codexAccounts = accounts;
|
|
671
731
|
saveRuntimeConfig(config, latestConfig);
|
|
@@ -714,9 +774,12 @@ export async function handleCodexAuthAPI(
|
|
|
714
774
|
if (url.pathname === "/api/codex-auth/login-status" && req.method === "GET") {
|
|
715
775
|
const flowId = url.searchParams.get("flowId");
|
|
716
776
|
const accountId = url.searchParams.get("accountId")?.trim();
|
|
777
|
+
// Reauth always has a pre-existing credential; never treat "credential exists" as success
|
|
778
|
+
// when the flow map entry is gone (would false-complete on lost/expired flow state).
|
|
779
|
+
const reauthStatus = url.searchParams.get("reauth") === "1";
|
|
717
780
|
if (flowId) {
|
|
718
781
|
const st = codexAuthLoginState.get(flowId);
|
|
719
|
-
if (!st && accountId && getCodexAccountCredential(accountId)) {
|
|
782
|
+
if (!st && accountId && !reauthStatus && getCodexAccountCredential(accountId)) {
|
|
720
783
|
return jsonResponse({ status: "done", accountId });
|
|
721
784
|
}
|
|
722
785
|
return jsonResponse(st ? { ...st, email: maskEmail(st.email) ?? undefined } : { status: "expired" });
|
|
@@ -44,10 +44,12 @@ export function checkAccountIdCollision(
|
|
|
44
44
|
chatgptAccountId: string,
|
|
45
45
|
email?: string | null,
|
|
46
46
|
plan?: string | null,
|
|
47
|
+
excludeAccountId?: string | null,
|
|
47
48
|
): { collision: true; reason: string } | { collision: false } {
|
|
48
49
|
const candidateEmail = normalizedEmail(email);
|
|
49
50
|
const candidateWorkspace = isWorkspacePlan(plan);
|
|
50
51
|
for (const account of loadConfig().codexAccounts ?? []) {
|
|
52
|
+
if (excludeAccountId && account.id === excludeAccountId) continue;
|
|
51
53
|
if (account.isMain) continue;
|
|
52
54
|
if (isWorkspacePlan(account.plan) !== candidateWorkspace) continue;
|
|
53
55
|
const cred = getCodexAccountCredential(account.id);
|