@bitkyc08/opencodex 2.58.0 → 2.59.0
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 +28 -10
- package/gui/dist/assets/index-C5IebErG.js +136 -0
- package/gui/dist/assets/{index-C5-RdDmD.css → index-OESInAjC.css} +1 -1
- package/gui/dist/index.html +2 -2
- package/gui/dist/provider-icons/crusoe.svg +1 -0
- package/gui/dist/provider-icons/opper.svg +3 -0
- package/package.json +1 -1
- package/src/adapters/base.ts +11 -1
- package/src/adapters/cursor/catalog.ts +11 -0
- package/src/adapters/cursor/effort-map.ts +16 -2
- package/src/adapters/cursor/envelope-echo.ts +55 -2
- package/src/adapters/cursor/message-mapper.ts +3 -2
- package/src/adapters/cursor/protobuf-request.ts +8 -5
- package/src/adapters/cursor/request-builder.ts +14 -3
- package/src/adapters/cursor/thread-continuity.ts +105 -31
- package/src/adapters/cursor/tool-guidance.ts +5 -4
- package/src/adapters/cursor.ts +42 -1
- package/src/adapters/devin/cloud-direct/chat.ts +11 -2
- package/src/adapters/devin/cloud-direct/index.ts +7 -0
- package/src/adapters/devin/cloud-direct/stated-reset-retry.ts +103 -0
- package/src/adapters/devin.ts +75 -13
- package/src/adapters/google-antigravity-wire.ts +29 -2
- package/src/adapters/google-http.ts +8 -1
- package/src/adapters/google.ts +23 -4
- package/src/adapters/openai-chat/response-events.ts +61 -0
- package/src/adapters/openai-chat.ts +5 -10
- package/src/adapters/openai-responses/passthrough.ts +10 -1
- package/src/adapters/openai-responses/tool-output-recovery.ts +75 -0
- package/src/adapters/openai-responses/tool-schema.ts +19 -7
- package/src/adapters/responses-tool-schema.ts +76 -46
- package/src/adapters/run-turn-queue.ts +17 -4
- package/src/bridge/response-json.ts +1 -1
- package/src/bridge/sse.ts +165 -24
- package/src/claude/context-windows.ts +22 -0
- package/src/claude/outbound.ts +35 -4
- package/src/cli/account-api.ts +4 -3
- package/src/cli/account-extended.ts +22 -2
- package/src/cli/account-orca-import.ts +63 -0
- package/src/cli/account.ts +32 -4
- package/src/cli/capabilities.ts +40 -0
- package/src/cli/claude.ts +29 -1
- package/src/cli/codex-cli-update.ts +97 -2
- package/src/cli/dispatch.ts +54 -0
- package/src/cli/doctor.ts +197 -2
- package/src/cli/help.ts +4 -1
- package/src/cli/index.ts +88 -20
- package/src/cli/models-runtime.ts +33 -4
- package/src/cli/registry.ts +11 -1
- package/src/cli/runtime-api.ts +44 -0
- package/src/cli/start-args.ts +94 -0
- package/src/cli/system-command.ts +2 -0
- package/src/client/machine-api.ts +4 -3
- package/src/client/machine-listener.ts +14 -1
- package/src/clients/config-export/constants.ts +2 -3
- package/src/clients/config-export.ts +5 -5
- package/src/codex/account-store.ts +81 -5
- package/src/codex/auth-api/pool-quota-probe.ts +14 -3
- package/src/codex/auth-api/routes.ts +17 -2
- package/src/codex/auth-context.ts +16 -12
- package/src/codex/catalog/build-entries.ts +25 -4
- package/src/codex/catalog/derive-entry.ts +8 -1
- package/src/codex/catalog/effort.ts +10 -6
- package/src/codex/catalog/gather-capture.ts +1 -0
- package/src/codex/catalog/model-hints.ts +37 -5
- package/src/codex/catalog/parsing.ts +83 -5
- package/src/codex/catalog/reserve-warn.ts +96 -0
- package/src/codex/catalog/retained-sync.ts +19 -0
- package/src/codex/catalog/routed-gather.ts +42 -3
- package/src/codex/cli-installation-identity.ts +210 -0
- package/src/codex/cli-installation-targets.ts +158 -0
- package/src/codex/convergence.ts +5 -0
- package/src/codex/history-provider.ts +4 -1
- package/src/codex/history-state-open.ts +105 -0
- package/src/codex/inject/config-toml.ts +44 -2
- package/src/codex/inject.ts +3 -2
- package/src/codex/lineage.ts +83 -32
- package/src/codex/loopback-target.ts +31 -0
- package/src/codex/main-account-hard-lock.ts +2 -1
- package/src/codex/main-account.ts +10 -3
- package/src/codex/main-device-reauth.ts +17 -9
- package/src/codex/model-entitlements.ts +60 -1
- package/src/codex/observed-model-denials.ts +137 -0
- package/src/codex/orca-auth-source.ts +94 -0
- package/src/codex/orca-import.ts +219 -0
- package/src/codex/prompt-text-probe.ts +282 -12
- package/src/codex/quota-401-recovery.ts +12 -0
- package/src/codex/quota-types.ts +65 -0
- package/src/codex/quota.ts +24 -19
- package/src/codex/routing/cooldown-math.ts +8 -47
- package/src/codex/routing/pin-drain.ts +57 -0
- package/src/codex/routing.ts +13 -15
- package/src/codex/subagent-model-fallback.ts +94 -0
- package/src/codex/windows-installation-files.ts +224 -0
- package/src/combos/failover.ts +122 -5
- package/src/config/diagnostics.ts +21 -0
- package/src/config/load-degrade.ts +15 -0
- package/src/config/pending-teardown.ts +8 -0
- package/src/config/process-state.ts +36 -3
- package/src/config/provider-relative-send-path.ts +16 -0
- package/src/config/proxy-env.ts +23 -5
- package/src/config/schema/config-schema.ts +21 -0
- package/src/config/schema/leaf-validators.ts +64 -17
- package/src/generated/compatibility-version.json +235 -163
- package/src/generated/model-metadata.ts +1 -1
- package/src/lib/bounded-body.ts +4 -2
- package/src/lib/destination-policy.ts +48 -6
- package/src/lib/errors.ts +3 -15
- package/src/lib/local-destinations.ts +32 -5
- package/src/lib/provider-outbound.ts +3 -3
- package/src/lib/proxy-env.ts +70 -3
- package/src/lib/request-execution-budget.ts +11 -3
- package/src/lib/response-body-inactivity.ts +193 -0
- package/src/lib/retry-delay.ts +69 -0
- package/src/lib/socks5-fetch.ts +631 -0
- package/src/lib/spend-reservation-ledger.ts +115 -9
- package/src/lib/workflow-budget.ts +145 -8
- package/src/oauth/account-quota-rank.ts +72 -15
- package/src/oauth/generic-account-failover.ts +40 -27
- package/src/oauth/orcarouter.ts +15 -2
- package/src/oauth/store.ts +8 -0
- package/src/providers/codex-capacity.ts +9 -0
- package/src/providers/devin-provider-merge-migration.ts +33 -12
- package/src/providers/free-directory.ts +20 -2
- package/src/providers/key-failover.ts +261 -7
- package/src/providers/model-rename-migration.ts +1 -0
- package/src/providers/openai-sidecar.ts +4 -0
- package/src/providers/opencode-go-transport.ts +14 -5
- package/src/providers/quota/report-cache.ts +3 -0
- package/src/providers/registry/entries-extended.ts +96 -0
- package/src/providers/registry/model-seeds.ts +78 -21
- package/src/responses/apply-patch-envelope.ts +44 -11
- package/src/responses/bridge-search-replay-cache.ts +152 -0
- package/src/responses/code-mode-helper-compat.ts +26 -16
- package/src/responses/custom-tool-compat.ts +1 -1
- package/src/responses/hosted-tool-policy.ts +85 -2
- package/src/responses/schema.ts +9 -2
- package/src/server/auth-cors.ts +26 -0
- package/src/server/chat-completions.ts +9 -4
- package/src/server/chat-native-sse.ts +26 -9
- package/src/server/chat-native.ts +10 -4
- package/src/server/claude-messages.ts +24 -2
- package/src/server/gui-static.ts +36 -2
- package/src/server/inbound-body-admission.ts +187 -0
- package/src/server/index.ts +15 -19
- package/src/server/management/api-access.ts +3 -4
- package/src/server/management/config-routes.ts +31 -6
- package/src/server/management/provider-capability-config.ts +35 -7
- package/src/server/management/provider-routes.ts +70 -18
- package/src/server/proxy-liveness.ts +97 -2
- package/src/server/relay.ts +17 -24
- package/src/server/request-log.ts +25 -1
- package/src/server/responses/adapter-continuation.ts +71 -27
- package/src/server/responses/adapter-delivery.ts +39 -8
- package/src/server/responses/adapter-dispatch.ts +52 -24
- package/src/server/responses/compact.ts +60 -11
- package/src/server/responses/core-codex-account.ts +83 -22
- package/src/server/responses/core-normalize.ts +12 -5
- package/src/server/responses/fetch-helpers.ts +68 -2
- package/src/server/responses/passthrough-delivery.ts +10 -1
- package/src/server/responses/passthrough-dispatch.ts +113 -48
- package/src/server/responses/passthrough-execution.ts +11 -1
- package/src/server/responses/request-prepare.ts +29 -0
- package/src/server/responses/request-send-budget.ts +84 -7
- package/src/server/responses/request-sidecar-auth.ts +16 -8
- package/src/server/responses/request-spend.ts +38 -9
- package/src/server/responses/request-transport.ts +13 -10
- package/src/server/responses/run-turn-execution.ts +20 -5
- package/src/server/responses/sidecar-execution.ts +2 -0
- package/src/server/responses/ws-upstream.ts +2 -1
- package/src/server/responses-custom-tool-repair.ts +2 -2
- package/src/server/sse-frame-buffer.ts +12 -10
- package/src/server/sse-payload-rewrite.ts +36 -9
- package/src/server/system-env-shell.ts +5 -1
- package/src/server/system-env.ts +7 -1
- package/src/server/workflow-refusal.ts +56 -2
- package/src/service/cli.ts +16 -6
- package/src/service/guards.ts +10 -0
- package/src/service/health.ts +43 -0
- package/src/service/state.ts +7 -2
- package/src/types/accounts.ts +4 -0
- package/src/types/config.ts +100 -3
- package/src/types/provider.ts +19 -0
- package/src/types/request.ts +7 -1
- package/src/types/wire.ts +9 -1
- package/src/usage/expected-prices.ts +28 -0
- package/src/usage/log.ts +87 -4
- package/src/web-search/passthrough-bridge.ts +39 -5
- package/gui/dist/assets/index-BbrHOIY0.js +0 -128
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
export const START_USAGE = "Usage: ocx start [--port <port>] [--socks5 [host:port] | --socks5-off]";
|
|
2
|
+
export const START_USAGE_LINE = "ocx start [--port <port>] [--socks5 [host:port] | --socks5-off]";
|
|
3
|
+
export const DEFAULT_SOCKS5_PROXY = "socks5://127.0.0.1:10808";
|
|
4
|
+
|
|
5
|
+
export class StartArgsError extends Error {
|
|
6
|
+
constructor(message: string) {
|
|
7
|
+
super(message);
|
|
8
|
+
this.name = "StartArgsError";
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export type StartOptions = {
|
|
13
|
+
port?: number;
|
|
14
|
+
/** SOCKS5 URL to persist. */
|
|
15
|
+
socks5?: string;
|
|
16
|
+
socks5Off?: boolean;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export function isSocksProxyUrl(proxy: string): boolean {
|
|
20
|
+
return /^socks5h?:\/\//i.test(proxy.trim());
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function normalizeSocks5(raw: string): string {
|
|
24
|
+
const trimmed = raw.trim();
|
|
25
|
+
if (!trimmed) return DEFAULT_SOCKS5_PROXY;
|
|
26
|
+
if (isSocksProxyUrl(trimmed)) {
|
|
27
|
+
try {
|
|
28
|
+
const parsed = new URL(trimmed);
|
|
29
|
+
const port = Number(parsed.port);
|
|
30
|
+
if (!parsed.hostname || !parsed.port || !Number.isInteger(port) || port <= 0 || port > 65535) {
|
|
31
|
+
throw new Error("invalid host or port");
|
|
32
|
+
}
|
|
33
|
+
return trimmed;
|
|
34
|
+
} catch {
|
|
35
|
+
throw new StartArgsError("Invalid SOCKS5 address; expected socks5://host:port");
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
if (/^socks4a?:\/\//i.test(trimmed)) {
|
|
39
|
+
throw new StartArgsError("Only SOCKS5 proxy URLs are supported");
|
|
40
|
+
}
|
|
41
|
+
if (/^https?:\/\//i.test(trimmed)) {
|
|
42
|
+
throw new StartArgsError("SOCKS5 proxy must be socks5://host:port, not an HTTP URL");
|
|
43
|
+
}
|
|
44
|
+
if (/^\d+$/.test(trimmed)) {
|
|
45
|
+
const port = Number(trimmed);
|
|
46
|
+
if (!Number.isInteger(port) || port <= 0 || port > 65535) {
|
|
47
|
+
throw new StartArgsError("Invalid SOCKS5 port number");
|
|
48
|
+
}
|
|
49
|
+
return `socks5://127.0.0.1:${port}`;
|
|
50
|
+
}
|
|
51
|
+
const hostPort = /^(\[[^\]]+\]|[^:]+):(\d+)$/.exec(trimmed);
|
|
52
|
+
if (hostPort) {
|
|
53
|
+
const port = Number(hostPort[2]);
|
|
54
|
+
if (!Number.isInteger(port) || port <= 0 || port > 65535) {
|
|
55
|
+
throw new StartArgsError("Invalid SOCKS5 port number");
|
|
56
|
+
}
|
|
57
|
+
return `socks5://${trimmed}`;
|
|
58
|
+
}
|
|
59
|
+
throw new StartArgsError("Invalid SOCKS5 address; expected socks5://host:port");
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function parseStartOptions(argv: string[]): StartOptions {
|
|
63
|
+
const options: StartOptions = {};
|
|
64
|
+
for (let i = 0; i < argv.length; i++) {
|
|
65
|
+
const arg = argv[i]!;
|
|
66
|
+
if (arg === "--port") {
|
|
67
|
+
const value = argv[++i];
|
|
68
|
+
const port = value && /^\d+$/.test(value) ? Number(value) : NaN;
|
|
69
|
+
if (!Number.isInteger(port) || port <= 0 || port > 65535) {
|
|
70
|
+
throw new StartArgsError("Invalid port number");
|
|
71
|
+
}
|
|
72
|
+
options.port = port;
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
if (arg === "--socks5") {
|
|
76
|
+
if (options.socks5Off) throw new StartArgsError("--socks5 and --socks5-off cannot be used together");
|
|
77
|
+
const next = argv[i + 1];
|
|
78
|
+
if (next && !next.startsWith("-")) {
|
|
79
|
+
options.socks5 = normalizeSocks5(next);
|
|
80
|
+
i += 1;
|
|
81
|
+
} else {
|
|
82
|
+
options.socks5 = DEFAULT_SOCKS5_PROXY;
|
|
83
|
+
}
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
if (arg === "--socks5-off") {
|
|
87
|
+
if (options.socks5 !== undefined) throw new StartArgsError("--socks5 and --socks5-off cannot be used together");
|
|
88
|
+
options.socks5Off = true;
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
throw new StartArgsError(START_USAGE);
|
|
92
|
+
}
|
|
93
|
+
return options;
|
|
94
|
+
}
|
|
@@ -21,6 +21,8 @@ const USAGE = `Usage:
|
|
|
21
21
|
ocx system codex-app-server [--json]
|
|
22
22
|
ocx system codex-restart --yes [--json]
|
|
23
23
|
ocx system codex-cli-update check [--json]
|
|
24
|
+
ocx system codex-cli-update attest [--json]
|
|
25
|
+
ocx system codex-cli-update attest --candidate <absolute-path> --npm-prefix <absolute-path> --npm-cli <absolute-path> --node <absolute-path> [--json]
|
|
24
26
|
ocx system update check [--channel <latest|preview>] [--json]
|
|
25
27
|
ocx system update run [--channel <latest|preview>] [--restart <on|off>] --yes [--json]
|
|
26
28
|
ocx system update status <job-id> [--json]
|
|
@@ -74,10 +74,11 @@ export async function handleMachineApi(
|
|
|
74
74
|
injected: MachineApiDeps = defaultDeps,
|
|
75
75
|
): Promise<Response | null> {
|
|
76
76
|
const deps = { ...defaultDeps, ...injected };
|
|
77
|
-
|
|
77
|
+
const readMethod = req.method === "GET" || req.method === "HEAD";
|
|
78
|
+
if (url.pathname === "/api/machine/status" && readMethod) {
|
|
78
79
|
return Response.json(statusPayload(req, state, deps), { headers: { "Cache-Control": "no-store" } });
|
|
79
80
|
}
|
|
80
|
-
if (url.pathname === "/api/machine/clients" &&
|
|
81
|
+
if (url.pathname === "/api/machine/clients" && readMethod) {
|
|
81
82
|
return Response.json({
|
|
82
83
|
selectedClients: [...state.selectedClients],
|
|
83
84
|
journalOwner: journalOwner(),
|
|
@@ -103,7 +104,7 @@ export async function handleMachineApi(
|
|
|
103
104
|
return Response.json({ success: false, error: message }, { status: 502 });
|
|
104
105
|
}
|
|
105
106
|
}
|
|
106
|
-
if (url.pathname === "/api/machine/shim" &&
|
|
107
|
+
if (url.pathname === "/api/machine/shim" && readMethod) {
|
|
107
108
|
return Response.json(diagnoseCodexShim(), { headers: { "Cache-Control": "no-store" } });
|
|
108
109
|
}
|
|
109
110
|
if (url.pathname === "/api/machine/shim" && req.method === "POST") {
|
|
@@ -46,9 +46,14 @@ export function machineRouteAllowed(url: URL, req: Request, relayEnabled: boolea
|
|
|
46
46
|
if (req.headers.get("upgrade")) return false;
|
|
47
47
|
const path = url.pathname;
|
|
48
48
|
if (req.method === "GET" && (path === "/healthz" || path === "/readyz" || path === "/" || path === "/opencodex-session")) return true;
|
|
49
|
-
if (req.method === "GET" && (path === "/api/machine/status" || path === "/api/machine/clients" || path === "/api/machine/shim")) return true;
|
|
49
|
+
if ((req.method === "GET" || req.method === "HEAD") && (path === "/api/machine/status" || path === "/api/machine/clients" || path === "/api/machine/shim")) return true;
|
|
50
50
|
if (req.method === "POST" && (path === "/api/machine/sync" || path === "/api/machine/shim" || path === "/api/machine/disconnect")) return true;
|
|
51
51
|
if (relayEnabled && path.startsWith("/api/machine/hub-relay/")) return true;
|
|
52
|
+
// Known machine endpoints are admitted for every method so an unsupported
|
|
53
|
+
// method reaches the authenticated method restriction (403) instead of a
|
|
54
|
+
// bare 404 that hides the endpoint entirely.
|
|
55
|
+
if (path === "/api/machine/status" || path === "/api/machine/clients" || path === "/api/machine/shim"
|
|
56
|
+
|| path === "/api/machine/sync" || path === "/api/machine/disconnect") return true;
|
|
52
57
|
if (req.method !== "GET" || path.startsWith("/api/") || path.startsWith("/v1/")) return false;
|
|
53
58
|
return GUI_SPA_PATHS.has(path)
|
|
54
59
|
|| path.startsWith("/integrations/")
|
|
@@ -111,6 +116,14 @@ export function startMachineListener(
|
|
|
111
116
|
if (managementPrincipal(req, managementAuth, config) !== "gui-session") {
|
|
112
117
|
return Response.json({ error: "opencodex machine GUI session required" }, { status: 401 });
|
|
113
118
|
}
|
|
119
|
+
// A loopback dashboard session proves possession, not user presence: any local
|
|
120
|
+
// process can fetch the dashboard bootstrap and replay its token and CSRF value.
|
|
121
|
+
// Keep the connected listener useful for status/diagnostics, but never let that
|
|
122
|
+
// credentialless bootstrap authorize durable machine changes. Those operations
|
|
123
|
+
// remain available through the explicit CLI commands.
|
|
124
|
+
if (req.method !== "GET" && req.method !== "HEAD") {
|
|
125
|
+
return Response.json({ error: "opencodex machine changes require the local CLI" }, { status: 403 });
|
|
126
|
+
}
|
|
114
127
|
return await handleMachineApi(req, url, connection, machineApiDeps) ?? json404(req);
|
|
115
128
|
}
|
|
116
129
|
|
|
@@ -37,9 +37,8 @@ export const OPENCLAW_API_KEY_ENV_REF = `\${${OPENCLAW_API_KEY_ENV}}`;
|
|
|
37
37
|
export const LOOPBACK_API_KEY_PLACEHOLDER = "opencodex-loopback";
|
|
38
38
|
|
|
39
39
|
/**
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
* unset, which would silently ship a bogus credential — so we never emit it.
|
|
40
|
+
* Legacy name retained for existing consumers. Gajae exports now use the
|
|
41
|
+
* non-secret loopback placeholder and do not emit an environment reference.
|
|
43
42
|
*/
|
|
44
43
|
export const GAJAE_API_KEY_ENV = "OPENCODEX_GAJAE_API_KEY";
|
|
45
44
|
|
|
@@ -41,7 +41,7 @@ export type { RaycastAbility, RaycastAbilityName, RaycastModelEntry, RaycastProv
|
|
|
41
41
|
export { buildRaycastClientConfig, summarizeRaycast, buildRaycastContribution } from "./config-export/raycast";
|
|
42
42
|
|
|
43
43
|
import type { OpencodeLaunchEnv, OpencodeCatalogModel, ExportContext, PiModelEntry, ManagedContribution, ManagedFragment, ExportClientId, ExportClientSpec } from "./config-export/contracts";
|
|
44
|
-
import { OPENCODE_API_KEY_ENV_REF, OPENCODE_PROVIDER_BLOCK_DEFAULT_CONFIG, OPENCODE_CONFIG_SCHEMA, OPENCODE_PROVIDER_ID, PI_API_DIALECT, LOOPBACK_API_KEY_PLACEHOLDER, HERMES_API_KEY_ENV_REF, OPENCLAW_API_KEY_ENV_REF,
|
|
44
|
+
import { OPENCODE_API_KEY_ENV_REF, OPENCODE_PROVIDER_BLOCK_DEFAULT_CONFIG, OPENCODE_CONFIG_SCHEMA, OPENCODE_PROVIDER_ID, PI_API_DIALECT, LOOPBACK_API_KEY_PLACEHOLDER, HERMES_API_KEY_ENV_REF, OPENCLAW_API_KEY_ENV_REF, OPENCODE_API_KEY_ENV, HERMES_API_KEY_ENV, OPENCLAW_API_KEY_ENV } from "./config-export/constants";
|
|
45
45
|
import { exportModelLabel, authoritativeContextWindow, outputBudgetFor, normalizeExportModels, inputModalitiesForClient, opencodeModelCapabilities, proxyAdmissionHeaders, singleFragment } from "./config-export/model-metadata";
|
|
46
46
|
import { buildOmpClientConfig, summarizeOmp, buildOmpContribution } from "./config-export/omp";
|
|
47
47
|
import { buildDshClientConfig, summarizeDsh, buildDshContribution } from "./config-export/dsh";
|
|
@@ -867,7 +867,7 @@ export interface GajaeModelEntry {
|
|
|
867
867
|
/** Gajae validates strictly: an unknown field fails the whole config. */
|
|
868
868
|
export interface GajaeProviderBlock {
|
|
869
869
|
baseUrl: string;
|
|
870
|
-
|
|
870
|
+
apiKey: string;
|
|
871
871
|
api: "openai-completions";
|
|
872
872
|
models: GajaeModelEntry[];
|
|
873
873
|
}
|
|
@@ -1056,7 +1056,7 @@ function buildGajaeClientConfig(ctx: ExportContext): GajaeGeneratedConfig {
|
|
|
1056
1056
|
providers: {
|
|
1057
1057
|
[OPENCODE_PROVIDER_ID]: {
|
|
1058
1058
|
baseUrl: ctx.baseUrl,
|
|
1059
|
-
|
|
1059
|
+
apiKey: LOOPBACK_API_KEY_PLACEHOLDER,
|
|
1060
1060
|
api: "openai-completions",
|
|
1061
1061
|
models,
|
|
1062
1062
|
},
|
|
@@ -1320,8 +1320,8 @@ export const EXPORT_CLIENTS: Record<ExportClientId, ExportClientSpec> = {
|
|
|
1320
1320
|
id: "gajae",
|
|
1321
1321
|
filename: "gajae-models.yaml",
|
|
1322
1322
|
destination: env => gajaeConfigPath(env),
|
|
1323
|
-
apiKeyEnv:
|
|
1324
|
-
exportHint:
|
|
1323
|
+
apiKeyEnv: "",
|
|
1324
|
+
exportHint: "No environment variable is needed for the loopback provider.",
|
|
1325
1325
|
build: buildGajaeClientConfig,
|
|
1326
1326
|
format: "yaml",
|
|
1327
1327
|
summarize: summarizeGajae,
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { readOrcaAuthSource } from "./orca-auth-source";
|
|
1
2
|
import { createHash } from "node:crypto";
|
|
2
3
|
import { closeSync, existsSync, fstatSync, readFileSync, mkdirSync, openSync, statSync, unlinkSync, writeFileSync } from "node:fs";
|
|
3
4
|
import { join } from "node:path";
|
|
@@ -52,11 +53,20 @@ function isObject(value: unknown): value is Record<string, unknown> {
|
|
|
52
53
|
}
|
|
53
54
|
|
|
54
55
|
function isCredential(value: unknown): value is CodexAccountCredentials {
|
|
56
|
+
const hasNoSource = isObject(value)
|
|
57
|
+
&& value.sourceAuthPath === undefined
|
|
58
|
+
&& value.sourceSubject === undefined;
|
|
59
|
+
const hasCompleteSource = isObject(value)
|
|
60
|
+
&& typeof value.sourceAuthPath === "string"
|
|
61
|
+
&& value.sourceAuthPath.length > 0
|
|
62
|
+
&& typeof value.sourceSubject === "string"
|
|
63
|
+
&& value.sourceSubject.length > 0;
|
|
55
64
|
return isObject(value)
|
|
56
65
|
&& typeof value.accessToken === "string"
|
|
57
66
|
&& typeof value.refreshToken === "string"
|
|
58
67
|
&& typeof value.expiresAt === "number"
|
|
59
|
-
&& typeof value.chatgptAccountId === "string"
|
|
68
|
+
&& typeof value.chatgptAccountId === "string"
|
|
69
|
+
&& (hasNoSource || hasCompleteSource);
|
|
60
70
|
}
|
|
61
71
|
|
|
62
72
|
function isCredentialRecord(value: unknown): value is CodexAccountCredentialRecord {
|
|
@@ -78,6 +88,7 @@ export function refreshGrantFingerprintForToken(refreshToken: string): string {
|
|
|
78
88
|
}
|
|
79
89
|
|
|
80
90
|
function recordGrantFingerprint(record: CodexAccountCredentialRecord): string | undefined {
|
|
91
|
+
if (record.credential?.sourceAuthPath) return undefined;
|
|
81
92
|
return record.refreshGrantFingerprint ?? (
|
|
82
93
|
record.credential ? refreshGrantFingerprintForToken(record.credential.refreshToken) : undefined
|
|
83
94
|
);
|
|
@@ -426,7 +437,7 @@ export function commitRefreshedCodexCredentialWithAliases(
|
|
|
426
437
|
return withCredentialMutationLockSync(() => {
|
|
427
438
|
const store = loadCodexAccountRecordStore();
|
|
428
439
|
const current = store[id];
|
|
429
|
-
if (!current || current.generation !== generation || current.deletedAt != null || !current.credential) {
|
|
440
|
+
if (!current || current.generation !== generation || current.deletedAt != null || !current.credential || current.credential.sourceAuthPath) {
|
|
430
441
|
return { committed: false, propagatedAliases: [] };
|
|
431
442
|
}
|
|
432
443
|
const priorCredential = current.credential;
|
|
@@ -458,7 +469,7 @@ export function commitRefreshedCodexCredentialWithAliases(
|
|
|
458
469
|
&& priorCredential.chatgptAccountId === cred.chatgptAccountId
|
|
459
470
|
) {
|
|
460
471
|
for (const [aliasId, alias] of Object.entries(store)) {
|
|
461
|
-
if (aliasId === id || alias.deletedAt != null || !alias.credential) continue;
|
|
472
|
+
if (aliasId === id || alias.deletedAt != null || !alias.credential || alias.credential.sourceAuthPath) continue;
|
|
462
473
|
if (recordGrantFingerprint(alias) !== priorFingerprint) continue;
|
|
463
474
|
if (alias.credential.accessToken !== priorCredential.accessToken) continue;
|
|
464
475
|
if (alias.credential.expiresAt !== priorCredential.expiresAt) continue;
|
|
@@ -630,7 +641,14 @@ export type CodexRefreshProvenance = "self-refresh" | "joined-lineage" | "extern
|
|
|
630
641
|
|
|
631
642
|
/** Terminal outcome of one forced refresh, as seen by the caller that requested it. */
|
|
632
643
|
export type ForcedRefreshOutcome =
|
|
633
|
-
| {
|
|
644
|
+
| {
|
|
645
|
+
kind: "resolved";
|
|
646
|
+
provenance: CodexRefreshProvenance;
|
|
647
|
+
generation: number;
|
|
648
|
+
rotated: boolean;
|
|
649
|
+
/** Same-grant records advanced by this refresh, at their committed generations. */
|
|
650
|
+
propagatedAliases?: { id: string; generation: number }[];
|
|
651
|
+
}
|
|
634
652
|
| { kind: "failed"; error: unknown };
|
|
635
653
|
const MAX_CODEX_REFRESH_FLIGHTS = 32;
|
|
636
654
|
const CODEX_REFRESH_FLIGHT_STALE_MS = 120_000;
|
|
@@ -776,7 +794,7 @@ function findFreshCredentialForGrant(
|
|
|
776
794
|
// require both ids to be present and exactly equal rather than inferring identity from the grant.
|
|
777
795
|
if (!expectedChatgptAccountId) return null;
|
|
778
796
|
for (const [candidateId, candidate] of Object.entries(records)) {
|
|
779
|
-
if (candidateId === excludeId || candidate.deletedAt != null || !candidate.credential) continue;
|
|
797
|
+
if (candidateId === excludeId || candidate.deletedAt != null || !candidate.credential || candidate.credential.sourceAuthPath) continue;
|
|
780
798
|
if (recordGrantFingerprint(candidate) !== refreshGrantFingerprint) continue;
|
|
781
799
|
if (!candidate.credential.chatgptAccountId) continue;
|
|
782
800
|
if (candidate.credential.chatgptAccountId !== expectedChatgptAccountId) continue;
|
|
@@ -926,6 +944,9 @@ export async function forceRefreshCodexPoolToken(
|
|
|
926
944
|
provenance: classify(resolved),
|
|
927
945
|
generation: resolved.generation,
|
|
928
946
|
rotated: resolved.accessToken !== options.rejectedAccessToken,
|
|
947
|
+
...(resolved.propagatedAliases?.length
|
|
948
|
+
? { propagatedAliases: resolved.propagatedAliases }
|
|
949
|
+
: {}),
|
|
929
950
|
});
|
|
930
951
|
},
|
|
931
952
|
error => {
|
|
@@ -967,6 +988,57 @@ export async function getValidCodexToken(
|
|
|
967
988
|
};
|
|
968
989
|
}
|
|
969
990
|
|
|
991
|
+
/** Source credentials never join refresh flights or spend a refresh grant, including after 401. */
|
|
992
|
+
function resolveOrcaSourceToken(id: string, forced?: ForcedRefreshFence): CodexRefreshResult {
|
|
993
|
+
const initial = readCodexAccountRecord(id);
|
|
994
|
+
const initialCredential = initial?.credential;
|
|
995
|
+
if (!initial || initial.deletedAt != null || !initialCredential?.sourceAuthPath || !initialCredential.sourceSubject) {
|
|
996
|
+
throw new CodexCredentialGenerationConflictError();
|
|
997
|
+
}
|
|
998
|
+
const sourceCredential = readOrcaAuthSource(initialCredential.sourceAuthPath);
|
|
999
|
+
if (sourceCredential.chatgptAccountId !== initialCredential.chatgptAccountId
|
|
1000
|
+
|| sourceCredential.sourceSubject !== initialCredential.sourceSubject) {
|
|
1001
|
+
throw new Error("Orca credential identity changed; reimport the account explicitly.");
|
|
1002
|
+
}
|
|
1003
|
+
if (sourceCredential.accessToken === initialCredential.accessToken
|
|
1004
|
+
&& sourceCredential.expiresAt === initialCredential.expiresAt) {
|
|
1005
|
+
if (forced?.rejectedAccessToken === sourceCredential.accessToken) {
|
|
1006
|
+
throw new Error("Orca bearer was rejected; update the account in Orca and retry.");
|
|
1007
|
+
}
|
|
1008
|
+
return { accessToken: sourceCredential.accessToken, chatgptAccountId: sourceCredential.chatgptAccountId,
|
|
1009
|
+
generation: initial.generation, provenance: "external-replacement" };
|
|
1010
|
+
}
|
|
1011
|
+
|
|
1012
|
+
return withCredentialMutationLockSync(() => {
|
|
1013
|
+
const store = loadCodexAccountRecordStore();
|
|
1014
|
+
const record = store[id];
|
|
1015
|
+
const prior = record?.credential;
|
|
1016
|
+
if (!record || record.deletedAt != null || !prior?.sourceAuthPath || !prior.sourceSubject) {
|
|
1017
|
+
throw new CodexCredentialGenerationConflictError();
|
|
1018
|
+
}
|
|
1019
|
+
if (record.generation !== initial.generation) throw new CodexCredentialGenerationConflictError();
|
|
1020
|
+
const credential = readOrcaAuthSource(prior.sourceAuthPath);
|
|
1021
|
+
if (credential.chatgptAccountId !== prior.chatgptAccountId || credential.sourceSubject !== prior.sourceSubject) {
|
|
1022
|
+
throw new Error("Orca credential identity changed; reimport the account explicitly.");
|
|
1023
|
+
}
|
|
1024
|
+
if (credential.accessToken !== prior.accessToken || credential.expiresAt !== prior.expiresAt) {
|
|
1025
|
+
store[id] = {
|
|
1026
|
+
credential, generation: record.generation + 1, replacedAt: Date.now(),
|
|
1027
|
+
// The source identity was checked above. Retire old-generation writers without
|
|
1028
|
+
// splitting this same account's retained quota history on every source rotation.
|
|
1029
|
+
quotaHistoryIdentity: record.quotaHistoryIdentity,
|
|
1030
|
+
...preservedValidationMetadata(record),
|
|
1031
|
+
};
|
|
1032
|
+
persistCredentialMutation(store);
|
|
1033
|
+
}
|
|
1034
|
+
if (forced?.rejectedAccessToken === credential.accessToken) {
|
|
1035
|
+
throw new Error("Orca bearer was rejected; update the account in Orca and retry.");
|
|
1036
|
+
}
|
|
1037
|
+
return { accessToken: credential.accessToken, chatgptAccountId: credential.chatgptAccountId,
|
|
1038
|
+
generation: store[id]!.generation, provenance: "external-replacement" };
|
|
1039
|
+
});
|
|
1040
|
+
}
|
|
1041
|
+
|
|
970
1042
|
async function resolveCodexToken(
|
|
971
1043
|
id: string,
|
|
972
1044
|
forced?: ForcedRefreshFence,
|
|
@@ -976,6 +1048,7 @@ async function resolveCodexToken(
|
|
|
976
1048
|
const record = readCodexAccountRecord(id);
|
|
977
1049
|
const cred = record?.deletedAt == null ? record?.credential : undefined;
|
|
978
1050
|
if (!record || !cred) throw new CodexCredentialUnavailableError();
|
|
1051
|
+
if (cred.sourceAuthPath) return resolveOrcaSourceToken(id, forced);
|
|
979
1052
|
const refreshGrantFingerprint = recordGrantFingerprint(record);
|
|
980
1053
|
if (!refreshGrantFingerprint) throw new CodexCredentialUnavailableError();
|
|
981
1054
|
|
|
@@ -1005,6 +1078,7 @@ async function resolveCodexToken(
|
|
|
1005
1078
|
const refreshed = await awaitOwnCancellation(existing.promise, callerSignal);
|
|
1006
1079
|
const current = readCodexAccountRecord(id);
|
|
1007
1080
|
const currentCred = current?.deletedAt == null ? current?.credential : undefined;
|
|
1081
|
+
if (currentCred?.sourceAuthPath) return resolveOrcaSourceToken(id, forced);
|
|
1008
1082
|
// The flight owner already committed this credential, and it is the one stored
|
|
1009
1083
|
// for this account: adopt the stored state instead of CAS-writing the identical
|
|
1010
1084
|
// bytes, which would bump the generation a second time and invalidate the
|
|
@@ -1126,6 +1200,7 @@ async function resolveCodexToken(
|
|
|
1126
1200
|
const lockedRecord = readCodexAccountRecord(id);
|
|
1127
1201
|
const lockedCred = lockedRecord?.deletedAt == null ? lockedRecord?.credential : undefined;
|
|
1128
1202
|
if (!lockedRecord || !lockedCred) throw new CodexCredentialGenerationConflictError();
|
|
1203
|
+
if (lockedCred.sourceAuthPath) return resolveOrcaSourceToken(id, forced);
|
|
1129
1204
|
const startGeneration = lockedRecord.generation;
|
|
1130
1205
|
const lockedRefreshGrantFingerprint = recordGrantFingerprint(lockedRecord);
|
|
1131
1206
|
if (lockedRefreshGrantFingerprint !== refreshGrantFingerprint) {
|
|
@@ -1331,6 +1406,7 @@ async function resolveCodexToken(
|
|
|
1331
1406
|
// Provenance rides out with the rest: a joiner that adopts this result needs the
|
|
1332
1407
|
// flight's own classification, not a guess made at the adoption site (#3019).
|
|
1333
1408
|
...(result.provenance !== undefined ? { provenance: result.provenance } : {}),
|
|
1409
|
+
...(result.propagatedAliases?.length ? { propagatedAliases: result.propagatedAliases } : {}),
|
|
1334
1410
|
...(result.resolvedGrantFingerprint !== undefined
|
|
1335
1411
|
? { resolvedGrantFingerprint: result.resolvedGrantFingerprint }
|
|
1336
1412
|
: {}),
|
|
@@ -9,7 +9,7 @@ import { codexWarmupFailureReason, warmCodexAccount } from "../warmup";
|
|
|
9
9
|
import { readBoundedResponseBody } from "../../lib/bounded-body";
|
|
10
10
|
import { ResourceAdmissionError } from "../../lib/admission";
|
|
11
11
|
import { WHAM_REQUEST_TIMEOUT_MS } from "../quota-recovery-timing";
|
|
12
|
-
import { claimQuotaRecovery, quotaRecoveryTerminalFor, releaseQuotaRecovery, settleQuotaRecovery, settleQuotaRecoveryTerminal } from "../quota-401-recovery";
|
|
12
|
+
import { claimQuotaRecovery, fencePropagatedQuotaRecovery, quotaRecoveryTerminalFor, releaseQuotaRecovery, settleQuotaRecovery, settleQuotaRecoveryTerminal } from "../quota-401-recovery";
|
|
13
13
|
import { seedLoginRowsForTests } from "./login-state";
|
|
14
14
|
import { nonEmptyPlan } from "./runtime-config";
|
|
15
15
|
|
|
@@ -205,6 +205,12 @@ export async function recoverPoolQuotaFrom401(ctx: {
|
|
|
205
205
|
onSettled: outcome => {
|
|
206
206
|
if (outcome.kind === "resolved") {
|
|
207
207
|
settleQuotaRecovery(accountId, claim.claimId, outcome);
|
|
208
|
+
// A propagated alias adopted the rotated grant without spending its own claim. Fence
|
|
209
|
+
// each committed alias generation so a later 401 on it cannot claim a second refresh
|
|
210
|
+
// of the same underlying grant.
|
|
211
|
+
for (const alias of outcome.propagatedAliases ?? []) {
|
|
212
|
+
fencePropagatedQuotaRecovery(alias.id, alias.generation);
|
|
213
|
+
}
|
|
208
214
|
} else if (outcome.error instanceof TokenRefreshError && isTerminalRefreshError(outcome.error)) {
|
|
209
215
|
// A revoked or expired grant does not become valid on the next poll. Releasing it
|
|
210
216
|
// into backoff would let the following bare 401 find a non-terminal record and
|
|
@@ -476,9 +482,14 @@ export async function fetchPoolAccountQuota(
|
|
|
476
482
|
&& generation !== undefined && record.generation === generation
|
|
477
483
|
&& isCompleteCodexQuotaRecoverySnapshot(result.freshQuota ?? null, result.freshPlan ?? configuredPlan)) {
|
|
478
484
|
try {
|
|
485
|
+
// Quota I/O may outlive the source capture. Never validate a rotated or revoked link
|
|
486
|
+
// using an older generation's observation. Explicit validation consent still applies.
|
|
487
|
+
const sourceToken = record.credential.sourceAuthPath ? await getValidToken(accountId) : null;
|
|
488
|
+
if (sourceToken && (sourceToken.generation !== generation
|
|
489
|
+
|| !isCodexAccountGenerationLive(accountId, generation))) return result;
|
|
479
490
|
await warmCodexAccount({
|
|
480
|
-
accessToken: record.credential.accessToken,
|
|
481
|
-
chatgptAccountId: record.credential.chatgptAccountId,
|
|
491
|
+
accessToken: sourceToken?.accessToken ?? record.credential.accessToken,
|
|
492
|
+
chatgptAccountId: sourceToken?.chatgptAccountId ?? record.credential.chatgptAccountId,
|
|
482
493
|
});
|
|
483
494
|
markCodexAccountValidated(accountId, Date.now(), generation);
|
|
484
495
|
clearAccountNeedsReauth(accountId, generation);
|
|
@@ -7,7 +7,7 @@ import { getAccountQuotaHistory, listAccountQuotas } from "../quota";
|
|
|
7
7
|
import { deleteCodexAccount } from "../account-lifecycle";
|
|
8
8
|
import { isCodexAccountPaused, setCodexAccountPaused } from "../account-pause";
|
|
9
9
|
import { clearCodexAccountPin, isCodexAccountPriorityKey, pinnedCodexAccountId, setCodexAccountPin, setCodexAccountPriority } from "../account-priority";
|
|
10
|
-
import { codexQuotaScopeForModel, clearCodexAccountCooldown, clearThreadAccountMapForAccount, getEffectiveActiveCodexAccountId, isEffectiveCodexAccountPinned, resetCodexRoutingForManualSelection } from "../routing";
|
|
10
|
+
import { codexAccountPinDrainReason, codexQuotaScopeForModel, clearCodexAccountCooldown, clearThreadAccountMapForAccount, getEffectiveActiveCodexAccountId, isEffectiveCodexAccountPinned, resetCodexRoutingForManualSelection } from "../routing";
|
|
11
11
|
import { DEFAULT_ACCOUNT_PRIORITY, MAX_ACCOUNT_PRIORITY, MIN_ACCOUNT_PRIORITY, normalizeAccountPoolStickyLimit, normalizeCodexAccountPoolStrategy, parseAccountPoolStickyLimit, parseCodexAccountPoolStrategy, parseAccountPriority } from "../pool-rotation";
|
|
12
12
|
import { MAIN_CODEX_ACCOUNT_ID } from "../main-account";
|
|
13
13
|
import { reconcileLiveStateStores } from "../../lib/state-store-registrations";
|
|
@@ -235,7 +235,22 @@ export async function handleCodexAuthAPI(
|
|
|
235
235
|
else setCodexAccountPin(runtimeConfig, targetAccountId);
|
|
236
236
|
resetCodexRoutingForManualSelection(targetAccountId);
|
|
237
237
|
saveRuntimeConfig(config, runtimeConfig);
|
|
238
|
-
|
|
238
|
+
// A pin this route accepts can still be dropped by the very next resolve, and saying
|
|
239
|
+
// nothing about that is what made the setting look ignored (#4521). The checks above
|
|
240
|
+
// refuse an account that cannot be selected at all; this reports the one remaining
|
|
241
|
+
// outcome they do not cover, from the same predicate routing releases on, so the two
|
|
242
|
+
// cannot drift. Absent means the pin survives — additive for existing clients.
|
|
243
|
+
// `appliesImmediately` is unchanged: it answers whether thread affinity was cleared,
|
|
244
|
+
// not whether the pin is durable.
|
|
245
|
+
const pinDrainReason = body.accountId == null
|
|
246
|
+
? undefined
|
|
247
|
+
: codexAccountPinDrainReason(runtimeConfig, targetAccountId);
|
|
248
|
+
return jsonResponse({
|
|
249
|
+
ok: true,
|
|
250
|
+
activeCodexAccountId: body.accountId,
|
|
251
|
+
appliesImmediately: true,
|
|
252
|
+
...(pinDrainReason !== undefined ? { pinDrained: true, pinDrainReason } : {}),
|
|
253
|
+
});
|
|
239
254
|
}
|
|
240
255
|
|
|
241
256
|
if (url.pathname === "/api/codex-auth/active" && req.method === "GET") {
|
|
@@ -135,20 +135,22 @@ export function requestOwnedMainPinState(
|
|
|
135
135
|
}
|
|
136
136
|
|
|
137
137
|
/**
|
|
138
|
-
*
|
|
138
|
+
* One conversation tree keys as ONE cohort (#4780).
|
|
139
139
|
*
|
|
140
|
-
*
|
|
141
|
-
*
|
|
142
|
-
*
|
|
143
|
-
*
|
|
144
|
-
*
|
|
140
|
+
* Upstream keys its prompt cache on something the whole tree shares -- `prompt_cache_key` is the
|
|
141
|
+
* session id, or `{source}:{parent_thread_id}` for an internal session -- and one `AgentControl`
|
|
142
|
+
* whose `session_id` is the root thread's id is shared with every sub-agent. While the proxy
|
|
143
|
+
* keyed per thread, two requests could carry an identical `prompt_cache_key` and be served by
|
|
144
|
+
* different accounts, so the split member asserted a warm prefix that was deterministically cold
|
|
145
|
+
* on its account. Nothing failed; the prompt was replayed in full and the tokens burned.
|
|
145
146
|
*
|
|
146
|
-
*
|
|
147
|
-
*
|
|
148
|
-
*
|
|
149
|
-
*
|
|
150
|
-
*
|
|
151
|
-
*
|
|
147
|
+
* THIS IS NOT A REVERT OF #4546 wp8. wp8 fixed a different defect -- a child bound under the RAW
|
|
148
|
+
* parent id, an identity unrelated to the root's own binding, so siblings shared an entry the
|
|
149
|
+
* root was not on and a grandchild landed on a key nobody had bound. A cohort key has no such
|
|
150
|
+
* incoherence, because the root's own binding IS the cohort key. What wp8 additionally gave each
|
|
151
|
+
* thread, a binding of its own, is what this deliberately gives up.
|
|
152
|
+
*
|
|
153
|
+
* Which requests produce no key at all is unchanged, through both units.
|
|
152
154
|
*
|
|
153
155
|
* The derivation itself lives in ./lineage so a lineage record's conversation key and the key
|
|
154
156
|
* the thread actually binds under can never drift apart.
|
|
@@ -1419,6 +1421,7 @@ export function materializeCodexUpstreamAuth(
|
|
|
1419
1421
|
if (!stored?.accessToken || !isMainAccountTokenLive()) {
|
|
1420
1422
|
throw new CodexMainSubstitutionUnavailableError();
|
|
1421
1423
|
}
|
|
1424
|
+
selected.delete("chatgpt-account-id");
|
|
1422
1425
|
selected.set("authorization", `Bearer ${stored.accessToken}`);
|
|
1423
1426
|
if (stored.chatgptAccountId) selected.set("chatgpt-account-id", stored.chatgptAccountId);
|
|
1424
1427
|
observeSelectedMainCredential(stored, writer);
|
|
@@ -1496,6 +1499,7 @@ export async function materializeCodexUpstreamAuthAsync(
|
|
|
1496
1499
|
...(options.nativeMainRefreshDependencies ?? {}),
|
|
1497
1500
|
});
|
|
1498
1501
|
if (!stored?.accessToken) throw new CodexMainSubstitutionUnavailableError();
|
|
1502
|
+
selected.delete("chatgpt-account-id");
|
|
1499
1503
|
selected.set("authorization", `Bearer ${stored.accessToken}`);
|
|
1500
1504
|
if (stored.chatgptAccountId) selected.set("chatgpt-account-id", stored.chatgptAccountId);
|
|
1501
1505
|
observeSelectedMainCredential(stored, writer);
|
|
@@ -26,7 +26,7 @@ import {
|
|
|
26
26
|
} from "./metadata";
|
|
27
27
|
import { resetBundledCatalogCacheForTests } from "./bundled";
|
|
28
28
|
import { isMultiAgentV2Enabled } from "../features";
|
|
29
|
-
import { ensureUltraReasoningLevel, isGpt56NativeSlug } from "./effort";
|
|
29
|
+
import { clampedDefaultEffort, ensureUltraReasoningLevel, isGpt56NativeSlug } from "./effort";
|
|
30
30
|
import { clearGatherRoutedModelsInflight, lastDropWarnSignature } from "./provider-fetch";
|
|
31
31
|
import {
|
|
32
32
|
accountSelectorShadowCollisionWarnings,
|
|
@@ -490,10 +490,14 @@ export interface ObservedCatalogMergeInput {
|
|
|
490
490
|
readonly includeNativeOpenAi: boolean;
|
|
491
491
|
readonly accountBoundEntries: readonly RawEntry[];
|
|
492
492
|
readonly suppressedBareNativeSlugs?: ReadonlySet<string>;
|
|
493
|
+
/** Routed slugs that must not gain a missing synthetic max rung during retained-row repair. */
|
|
494
|
+
readonly suppressedSyntheticMaxSlugs?: ReadonlySet<string>;
|
|
493
495
|
readonly policy: ObservedCatalogMergePolicy;
|
|
494
496
|
readonly openaiContextCap?: NativeContextLimitsInput;
|
|
495
497
|
/** Exact display-only labels for bare native OpenAI models. */
|
|
496
498
|
readonly nativeDisplayNames?: Readonly<Record<string, string>>;
|
|
499
|
+
/** Pristine installed-catalog multi-agent pins; see MultiAgentModeOptions.nativeDefaults. */
|
|
500
|
+
readonly nativeMultiAgentDefaults?: ReadonlyMap<string, string | null>;
|
|
497
501
|
}
|
|
498
502
|
|
|
499
503
|
/**
|
|
@@ -526,9 +530,11 @@ export function mergeCatalogEntriesFromObservedState({
|
|
|
526
530
|
includeNativeOpenAi,
|
|
527
531
|
accountBoundEntries,
|
|
528
532
|
suppressedBareNativeSlugs = new Set(),
|
|
533
|
+
suppressedSyntheticMaxSlugs = new Set(),
|
|
529
534
|
policy,
|
|
530
535
|
openaiContextCap,
|
|
531
536
|
nativeDisplayNames,
|
|
537
|
+
nativeMultiAgentDefaults,
|
|
532
538
|
}: ObservedCatalogMergeInput): RawEntry[] {
|
|
533
539
|
// Raw catalog rows contain nested arrays/objects that normalization mutates. Detach every row at
|
|
534
540
|
// the observed-core boundary so callers can safely retain evidence objects or repeat the merge.
|
|
@@ -860,16 +866,29 @@ export function mergeCatalogEntriesFromObservedState({
|
|
|
860
866
|
});
|
|
861
867
|
// Mock-max universality (260709): preserved routed entries from disk may predate
|
|
862
868
|
// the max rung — ensure it here so subagent max spawns validate on every
|
|
863
|
-
// reasoning-capable entry.
|
|
869
|
+
// reasoning-capable entry. A suppressed preserved row that already contains max keeps it;
|
|
870
|
+
// without persisted provenance, only a healthy provider rebuild can distinguish and remove
|
|
871
|
+
// an older synthetic rung from a real provider-declared rung. max only: 5.6 exact ladders
|
|
872
|
+
// (luna: no ultra) stay intact.
|
|
864
873
|
if (!freshCustomEntries.has(m) && !exactCombo && !reserveProjection && !String(e.slug ?? "").startsWith("opencode-go/")) {
|
|
865
874
|
const levels = Array.isArray(e.supported_reasoning_levels)
|
|
866
875
|
? e.supported_reasoning_levels as Array<{ effort?: string }>
|
|
867
876
|
: [];
|
|
868
|
-
if (levels.length > 0
|
|
877
|
+
if (levels.length > 0
|
|
878
|
+
&& !suppressedSyntheticMaxSlugs.has(String(e.slug ?? ""))
|
|
879
|
+
&& !levels.some(level => level.effort === "max")) {
|
|
869
880
|
levels.push(CODEX_REASONING_LEVELS.find(level => level.effort === "max")
|
|
870
881
|
?? { effort: "max", description: "Maximum reasoning depth for the hardest problems" });
|
|
871
882
|
e.supported_reasoning_levels = levels;
|
|
872
883
|
}
|
|
884
|
+
if (suppressedSyntheticMaxSlugs.has(String(e.slug ?? ""))
|
|
885
|
+
&& typeof e.default_reasoning_level === "string"
|
|
886
|
+
&& !levels.some(level => level.effort === e.default_reasoning_level)) {
|
|
887
|
+
e.default_reasoning_level = clampedDefaultEffort(
|
|
888
|
+
e.default_reasoning_level,
|
|
889
|
+
levels.flatMap(level => typeof level.effort === "string" ? [level.effort] : []),
|
|
890
|
+
);
|
|
891
|
+
}
|
|
873
892
|
}
|
|
874
893
|
if (wsEnabled) e.supports_websockets = true;
|
|
875
894
|
else {
|
|
@@ -886,7 +905,7 @@ export function mergeCatalogEntriesFromObservedState({
|
|
|
886
905
|
applyNativeVisibility(mergedEntries, disabledModels, alignedAccountBoundEntries.length > 0, observedNativeSlugs),
|
|
887
906
|
multiAgentMode,
|
|
888
907
|
multiAgentV2Enabled,
|
|
889
|
-
{ keepNativeChatGptOnV1, preserveDefaultMultiAgentVersion: isReserveCatalogProjection },
|
|
908
|
+
{ keepNativeChatGptOnV1, preserveDefaultMultiAgentVersion: isReserveCatalogProjection, nativeDefaults: nativeMultiAgentDefaults },
|
|
890
909
|
);
|
|
891
910
|
applyFullModelPickerOrder(versionedEntries, modelPickerOrder);
|
|
892
911
|
for (const entry of versionedEntries) {
|
|
@@ -937,6 +956,7 @@ export function mergeCatalogEntriesForSync(
|
|
|
937
956
|
),
|
|
938
957
|
openaiContextCap?: NativeContextLimitsInput,
|
|
939
958
|
keepNativeChatGptOnV1 = false,
|
|
959
|
+
nativeMultiAgentDefaults?: ReadonlyMap<string, string | null>,
|
|
940
960
|
): RawEntry[] {
|
|
941
961
|
// Retained for source compatibility with the original helper contract. Raw provider ids must
|
|
942
962
|
// not suppress same-named native rows; actual admitted combo entries own that decision now.
|
|
@@ -973,6 +993,7 @@ export function mergeCatalogEntriesForSync(
|
|
|
973
993
|
accountBoundEntries,
|
|
974
994
|
suppressedBareNativeSlugs,
|
|
975
995
|
openaiContextCap,
|
|
996
|
+
nativeMultiAgentDefaults,
|
|
976
997
|
policy: {
|
|
977
998
|
...CANONICAL_NATIVE_CATALOG_CONTENT_POLICY,
|
|
978
999
|
warningPolicy: "emit",
|
|
@@ -160,6 +160,7 @@ export function deriveEntry(
|
|
|
160
160
|
preserveExactReasoning
|
|
161
161
|
|| codexForwardNativeCapabilityAlias !== null
|
|
162
162
|
|| preservePinnedNativeCustomReasoning(model),
|
|
163
|
+
model?.suppressSyntheticMax === true,
|
|
163
164
|
);
|
|
164
165
|
// This exact provider/model pair is the ChatGPT/Codex forward surface. Keep the pinned
|
|
165
166
|
// native tool/search/responses-lite contract while preserving the routed slug and wire id.
|
|
@@ -208,7 +209,13 @@ export function deriveEntry(
|
|
|
208
209
|
};
|
|
209
210
|
if (isRouted) {
|
|
210
211
|
applyRoutedCodexToolMode(entry, model?.codexToolMode);
|
|
211
|
-
applyReasoningLevels(
|
|
212
|
+
applyReasoningLevels(
|
|
213
|
+
entry,
|
|
214
|
+
model?.reasoningEfforts,
|
|
215
|
+
model?.defaultReasoningEffort,
|
|
216
|
+
preserveExactReasoning || preservePinnedNativeCustomReasoning(model),
|
|
217
|
+
model?.suppressSyntheticMax === true,
|
|
218
|
+
);
|
|
212
219
|
}
|
|
213
220
|
else {
|
|
214
221
|
applyReasoningLevels(entry, isGpt56NativeSlug(slug) ? undefined : ["low", "medium", "high", "xhigh"]);
|