@bitkyc08/opencodex 2.14.1 → 2.15.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/gui/dist/assets/index-B5T5ADgY.js +76 -0
- package/gui/dist/assets/{index-DWhX3yMp.css → index-DUCH59lJ.css} +1 -1
- package/gui/dist/index.html +2 -2
- package/package.json +1 -1
- package/src/adapters/command-code.ts +15 -4
- package/src/adapters/cursor/effort-map.ts +4 -5
- package/src/adapters/cursor/request-builder.ts +55 -11
- package/src/adapters/cursor/tool-definitions.ts +24 -0
- package/src/adapters/kiro.ts +10 -1
- package/src/adapters/openai-chat.ts +5 -3
- package/src/adapters/openai-responses.ts +109 -0
- package/src/adapters/tool-catalog-nudge.ts +26 -4
- package/src/bridge.ts +50 -3
- package/src/cli/init.ts +4 -17
- package/src/codex/catalog/effort.ts +2 -1
- package/src/codex/catalog/metadata.ts +62 -12
- package/src/codex/catalog/native-models.ts +27 -0
- package/src/codex/catalog/parsing.ts +17 -2
- package/src/codex/catalog/provider-fetch.ts +47 -5
- package/src/codex/catalog/sync.ts +21 -7
- package/src/codex/catalog.ts +1 -1
- package/src/config.ts +79 -4
- package/src/generated/compatibility-version.json +54 -42
- package/src/generated/model-metadata.ts +1 -1
- package/src/lib/app-owned-memory-stores.ts +22 -0
- package/src/lib/tool-argument-integers.ts +158 -0
- package/src/oauth/index.ts +3 -0
- package/src/oauth/nous.ts +58 -9
- package/src/providers/antigravity-models.ts +93 -28
- package/src/providers/base-url-choices.ts +10 -0
- package/src/providers/command-code-efforts.ts +18 -0
- package/src/providers/model-rename-migration.ts +255 -0
- package/src/providers/model-rename-startup.ts +28 -0
- package/src/providers/openai-tier-startup.ts +31 -2
- package/src/providers/quota.ts +9 -2
- package/src/providers/registry.ts +13 -6
- package/src/responses/spill-store.ts +5 -1
- package/src/responses/state.ts +50 -2
- package/src/server/index.ts +2 -1
- package/src/server/management/api-key-usage.ts +31 -5
- package/src/server/management/logs-usage-routes.ts +48 -10
- package/src/server/management/provider-routes.ts +2 -1
- package/src/server/management/usage-summary-cache.ts +7 -1
- package/src/server/responses/collaboration.ts +12 -2
- package/src/server/responses/core.ts +33 -16
- package/src/server/startup-health-cache.ts +12 -0
- package/src/usage/expected-prices.ts +13 -0
- package/src/usage/log.ts +430 -12
- package/gui/dist/assets/index-DuaUVm_d.js +0 -76
package/src/oauth/nous.ts
CHANGED
|
@@ -39,6 +39,7 @@ import { join } from "node:path";
|
|
|
39
39
|
import type { OAuthController, OAuthCredentials } from "./types";
|
|
40
40
|
import { getAuthStorePath } from "./store";
|
|
41
41
|
import { atomicWriteFile, hardenConfigDir, hardenExistingSecret } from "../config";
|
|
42
|
+
import { BOUNDED_BODY_MAX_BYTES, readBoundedResponseBytes } from "../lib/bounded-body";
|
|
42
43
|
|
|
43
44
|
export const NOUS_PORTAL_BASE_URL = "https://portal.nousresearch.com";
|
|
44
45
|
export const NOUS_INFERENCE_BASE_URL = "https://inference-api.nousresearch.com/v1";
|
|
@@ -87,6 +88,44 @@ interface NousJwtPayload {
|
|
|
87
88
|
[key: string]: unknown;
|
|
88
89
|
}
|
|
89
90
|
|
|
91
|
+
async function readOAuthBytes(response: Response, signal: AbortSignal): Promise<Uint8Array> {
|
|
92
|
+
const { bytes, oversized } = await readBoundedResponseBytes(response, {
|
|
93
|
+
maxBytes: BOUNDED_BODY_MAX_BYTES,
|
|
94
|
+
signal,
|
|
95
|
+
});
|
|
96
|
+
if (oversized) {
|
|
97
|
+
throw new NousTokenError(
|
|
98
|
+
response.status,
|
|
99
|
+
"response_too_large",
|
|
100
|
+
`Nous Portal OAuth response exceeded the ${BOUNDED_BODY_MAX_BYTES}-byte limit`,
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
return bytes;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function parseOAuthJson(bytes: Uint8Array): unknown {
|
|
107
|
+
const parsed = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(bytes));
|
|
108
|
+
return parsed !== null && typeof parsed === "object" && !Array.isArray(parsed)
|
|
109
|
+
? parsed as Record<string, unknown>
|
|
110
|
+
: {};
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
async function readOAuthJson(response: Response, signal: AbortSignal): Promise<unknown> {
|
|
114
|
+
return parseOAuthJson(await readOAuthBytes(response, signal));
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
async function readOAuthJsonOrEmpty(response: Response, signal: AbortSignal): Promise<unknown> {
|
|
118
|
+
const bytes = await readOAuthBytes(response, signal);
|
|
119
|
+
try {
|
|
120
|
+
return parseOAuthJson(bytes);
|
|
121
|
+
} catch {
|
|
122
|
+
// Preserve the pre-PR behavior for empty/HTML/malformed JSON only. Body
|
|
123
|
+
// read failures, timeouts, caller cancellation, and size-limit errors have
|
|
124
|
+
// already escaped readOAuthBytes and must retain their real identity.
|
|
125
|
+
return {};
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
90
129
|
// ── Durable refresh-intent (review blocker #2) ──────────────────────────────
|
|
91
130
|
// A refresh-intent file records that we submitted `refreshToken` to the Portal
|
|
92
131
|
// and whether we are certain the rotated token was persisted. It lives next to
|
|
@@ -495,6 +534,7 @@ async function requestDeviceAuthorization(signal?: AbortSignal): Promise<{
|
|
|
495
534
|
expiresInMs: number;
|
|
496
535
|
intervalMs: number;
|
|
497
536
|
}> {
|
|
537
|
+
const effectiveSignal = requestSignal(signal);
|
|
498
538
|
const response = await fetch(`${resolvePortalBaseUrl()}/api/oauth/device/code`, {
|
|
499
539
|
method: "POST",
|
|
500
540
|
headers: { Accept: "application/json", "Content-Type": "application/x-www-form-urlencoded" },
|
|
@@ -503,14 +543,16 @@ async function requestDeviceAuthorization(signal?: AbortSignal): Promise<{
|
|
|
503
543
|
scope: NOUS_OAUTH_SCOPE,
|
|
504
544
|
}),
|
|
505
545
|
redirect: "error",
|
|
506
|
-
signal:
|
|
546
|
+
signal: effectiveSignal,
|
|
507
547
|
});
|
|
508
|
-
if (!response.ok)
|
|
548
|
+
if (!response.ok) {
|
|
549
|
+
throw tokenErrorFromPayload(response.status, await readOAuthJsonOrEmpty(response, effectiveSignal));
|
|
550
|
+
}
|
|
509
551
|
// A successful HTTP response may still carry an empty/HTML/non-JSON body.
|
|
510
552
|
// Fall back to an empty object so the required-field check below produces the
|
|
511
553
|
// clear "missing required fields" validation error instead of leaking a raw
|
|
512
554
|
// JSON parser exception.
|
|
513
|
-
const payload =
|
|
555
|
+
const payload = await readOAuthJsonOrEmpty(response, effectiveSignal) as NousDeviceAuthorizationResponse;
|
|
514
556
|
const userCode = nonEmptyString(payload.user_code);
|
|
515
557
|
const deviceCode = nonEmptyString(payload.device_code);
|
|
516
558
|
const verificationUri = nonEmptyString(payload.verification_uri_complete) ?? nonEmptyString(payload.verification_uri);
|
|
@@ -549,6 +591,7 @@ async function pollForToken(
|
|
|
549
591
|
while (Date.now() < deadline) {
|
|
550
592
|
if (signal?.aborted) throw new Error("Login cancelled");
|
|
551
593
|
let response: Response;
|
|
594
|
+
const effectiveSignal = requestSignal(signal);
|
|
552
595
|
try {
|
|
553
596
|
response = await fetch(`${resolvePortalBaseUrl()}/api/oauth/token`, {
|
|
554
597
|
method: "POST",
|
|
@@ -559,7 +602,7 @@ async function pollForToken(
|
|
|
559
602
|
grant_type: "urn:ietf:params:oauth:grant-type:device_code",
|
|
560
603
|
}),
|
|
561
604
|
redirect: "error",
|
|
562
|
-
signal:
|
|
605
|
+
signal: effectiveSignal,
|
|
563
606
|
});
|
|
564
607
|
} catch (netErr) {
|
|
565
608
|
// Genuine cancellation must abort immediately. Any other transport-level
|
|
@@ -571,13 +614,15 @@ async function pollForToken(
|
|
|
571
614
|
if (await sleepUntilDeadline(waitMs)) continue;
|
|
572
615
|
break;
|
|
573
616
|
}
|
|
617
|
+
// Parse under the same deadline that covered the request headers. Keep the
|
|
618
|
+
// read outside the fetch retry catch: a bounded-reader error or caller
|
|
619
|
+
// cancellation is an observed response failure, not a safe poll retry.
|
|
620
|
+
const payload = await readOAuthJsonOrEmpty(response, effectiveSignal) as NousTokenResponse;
|
|
574
621
|
// Parse once and pass the payload through to the failure path (review #8),
|
|
575
622
|
// so we never try to re-read a body that has already been consumed.
|
|
576
623
|
// Normalize a successful-but-non-object body (for example valid JSON
|
|
577
624
|
// `null`) to an empty object so the required-field validation below
|
|
578
625
|
// produces a terminal NousTokenError instead of a raw TypeError.
|
|
579
|
-
const parsed = (await response.json().catch(() => ({}))) as unknown;
|
|
580
|
-
const payload = (parsed && typeof parsed === "object" ? parsed : {}) as NousTokenResponse;
|
|
581
626
|
if (Date.now() >= deadline) break;
|
|
582
627
|
if (response.ok) return parseTokenPayload(payload, "");
|
|
583
628
|
const error = payload.error;
|
|
@@ -670,6 +715,7 @@ export async function refreshNousToken(refreshToken: string, signal?: AbortSigna
|
|
|
670
715
|
}
|
|
671
716
|
|
|
672
717
|
let response: Response;
|
|
718
|
+
const effectiveSignal = requestSignal(signal);
|
|
673
719
|
try {
|
|
674
720
|
response = await fetch(`${baseUrl}/api/oauth/token`, {
|
|
675
721
|
method: "POST",
|
|
@@ -683,7 +729,7 @@ export async function refreshNousToken(refreshToken: string, signal?: AbortSigna
|
|
|
683
729
|
client_id: NOUS_OAUTH_CLIENT_ID,
|
|
684
730
|
}),
|
|
685
731
|
redirect: "error",
|
|
686
|
-
signal:
|
|
732
|
+
signal: effectiveSignal,
|
|
687
733
|
});
|
|
688
734
|
} catch (netErr) {
|
|
689
735
|
// The request may have reached the server and rotated the token even on a
|
|
@@ -703,7 +749,6 @@ export async function refreshNousToken(refreshToken: string, signal?: AbortSigna
|
|
|
703
749
|
|
|
704
750
|
if (!response.ok) {
|
|
705
751
|
const status = response.status;
|
|
706
|
-
const payload = await response.json().catch(() => ({}));
|
|
707
752
|
// The request reached the Portal's token endpoint. A non-2xx response does
|
|
708
753
|
// NOT establish that the single-use refresh token was not consumed: 429
|
|
709
754
|
// rate limits, unknown/custom 4xx, and gateway-generated client-class
|
|
@@ -719,6 +764,7 @@ export async function refreshNousToken(refreshToken: string, signal?: AbortSigna
|
|
|
719
764
|
// The pre-dispatch "submitted" intent is still on disk, which also
|
|
720
765
|
// blocks replay; surface the original HTTP error below.
|
|
721
766
|
}
|
|
767
|
+
const payload = await readOAuthJsonOrEmpty(response, effectiveSignal);
|
|
722
768
|
throw tokenErrorFromPayload(status, payload);
|
|
723
769
|
}
|
|
724
770
|
|
|
@@ -727,7 +773,10 @@ export async function refreshNousToken(refreshToken: string, signal?: AbortSigna
|
|
|
727
773
|
// replay it. On success we deliberately LEAVE the intent as "submitted"
|
|
728
774
|
// (the store clears it once the rotated token is persisted).
|
|
729
775
|
try {
|
|
730
|
-
const creds = parseTokenPayload(
|
|
776
|
+
const creds = parseTokenPayload(
|
|
777
|
+
(await readOAuthJson(response, effectiveSignal)) as NousTokenResponse,
|
|
778
|
+
refreshToken,
|
|
779
|
+
);
|
|
731
780
|
return creds;
|
|
732
781
|
} catch (e) {
|
|
733
782
|
try {
|
|
@@ -10,10 +10,39 @@ import { isValidModelDiscoveryModelId, MODEL_DISCOVERY_MAX_MODELS } from "./mode
|
|
|
10
10
|
// returned wire id remains visible so an unavailable tier cannot be selected.
|
|
11
11
|
|
|
12
12
|
// ── Wire IDs (what CCA :fetchAvailableModels returns) ──
|
|
13
|
+
|
|
14
|
+
/** Current Antigravity Flash generation. */
|
|
15
|
+
const GEMINI_FLASH_CURRENT = "gemini-3.7-flash";
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Retired Flash ids → the reasoning tier they used to encode.
|
|
19
|
+
*
|
|
20
|
+
* Google pulls the previous Flash model from CCA almost immediately when the next
|
|
21
|
+
* one ships, so a saved selection cannot keep pointing at it. A flat alias would
|
|
22
|
+
* strand the user's tier: `resolveAntigravityEffortWireModel` rule 1 treats any
|
|
23
|
+
* alias as "the suffix IS the effort" and deliberately sends no thinkingConfig, so
|
|
24
|
+
* `gemini-3.6-flash-high` would silently become an untiered 3.7 call. Carrying the
|
|
25
|
+
* level here preserves what the user actually chose.
|
|
26
|
+
*
|
|
27
|
+
* 3.7 exposes tiers through `thinkingLevel` on ONE wire id rather than through
|
|
28
|
+
* suffixed wire ids, which is why the mapping is id → level and not id → id.
|
|
29
|
+
*/
|
|
30
|
+
const RETIRED_FLASH_TIERS: Record<string, string> = {
|
|
31
|
+
// 3.6 generation.
|
|
32
|
+
"gemini-3.6-flash": "medium", // bare base carried a medium default
|
|
33
|
+
"gemini-3.6-flash-low": "low",
|
|
34
|
+
"gemini-3.6-flash-medium": "medium",
|
|
35
|
+
"gemini-3.6-flash-high": "high",
|
|
36
|
+
// 3.5 generation — these already pointed at 3.6 wire ids, which are now dead too.
|
|
37
|
+
"gemini-3.5-flash-extra-low": "low",
|
|
38
|
+
"gemini-3.5-flash-low": "medium",
|
|
39
|
+
"gemini-3.5-flash-mid": "medium",
|
|
40
|
+
"gemini-3.5-flash-high": "high",
|
|
41
|
+
"gemini-3-flash-agent": "high",
|
|
42
|
+
};
|
|
43
|
+
|
|
13
44
|
const ANTIGRAVITY_WIRE_MODELS = [
|
|
14
|
-
"gemini-3.
|
|
15
|
-
"gemini-3.6-flash-medium",
|
|
16
|
-
"gemini-3.6-flash-high",
|
|
45
|
+
"gemini-3.7-flash",
|
|
17
46
|
"gemini-3.1-pro-low",
|
|
18
47
|
"gemini-pro-agent",
|
|
19
48
|
"gemini-3.1-flash-image",
|
|
@@ -23,9 +52,6 @@ const ANTIGRAVITY_WIRE_MODELS = [
|
|
|
23
52
|
];
|
|
24
53
|
|
|
25
54
|
const ANTIGRAVITY_PICKER_MODEL_BY_WIRE_ID: Record<string, string> = {
|
|
26
|
-
"gemini-3.6-flash-low": "gemini-3.6-flash",
|
|
27
|
-
"gemini-3.6-flash-medium": "gemini-3.6-flash",
|
|
28
|
-
"gemini-3.6-flash-high": "gemini-3.6-flash",
|
|
29
55
|
"gemini-3.1-pro-low": "gemini-3.1-pro",
|
|
30
56
|
"gemini-pro-agent": "gemini-3.1-pro",
|
|
31
57
|
};
|
|
@@ -41,7 +67,7 @@ const ANTIGRAVITY_WIRE_IDS_BY_PICKER_MODEL: Record<string, string[]> = Object.en
|
|
|
41
67
|
// Gemini models: effort → wire model suffix (official agy UI pattern).
|
|
42
68
|
// Claude Opus: effort → thinkingConfig.thinkingLevel (CLIProxyAPI proven pattern).
|
|
43
69
|
export const ANTIGRAVITY_MODEL_EFFORTS: Record<string, string[]> = {
|
|
44
|
-
"gemini-3.
|
|
70
|
+
"gemini-3.7-flash": ["low", "medium", "high"],
|
|
45
71
|
"gemini-3.1-pro": ["low", "high"],
|
|
46
72
|
"claude-sonnet-4-6": ["low", "medium", "high", "max"],
|
|
47
73
|
"claude-opus-4-6-thinking": ["low", "medium", "high", "max"],
|
|
@@ -49,11 +75,6 @@ export const ANTIGRAVITY_MODEL_EFFORTS: Record<string, string[]> = {
|
|
|
49
75
|
|
|
50
76
|
// ── Effort → wire model map for Gemini base models ──
|
|
51
77
|
const ANTIGRAVITY_EFFORT_WIRE_MAP: Record<string, Record<string, string>> = {
|
|
52
|
-
"gemini-3.6-flash": {
|
|
53
|
-
low: "gemini-3.6-flash-low",
|
|
54
|
-
medium: "gemini-3.6-flash-medium",
|
|
55
|
-
high: "gemini-3.6-flash-high",
|
|
56
|
-
},
|
|
57
78
|
"gemini-3.1-pro": {
|
|
58
79
|
low: "gemini-3.1-pro-low",
|
|
59
80
|
high: "gemini-pro-agent",
|
|
@@ -62,11 +83,20 @@ const ANTIGRAVITY_EFFORT_WIRE_MAP: Record<string, Record<string, string>> = {
|
|
|
62
83
|
|
|
63
84
|
// ── Default effort per Gemini base model ──
|
|
64
85
|
const ANTIGRAVITY_DEFAULT_EFFORT: Record<string, string> = {
|
|
65
|
-
"gemini-3.6-flash": "medium",
|
|
66
86
|
"gemini-3.1-pro": "high",
|
|
67
87
|
};
|
|
68
88
|
|
|
69
|
-
|
|
89
|
+
/**
|
|
90
|
+
* Gemini base models whose efforts ride on `thinkingLevel` against a single wire id
|
|
91
|
+
* instead of suffixed wire ids, with the level applied when the caller names none.
|
|
92
|
+
*/
|
|
93
|
+
const ANTIGRAVITY_THINKING_LEVEL_MODELS: Record<string, string> = {
|
|
94
|
+
"gemini-3.7-flash": "medium",
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
// `minimal` is deliberately absent: Google documents it as unsupported for the current
|
|
98
|
+
// Flash generation, where it is an error rather than a quieter tier.
|
|
99
|
+
const ANTIGRAVITY_THINKING_LEVELS = new Set(["low", "medium", "high"]);
|
|
70
100
|
|
|
71
101
|
function resolveAntigravityThinkingLevel(effort: string): string | undefined {
|
|
72
102
|
if (effort === "xhigh" || effort === "max" || effort === "ultra") return "high";
|
|
@@ -83,16 +113,17 @@ const ANTIGRAVITY_VISIBLE_MODEL_ALIASES: Record<string, string> = {
|
|
|
83
113
|
// Wire suffix IDs are identity aliases — they resolve to themselves so saved configs
|
|
84
114
|
// with explicit suffixes (e.g. gemini-3.6-flash-low) continue to work.
|
|
85
115
|
const ANTIGRAVITY_COMPATIBILITY_MODEL_ALIASES: Record<string, string> = {
|
|
86
|
-
"gemini-3.6-flash-low": "gemini-3.6-flash-low",
|
|
87
|
-
"gemini-3.6-flash-medium": "gemini-3.6-flash-medium",
|
|
88
|
-
"gemini-3.6-flash-high": "gemini-3.6-flash-high",
|
|
89
116
|
"gemini-3.1-pro-low": "gemini-3.1-pro-low",
|
|
90
117
|
"gemini-pro-agent": "gemini-pro-agent",
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
118
|
+
// ── Retired Flash generations ──
|
|
119
|
+
// Google takes the previous Antigravity Flash model offline almost immediately
|
|
120
|
+
// once its successor ships, so 3.6 (and the 3.5 ids that used to land on it)
|
|
121
|
+
// route to 3.7. These stay in the alias map — not only in the tier map below —
|
|
122
|
+
// because `parseAntigravityAvailableModels` uses THIS map to keep a stale CCA
|
|
123
|
+
// payload from republishing a dead wire id as a picker row.
|
|
124
|
+
...Object.fromEntries(
|
|
125
|
+
Object.keys(RETIRED_FLASH_TIERS).map(retired => [retired, GEMINI_FLASH_CURRENT]),
|
|
126
|
+
),
|
|
96
127
|
};
|
|
97
128
|
|
|
98
129
|
export const ANTIGRAVITY_MODEL_ALIASES: Record<string, string> = {
|
|
@@ -102,7 +133,7 @@ export const ANTIGRAVITY_MODEL_ALIASES: Record<string, string> = {
|
|
|
102
133
|
|
|
103
134
|
// Picker-visible: collapsed base models only.
|
|
104
135
|
export const ANTIGRAVITY_MODELS = [
|
|
105
|
-
|
|
136
|
+
GEMINI_FLASH_CURRENT,
|
|
106
137
|
"gemini-3.1-pro",
|
|
107
138
|
"gemini-3.1-flash-image",
|
|
108
139
|
"claude-sonnet-4-6",
|
|
@@ -112,9 +143,7 @@ export const ANTIGRAVITY_MODELS = [
|
|
|
112
143
|
|
|
113
144
|
// Context windows from the upstream `:fetchAvailableModels` maxTokens per model.
|
|
114
145
|
const ANTIGRAVITY_WIRE_MODEL_CONTEXT_WINDOWS: Record<string, number> = {
|
|
115
|
-
"gemini-3.
|
|
116
|
-
"gemini-3.6-flash-medium": 1_048_576,
|
|
117
|
-
"gemini-3.6-flash-high": 1_048_576,
|
|
146
|
+
"gemini-3.7-flash": 1_048_576,
|
|
118
147
|
"gemini-3.1-pro-low": 1_048_576,
|
|
119
148
|
"gemini-pro-agent": 1_048_576,
|
|
120
149
|
"gemini-3.1-flash-image": 1_048_576,
|
|
@@ -125,7 +154,7 @@ const ANTIGRAVITY_WIRE_MODEL_CONTEXT_WINDOWS: Record<string, number> = {
|
|
|
125
154
|
|
|
126
155
|
export const ANTIGRAVITY_MODEL_CONTEXT_WINDOWS: Record<string, number> = {
|
|
127
156
|
// Collapsed base IDs — explicit entries for the picker.
|
|
128
|
-
"gemini-3.
|
|
157
|
+
"gemini-3.7-flash": 1_048_576,
|
|
129
158
|
"gemini-3.1-pro": 1_048_576,
|
|
130
159
|
// Wire IDs and aliases via derivation.
|
|
131
160
|
...ANTIGRAVITY_WIRE_MODEL_CONTEXT_WINDOWS,
|
|
@@ -138,7 +167,11 @@ export const ANTIGRAVITY_MODEL_CONTEXT_WINDOWS: Record<string, number> = {
|
|
|
138
167
|
};
|
|
139
168
|
|
|
140
169
|
export const ANTIGRAVITY_MODEL_INPUT_MODALITIES: Record<string, string[]> = {
|
|
141
|
-
|
|
170
|
+
// Google documents 3.7 Flash as also accepting video, audio and PDF, but this proxy
|
|
171
|
+
// carries only text and image parts (`OcxImageContent`, src/types.ts) and the Codex
|
|
172
|
+
// catalog normalizes `input_modalities` against a closed enum. Advertising a modality
|
|
173
|
+
// the wire cannot carry would be a promise we break at request time.
|
|
174
|
+
"gemini-3.7-flash": ["text", "image"],
|
|
142
175
|
"gemini-3.1-pro": ["text", "image"],
|
|
143
176
|
"gemini-3.1-flash-image": ["text", "image"],
|
|
144
177
|
"claude-sonnet-4-6": ["text", "image"],
|
|
@@ -244,6 +277,11 @@ export function isAntigravitySuffixModelId(modelId: string): boolean {
|
|
|
244
277
|
return !(ANTIGRAVITY_MODELS as string[]).includes(modelId);
|
|
245
278
|
}
|
|
246
279
|
|
|
280
|
+
/** The reasoning tier a retired Flash id used to encode, if it is one. */
|
|
281
|
+
export function retiredAntigravityFlashTier(modelId: string): string | undefined {
|
|
282
|
+
return RETIRED_FLASH_TIERS[modelId];
|
|
283
|
+
}
|
|
284
|
+
|
|
247
285
|
/**
|
|
248
286
|
* Resolve a picker-visible base model + optional reasoning effort to the CCA wire model ID.
|
|
249
287
|
*
|
|
@@ -258,11 +296,32 @@ export function resolveAntigravityEffortWireModel(
|
|
|
258
296
|
modelId: string,
|
|
259
297
|
effort?: string,
|
|
260
298
|
): { wireModelId: string; thinkingLevel?: string } {
|
|
299
|
+
// Rule 0: retired Flash id — Google has taken the wire id offline, so route to the
|
|
300
|
+
// current generation and carry the tier the retired id encoded. This runs BEFORE the
|
|
301
|
+
// suffix check because those ids are aliases, and rule 1 would drop the tier.
|
|
302
|
+
const retiredTier = RETIRED_FLASH_TIERS[modelId];
|
|
303
|
+
if (retiredTier) {
|
|
304
|
+
return {
|
|
305
|
+
wireModelId: GEMINI_FLASH_CURRENT,
|
|
306
|
+
thinkingLevel: effort ? resolveAntigravityThinkingLevel(effort) ?? retiredTier : retiredTier,
|
|
307
|
+
};
|
|
308
|
+
}
|
|
309
|
+
|
|
261
310
|
// Rule 1: suffix/compat alias — suffix IS the effort.
|
|
262
311
|
if (isAntigravitySuffixModelId(modelId)) {
|
|
263
312
|
return { wireModelId: resolveAntigravityWireModelId(modelId) };
|
|
264
313
|
}
|
|
265
314
|
|
|
315
|
+
// Rule 1b: single-wire-id Gemini model whose tiers ride on thinkingLevel. Without
|
|
316
|
+
// this the model falls to rule 5 and silently loses reasoning control entirely.
|
|
317
|
+
const defaultLevel = ANTIGRAVITY_THINKING_LEVEL_MODELS[modelId];
|
|
318
|
+
if (defaultLevel) {
|
|
319
|
+
return {
|
|
320
|
+
wireModelId: modelId,
|
|
321
|
+
thinkingLevel: effort ? resolveAntigravityThinkingLevel(effort) ?? defaultLevel : defaultLevel,
|
|
322
|
+
};
|
|
323
|
+
}
|
|
324
|
+
|
|
266
325
|
// Rule 2/3: mapped Gemini base model.
|
|
267
326
|
const effortMap = ANTIGRAVITY_EFFORT_WIRE_MAP[modelId];
|
|
268
327
|
if (effortMap) {
|
|
@@ -301,6 +360,12 @@ const ANTIGRAVITY_USAGE_BASE_BY_ID: Record<string, string> = (() => {
|
|
|
301
360
|
// If alias is itself a base/wire already mapped, keep that mapping.
|
|
302
361
|
else if (rev[wire]) rev[alias] = rev[wire]!;
|
|
303
362
|
}
|
|
363
|
+
// Retired ids keep their OWN identity for usage aggregation, overriding the alias
|
|
364
|
+
// pass above. Routing sends new 3.6 calls to 3.7, but a usage row written months ago
|
|
365
|
+
// records a model the user actually called: relabelling it would move historical spend
|
|
366
|
+
// onto a model that did not exist then, and away from the 3.6 price row that still
|
|
367
|
+
// prices it correctly. Retirement changes what we CALL, not what we RECORD.
|
|
368
|
+
for (const retired of Object.keys(RETIRED_FLASH_TIERS)) rev[retired] = retired;
|
|
304
369
|
// Visible aliases that only appear in ANTIGRAVITY_VISIBLE_MODEL_ALIASES are already
|
|
305
370
|
// included via ANTIGRAVITY_MODEL_ALIASES. Identity bases without effort maps remain.
|
|
306
371
|
return rev;
|
|
@@ -62,3 +62,13 @@ export function matchBaseUrlChoice(
|
|
|
62
62
|
}
|
|
63
63
|
return choices.some(c => c.id === "custom") ? "custom" : choices[0]!.id;
|
|
64
64
|
}
|
|
65
|
+
|
|
66
|
+
/** Moonshot/Kimi API endpoint presets (international default; China selectable). */
|
|
67
|
+
export const MOONSHOT_INTL_BASE_URL = "https://api.moonshot.ai/v1";
|
|
68
|
+
export const MOONSHOT_CN_BASE_URL = "https://api.moonshot.cn/v1";
|
|
69
|
+
|
|
70
|
+
export const MOONSHOT_BASE_URL_CHOICES: readonly ProviderBaseUrlChoice[] = [
|
|
71
|
+
{ id: "international", label: "International (.ai)", baseUrl: MOONSHOT_INTL_BASE_URL },
|
|
72
|
+
{ id: "china", label: "China (.cn)", baseUrl: MOONSHOT_CN_BASE_URL },
|
|
73
|
+
{ id: "custom", label: "Custom" },
|
|
74
|
+
];
|
|
@@ -13,6 +13,24 @@ const COMMAND_CODE_MODEL_EFFORTS = {
|
|
|
13
13
|
efforts: ["high", "max"],
|
|
14
14
|
profileUrl: "https://commandcode.ai/models/glm-5-2",
|
|
15
15
|
},
|
|
16
|
+
// Muse Spark: CLI currently prints "has no adjustable reasoning effort" and
|
|
17
|
+
// blocks --effort locally, but the upstream /alpha/generate endpoint accepts
|
|
18
|
+
// reasoning_effort low..max for meta/muse-spark-1.2-contributor (verified
|
|
19
|
+
// 2026-08-13: direct upstream POST with low/medium/high/xhigh/max all 200,
|
|
20
|
+
// ultra 400; reasoningTokens differentiated 114..253; proxy previously stripped
|
|
21
|
+
// the field so effort changes had no effect).
|
|
22
|
+
"meta/muse-spark-1.2": {
|
|
23
|
+
efforts: ["low", "medium", "high", "xhigh", "max"],
|
|
24
|
+
profileUrl: "https://commandcode.ai/models/meta-muse-spark-1.2",
|
|
25
|
+
},
|
|
26
|
+
"meta/muse-spark-1.2-contributor": {
|
|
27
|
+
efforts: ["low", "medium", "high", "xhigh", "max"],
|
|
28
|
+
profileUrl: "https://commandcode.ai/models/meta-muse-spark-1.2-contributor",
|
|
29
|
+
},
|
|
30
|
+
"meta/muse-spark-1.1": {
|
|
31
|
+
efforts: ["low", "medium", "high", "xhigh", "max"],
|
|
32
|
+
profileUrl: "https://commandcode.ai/models/meta-muse-spark-1.1",
|
|
33
|
+
},
|
|
16
34
|
} as const;
|
|
17
35
|
|
|
18
36
|
/**
|
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
// Registry model renames do not reach a saved provider config on their own.
|
|
2
|
+
//
|
|
3
|
+
// `reconcileOAuthProviders` refuses to touch a row whose `authMode` is not
|
|
4
|
+
// `oauth` (src/oauth/index.ts), and `enrichProviderFromRegistry` is fill-only by
|
|
5
|
+
// design: it backfills a MISSING field and never rewrites a present one, so a
|
|
6
|
+
// user's hand-edited model list survives an upgrade. Both postures are correct.
|
|
7
|
+
// Their gap is the case where the registry did not ADD a model but RENAMED one:
|
|
8
|
+
// the saved row keeps a retired id forever, the supported id never appears, and
|
|
9
|
+
// the capability metadata stays keyed to an id the vendor is taking offline
|
|
10
|
+
// (issue #1610 — `qwen3.8-max-preview` persisted through the `qwen3.8-max`
|
|
11
|
+
// rename in six separate fields, including a reasoning ladder that had since
|
|
12
|
+
// diverged from the registry's).
|
|
13
|
+
//
|
|
14
|
+
// This migration is deliberately NOT general reconciliation. It rewrites exactly
|
|
15
|
+
// one thing: an id this file declares retired, on a provider that still carries
|
|
16
|
+
// the registry's transport, and only when the registry currently seeds the
|
|
17
|
+
// replacement. Everything else in the row is left alone.
|
|
18
|
+
|
|
19
|
+
import { PROVIDER_REGISTRY } from "./registry";
|
|
20
|
+
import type { OcxConfig, OcxProviderConfig } from "../types";
|
|
21
|
+
|
|
22
|
+
export interface ModelRename {
|
|
23
|
+
/** Registry provider id whose saved rows may carry the retired model id. */
|
|
24
|
+
provider: string;
|
|
25
|
+
from: string;
|
|
26
|
+
to: string;
|
|
27
|
+
/** Why the vendor retired it, for the startup warning and future readers. */
|
|
28
|
+
reason: string;
|
|
29
|
+
/**
|
|
30
|
+
* Drop the retired key from `modelReasoningEffortMap` instead of renaming it.
|
|
31
|
+
*
|
|
32
|
+
* Renaming preserves the VALUE, which is right for records whose values describe the
|
|
33
|
+
* model — a context window or an effort ladder survives a rename — and wrong for the
|
|
34
|
+
* one record whose values are themselves wire ids. An effort map saved as
|
|
35
|
+
* `high -> gemini-3.6-flash-high` would keep naming a dead wire id under the new key,
|
|
36
|
+
* and the adapter maps effort BEFORE resolving CCA routing, so that value arrives as
|
|
37
|
+
* an unrecognised effort and silently degrades to the default tier.
|
|
38
|
+
*
|
|
39
|
+
* Only that one record is dropped. Emptying the others would be worse than the bug:
|
|
40
|
+
* catalog enrichment treats an existing `{}` as "already populated" and will not
|
|
41
|
+
* restore the registry's records, so the migrated user would keep routing correctly
|
|
42
|
+
* but lose the reasoning picker entirely.
|
|
43
|
+
*/
|
|
44
|
+
dropReasoningEffortMap?: boolean;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Renames already applied to `PROVIDER_REGISTRY`. An entry stays here after the
|
|
49
|
+
* registry moves on: it is what repairs configs saved before that move. Removing
|
|
50
|
+
* one strands every config that has not started since the rename shipped.
|
|
51
|
+
*/
|
|
52
|
+
export const MODEL_RENAMES: readonly ModelRename[] = [
|
|
53
|
+
{
|
|
54
|
+
provider: "alibaba-token-plan",
|
|
55
|
+
from: "qwen3.8-max-preview",
|
|
56
|
+
to: "qwen3.8-max",
|
|
57
|
+
reason: "Alibaba shipped Qwen3.8-Max as stable and documents the preview endpoint as liable to be taken offline once preview concludes",
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
provider: "alibaba-token-plan-intl",
|
|
61
|
+
from: "qwen3.8-max-preview",
|
|
62
|
+
to: "qwen3.8-max",
|
|
63
|
+
reason: "Alibaba shipped Qwen3.8-Max as stable and documents the preview endpoint as liable to be taken offline once preview concludes",
|
|
64
|
+
},
|
|
65
|
+
// Antigravity Flash generations. Google takes the previous Flash model off Cloud Code
|
|
66
|
+
// Assist almost immediately when the next ships, so a saved 3.6 (or older 3.5) id is a
|
|
67
|
+
// dead selection rather than a merely outdated one. Routing already redirects these ids
|
|
68
|
+
// at request time; this migration repairs the saved config so the picker, the allowlist
|
|
69
|
+
// and the capability maps stop naming a model the backend no longer serves.
|
|
70
|
+
...(["gemini-3.6-flash", "gemini-3.6-flash-low", "gemini-3.6-flash-medium", "gemini-3.6-flash-high",
|
|
71
|
+
"gemini-3.5-flash-extra-low", "gemini-3.5-flash-low", "gemini-3.5-flash-mid", "gemini-3.5-flash-high",
|
|
72
|
+
"gemini-3-flash-agent"] as const).map(from => ({
|
|
73
|
+
provider: "google-antigravity",
|
|
74
|
+
from,
|
|
75
|
+
to: "gemini-3.7-flash",
|
|
76
|
+
reason: "Google retires the previous Antigravity Flash generation from Cloud Code Assist when its successor ships, so the saved id no longer resolves to a live model",
|
|
77
|
+
// The retired Flash tiers were wire ids, so any saved per-model record keyed by one
|
|
78
|
+
// may also hold one as a value. 3.7 expresses tiers as thinkingLevel names instead.
|
|
79
|
+
dropReasoningEffortMap: true,
|
|
80
|
+
})),
|
|
81
|
+
];
|
|
82
|
+
|
|
83
|
+
/** Provider fields that key metadata by model id. */
|
|
84
|
+
const MODEL_KEYED_RECORDS = [
|
|
85
|
+
"modelContextWindows",
|
|
86
|
+
"modelMaxOutputTokens",
|
|
87
|
+
"modelInputModalities",
|
|
88
|
+
"modelReasoningEfforts",
|
|
89
|
+
"modelDefaultReasoningEfforts",
|
|
90
|
+
"modelReasoningEffortMap",
|
|
91
|
+
] as const;
|
|
92
|
+
|
|
93
|
+
/** Provider fields that are flat lists of model ids. */
|
|
94
|
+
const MODEL_ID_LISTS = [
|
|
95
|
+
"models",
|
|
96
|
+
// A retired id left here is worse than a stale label: `filterCatalogModels` treats
|
|
97
|
+
// `selectedModels` as an exact-match allowlist, so a user who allowlisted only the
|
|
98
|
+
// retired model gets NO replacement row at all — the model silently vanishes from
|
|
99
|
+
// their catalog instead of being renamed. OAuth reconciliation does not cover this
|
|
100
|
+
// field, so the rename has to.
|
|
101
|
+
"selectedModels",
|
|
102
|
+
"noVisionModels",
|
|
103
|
+
"noReasoningModels",
|
|
104
|
+
"noTemperatureModels",
|
|
105
|
+
"noTopPModels",
|
|
106
|
+
"noPenaltyModels",
|
|
107
|
+
"autoToolChoiceOnlyModels",
|
|
108
|
+
"preserveReasoningContentModels",
|
|
109
|
+
"thinkingBudgetModels",
|
|
110
|
+
"directReasoningEffortModels",
|
|
111
|
+
] as const;
|
|
112
|
+
|
|
113
|
+
function renameInList(value: unknown, from: string, to: string): string[] | null {
|
|
114
|
+
if (!Array.isArray(value) || !value.includes(from)) return null;
|
|
115
|
+
const seen = new Set<string>();
|
|
116
|
+
const next: string[] = [];
|
|
117
|
+
// Rename in place to preserve ordering, and collapse a duplicate if the target
|
|
118
|
+
// id was already present alongside the retired one.
|
|
119
|
+
for (const entry of value) {
|
|
120
|
+
if (typeof entry !== "string") continue;
|
|
121
|
+
const mapped = entry === from ? to : entry;
|
|
122
|
+
if (seen.has(mapped)) continue;
|
|
123
|
+
seen.add(mapped);
|
|
124
|
+
next.push(mapped);
|
|
125
|
+
}
|
|
126
|
+
return next;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function renameInRecord(value: unknown, from: string, to: string): Record<string, unknown> | null {
|
|
130
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return null;
|
|
131
|
+
const record = value as Record<string, unknown>;
|
|
132
|
+
if (!(from in record)) return null;
|
|
133
|
+
const next: Record<string, unknown> = {};
|
|
134
|
+
for (const [key, entry] of Object.entries(record)) {
|
|
135
|
+
const mapped = key === from ? to : key;
|
|
136
|
+
if (mapped in next) continue;
|
|
137
|
+
// An explicit entry already saved under the new id is the newer intent.
|
|
138
|
+
next[mapped] = key === from && to in record ? record[to] : entry;
|
|
139
|
+
}
|
|
140
|
+
return next;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/** Drop the retired key entirely, leaving any entry already saved under the new id. */
|
|
144
|
+
function dropFromRecord(value: unknown, from: string): Record<string, unknown> | null {
|
|
145
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return null;
|
|
146
|
+
const record = value as Record<string, unknown>;
|
|
147
|
+
if (!(from in record)) return null;
|
|
148
|
+
const next: Record<string, unknown> = {};
|
|
149
|
+
for (const [key, entry] of Object.entries(record)) {
|
|
150
|
+
if (key === from) continue;
|
|
151
|
+
next[key] = entry;
|
|
152
|
+
}
|
|
153
|
+
return next;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* `provider/model` rows in the top-level `disabledModels` list.
|
|
158
|
+
*
|
|
159
|
+
* The retired row is DROPPED rather than renamed: carrying its disabled state to
|
|
160
|
+
* the new id would hide the supported model behind a toggle the user set for a
|
|
161
|
+
* different model. An existing row for the new id is left untouched.
|
|
162
|
+
*/
|
|
163
|
+
function renameDisabledModels(config: OcxConfig, rename: ModelRename): boolean {
|
|
164
|
+
const list = config.disabledModels;
|
|
165
|
+
if (!Array.isArray(list)) return false;
|
|
166
|
+
const retired = `${rename.provider}/${rename.from}`;
|
|
167
|
+
if (!list.includes(retired)) return false;
|
|
168
|
+
config.disabledModels = list.filter(entry => entry !== retired);
|
|
169
|
+
return true;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Only migrate a row that still points at the registry's own endpoint. A user who
|
|
174
|
+
* repointed `baseUrl` at a different vendor owns their model ids.
|
|
175
|
+
*/
|
|
176
|
+
function providerStillMatchesRegistry(name: string, prov: OcxProviderConfig): boolean {
|
|
177
|
+
const entry = PROVIDER_REGISTRY.find(row => row.id === name);
|
|
178
|
+
if (!entry) return false;
|
|
179
|
+
if (!prov.baseUrl || !entry.baseUrl) return true;
|
|
180
|
+
const choices = entry.baseUrlChoices?.map(choice => choice.baseUrl) ?? [];
|
|
181
|
+
const known = [entry.baseUrl, ...choices]
|
|
182
|
+
.filter((url): url is string => typeof url === "string")
|
|
183
|
+
.map(url => url.replace(/\/+$/, ""));
|
|
184
|
+
return known.includes(prov.baseUrl.replace(/\/+$/, ""));
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/** Guard against a stale rename: only apply when the registry actually seeds `to`. */
|
|
188
|
+
function registrySeedsTarget(rename: ModelRename): boolean {
|
|
189
|
+
const entry = PROVIDER_REGISTRY.find(row => row.id === rename.provider);
|
|
190
|
+
return !!entry?.models?.includes(rename.to);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
export interface ModelRenameProjection {
|
|
194
|
+
config: OcxConfig;
|
|
195
|
+
changed: boolean;
|
|
196
|
+
warnings: string[];
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Pure projection: apply every applicable rename and report what changed. The
|
|
201
|
+
* caller decides whether to persist.
|
|
202
|
+
*/
|
|
203
|
+
export function projectModelRenames(
|
|
204
|
+
config: OcxConfig,
|
|
205
|
+
renames: readonly ModelRename[] = MODEL_RENAMES,
|
|
206
|
+
): ModelRenameProjection {
|
|
207
|
+
const warnings: string[] = [];
|
|
208
|
+
let changed = false;
|
|
209
|
+
|
|
210
|
+
for (const rename of renames) {
|
|
211
|
+
const prov = config.providers?.[rename.provider];
|
|
212
|
+
if (!prov) continue;
|
|
213
|
+
if (!registrySeedsTarget(rename)) {
|
|
214
|
+
warnings.push(
|
|
215
|
+
`registry no longer seeds "${rename.to}" for "${rename.provider}"; skipping the `
|
|
216
|
+
+ `"${rename.from}" rename rather than writing an id the registry does not know.`,
|
|
217
|
+
);
|
|
218
|
+
continue;
|
|
219
|
+
}
|
|
220
|
+
if (!providerStillMatchesRegistry(rename.provider, prov)) continue;
|
|
221
|
+
|
|
222
|
+
// Provider config is a closed interface, so index through one unknown-cast
|
|
223
|
+
// view rather than casting at each assignment.
|
|
224
|
+
const row = prov as unknown as Record<string, unknown>;
|
|
225
|
+
let touched = false;
|
|
226
|
+
for (const field of MODEL_ID_LISTS) {
|
|
227
|
+
const next = renameInList(row[field], rename.from, rename.to);
|
|
228
|
+
if (!next) continue;
|
|
229
|
+
row[field] = next;
|
|
230
|
+
touched = true;
|
|
231
|
+
}
|
|
232
|
+
for (const field of MODEL_KEYED_RECORDS) {
|
|
233
|
+
const next = rename.dropReasoningEffortMap && field === "modelReasoningEffortMap"
|
|
234
|
+
? dropFromRecord(row[field], rename.from)
|
|
235
|
+
: renameInRecord(row[field], rename.from, rename.to);
|
|
236
|
+
if (!next) continue;
|
|
237
|
+
row[field] = next;
|
|
238
|
+
touched = true;
|
|
239
|
+
}
|
|
240
|
+
if (prov.defaultModel === rename.from) {
|
|
241
|
+
prov.defaultModel = rename.to;
|
|
242
|
+
touched = true;
|
|
243
|
+
}
|
|
244
|
+
if (renameDisabledModels(config, rename)) touched = true;
|
|
245
|
+
|
|
246
|
+
if (touched) {
|
|
247
|
+
changed = true;
|
|
248
|
+
warnings.push(
|
|
249
|
+
`renamed "${rename.provider}/${rename.from}" to "${rename.to}" in the saved config: ${rename.reason}.`,
|
|
250
|
+
);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
return { config, changed, warnings };
|
|
255
|
+
}
|