@bitkyc08/opencodex 2.11.0 → 2.11.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/gui/dist/assets/{index-Bk-PN-70.css → index-BA1dgo4q.css} +1 -1
- package/gui/dist/assets/index-DDZpgzKk.js +70 -0
- package/gui/dist/index.html +2 -2
- package/package.json +6 -4
- package/src/adapters/anthropic.ts +10 -2
- package/src/adapters/cursor/native-exec-common.ts +6 -2
- package/src/adapters/google-antigravity-replay.ts +3 -2
- package/src/adapters/google-antigravity-wire.ts +38 -6
- package/src/adapters/google.ts +57 -10
- package/src/adapters/openai-chat.ts +19 -2
- package/src/claude/model-info.ts +1 -1
- package/src/cli/account-auth.ts +3 -1
- package/src/cli/account-catalog-refresh.ts +14 -0
- package/src/cli/account-extended.ts +15 -1
- package/src/cli/codex-shim-readiness.ts +69 -0
- package/src/cli/combo.ts +5 -0
- package/src/cli/config-command.ts +33 -4
- package/src/cli/doctor.ts +21 -0
- package/src/cli/export-command.ts +6 -6
- package/src/cli/help.ts +6 -6
- package/src/cli/index.ts +22 -5
- package/src/cli/models-runtime.ts +13 -1
- package/src/cli/provider.ts +7 -0
- package/src/clients/config-export.ts +67 -5
- package/src/codex/account-lifecycle.ts +99 -10
- package/src/codex/auth-api.ts +222 -34
- package/src/codex/catalog/account-models.ts +9 -4
- package/src/codex/catalog/aggregation.ts +41 -8
- package/src/codex/catalog/bundled.ts +54 -22
- package/src/codex/catalog/effort.ts +47 -20
- package/src/codex/catalog/kinds.ts +2 -0
- package/src/codex/catalog/metadata.ts +70 -18
- package/src/codex/catalog/native-models.ts +7 -0
- package/src/codex/catalog/parsing.ts +29 -11
- package/src/codex/catalog/provider-fetch.ts +335 -50
- package/src/codex/catalog/sync.ts +588 -126
- package/src/codex/catalog-refresh-status.ts +87 -0
- package/src/codex/catalog-write-serialization.ts +2 -1
- package/src/codex/catalog.ts +4 -3
- package/src/codex/convergence-types.ts +1 -1
- package/src/codex/convergence.ts +190 -52
- package/src/codex/custom-model-catalog-migration.ts +176 -0
- package/src/codex/features.ts +79 -1
- package/src/codex/history-job.ts +81 -1
- package/src/codex/history-lock.ts +2 -1
- package/src/codex/history-provider.ts +4 -3
- package/src/codex/inject.ts +56 -12
- package/src/codex/model-cache.ts +50 -10
- package/src/codex/transition-state.ts +10 -2
- package/src/codex/user-identity.ts +110 -2
- package/src/combos/index.ts +3 -0
- package/src/combos/types.ts +75 -9
- package/src/config.ts +26 -12
- package/src/generated/{jawcode-model-metadata.ts → model-metadata.ts} +12 -10
- package/src/integrations/registry.ts +7 -0
- package/src/integrations/writer.ts +1 -1
- package/src/lib/pinned-http.ts +40 -9
- package/src/lib/process-control.ts +4 -1
- package/src/lib/provider-outbound.ts +42 -9
- package/src/oauth/cursor.ts +25 -5
- package/src/oauth/index.ts +59 -45
- package/src/providers/antigravity-models.ts +115 -3
- package/src/providers/context-cap.ts +13 -5
- package/src/providers/model-discovery-limits.ts +16 -0
- package/src/providers/model-discovery.ts +14 -15
- package/src/providers/provider-id-rewrite.ts +29 -0
- package/src/providers/quota.ts +93 -19
- package/src/providers/registry.ts +16 -5
- package/src/providers/slug-codec.ts +13 -6
- package/src/reasoning-effort.ts +22 -0
- package/src/router.ts +0 -20
- package/src/routing/history/indexer.ts +54 -39
- package/src/routing/quota.ts +77 -56
- package/src/server/index.ts +32 -7
- package/src/server/management/combo-routes.ts +43 -19
- package/src/server/management/config-routes.ts +115 -20
- package/src/server/management/model-routes.ts +10 -7
- package/src/server/management/model-rows.ts +6 -2
- package/src/server/management/oauth-account-routes.ts +12 -0
- package/src/server/management/provider-routes.ts +114 -40
- package/src/server/management/routing-profile-routes.ts +0 -22
- package/src/server/management-api.ts +7 -25
- package/src/server/responses/core.ts +3 -3
- package/src/server/responses/policy-fallback.ts +152 -0
- package/src/server/responses.ts +3 -2
- package/src/types.ts +15 -2
- package/src/usage/cost.ts +0 -0
- package/src/vision/describe.ts +3 -1
- package/src/vision/index.ts +17 -6
- package/src/vision/reasoning.ts +55 -0
- package/src/web-search/parse.ts +15 -3
- package/gui/dist/assets/index-BynIEIV-.js +0 -70
package/src/oauth/index.ts
CHANGED
|
@@ -13,6 +13,7 @@ import { loginAntigravity, refreshAntigravityToken } from "./google-antigravity"
|
|
|
13
13
|
import { loginCursor, refreshCursorToken } from "./cursor";
|
|
14
14
|
import { loginGithubCopilot, refreshGithubCopilotToken, validateCopilotApiBaseUrl } from "./github-copilot";
|
|
15
15
|
import { loginCommandCode, refreshCommandCodeToken } from "./command-code";
|
|
16
|
+
import { ANTIGRAVITY_REQUEST_UA } from "../adapters/google-antigravity-wire";
|
|
16
17
|
import { deriveOAuthDefaultModel, deriveOAuthProviderConfig } from "../providers/derive";
|
|
17
18
|
import { apiKeyPoolEntryId, sanitizeApiKeyValue } from "../providers/api-keys";
|
|
18
19
|
import { effectiveGoogleMode, getProviderRegistryEntry, providerMatchesRegistryTransport } from "../providers/registry";
|
|
@@ -53,6 +54,8 @@ export interface OAuthAccessSnapshot {
|
|
|
53
54
|
accountId: string;
|
|
54
55
|
generation: string;
|
|
55
56
|
accessToken: string;
|
|
57
|
+
/** Cloud Code Assist project selected during Antigravity login. */
|
|
58
|
+
projectId?: string;
|
|
56
59
|
/** Safe request-routing subset; refresh-only Kiro client secrets never leave the credential store. */
|
|
57
60
|
kiro?: Pick<KiroOAuthMetadata, "profileArn" | "apiRegion" | "ssoRegion">;
|
|
58
61
|
}
|
|
@@ -204,7 +207,7 @@ export const OAUTH_PROVIDERS: Record<string, OAuthProviderDef> = {
|
|
|
204
207
|
defaultModel: oauthDefaultModel("google-antigravity"),
|
|
205
208
|
},
|
|
206
209
|
cursor: {
|
|
207
|
-
login: (ctrl) => loginCursor(ctrl),
|
|
210
|
+
login: (ctrl, opts) => loginCursor(ctrl, undefined, { forceLogin: opts?.forceLogin }),
|
|
208
211
|
refresh: refreshCursorToken,
|
|
209
212
|
providerConfig: oauthConfig("cursor"),
|
|
210
213
|
defaultModel: oauthDefaultModel("cursor"),
|
|
@@ -289,6 +292,7 @@ function accessSnapshot(provider: string, accountId: string, cred: OAuthCredenti
|
|
|
289
292
|
accountId,
|
|
290
293
|
generation: credentialGeneration(cred),
|
|
291
294
|
accessToken: cred.access,
|
|
295
|
+
...(cred.projectId ? { projectId: cred.projectId } : {}),
|
|
292
296
|
// Stored account metadata remains authoritative. Metadata-less legacy/environment credentials
|
|
293
297
|
// may use explicit environment routing, but never borrow the currently signed-in local CLI account.
|
|
294
298
|
...(provider === "kiro"
|
|
@@ -628,15 +632,15 @@ function modelDiscoveryTransportSeed(providerName: string, prov: OcxProviderConf
|
|
|
628
632
|
}
|
|
629
633
|
|
|
630
634
|
/**
|
|
631
|
-
* Provider-correct
|
|
635
|
+
* Provider-correct model-discovery request (URL + headers), so both model-listing paths fetch the
|
|
632
636
|
* LIVE catalog correctly per adapter. Anthropic is the special case: its endpoint is `/v1/models`
|
|
633
637
|
* (not `/models`), it needs `anthropic-version`, and it authenticates with `x-api-key` by default
|
|
634
638
|
* (or `Authorization: Bearer` when `apiKeyTransport = "bearer"`), plus the OAuth beta for oauth
|
|
635
639
|
* mode — not a bare Bearer. Google (ai-studio mode)
|
|
636
640
|
* is the other special case: `x-goog-api-key` + `/v1beta/models`, returning `{ models: [...] }`.
|
|
637
641
|
* The catalog authority gate intentionally degrades that non-OpenAI shape to stale/static data.
|
|
638
|
-
*
|
|
639
|
-
* response.
|
|
642
|
+
* Antigravity uses its CCA `:fetchAvailableModels` RPC; everyone else uses the OpenAI-style
|
|
643
|
+
* `/models` + Bearer with a `{ data: [{ id, owned_by? }] }` response.
|
|
640
644
|
*/
|
|
641
645
|
export interface ModelsRequestObservedAuth {
|
|
642
646
|
readonly oauthApiBaseUrl?: string;
|
|
@@ -647,7 +651,7 @@ export function buildModelsRequest(
|
|
|
647
651
|
apiKey: string | undefined,
|
|
648
652
|
providerName = "",
|
|
649
653
|
observedAuth?: ModelsRequestObservedAuth,
|
|
650
|
-
): { url: string; headers: Record<string, string> } {
|
|
654
|
+
): { method?: "POST"; url: string; headers: Record<string, string> } {
|
|
651
655
|
const transportSeed = modelDiscoveryTransportSeed(providerName, prov);
|
|
652
656
|
const copilotApiBaseUrl = observedAuth === undefined
|
|
653
657
|
? (providerName === "github-copilot" ? getOAuthCredentialApiBaseUrl(providerName) : undefined)
|
|
@@ -665,6 +669,17 @@ export function buildModelsRequest(
|
|
|
665
669
|
effectiveProvider.baseUrl,
|
|
666
670
|
defaultUrl,
|
|
667
671
|
);
|
|
672
|
+
if (effectiveGoogleMode(providerName, effectiveProvider) === "cloud-code-assist") {
|
|
673
|
+
headers.Accept = "application/json";
|
|
674
|
+
headers["Content-Type"] = "application/json";
|
|
675
|
+
headers["User-Agent"] = ANTIGRAVITY_REQUEST_UA;
|
|
676
|
+
if (apiKey) headers.Authorization = `Bearer ${apiKey}`;
|
|
677
|
+
return {
|
|
678
|
+
method: "POST",
|
|
679
|
+
url: discoveryUrl(`${effectiveProvider.baseUrl.replace(/\/+$/, "")}/v1internal:fetchAvailableModels`),
|
|
680
|
+
headers,
|
|
681
|
+
};
|
|
682
|
+
}
|
|
668
683
|
if (effectiveGoogleMode(providerName, effectiveProvider) === "ai-studio") {
|
|
669
684
|
// Generative Language API: API key goes in x-goog-api-key (never Authorization: Bearer),
|
|
670
685
|
// models live under /v1beta (v1 misses preview models), and pageSize maxes at 1000 —
|
|
@@ -698,9 +713,7 @@ export function buildModelsRequest(
|
|
|
698
713
|
*
|
|
699
714
|
* Only touches providers that are registry-managed AND still `authMode: "oauth"`. Preset fields
|
|
700
715
|
* are refreshed, while the registry's `liveModels` default is normally filled only when no value
|
|
701
|
-
* is stored.
|
|
702
|
-
* cannot be distinguished from a hand-written pre-migration `true`. Persists + returns true when
|
|
703
|
-
* anything changed.
|
|
716
|
+
* is stored. Persists + returns true when anything changed.
|
|
704
717
|
*/
|
|
705
718
|
function cloneProviderField(value: unknown): unknown {
|
|
706
719
|
if (Array.isArray(value)) return [...value];
|
|
@@ -729,7 +742,7 @@ const OAUTH_RECONCILE_FIELDS: (keyof OcxProviderConfig)[] = [
|
|
|
729
742
|
];
|
|
730
743
|
|
|
731
744
|
const GOOGLE_ANTIGRAVITY_PROVIDER = "google-antigravity";
|
|
732
|
-
const
|
|
745
|
+
const GOOGLE_ANTIGRAVITY_LIVE_DISCOVERY_VERSION = 2 as const;
|
|
733
746
|
|
|
734
747
|
/** Only migrate the three-model experimental seed; an operator's later `liveModels: false` wins. */
|
|
735
748
|
function isLegacyCommandCodeStaticCatalog(provider: OcxProviderConfig): boolean {
|
|
@@ -738,10 +751,34 @@ function isLegacyCommandCodeStaticCatalog(provider: OcxProviderConfig): boolean
|
|
|
738
751
|
&& JSON.stringify(provider.models) === JSON.stringify(["deepseek-v4-flash", "kimi-k3", "glm-5.2"]);
|
|
739
752
|
}
|
|
740
753
|
|
|
754
|
+
function isLegacyAntigravityStaticCatalog(provider: OcxProviderConfig): boolean {
|
|
755
|
+
return provider.liveModels === false
|
|
756
|
+
&& provider.adapter === "google"
|
|
757
|
+
&& provider.baseUrl === "https://daily-cloudcode-pa.googleapis.com"
|
|
758
|
+
&& provider.authMode === "oauth"
|
|
759
|
+
&& provider.googleMode === "cloud-code-assist"
|
|
760
|
+
&& provider.defaultModel === "gemini-3.6-flash"
|
|
761
|
+
&& JSON.stringify(provider.models) === JSON.stringify([
|
|
762
|
+
"gemini-3.6-flash",
|
|
763
|
+
"gemini-3.1-pro",
|
|
764
|
+
"gemini-3.1-flash-image",
|
|
765
|
+
"claude-sonnet-4-6",
|
|
766
|
+
"claude-opus-4-6-thinking",
|
|
767
|
+
"gpt-oss-120b-medium",
|
|
768
|
+
]);
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
/** Promote only the versioned canonical static seed; unmarked `liveModels: false` remains user intent. */
|
|
772
|
+
function migrateLegacyAntigravityStaticCatalog(config: OcxConfig): boolean {
|
|
773
|
+
if (config.googleAntigravityStaticCatalogVersion !== 1) return false;
|
|
774
|
+
const provider = config.providers[GOOGLE_ANTIGRAVITY_PROVIDER];
|
|
775
|
+
if (provider && isLegacyAntigravityStaticCatalog(provider)) provider.liveModels = true;
|
|
776
|
+
config.googleAntigravityStaticCatalogVersion = GOOGLE_ANTIGRAVITY_LIVE_DISCOVERY_VERSION;
|
|
777
|
+
return true;
|
|
778
|
+
}
|
|
779
|
+
|
|
741
780
|
export function reconcileOAuthProviders(config: OcxConfig): boolean {
|
|
742
|
-
let changed =
|
|
743
|
-
const migrateAntigravityStaticCatalog =
|
|
744
|
-
config.googleAntigravityStaticCatalogVersion !== GOOGLE_ANTIGRAVITY_STATIC_CATALOG_VERSION;
|
|
781
|
+
let changed = migrateLegacyAntigravityStaticCatalog(config);
|
|
745
782
|
for (const [name, prov] of Object.entries(config.providers)) {
|
|
746
783
|
const def = OAUTH_PROVIDERS[name];
|
|
747
784
|
if (name === "command-code" && isLegacyCommandCodeStaticCatalog(prov)) {
|
|
@@ -750,21 +787,7 @@ export function reconcileOAuthProviders(config: OcxConfig): boolean {
|
|
|
750
787
|
prov.liveModels = true;
|
|
751
788
|
changed = true;
|
|
752
789
|
}
|
|
753
|
-
|
|
754
|
-
// manual edit both persist the same bare `true`, with no source metadata, so every ambiguous
|
|
755
|
-
// pre-marker value is reset once. A deliberate live-discovery choice can be re-enabled after
|
|
756
|
-
// the marker and is then preserved. Do this before the guard so omitted/non-OAuth authMode
|
|
757
|
-
// rows do not get stamped without actually receiving the new static default.
|
|
758
|
-
if (name === GOOGLE_ANTIGRAVITY_PROVIDER && migrateAntigravityStaticCatalog && prov.liveModels !== false) {
|
|
759
|
-
prov.liveModels = false;
|
|
760
|
-
changed = true;
|
|
761
|
-
}
|
|
762
|
-
// During the one-time Antigravity static-catalog migration, also refresh preset catalog
|
|
763
|
-
// fields when authMode is omitted or non-oauth. Otherwise liveModels flips to static while
|
|
764
|
-
// a stale models[] remains the published catalog forever.
|
|
765
|
-
const migrateAntigravityCatalogFields =
|
|
766
|
-
name === GOOGLE_ANTIGRAVITY_PROVIDER && migrateAntigravityStaticCatalog;
|
|
767
|
-
if (!def || (prov.authMode !== "oauth" && !migrateAntigravityCatalogFields)) continue;
|
|
790
|
+
if (!def || prov.authMode !== "oauth") continue;
|
|
768
791
|
const preset = def.providerConfig;
|
|
769
792
|
for (const field of OAUTH_RECONCILE_FIELDS) {
|
|
770
793
|
if (JSON.stringify(prov[field]) === JSON.stringify(preset[field])) continue;
|
|
@@ -775,9 +798,6 @@ export function reconcileOAuthProviders(config: OcxConfig): boolean {
|
|
|
775
798
|
}
|
|
776
799
|
changed = true;
|
|
777
800
|
}
|
|
778
|
-
// Before this marker existed, the GUI materialized an omitted `liveModels` as `true` on any
|
|
779
|
-
// settings save. Since persisted values have no provenance, the pre-guard normalization above
|
|
780
|
-
// intentionally resets all pre-marker `true` values once. Later choices are version-bounded.
|
|
781
801
|
if (prov.liveModels === undefined && preset.liveModels !== undefined) {
|
|
782
802
|
prov.liveModels = preset.liveModels;
|
|
783
803
|
changed = true;
|
|
@@ -791,10 +811,6 @@ export function reconcileOAuthProviders(config: OcxConfig): boolean {
|
|
|
791
811
|
changed = true;
|
|
792
812
|
}
|
|
793
813
|
}
|
|
794
|
-
if (migrateAntigravityStaticCatalog) {
|
|
795
|
-
config.googleAntigravityStaticCatalogVersion = GOOGLE_ANTIGRAVITY_STATIC_CATALOG_VERSION;
|
|
796
|
-
changed = true;
|
|
797
|
-
}
|
|
798
814
|
if (changed) saveConfig(config);
|
|
799
815
|
return changed;
|
|
800
816
|
}
|
|
@@ -851,17 +867,14 @@ export function upsertOAuthProvider(config: OcxConfig, provider: string): void {
|
|
|
851
867
|
if (provider === "chatgpt") return;
|
|
852
868
|
const def = OAUTH_PROVIDERS[provider];
|
|
853
869
|
if (!def) return;
|
|
870
|
+
if (provider === GOOGLE_ANTIGRAVITY_PROVIDER) migrateLegacyAntigravityStaticCatalog(config);
|
|
854
871
|
const namespaceCollision = codexAccountNamespaceProviderCollisionError(config.codexAccountNamespaces, provider);
|
|
855
872
|
if (namespaceCollision) throw new Error(namespaceCollision);
|
|
856
873
|
const existing = config.providers[provider];
|
|
857
874
|
const next: OcxProviderConfig = { ...def.providerConfig };
|
|
858
|
-
// `liveModels` is a user-facing provider toggle.
|
|
859
|
-
//
|
|
860
|
-
|
|
861
|
-
// reset once; users who deliberately forced discovery can re-enable it after migration.
|
|
862
|
-
const preserveExistingLiveModels = provider !== GOOGLE_ANTIGRAVITY_PROVIDER
|
|
863
|
-
|| config.googleAntigravityStaticCatalogVersion === GOOGLE_ANTIGRAVITY_STATIC_CATALOG_VERSION;
|
|
864
|
-
if (preserveExistingLiveModels && typeof existing?.liveModels === "boolean" && !isLegacyCommandCodeStaticCatalog(existing)) {
|
|
875
|
+
// `liveModels` is a user-facing provider toggle. Preserve either explicit setting across login;
|
|
876
|
+
// Antigravity's CCA discovery now uses its real RPC, so legacy `true` remains a valid choice.
|
|
877
|
+
if (typeof existing?.liveModels === "boolean" && !isLegacyCommandCodeStaticCatalog(existing)) {
|
|
865
878
|
next.liveModels = existing.liveModels;
|
|
866
879
|
}
|
|
867
880
|
// The Command Code protocol-version pin is an operator compatibility control. A re-login,
|
|
@@ -893,9 +906,6 @@ export function upsertOAuthProvider(config: OcxConfig, provider: string): void {
|
|
|
893
906
|
}
|
|
894
907
|
}
|
|
895
908
|
config.providers[provider] = next;
|
|
896
|
-
if (provider === GOOGLE_ANTIGRAVITY_PROVIDER) {
|
|
897
|
-
config.googleAntigravityStaticCatalogVersion = GOOGLE_ANTIGRAVITY_STATIC_CATALOG_VERSION;
|
|
898
|
-
}
|
|
899
909
|
}
|
|
900
910
|
|
|
901
911
|
interface RunLoginDeps {
|
|
@@ -1023,11 +1033,15 @@ export async function runLogin(
|
|
|
1023
1033
|
settleKiroTransaction(rawCred, true);
|
|
1024
1034
|
if (provider !== "chatgpt") {
|
|
1025
1035
|
try {
|
|
1036
|
+
const { clearModelCache } = await import("../codex/model-cache");
|
|
1037
|
+
const { clearGatherRoutedModelsInflight } = await import("../codex/catalog");
|
|
1038
|
+
clearModelCache(provider);
|
|
1039
|
+
clearGatherRoutedModelsInflight();
|
|
1026
1040
|
const { clearAccountQuotaCache, clearProviderQuotaCache } = await import("../providers/quota");
|
|
1027
1041
|
clearProviderQuotaCache();
|
|
1028
1042
|
clearAccountQuotaCache(provider);
|
|
1029
1043
|
} catch {
|
|
1030
|
-
//
|
|
1044
|
+
// Optional state modules may be unavailable in tightly scoped unit tests.
|
|
1031
1045
|
}
|
|
1032
1046
|
}
|
|
1033
1047
|
return cred;
|
|
@@ -1,10 +1,13 @@
|
|
|
1
|
+
import { isValidModelDiscoveryModelId, MODEL_DISCOVERY_MAX_MODELS } from "./model-discovery-limits";
|
|
2
|
+
|
|
1
3
|
// Google Antigravity (Cloud Code Assist) bundled model list.
|
|
2
4
|
//
|
|
3
5
|
// Single source of truth: the Antigravity `:fetchAvailableModels` backend, the same one the `agy`
|
|
4
6
|
// CLI resolves labels against. The ids below separate CCA wire ids, collapsed picker entries,
|
|
5
7
|
// and hidden compatibility aliases for saved selections. The CCA envelope's `model` field must
|
|
6
8
|
// receive the wire id (for example "Gemini 3.1 Pro (High)" => gemini-pro-agent), while the
|
|
7
|
-
// picker exposes collapsed base models
|
|
9
|
+
// picker exposes collapsed base models only when CCA returns every known tier; otherwise each
|
|
10
|
+
// returned wire id remains visible so an unavailable tier cannot be selected.
|
|
8
11
|
|
|
9
12
|
// ── Wire IDs (what CCA :fetchAvailableModels returns) ──
|
|
10
13
|
const ANTIGRAVITY_WIRE_MODELS = [
|
|
@@ -19,6 +22,21 @@ const ANTIGRAVITY_WIRE_MODELS = [
|
|
|
19
22
|
"gpt-oss-120b-medium",
|
|
20
23
|
];
|
|
21
24
|
|
|
25
|
+
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
|
+
"gemini-3.1-pro-low": "gemini-3.1-pro",
|
|
30
|
+
"gemini-pro-agent": "gemini-3.1-pro",
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
const ANTIGRAVITY_WIRE_IDS_BY_PICKER_MODEL: Record<string, string[]> = Object.entries(
|
|
34
|
+
ANTIGRAVITY_PICKER_MODEL_BY_WIRE_ID,
|
|
35
|
+
).reduce<Record<string, string[]>>((out, [wireId, pickerId]) => {
|
|
36
|
+
(out[pickerId] ??= []).push(wireId);
|
|
37
|
+
return out;
|
|
38
|
+
}, {});
|
|
39
|
+
|
|
22
40
|
// ── Effort ladders per collapsed base model ──
|
|
23
41
|
// Gemini models: effort → wire model suffix (official agy UI pattern).
|
|
24
42
|
// Claude Opus: effort → thinkingConfig.thinkingLevel (CLIProxyAPI proven pattern).
|
|
@@ -100,8 +118,8 @@ const ANTIGRAVITY_WIRE_MODEL_CONTEXT_WINDOWS: Record<string, number> = {
|
|
|
100
118
|
"gemini-3.1-pro-low": 1_048_576,
|
|
101
119
|
"gemini-pro-agent": 1_048_576,
|
|
102
120
|
"gemini-3.1-flash-image": 1_048_576,
|
|
103
|
-
"claude-sonnet-4-6":
|
|
104
|
-
"claude-opus-4-6-thinking":
|
|
121
|
+
"claude-sonnet-4-6": 250_000,
|
|
122
|
+
"claude-opus-4-6-thinking": 250_000,
|
|
105
123
|
"gpt-oss-120b-medium": 131_072,
|
|
106
124
|
};
|
|
107
125
|
|
|
@@ -119,6 +137,100 @@ export const ANTIGRAVITY_MODEL_CONTEXT_WINDOWS: Record<string, number> = {
|
|
|
119
137
|
),
|
|
120
138
|
};
|
|
121
139
|
|
|
140
|
+
export const ANTIGRAVITY_MODEL_INPUT_MODALITIES: Record<string, string[]> = {
|
|
141
|
+
"gemini-3.6-flash": ["text", "image"],
|
|
142
|
+
"gemini-3.1-pro": ["text", "image"],
|
|
143
|
+
"gemini-3.1-flash-image": ["text", "image"],
|
|
144
|
+
"claude-sonnet-4-6": ["text", "image"],
|
|
145
|
+
"claude-opus-4-6-thinking": ["text", "image"],
|
|
146
|
+
"gpt-oss-120b-medium": ["text"],
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
export interface AntigravityAvailableModel {
|
|
150
|
+
id: string;
|
|
151
|
+
contextWindow?: number;
|
|
152
|
+
inputModalities?: string[];
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function antigravityRecord(value: unknown): Record<string, unknown> | undefined {
|
|
156
|
+
return value !== null && typeof value === "object" && !Array.isArray(value)
|
|
157
|
+
? value as Record<string, unknown>
|
|
158
|
+
: undefined;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function antigravityPositiveInteger(value: unknown): number | undefined {
|
|
162
|
+
return typeof value === "number" && Number.isSafeInteger(value) && value > 0 ? value : undefined;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Extract the CCA models that are valid for agent requests. The endpoint also returns tab,
|
|
167
|
+
* command, commit-message, transcription, and standalone image-generation models; those are not
|
|
168
|
+
* callable through the CCA agent envelope and must not be published to the Codex catalog.
|
|
169
|
+
*/
|
|
170
|
+
export function parseAntigravityAvailableModels(
|
|
171
|
+
payload: unknown,
|
|
172
|
+
maxModels = MODEL_DISCOVERY_MAX_MODELS,
|
|
173
|
+
): AntigravityAvailableModel[] | null {
|
|
174
|
+
const limit = Number.isSafeInteger(maxModels) && maxModels > 0
|
|
175
|
+
? Math.min(maxModels, MODEL_DISCOVERY_MAX_MODELS)
|
|
176
|
+
: MODEL_DISCOVERY_MAX_MODELS;
|
|
177
|
+
const body = antigravityRecord(payload);
|
|
178
|
+
if (!body) return null;
|
|
179
|
+
const models = antigravityRecord(body?.models);
|
|
180
|
+
const sorts = Array.isArray(body?.agentModelSorts) ? body.agentModelSorts : undefined;
|
|
181
|
+
if (!models || !sorts) return null;
|
|
182
|
+
|
|
183
|
+
const ids: string[] = [];
|
|
184
|
+
for (const sort of sorts) {
|
|
185
|
+
const groups = antigravityRecord(sort)?.groups;
|
|
186
|
+
if (!Array.isArray(groups)) return null;
|
|
187
|
+
for (const group of groups) {
|
|
188
|
+
const modelIds = antigravityRecord(group)?.modelIds;
|
|
189
|
+
if (!Array.isArray(modelIds)) return null;
|
|
190
|
+
for (const id of modelIds) {
|
|
191
|
+
if (!isValidModelDiscoveryModelId(id)
|
|
192
|
+
|| !antigravityRecord(models[id])
|
|
193
|
+
|| ids.length >= limit) return null;
|
|
194
|
+
ids.push(id);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
// This model is exposed by Antigravity's agent chat surface even though it is grouped under
|
|
199
|
+
// image generation in the discovery response.
|
|
200
|
+
if (Array.isArray(body.imageGenerationModelIds)
|
|
201
|
+
&& body.imageGenerationModelIds.includes("gemini-3.1-flash-image")) {
|
|
202
|
+
if (ids.length >= limit) return null;
|
|
203
|
+
ids.push("gemini-3.1-flash-image");
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const available = new Map<string, Record<string, unknown>>();
|
|
207
|
+
for (const wireId of ids) {
|
|
208
|
+
const info = antigravityRecord(models[wireId]);
|
|
209
|
+
if (!info || available.has(wireId)) continue;
|
|
210
|
+
// Legacy compatibility aliases are deliberately routed to newer wire ids for saved
|
|
211
|
+
// selections. They are not safe as independently discovered picker rows.
|
|
212
|
+
if (ANTIGRAVITY_MODEL_ALIASES[wireId] && ANTIGRAVITY_MODEL_ALIASES[wireId] !== wireId) continue;
|
|
213
|
+
available.set(wireId, info);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
const out: AntigravityAvailableModel[] = [];
|
|
217
|
+
const seen = new Set<string>();
|
|
218
|
+
for (const [wireId, info] of available) {
|
|
219
|
+
const pickerId = ANTIGRAVITY_PICKER_MODEL_BY_WIRE_ID[wireId];
|
|
220
|
+
const completePickerSet = pickerId !== undefined
|
|
221
|
+
&& ANTIGRAVITY_WIRE_IDS_BY_PICKER_MODEL[pickerId]!.every(id => available.has(id));
|
|
222
|
+
const id = completePickerSet ? pickerId! : wireId;
|
|
223
|
+
if (seen.has(id)) continue;
|
|
224
|
+
seen.add(id);
|
|
225
|
+
out.push({
|
|
226
|
+
id,
|
|
227
|
+
...(antigravityPositiveInteger(info.maxTokens) ? { contextWindow: antigravityPositiveInteger(info.maxTokens) } : {}),
|
|
228
|
+
inputModalities: info.supportsImages === true ? ["text", "image"] : ["text"],
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
return out;
|
|
232
|
+
}
|
|
233
|
+
|
|
122
234
|
export function resolveAntigravityWireModelId(modelId: string): string {
|
|
123
235
|
return ANTIGRAVITY_MODEL_ALIASES[modelId] ?? modelId;
|
|
124
236
|
}
|
|
@@ -33,19 +33,27 @@ export function globalContextCapValue(config: Pick<OcxConfig, "contextCapValue">
|
|
|
33
33
|
return isValidContextCap(value) ? Math.floor(value) : DEFAULT_PROVIDER_CONTEXT_CAP;
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
-
export function setProviderContextCap(config: OcxConfig, provider: string, enabled: boolean): void {
|
|
36
|
+
export function setProviderContextCap(config: OcxConfig, provider: string, enabled: boolean, value?: number): void {
|
|
37
37
|
const next = providerContextCaps(config);
|
|
38
|
-
if (enabled)
|
|
39
|
-
|
|
38
|
+
if (enabled) {
|
|
39
|
+
next[provider] = isValidContextCap(value) ? Math.floor(value) : globalContextCapValue(config);
|
|
40
|
+
} else {
|
|
41
|
+
delete next[provider];
|
|
42
|
+
}
|
|
40
43
|
if (Object.keys(next).length > 0) config.providerContextCaps = next;
|
|
41
44
|
else delete config.providerContextCaps;
|
|
42
45
|
}
|
|
43
46
|
|
|
44
|
-
/**
|
|
45
|
-
|
|
47
|
+
/**
|
|
48
|
+
* Set the global cap value (the default used by per-provider toggles and "set all").
|
|
49
|
+
* Re-points every already-enabled provider only when `applyToAll` is true (the dashboard's
|
|
50
|
+
* "apply to every routed provider" toggle); otherwise each provider keeps its own cap value.
|
|
51
|
+
*/
|
|
52
|
+
export function setGlobalContextCapValue(config: OcxConfig, value: number, applyToAll: boolean): void {
|
|
46
53
|
if (!isValidContextCap(value)) return;
|
|
47
54
|
const next = Math.floor(value);
|
|
48
55
|
config.contextCapValue = next;
|
|
56
|
+
if (!applyToAll) return;
|
|
49
57
|
const caps = providerContextCaps(config);
|
|
50
58
|
for (const provider of Object.keys(caps)) caps[provider] = next;
|
|
51
59
|
if (Object.keys(caps).length > 0) config.providerContextCaps = caps;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/** Hard process-wide limits shared by all live model-discovery parsers. */
|
|
2
|
+
export const MODEL_DISCOVERY_MAX_RESPONSE_BYTES = 4 * 1024 * 1024;
|
|
3
|
+
export const MODEL_DISCOVERY_MAX_MODELS = 2_000;
|
|
4
|
+
export const MODEL_DISCOVERY_MAX_MODEL_ID_LENGTH = 1_024;
|
|
5
|
+
|
|
6
|
+
const MODEL_DISCOVERY_MODEL_ID_CONTROL_CHARS = /[\u0000-\u001f\u007f-\u009f\u2028\u2029]/;
|
|
7
|
+
|
|
8
|
+
/** Reject model IDs that cannot safely be published as callable catalog selectors. */
|
|
9
|
+
export function isValidModelDiscoveryModelId(value: unknown): value is string {
|
|
10
|
+
if (typeof value !== "string") return false;
|
|
11
|
+
const normalized = value.trim();
|
|
12
|
+
return Boolean(normalized)
|
|
13
|
+
&& normalized === value
|
|
14
|
+
&& normalized.length <= MODEL_DISCOVERY_MAX_MODEL_ID_LENGTH
|
|
15
|
+
&& !MODEL_DISCOVERY_MODEL_ID_CONTROL_CHARS.test(normalized);
|
|
16
|
+
}
|
|
@@ -1,4 +1,16 @@
|
|
|
1
1
|
import type { OcxProviderConfig } from "../types";
|
|
2
|
+
import {
|
|
3
|
+
isValidModelDiscoveryModelId,
|
|
4
|
+
MODEL_DISCOVERY_MAX_MODEL_ID_LENGTH,
|
|
5
|
+
MODEL_DISCOVERY_MAX_MODELS,
|
|
6
|
+
MODEL_DISCOVERY_MAX_RESPONSE_BYTES,
|
|
7
|
+
} from "./model-discovery-limits";
|
|
8
|
+
export {
|
|
9
|
+
isValidModelDiscoveryModelId,
|
|
10
|
+
MODEL_DISCOVERY_MAX_MODEL_ID_LENGTH,
|
|
11
|
+
MODEL_DISCOVERY_MAX_MODELS,
|
|
12
|
+
MODEL_DISCOVERY_MAX_RESPONSE_BYTES,
|
|
13
|
+
} from "./model-discovery-limits";
|
|
2
14
|
import {
|
|
3
15
|
getProviderRegistryEntry,
|
|
4
16
|
providerMatchesRegistryTransport,
|
|
@@ -9,13 +21,8 @@ import {
|
|
|
9
21
|
type ProviderModelDiscoverySpec,
|
|
10
22
|
} from "./registry";
|
|
11
23
|
|
|
12
|
-
/** Hard process-wide limits. Registry entries may lower, but never raise, these ceilings. */
|
|
13
|
-
export const MODEL_DISCOVERY_MAX_RESPONSE_BYTES = 4 * 1024 * 1024;
|
|
14
|
-
export const MODEL_DISCOVERY_MAX_MODELS = 2_000;
|
|
15
|
-
export const MODEL_DISCOVERY_MAX_MODEL_ID_LENGTH = 1_024;
|
|
16
24
|
const MODEL_DISCOVERY_MAX_FILTER_VALUES = 256;
|
|
17
25
|
const MODEL_DISCOVERY_MAX_FILTER_STRING_LENGTH = 1_024;
|
|
18
|
-
const MODEL_DISCOVERY_MODEL_ID_CONTROL_CHARS = /[\u0000-\u001f\u007f-\u009f\u2028\u2029]/;
|
|
19
26
|
|
|
20
27
|
export interface ResolvedProviderModelDiscovery {
|
|
21
28
|
spec?: ProviderModelDiscoverySpec;
|
|
@@ -343,16 +350,8 @@ export function extractProviderModelItems(
|
|
|
343
350
|
return { ok: false, reason: "invalid_shape" };
|
|
344
351
|
}
|
|
345
352
|
const id = (raw as { id?: unknown }).id;
|
|
346
|
-
if (
|
|
347
|
-
const normalizedId = id
|
|
348
|
-
if (
|
|
349
|
-
!normalizedId
|
|
350
|
-
|| normalizedId !== id
|
|
351
|
-
|| normalizedId.length > MODEL_DISCOVERY_MAX_MODEL_ID_LENGTH
|
|
352
|
-
|| MODEL_DISCOVERY_MODEL_ID_CONTROL_CHARS.test(normalizedId)
|
|
353
|
-
) {
|
|
354
|
-
return { ok: false, reason: "invalid_shape" };
|
|
355
|
-
}
|
|
353
|
+
if (!isValidModelDiscoveryModelId(id)) return { ok: false, reason: "invalid_shape" };
|
|
354
|
+
const normalizedId = id;
|
|
356
355
|
const item = raw as ProviderModelsApiItem;
|
|
357
356
|
if (!providerModelMatchesDiscoveryFilter(item, discovery.spec?.filter) || seen.has(normalizedId)) continue;
|
|
358
357
|
seen.add(normalizedId);
|
|
@@ -148,3 +148,32 @@ export function rewriteProviderReferences(config: OcxConfig, from: string, to: s
|
|
|
148
148
|
|
|
149
149
|
return { changed, collisions };
|
|
150
150
|
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Drop the custom-model rows that belonged to a provider being removed.
|
|
154
|
+
*
|
|
155
|
+
* The sibling of the rename pass above. `rewriteProviderReferences` already
|
|
156
|
+
* carries `customModels[].provider` across a rename, so the array tracks the
|
|
157
|
+
* provider lifecycle — but removal used to delete only `config.providers[name]`
|
|
158
|
+
* and leave the rows behind. Those orphans still reach `/api/models` and the
|
|
159
|
+
* generated Codex catalog, which key on the row rather than on provider
|
|
160
|
+
* existence, so they surface as models that resolve to nothing (#1273).
|
|
161
|
+
*
|
|
162
|
+
* Only the rows are touched: the `customModelCatalogMigration` marker records
|
|
163
|
+
* one-time ownership of pre-marker rows and must survive removal unchanged, or
|
|
164
|
+
* an older binary's view of that ownership silently changes.
|
|
165
|
+
*
|
|
166
|
+
* Returns the number of rows dropped so callers can report it.
|
|
167
|
+
*/
|
|
168
|
+
export function dropProviderCustomModels(config: OcxConfig, provider: string): number {
|
|
169
|
+
const existing = config.customModels;
|
|
170
|
+
if (!Array.isArray(existing) || existing.length === 0) return 0;
|
|
171
|
+
const kept = existing.filter(model => model.provider !== provider);
|
|
172
|
+
if (kept.length === existing.length) return 0;
|
|
173
|
+
// Match the add/remove routes: an emptied list is dropped rather than left as
|
|
174
|
+
// `[]`, so the `customModels` field is absent either way. Only that field —
|
|
175
|
+
// the `customModelCatalogMigration` marker is deliberately left in place.
|
|
176
|
+
if (kept.length > 0) config.customModels = kept;
|
|
177
|
+
else delete config.customModels;
|
|
178
|
+
return existing.length - kept.length;
|
|
179
|
+
}
|
package/src/providers/quota.ts
CHANGED
|
@@ -11,6 +11,7 @@ import { getValidAccessToken, getValidAccessTokenForAccount } from "../oauth";
|
|
|
11
11
|
import { getAccountCredential, getAccountSet, getCredential } from "../oauth/store";
|
|
12
12
|
import { antigravityUserAgent } from "../adapters/client-fingerprint";
|
|
13
13
|
import { apiKeyPoolEntryId } from "./api-keys";
|
|
14
|
+
import { XAI_GROK_CLIENT_VERSION, XAI_GROK_COMPATIBILITY } from "./xai-transport";
|
|
14
15
|
import { getProviderRegistryEntry, providerCodexAccountMode } from "./registry";
|
|
15
16
|
import type { OcxConfig, OcxProviderConfig } from "../types";
|
|
16
17
|
import { isCanonicalOpenAiForwardProvider, OPENAI_CODEX_PROVIDER_ID } from "./openai-tiers";
|
|
@@ -44,6 +45,8 @@ const VENICE_BASE_URL = "https://api.venice.ai/api/v1";
|
|
|
44
45
|
const SYNTHETIC_BASE_URL = "https://api.synthetic.new/v2";
|
|
45
46
|
const DEEPINFRA_BASE_URL = "https://api.deepinfra.com";
|
|
46
47
|
const NEURALWATT_BASE_URL = "https://api.neuralwatt.com/v1";
|
|
48
|
+
const XAI_BILLING_URL = "https://cli-chat-proxy.grok.com/v1/billing";
|
|
49
|
+
const XAI_CREDITS_URL = `${XAI_BILLING_URL}?format=credits`;
|
|
47
50
|
/** Keep a failed probe's previous row at most this long before dropping it. */
|
|
48
51
|
const LAST_GOOD_MAX_AGE_MS = CODEX_CAPACITY_MAX_QUOTA_AGE_MS;
|
|
49
52
|
const nativeMainReportGenerations = new WeakMap<ProviderQuotaReport, number>();
|
|
@@ -977,6 +980,65 @@ function centsValue(value: unknown): number | undefined {
|
|
|
977
980
|
return rec ? toFiniteNumber(rec.val) : undefined;
|
|
978
981
|
}
|
|
979
982
|
|
|
983
|
+
/** Decode JWT payload `sub` for xAI weekly credits when the stored credential lacks accountId. */
|
|
984
|
+
function xaiUserIdFromAccessToken(accessToken: string): string | undefined {
|
|
985
|
+
const parts = accessToken.split(".");
|
|
986
|
+
if (parts.length < 2 || !parts[1]) return undefined;
|
|
987
|
+
try {
|
|
988
|
+
const payload = JSON.parse(Buffer.from(parts[1], "base64url").toString("utf8")) as { sub?: unknown };
|
|
989
|
+
return typeof payload.sub === "string" && payload.sub.trim() ? payload.sub.trim() : undefined;
|
|
990
|
+
} catch {
|
|
991
|
+
return undefined;
|
|
992
|
+
}
|
|
993
|
+
}
|
|
994
|
+
|
|
995
|
+
/**
|
|
996
|
+
* Grok Build weekly credits envelope:
|
|
997
|
+
* `{ config: { creditUsagePercent?, currentPeriod: { type: "USAGE_PERIOD_TYPE_WEEKLY", end } } }`.
|
|
998
|
+
* Omitted percent is treated as 0 (proto3 default).
|
|
999
|
+
*/
|
|
1000
|
+
export function parseXaiCreditsResponse(value: unknown): { percent: number; resetAt?: number } | null {
|
|
1001
|
+
const body = asRecord(value);
|
|
1002
|
+
const config = asRecord(body?.config);
|
|
1003
|
+
if (!config) return null;
|
|
1004
|
+
const period = asRecord(config.currentPeriod);
|
|
1005
|
+
if (!period || period.type !== "USAGE_PERIOD_TYPE_WEEKLY") return null;
|
|
1006
|
+
const resetAt = normalizeResetAt(period.end);
|
|
1007
|
+
if (resetAt === undefined) return null;
|
|
1008
|
+
if (config.creditUsagePercent !== undefined) {
|
|
1009
|
+
const percent = normalizePercent(config.creditUsagePercent);
|
|
1010
|
+
if (percent === undefined) return null;
|
|
1011
|
+
return { percent, resetAt };
|
|
1012
|
+
}
|
|
1013
|
+
return { percent: 0, resetAt };
|
|
1014
|
+
}
|
|
1015
|
+
|
|
1016
|
+
async function fetchXaiWeeklyCredits(accessToken: string, userId: string): Promise<ProviderQuota | null> {
|
|
1017
|
+
try {
|
|
1018
|
+
const response = await fetch(XAI_CREDITS_URL, {
|
|
1019
|
+
headers: {
|
|
1020
|
+
Accept: "application/json",
|
|
1021
|
+
Authorization: `Bearer ${accessToken}`,
|
|
1022
|
+
[XAI_GROK_COMPATIBILITY.headers.tokenAuth]: "xai-grok-cli",
|
|
1023
|
+
[XAI_GROK_COMPATIBILITY.headers.authenticateResponse]: "authenticate-response",
|
|
1024
|
+
"x-userid": userId,
|
|
1025
|
+
[XAI_GROK_COMPATIBILITY.headers.clientVersion]: XAI_GROK_CLIENT_VERSION,
|
|
1026
|
+
},
|
|
1027
|
+
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
|
|
1028
|
+
});
|
|
1029
|
+
if (!response.ok) return null;
|
|
1030
|
+
const parsed = parseXaiCreditsResponse(await response.json().catch(() => null));
|
|
1031
|
+
if (!parsed) return null;
|
|
1032
|
+
return {
|
|
1033
|
+
weeklyPercent: parsed.percent,
|
|
1034
|
+
...(parsed.resetAt !== undefined ? { weeklyResetAt: parsed.resetAt } : {}),
|
|
1035
|
+
updatedAt: Date.now(),
|
|
1036
|
+
};
|
|
1037
|
+
} catch {
|
|
1038
|
+
return null;
|
|
1039
|
+
}
|
|
1040
|
+
}
|
|
1041
|
+
|
|
980
1042
|
async function fetchXaiQuota(provider: string): Promise<ProviderQuotaReport | null> {
|
|
981
1043
|
let accessToken: string;
|
|
982
1044
|
try {
|
|
@@ -984,25 +1046,37 @@ async function fetchXaiQuota(provider: string): Promise<ProviderQuotaReport | nu
|
|
|
984
1046
|
} catch {
|
|
985
1047
|
return null;
|
|
986
1048
|
}
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1049
|
+
|
|
1050
|
+
// Prefer the SuperGrok weekly credits window that actually gates prompting (#1283).
|
|
1051
|
+
const userId = getCredential("xai")?.accountId?.trim() || xaiUserIdFromAccessToken(accessToken);
|
|
1052
|
+
if (userId) {
|
|
1053
|
+
const weekly = await fetchXaiWeeklyCredits(accessToken, userId);
|
|
1054
|
+
if (weekly) return report(provider, "xai:grok-billing-credits", weekly);
|
|
1055
|
+
}
|
|
1056
|
+
|
|
1057
|
+
// Legacy monthly dollar pool — retained when weekly is unavailable.
|
|
1058
|
+
try {
|
|
1059
|
+
const response = await fetch(XAI_BILLING_URL, {
|
|
1060
|
+
headers: { Accept: "application/json", Authorization: `Bearer ${accessToken}` },
|
|
1061
|
+
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
|
|
1062
|
+
});
|
|
1063
|
+
if (!response.ok) return null;
|
|
1064
|
+
const body = asRecord(await response.json().catch(() => null));
|
|
1065
|
+
const config = asRecord(body?.config);
|
|
1066
|
+
if (!config) return null;
|
|
1067
|
+
const limitCents = centsValue(config.monthlyLimit);
|
|
1068
|
+
const usedCents = centsValue(config.used);
|
|
1069
|
+
if (limitCents === undefined || usedCents === undefined || limitCents <= 0) return null;
|
|
1070
|
+
const percent = normalizePercent((usedCents / limitCents) * 100);
|
|
1071
|
+
if (percent === undefined) return null;
|
|
1072
|
+
return report(provider, "xai:grok-billing", {
|
|
1073
|
+
monthlyPercent: percent,
|
|
1074
|
+
monthlyResetAt: normalizeResetAt(config.billingPeriodEnd),
|
|
1075
|
+
updatedAt: Date.now(),
|
|
1076
|
+
});
|
|
1077
|
+
} catch {
|
|
1078
|
+
return null;
|
|
1079
|
+
}
|
|
1006
1080
|
}
|
|
1007
1081
|
|
|
1008
1082
|
function parseClaudeBucket(value: unknown): { percent?: number; resetAt?: number } | null {
|