@centerforagenticai/pi-multi-account 0.1.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.
Files changed (124) hide show
  1. package/LICENSE +21 -0
  2. package/NOTICE +29 -0
  3. package/README.md +999 -0
  4. package/config/models/pi-multi-account.v1.json +32 -0
  5. package/config/subscription-plans.v1.json +122 -0
  6. package/package.json +76 -0
  7. package/packages/pi-anthropic-oauth/LICENSE +21 -0
  8. package/packages/pi-anthropic-oauth/package.json +54 -0
  9. package/packages/pi-anthropic-oauth/src/auth.ts +396 -0
  10. package/packages/pi-anthropic-oauth/src/context.ts +116 -0
  11. package/packages/pi-anthropic-oauth/src/convert.ts +303 -0
  12. package/packages/pi-anthropic-oauth/src/index.ts +37 -0
  13. package/packages/pi-anthropic-oauth/src/prompt.ts +137 -0
  14. package/packages/pi-anthropic-oauth/src/stream.ts +476 -0
  15. package/packages/pi-antigravity/LICENSE +21 -0
  16. package/packages/pi-antigravity/package.json +77 -0
  17. package/packages/pi-antigravity/src/auth/index.ts +14 -0
  18. package/packages/pi-antigravity/src/auth/oauth.ts +442 -0
  19. package/packages/pi-antigravity/src/client/client.ts +561 -0
  20. package/packages/pi-antigravity/src/client/index.ts +1 -0
  21. package/packages/pi-antigravity/src/context.ts +110 -0
  22. package/packages/pi-antigravity/src/diagnostics/diagnostics.ts +96 -0
  23. package/packages/pi-antigravity/src/diagnostics/index.ts +1 -0
  24. package/packages/pi-antigravity/src/image/image.ts +336 -0
  25. package/packages/pi-antigravity/src/image/index.ts +1 -0
  26. package/packages/pi-antigravity/src/index.ts +280 -0
  27. package/packages/pi-antigravity/src/models/discovery.ts +154 -0
  28. package/packages/pi-antigravity/src/models/grouping.ts +424 -0
  29. package/packages/pi-antigravity/src/models/index.ts +3 -0
  30. package/packages/pi-antigravity/src/models/models.ts +500 -0
  31. package/packages/pi-antigravity/src/stream/index.ts +1 -0
  32. package/packages/pi-antigravity/src/stream/stream.ts +1478 -0
  33. package/packages/pi-antigravity/src/types/enums.ts +42 -0
  34. package/packages/pi-antigravity/src/types/index.ts +2 -0
  35. package/packages/pi-antigravity/src/types/types.ts +292 -0
  36. package/packages/pi-antigravity/src/usage/index.ts +1 -0
  37. package/packages/pi-antigravity/src/usage/usage.ts +416 -0
  38. package/packages/pi-antigravity/src/utils/http.ts +91 -0
  39. package/packages/pi-antigravity/src/utils/index.ts +3 -0
  40. package/packages/pi-antigravity/src/utils/security.ts +73 -0
  41. package/packages/pi-antigravity/src/utils/util.ts +132 -0
  42. package/scripts/multi-account.mjs +44 -0
  43. package/src/account-labels.ts +223 -0
  44. package/src/account-plan-assignment.ts +340 -0
  45. package/src/account-rate-history.ts +372 -0
  46. package/src/anthropic-adaptive-stream.ts +531 -0
  47. package/src/anthropic-alias-stream.ts +140 -0
  48. package/src/anthropic-context-compat.ts +80 -0
  49. package/src/api-pricing.ts +579 -0
  50. package/src/bounded-file-lines.ts +97 -0
  51. package/src/catalog-rebinding.ts +177 -0
  52. package/src/catalog-registration-probe.ts +111 -0
  53. package/src/codex-adapter.ts +345 -0
  54. package/src/codex-model-defaults.ts +785 -0
  55. package/src/command-completions.ts +404 -0
  56. package/src/commands.ts +2000 -0
  57. package/src/compaction.ts +14 -0
  58. package/src/config.ts +1317 -0
  59. package/src/continuation.ts +569 -0
  60. package/src/cooldowns.ts +110 -0
  61. package/src/cost-digest-store.ts +332 -0
  62. package/src/cost-digest.ts +1044 -0
  63. package/src/cost-history.ts +251 -0
  64. package/src/cost-period-closer.ts +160 -0
  65. package/src/cost-report-json.ts +318 -0
  66. package/src/cost-report-reader.ts +368 -0
  67. package/src/cost-report-render.ts +207 -0
  68. package/src/cost-report.ts +1104 -0
  69. package/src/coverage-attestation.ts +397 -0
  70. package/src/credential-lifecycle.ts +169 -0
  71. package/src/credential-refresh.ts +248 -0
  72. package/src/declaration-notice-marker.ts +238 -0
  73. package/src/diagnostic-store.ts +276 -0
  74. package/src/diagnostics.ts +309 -0
  75. package/src/discovery.ts +471 -0
  76. package/src/duration.ts +13 -0
  77. package/src/error-classification.ts +256 -0
  78. package/src/fuzzy.ts +15 -0
  79. package/src/group-policy.ts +81 -0
  80. package/src/history-store.ts +897 -0
  81. package/src/index.ts +5572 -0
  82. package/src/lifecycle.ts +378 -0
  83. package/src/logical-dispatch.ts +279 -0
  84. package/src/logical-model-selector.ts +254 -0
  85. package/src/logical-model-switcher.ts +430 -0
  86. package/src/logical-provider-attribution.ts +544 -0
  87. package/src/logical-provider.ts +1237 -0
  88. package/src/logical-route-indicator.ts +215 -0
  89. package/src/machine-lease.ts +445 -0
  90. package/src/model-support.ts +66 -0
  91. package/src/models-declaration.ts +1091 -0
  92. package/src/openai-adapter.ts +117 -0
  93. package/src/openrouter-budget.ts +304 -0
  94. package/src/openrouter-fallback.ts +146 -0
  95. package/src/period-boundaries.ts +376 -0
  96. package/src/pi-anthropic-oauth.d.ts +6 -0
  97. package/src/preflight.ts +253 -0
  98. package/src/pricing-cache.ts +235 -0
  99. package/src/project-identity.ts +100 -0
  100. package/src/provider-registration.ts +942 -0
  101. package/src/rate-formula.ts +163 -0
  102. package/src/recovery-engine.ts +853 -0
  103. package/src/recovery-output.ts +837 -0
  104. package/src/recovery-plan.ts +239 -0
  105. package/src/report-range.ts +203 -0
  106. package/src/route-resolver.ts +789 -0
  107. package/src/routing-config-transaction.ts +232 -0
  108. package/src/routing.ts +1163 -0
  109. package/src/runtime-state.ts +630 -0
  110. package/src/session-account-groups.ts +284 -0
  111. package/src/session-restore.ts +287 -0
  112. package/src/shared-usage.ts +1392 -0
  113. package/src/standalone-cli.ts +720 -0
  114. package/src/status-view.ts +578 -0
  115. package/src/subscription-plan-catalog.ts +346 -0
  116. package/src/tier-model-resolver.ts +46 -0
  117. package/src/upstream-anthropic.ts +315 -0
  118. package/src/upstream-antigravity.ts +327 -0
  119. package/src/usage-fetch.ts +1634 -0
  120. package/src/usage.ts +1026 -0
  121. package/src/vendor.ts +87 -0
  122. package/src/warmer.ts +231 -0
  123. package/src/watchdog.ts +219 -0
  124. package/src/window-history.ts +270 -0
@@ -0,0 +1,280 @@
1
+ import type { ExtensionAPI, ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
2
+ import { StringEnum, Type } from "@earendil-works/pi-ai";
3
+ import { registerApiProvider } from "@earendil-works/pi-ai/compat";
4
+ import { getApiKey, loginAntigravity, refreshAntigravityToken } from "./auth/index.js";
5
+ import { DEFAULT_ENDPOINT, endpointCandidates } from "./client/index.js";
6
+ import { getLastDiagnostics, runWithDiagnostics } from "./diagnostics/index.js";
7
+ import {
8
+ DEFAULT_IMAGE_MODEL,
9
+ generateAntigravityImage,
10
+ IMAGE_ASPECT_RATIOS,
11
+ parseImageCommandArgs,
12
+ } from "./image/index.js";
13
+ import {
14
+ applyAntigravityCatalog,
15
+ discoverAntigravityModels,
16
+ getCurrentAntigravityCatalog,
17
+ PROVIDER_ID,
18
+ PROVIDER_NAME,
19
+ refreshAntigravityModels,
20
+ resolvedCatalog,
21
+ } from "./models/index.js";
22
+ import { ANTIGRAVITY_API, streamAntigravity } from "./stream/index.js";
23
+ import {
24
+ fetchAccountUsage,
25
+ formatModelsList,
26
+ formatUsageSummary,
27
+ resolveApiKeyFromContext,
28
+ } from "./usage/index.js";
29
+ import { prewarmConnection, redactSecrets } from "./utils/index.js";
30
+
31
+ /**
32
+ * Pi's interactive `notify` writes into the chat transcript. `console.log` in that
33
+ * mode prints to the raw terminal and paints over the TUI. Use one channel only.
34
+ */
35
+ function emitCommandOutput(
36
+ ctx: ExtensionCommandContext,
37
+ text: string,
38
+ type: "info" | "warning" | "error" = "info",
39
+ ): void {
40
+ if (ctx.hasUI) {
41
+ ctx.ui.notify(text, type);
42
+ return;
43
+ }
44
+ if (type === "warning" || type === "error") console.error(text);
45
+ else console.log(text);
46
+ }
47
+
48
+ async function withUsage(
49
+ ctx: ExtensionCommandContext,
50
+ fn: (usage: Awaited<ReturnType<typeof fetchAccountUsage>>) => string,
51
+ ): Promise<void> {
52
+ try {
53
+ const apiKey = await resolveApiKeyFromContext(ctx);
54
+ if (!apiKey) {
55
+ emitCommandOutput(
56
+ ctx,
57
+ "No Antigravity credentials. Run /login antigravity first.",
58
+ "warning",
59
+ );
60
+ return;
61
+ }
62
+ if (ctx.hasUI) ctx.ui.notify("Fetching Antigravity usage…", "info");
63
+ const usage = await runWithDiagnostics(() => fetchAccountUsage(apiKey));
64
+ emitCommandOutput(ctx, fn(usage));
65
+ } catch (error) {
66
+ const msg = error instanceof Error ? error.message : String(error);
67
+ emitCommandOutput(ctx, `Antigravity usage failed: ${msg}`, "warning");
68
+ }
69
+ }
70
+
71
+ export default function (pi: ExtensionAPI): void {
72
+ // Open the TLS connection up front so the first message of a session does not pay
73
+ // the handshake. Opt out with ANTIGRAVITY_NO_PREWARM=1.
74
+ const primaryEndpoint = endpointCandidates()[0];
75
+ if (primaryEndpoint) prewarmConnection(primaryEndpoint);
76
+
77
+ registerApiProvider({
78
+ api: ANTIGRAVITY_API,
79
+ stream: streamAntigravity,
80
+ streamSimple: streamAntigravity,
81
+ });
82
+
83
+ const initialCatalog = getCurrentAntigravityCatalog();
84
+
85
+ pi.registerProvider(PROVIDER_ID, {
86
+ name: PROVIDER_NAME,
87
+ baseUrl: DEFAULT_ENDPOINT,
88
+ api: ANTIGRAVITY_API,
89
+ models: initialCatalog.models,
90
+ refreshModels: refreshAntigravityModels,
91
+ oauth: {
92
+ name: PROVIDER_NAME,
93
+ login: loginAntigravity,
94
+ refreshToken: refreshAntigravityToken,
95
+ getApiKey,
96
+ },
97
+ streamSimple: streamAntigravity,
98
+ });
99
+
100
+ pi.registerCommand("antigravity.usage", {
101
+ description: "Show Antigravity shared quota pools (Gemini / Claude+GPT, 5h + weekly)",
102
+ handler: async (_args, ctx) => {
103
+ await withUsage(ctx, formatUsageSummary);
104
+ },
105
+ });
106
+
107
+ pi.registerCommand("antigravity.models", {
108
+ description: "List Antigravity runtime models + remaining pool fraction",
109
+ handler: async (args, ctx) => {
110
+ const all = /\ball\b/i.test(args || "");
111
+ await withUsage(ctx, (usage) => formatModelsList(usage, { all }));
112
+ },
113
+ });
114
+
115
+ pi.registerCommand("antigravity.refresh", {
116
+ description: "Force refresh Antigravity dynamic model catalog",
117
+ handler: async (_args, ctx) => {
118
+ const apiKey = await resolveApiKeyFromContext(ctx);
119
+ if (!apiKey) {
120
+ emitCommandOutput(
121
+ ctx,
122
+ "No Antigravity credentials. Run /login antigravity first.",
123
+ "warning",
124
+ );
125
+ return;
126
+ }
127
+ if (ctx.hasUI) ctx.ui.notify("Refreshing Antigravity models…", "info");
128
+ try {
129
+ if (typeof ctx.modelRegistry?.refresh === "function") {
130
+ const result = await ctx.modelRegistry.refresh({
131
+ force: true,
132
+ providers: [PROVIDER_ID],
133
+ });
134
+ if (result?.errors?.has(PROVIDER_ID)) {
135
+ throw result.errors.get(PROVIDER_ID)!;
136
+ }
137
+ } else {
138
+ const discovered = await discoverAntigravityModels(apiKey);
139
+ const next = resolvedCatalog(discovered, getCurrentAntigravityCatalog());
140
+ if (discovered.models.length > 0) {
141
+ applyAntigravityCatalog(next);
142
+ }
143
+ }
144
+ const catalog = getCurrentAntigravityCatalog();
145
+ const count = catalog.models.length;
146
+ const sample = catalog.models
147
+ .slice(0, 4)
148
+ .map((m) => m.name || m.id)
149
+ .join(", ");
150
+ emitCommandOutput(
151
+ ctx,
152
+ `Antigravity models refreshed (${count} available: ${sample}${count > 4 ? ", …" : ""})`,
153
+ "info",
154
+ );
155
+ } catch (error) {
156
+ const msg = error instanceof Error ? error.message : String(error);
157
+ emitCommandOutput(ctx, `Antigravity model refresh failed: ${redactSecrets(msg)}`, "error");
158
+ }
159
+ },
160
+ });
161
+
162
+ pi.registerCommand("antigravity.doctor", {
163
+ description: "Show sanitized Antigravity provider diagnostics",
164
+ handler: async (_args, ctx) => {
165
+ const d = getLastDiagnostics();
166
+ const lines = [
167
+ `provider=${PROVIDER_ID}`,
168
+ `lastResolvedRuntimeModel=${d.resolvedRuntimeModel || "none"}`,
169
+ `availableModels=${d.availableModels || "none"}`,
170
+ `matchedModel=${d.matchedModelDebug || "none"}`,
171
+ `lastEndpoint=${d.endpoint || "none"}`,
172
+ `lastStatus=${d.status ?? "none"}`,
173
+ `lastProjectId=${d.projectId || "none"}`,
174
+ ...(d.latencyMs !== undefined ? [`lastLatencyMs=${d.latencyMs}`] : []),
175
+ `toolSchemaWarnings=${d.toolSchemaWarnings || "none"}`,
176
+ `lastError=${d.error ? redactSecrets(d.error) : "none"}`,
177
+ "transport=native-streamSimple",
178
+ "runtimeCli=not-used",
179
+ "commands=/antigravity.usage /antigravity.models /antigravity.refresh /antigravity.doctor /antigravity.image",
180
+ ];
181
+ emitCommandOutput(ctx, `Antigravity doctor\n${lines.join("\n")}`);
182
+ },
183
+ });
184
+
185
+ pi.registerCommand("antigravity.image", {
186
+ description:
187
+ "Generate an image via Antigravity (usage: /antigravity.image [--ratio 16:9] <prompt>)",
188
+ handler: async (args, ctx) => {
189
+ const parsed = parseImageCommandArgs(args || "");
190
+ if (!parsed.prompt) {
191
+ emitCommandOutput(
192
+ ctx,
193
+ "Usage: /antigravity.image [--ratio 16:9] [--model gemini-3-pro-image] [--path file.png] <prompt>",
194
+ "warning",
195
+ );
196
+ return;
197
+ }
198
+ try {
199
+ const apiKey = await resolveApiKeyFromContext(ctx);
200
+ if (!apiKey) {
201
+ emitCommandOutput(
202
+ ctx,
203
+ "No Antigravity credentials. Run /login antigravity first.",
204
+ "warning",
205
+ );
206
+ return;
207
+ }
208
+ if (ctx.hasUI) ctx.ui.notify("Generating Antigravity image…", "info");
209
+ const result = await generateAntigravityImage({
210
+ apiKey,
211
+ cwd: ctx.cwd,
212
+ prompt: parsed.prompt,
213
+ aspectRatio: parsed.aspectRatio,
214
+ model: parsed.model,
215
+ path: parsed.path,
216
+ });
217
+ emitCommandOutput(ctx, `Saved image to ${result.savedPaths.join(", ")}`);
218
+ } catch (error) {
219
+ const msg = error instanceof Error ? error.message : String(error);
220
+ emitCommandOutput(ctx, `Antigravity image failed: ${redactSecrets(msg)}`, "warning");
221
+ }
222
+ },
223
+ });
224
+
225
+ pi.registerTool({
226
+ name: "generate_image",
227
+ label: "Generate image",
228
+ description:
229
+ "Generate an image via Antigravity using the signed-in Google account. Saves under .pi/generated-images/ unless path is set.",
230
+ promptSnippet: "Generate images via Antigravity OAuth (Gemini image models)",
231
+ promptGuidelines: [
232
+ "Use generate_image when the user asks to create, draw, or generate an image.",
233
+ ],
234
+ parameters: Type.Object({
235
+ prompt: Type.String({ description: "Image description." }),
236
+ aspectRatio: Type.Optional(StringEnum(IMAGE_ASPECT_RATIOS)),
237
+ model: Type.Optional(
238
+ Type.String({
239
+ description: `Image model id. Default: ${DEFAULT_IMAGE_MODEL}.`,
240
+ }),
241
+ ),
242
+ path: Type.Optional(
243
+ Type.String({
244
+ description: "Project-relative file or directory to save the image.",
245
+ }),
246
+ ),
247
+ }),
248
+ async execute(_toolCallId, params, signal, onUpdate, ctx) {
249
+ const apiKey = await ctx.modelRegistry.getApiKeyForProvider("antigravity");
250
+ if (!apiKey) {
251
+ throw new Error("No Antigravity credentials. Run /login antigravity first.");
252
+ }
253
+ onUpdate?.({ content: [{ type: "text", text: "Generating image…" }], details: {} });
254
+ const result = await generateAntigravityImage({
255
+ apiKey,
256
+ cwd: ctx.cwd,
257
+ prompt: params.prompt,
258
+ aspectRatio: params.aspectRatio,
259
+ model: params.model,
260
+ path: params.path,
261
+ signal,
262
+ });
263
+ const notes = result.text.join(" ").trim();
264
+ return {
265
+ content: [
266
+ {
267
+ type: "text" as const,
268
+ text: `Saved image to ${result.savedPaths.join(", ")}${notes ? `. ${notes}` : ""}`,
269
+ },
270
+ ...result.images.map((image) => ({
271
+ type: "image" as const,
272
+ data: image.data,
273
+ mimeType: image.mimeType,
274
+ })),
275
+ ],
276
+ details: { model: result.model, savedPaths: result.savedPaths },
277
+ };
278
+ },
279
+ });
280
+ }
@@ -0,0 +1,154 @@
1
+ import type {
2
+ Api,
3
+ Credential,
4
+ Model,
5
+ ModelsStoreEntry,
6
+ RefreshModelsContext,
7
+ } from "@earendil-works/pi-ai";
8
+ import type { ProviderModelConfig } from "@earendil-works/pi-coding-agent";
9
+ import { getApiKey } from "../auth/index.js";
10
+ import { DEFAULT_ENDPOINT, fetchAvailableModelsCatalog, parseApiKey } from "../client/index.js";
11
+ import { ANTIGRAVITY_API } from "../types/types.js";
12
+ import { antigravityEnv, isRecord } from "../utils/util.js";
13
+ import { buildAntigravityCatalog, resolvedCatalog, type AntigravityCatalog } from "./grouping.js";
14
+ import {
15
+ ANTIGRAVITY_MODELS,
16
+ ANTIGRAVITY_ROUTING,
17
+ applyAntigravityCatalog,
18
+ getCurrentAntigravityCatalog,
19
+ registerDiscoveredModelEnums,
20
+ restoreDynamicModelEnums,
21
+ snapshotDynamicModelEnums,
22
+ } from "./models.js";
23
+
24
+ export const DEFAULT_CATALOG_REFRESH_INTERVAL_MS = 4 * 60 * 60 * 1000;
25
+ export const ANTIGRAVITY_PERSIST_KEY = "pi-antigravity";
26
+
27
+ type PersistedAntigravityCatalog = {
28
+ catalog: AntigravityCatalog;
29
+ checkedAt: number;
30
+ modelEnums: Record<string, string>;
31
+ };
32
+
33
+ export function getCatalogRefreshIntervalMs(): number {
34
+ const envVal =
35
+ antigravityEnv("CATALOG_REFRESH_INTERVAL_MS") ?? antigravityEnv("REFRESH_INTERVAL_MS");
36
+ if (envVal) {
37
+ const parsed = Number.parseInt(envVal, 10);
38
+ if (!Number.isNaN(parsed) && parsed >= 0) return parsed;
39
+ }
40
+ return DEFAULT_CATALOG_REFRESH_INTERVAL_MS;
41
+ }
42
+
43
+ const fallbackCatalog = (): AntigravityCatalog => ({
44
+ models: ANTIGRAVITY_MODELS,
45
+ routing: { ...ANTIGRAVITY_ROUTING },
46
+ });
47
+
48
+ /** Restore provider state supplied by Pi before checking offline mode or refresh TTL. */
49
+ export function hydrateAntigravityCatalog(stored: unknown): number {
50
+ if (!isRecord(stored)) return 0;
51
+ const persisted = stored[ANTIGRAVITY_PERSIST_KEY];
52
+ if (!isRecord(persisted)) return 0;
53
+
54
+ if (isStringMap(persisted.modelEnums)) restoreDynamicModelEnums(persisted.modelEnums);
55
+ if (isCatalog(persisted.catalog)) applyAntigravityCatalog(persisted.catalog);
56
+ return typeof persisted.checkedAt === "number" && persisted.checkedAt > 0
57
+ ? persisted.checkedAt
58
+ : 0;
59
+ }
60
+
61
+ export async function discoverAntigravityModels(
62
+ apiKey: string,
63
+ signal?: AbortSignal,
64
+ ): Promise<AntigravityCatalog> {
65
+ const creds = parseApiKey(apiKey);
66
+ const available = await fetchAvailableModelsCatalog(creds.token, creds.projectId, signal);
67
+ const models = available.data.models;
68
+ if (!models || Object.keys(models).length === 0) {
69
+ return { models: [], routing: {} };
70
+ }
71
+ registerDiscoveredModelEnums(models);
72
+ return buildAntigravityCatalog(models, fallbackCatalog());
73
+ }
74
+
75
+ export async function refreshAntigravityModels(
76
+ context: RefreshModelsContext,
77
+ ): Promise<ProviderModelConfig[]> {
78
+ const checkedAt = hydrateAntigravityCatalog(context.stored);
79
+ const current = getCurrentAntigravityCatalog();
80
+ if (!context.allowNetwork) return current.models;
81
+
82
+ const apiKey = apiKeyFromCredential(context.credential);
83
+ if (!apiKey || context.signal.aborted) return current.models;
84
+
85
+ const now = Date.now();
86
+ if (
87
+ !context.force &&
88
+ checkedAt > 0 &&
89
+ now >= checkedAt &&
90
+ now - checkedAt < getCatalogRefreshIntervalMs()
91
+ ) {
92
+ return current.models;
93
+ }
94
+
95
+ try {
96
+ const discovered = await discoverAntigravityModels(apiKey, context.signal);
97
+ if (context.signal.aborted) return current.models;
98
+ const next = resolvedCatalog(discovered, current);
99
+ if (next.models.length > 0 && discovered.models.length > 0) {
100
+ applyAntigravityCatalog(next);
101
+ const refreshedAt = Date.now();
102
+ await context.publish({
103
+ persist: {
104
+ models: toStoredModels(next.models),
105
+ [ANTIGRAVITY_PERSIST_KEY]: {
106
+ catalog: next,
107
+ checkedAt: refreshedAt,
108
+ modelEnums: snapshotDynamicModelEnums(),
109
+ } satisfies PersistedAntigravityCatalog,
110
+ } as unknown as ModelsStoreEntry,
111
+ });
112
+ return next.models;
113
+ }
114
+ } catch (error) {
115
+ // Keep last-known-good models; a failed refresh must not wipe the catalog.
116
+ // Forced/manual refreshes must still report the failure to their caller.
117
+ if (context.force) throw error;
118
+ }
119
+
120
+ return getCurrentAntigravityCatalog().models;
121
+ }
122
+
123
+ function isCatalog(value: unknown): value is AntigravityCatalog {
124
+ return (
125
+ isRecord(value) &&
126
+ Array.isArray(value.models) &&
127
+ value.models.length > 0 &&
128
+ isRecord(value.routing)
129
+ );
130
+ }
131
+
132
+ function isStringMap(value: unknown): value is Record<string, string> {
133
+ return isRecord(value) && Object.values(value).every((entry) => typeof entry === "string");
134
+ }
135
+
136
+ function apiKeyFromCredential(credential: Credential | undefined): string | undefined {
137
+ if (!credential) return undefined;
138
+ if (credential.type === "api_key") {
139
+ return typeof credential.key === "string" && credential.key ? credential.key : undefined;
140
+ }
141
+ if (credential.type === "oauth" && typeof credential.access === "string") {
142
+ return getApiKey(credential);
143
+ }
144
+ return undefined;
145
+ }
146
+
147
+ function toStoredModels(models: ProviderModelConfig[]): Model<Api>[] {
148
+ return models.map((model) => ({
149
+ ...model,
150
+ api: ANTIGRAVITY_API,
151
+ provider: "antigravity",
152
+ baseUrl: DEFAULT_ENDPOINT,
153
+ }));
154
+ }