@tt-a1i/openpi 0.4.0 → 0.6.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.
Files changed (141) hide show
  1. package/README.md +116 -46
  2. package/SETUP.md +29 -7
  3. package/THIRD_PARTY_NOTICES.md +16 -0
  4. package/assets/openpi-launch-card-v1.webp +0 -0
  5. package/bin/openpi.js +155 -0
  6. package/extensions/ai-providers/LICENSE.upstream +23 -0
  7. package/extensions/ai-providers/README.md +59 -0
  8. package/extensions/ai-providers/antigravity/credentials.ts +52 -0
  9. package/extensions/ai-providers/antigravity/discovery.ts +130 -0
  10. package/extensions/ai-providers/antigravity/google-conversion.ts +455 -0
  11. package/extensions/ai-providers/antigravity/models.ts +84 -0
  12. package/extensions/ai-providers/antigravity/oauth.ts +700 -0
  13. package/extensions/ai-providers/antigravity/provider.ts +1116 -0
  14. package/extensions/ai-providers/antigravity/routing.ts +340 -0
  15. package/extensions/ai-providers/antigravity/with-resolvers.d.ts +19 -0
  16. package/extensions/ai-providers/cursor/constants.ts +5 -0
  17. package/extensions/ai-providers/cursor/credentials.ts +14 -0
  18. package/extensions/ai-providers/cursor/discovery.ts +291 -0
  19. package/extensions/ai-providers/cursor/input-images.ts +106 -0
  20. package/extensions/ai-providers/cursor/models.ts +45 -0
  21. package/extensions/ai-providers/cursor/oauth.ts +263 -0
  22. package/extensions/ai-providers/cursor/proto.ts +1064 -0
  23. package/extensions/ai-providers/cursor/protobuf.ts +1171 -0
  24. package/extensions/ai-providers/cursor/provider.ts +1175 -0
  25. package/extensions/ai-providers/cursor/proxy.ts +213 -0
  26. package/extensions/ai-providers/cursor/with-resolvers.d.ts +12 -0
  27. package/extensions/ai-providers/index.ts +86 -0
  28. package/extensions/ai-providers/oauth-adapter.ts +81 -0
  29. package/extensions/ai-providers/usage.ts +10 -0
  30. package/extensions/background-terminals/index.ts +38 -3
  31. package/extensions/background-terminals/src/domain.ts +2 -0
  32. package/extensions/background-terminals/src/manager.ts +484 -106
  33. package/extensions/background-terminals/src/output.ts +33 -0
  34. package/extensions/background-terminals/src/prompt.ts +13 -5
  35. package/extensions/background-terminals/src/result-delivery.ts +47 -24
  36. package/extensions/clear-context/index.ts +83 -0
  37. package/extensions/context-pivot/index.ts +16 -6
  38. package/extensions/cron/index.ts +68 -27
  39. package/extensions/cron/schedule.ts +12 -2
  40. package/extensions/file-mutation-display/render.ts +17 -257
  41. package/extensions/file-search/src/binaries.ts +57 -41
  42. package/extensions/git-read/index.ts +1 -3
  43. package/extensions/model-info/cache-diagnostics.ts +220 -0
  44. package/extensions/model-info/index.ts +65 -33
  45. package/extensions/model-info/session-metrics.ts +96 -0
  46. package/extensions/plan-mode/bash-policy.ts +54 -9
  47. package/extensions/plan-mode/index.ts +82 -6
  48. package/extensions/post-edit/index.ts +16 -6
  49. package/extensions/sessions/git-stats.ts +258 -72
  50. package/extensions/sessions/index.ts +153 -86
  51. package/extensions/sessions/preview-cache.ts +104 -0
  52. package/extensions/sessions/preview-loader.ts +856 -0
  53. package/extensions/sessions/sessions.ts +43 -4
  54. package/extensions/setup/index.ts +138 -130
  55. package/extensions/shared/activity-status.ts +30 -0
  56. package/extensions/shared/agent-session-page.ts +319 -0
  57. package/extensions/shared/agent-tool-renderer.ts +218 -0
  58. package/extensions/shared/agent-transcript.ts +524 -0
  59. package/extensions/shared/capability-intent.ts +1 -1
  60. package/extensions/shared/child-session.ts +457 -21
  61. package/extensions/shared/completion-inbox.ts +193 -0
  62. package/extensions/shared/result-delivery.ts +34 -0
  63. package/extensions/shared/setup-config.ts +83 -34
  64. package/extensions/shared/setup-episode-state.ts +1 -1
  65. package/extensions/shared/structured-output.ts +154 -0
  66. package/extensions/shared/terminal-text.ts +110 -23
  67. package/extensions/shared/text-projection.ts +72 -15
  68. package/extensions/shared/tool-activity.ts +382 -0
  69. package/extensions/shared/tool-surface.ts +29 -2
  70. package/extensions/shared/transcript-viewport.ts +46 -0
  71. package/extensions/shared/web-observer-registry.ts +390 -0
  72. package/extensions/shared/worktree.ts +11 -0
  73. package/extensions/subagents/index.ts +313 -62
  74. package/extensions/subagents/navigation.ts +34 -5
  75. package/extensions/subagents/src/backend.ts +12 -1
  76. package/extensions/subagents/src/backends/pi.ts +450 -70
  77. package/extensions/subagents/src/domain.ts +21 -1
  78. package/extensions/subagents/src/manager.ts +39 -2
  79. package/extensions/subagents/src/prompt.ts +49 -7
  80. package/extensions/subagents/src/result-artifact.ts +36 -0
  81. package/extensions/subagents/src/result-delivery.ts +39 -14
  82. package/extensions/subagents/src/runtime.ts +15 -1
  83. package/extensions/subagents/src/ui/takeover.ts +73 -257
  84. package/extensions/subagents/src/ui/transcript.ts +38 -535
  85. package/extensions/subagents/src/ui/wait-result.ts +103 -15
  86. package/extensions/suggestions/src/ui.ts +10 -4
  87. package/extensions/tasks/index.ts +0 -3
  88. package/extensions/ui-customization/footer.ts +16 -45
  89. package/extensions/ui-customization/index.ts +0 -4
  90. package/extensions/user-input-fold/index.ts +42 -6
  91. package/extensions/web/index.ts +257 -0
  92. package/extensions/workflows/acceptance.ts +43 -19
  93. package/extensions/workflows/artifacts.ts +137 -47
  94. package/extensions/workflows/completion-projection.ts +459 -0
  95. package/extensions/workflows/coordinator.ts +8 -10
  96. package/extensions/workflows/dashboard.ts +175 -228
  97. package/extensions/workflows/handoff.ts +70 -16
  98. package/extensions/workflows/index.ts +501 -198
  99. package/extensions/workflows/journal.ts +148 -13
  100. package/extensions/workflows/model.ts +79 -5
  101. package/extensions/workflows/navigation.ts +32 -8
  102. package/extensions/workflows/progress-projection.ts +306 -0
  103. package/extensions/workflows/prompt.ts +70 -16
  104. package/extensions/workflows/replay-safety.ts +42 -21
  105. package/extensions/workflows/result-delivery.ts +214 -76
  106. package/extensions/workflows/retention.ts +599 -0
  107. package/extensions/workflows/runner.ts +389 -345
  108. package/extensions/workflows/sandbox-child.cjs +25 -3
  109. package/extensions/workflows/sandbox.ts +62 -8
  110. package/extensions/workflows/serialization.ts +325 -17
  111. package/extensions/workflows/tool-renderer.ts +22 -0
  112. package/extensions/workflows/transcript.ts +149 -0
  113. package/extensions/workspace-cleanup-guard/index.ts +54 -0
  114. package/extensions/workspace-cleanup-guard/workspace-provenance.ts +563 -0
  115. package/package.json +34 -14
  116. package/skills/subagents/REFERENCE.md +190 -0
  117. package/skills/subagents/SKILL.md +2 -1
  118. package/skills/workflows/REFERENCE.md +6 -4
  119. package/skills/workflows/SKILL.md +1 -1
  120. package/web/adapter/pi-adapter.ts +664 -0
  121. package/web/host/browser-launcher.ts +20 -0
  122. package/web/host/pi-coding-agent-entry.ts +162 -0
  123. package/web/host/static-assets.ts +4 -0
  124. package/web/host/terminal-status.ts +38 -0
  125. package/web/host/web-host.ts +1069 -0
  126. package/web/http-dispatcher.ts +125 -0
  127. package/web/protocol/types.ts +467 -0
  128. package/web/runtime/pi-runtime.ts +1206 -0
  129. package/web/runtime/types.ts +102 -0
  130. package/web/runtime/web-host-lease.ts +497 -0
  131. package/web/trace.ts +18 -0
  132. package/web/ui/app.js +1700 -0
  133. package/web/ui/index.html +142 -0
  134. package/web/ui/styles.css +680 -0
  135. package/web/vite.config.mjs +34 -0
  136. package/extensions/execution-convergence/active-evidence.ts +0 -129
  137. package/extensions/execution-convergence/index.ts +0 -442
  138. package/extensions/execution-convergence/workspace-provenance.ts +0 -338
  139. package/extensions/setup/intercom-fs-helper.cjs +0 -130
  140. package/extensions/setup/intercom.ts +0 -603
  141. package/extensions/subagents/src/backends/stub.ts +0 -303
package/bin/openpi.js ADDED
@@ -0,0 +1,155 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { resolve } from "node:path";
4
+ import { createJiti } from "jiti";
5
+
6
+ function printHelp() {
7
+ console.log(`OpenPI Web Workbench
8
+
9
+ Usage:
10
+ openpi web [workspace]
11
+ openpi [workspace] Alias for openpi web [workspace]
12
+
13
+ Options:
14
+ --port <number> Bind a specific loopback port (development)
15
+ --no-open Do not open a browser (development)
16
+
17
+ Starts an isolated local Web runtime. Browser conversations and session changes
18
+ never enter an interactive terminal Pi session.`);
19
+ }
20
+
21
+ const args = process.argv.slice(2);
22
+ if (args.includes("--help") || args.includes("-h")) {
23
+ printHelp();
24
+ process.exit(0);
25
+ }
26
+ const command = args[0] === "web" ? args.slice(1) : args;
27
+ const noOpen = command.includes("--no-open");
28
+ const noWorkspace = command.includes("--no-workspace");
29
+ const portIndex = command.indexOf("--port");
30
+ const portText = portIndex >= 0 ? command[portIndex + 1] : undefined;
31
+ const workspaceArgs = [];
32
+ for (let index = 0; index < command.length; index++) {
33
+ const value = command[index];
34
+ if (value === "--no-open") continue;
35
+ if (value === "--no-workspace") continue;
36
+ if (value === "--port") {
37
+ index++;
38
+ continue;
39
+ }
40
+ workspaceArgs.push(value);
41
+ }
42
+ const configuredPort = portText ?? process.env.OPENPI_WEB_PORT;
43
+ const port = configuredPort === undefined ? undefined : Number(configuredPort);
44
+ if (portIndex >= 0 && (!portText || portText.startsWith("--"))) {
45
+ console.error("--port requires a value");
46
+ process.exit(1);
47
+ }
48
+ if (port !== undefined && (!Number.isInteger(port) || port < 0 || port > 65535)) {
49
+ console.error("--port must be an integer between 0 and 65535");
50
+ process.exit(1);
51
+ }
52
+ if (workspaceArgs.length > 1) {
53
+ console.error("Usage: openpi web [workspace]");
54
+ process.exit(1);
55
+ }
56
+ if (noWorkspace && workspaceArgs.length > 0) {
57
+ console.error("--no-workspace cannot be combined with a workspace");
58
+ process.exit(1);
59
+ }
60
+
61
+ let host;
62
+ let runtime;
63
+ let stopping;
64
+ const stop = () => {
65
+ stopping ??= host?.stop() ?? runtime?.dispose() ?? Promise.resolve();
66
+ return stopping;
67
+ };
68
+
69
+ try {
70
+ const bootstrap = createJiti(import.meta.url);
71
+ const { missingPiCodingAgentDiagnostic, resolveStandaloneJitiAliases } =
72
+ await bootstrap.import("../web/host/pi-coding-agent-entry.ts");
73
+ const aliases = resolveStandaloneJitiAliases({
74
+ fromUrl: import.meta.url,
75
+ });
76
+ if (!aliases["@earendil-works/pi-coding-agent"]) {
77
+ console.error(missingPiCodingAgentDiagnostic());
78
+ process.exit(1);
79
+ }
80
+ const jiti = createJiti(import.meta.url, { alias: aliases });
81
+ const [browserModule, hostModule, runtimeModule, statusModule, traceModule] =
82
+ await Promise.all([
83
+ jiti.import("../web/host/browser-launcher.ts"),
84
+ jiti.import("../web/host/web-host.ts"),
85
+ jiti.import("../web/runtime/pi-runtime.ts"),
86
+ jiti.import("../web/host/terminal-status.ts"),
87
+ jiti.import("../web/trace.ts"),
88
+ ]);
89
+ const { openBrowser } = browserModule;
90
+ const { WebHost } = hostModule;
91
+ const { PiWebRuntime } = runtimeModule;
92
+ const { formatWebReadyScreen } = statusModule;
93
+ const { traceWeb } = traceModule;
94
+ runtime = noWorkspace
95
+ ? await PiWebRuntime.createWithoutWorkspace()
96
+ : await PiWebRuntime.create(resolve(workspaceArgs[0] ?? process.cwd()));
97
+ host = new WebHost({
98
+ runtime,
99
+ ...(port === undefined ? {} : { port }),
100
+ ...(process.env.OPENPI_WEB_TOKEN
101
+ ? { token: process.env.OPENPI_WEB_TOKEN }
102
+ : {}),
103
+ ...(process.env.OPENPI_WEB_ALLOWED_ORIGIN
104
+ ? { allowedOrigins: [process.env.OPENPI_WEB_ALLOWED_ORIGIN] }
105
+ : {}),
106
+ });
107
+ await host.start();
108
+ const onStopSignal = () => {
109
+ void stop().then(
110
+ () => process.exit(0),
111
+ (error) => {
112
+ console.error(
113
+ `Failed to stop OpenPI Web Workbench: ${error instanceof Error ? error.message : String(error)}`,
114
+ );
115
+ process.exit(1);
116
+ },
117
+ );
118
+ };
119
+ for (const signal of ["SIGINT", "SIGTERM"]) {
120
+ process.once(signal, onStopSignal);
121
+ }
122
+ traceWeb("web_started", {
123
+ ...(runtime.workspaceSelected === true ? { cwd: runtime.cwd } : {}),
124
+ origin: host.origin,
125
+ });
126
+ const opened = noOpen ? false : await openBrowser(host.url);
127
+ if (noWorkspace) {
128
+ console.log(
129
+ formatWebReadyScreen({
130
+ origin: host.origin,
131
+ url: host.url,
132
+ opened,
133
+ }),
134
+ );
135
+ } else {
136
+ console.log(`OpenPI Web Workbench is running at ${host.origin}`);
137
+ if (!opened) console.log(`Open this URL in a browser: ${host.url}`);
138
+ }
139
+ } catch (error) {
140
+ let cleanupError;
141
+ try {
142
+ await stop();
143
+ } catch (caught) {
144
+ cleanupError = caught;
145
+ }
146
+ console.error(
147
+ `Failed to start OpenPI Web Workbench: ${error instanceof Error ? error.message : String(error)}`,
148
+ );
149
+ if (cleanupError) {
150
+ console.error(
151
+ `Failed to clean up OpenPI Web Workbench: ${cleanupError instanceof Error ? cleanupError.message : String(cleanupError)}`,
152
+ );
153
+ }
154
+ process.exit(1);
155
+ }
@@ -0,0 +1,23 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Mario Zechner
4
+ Copyright (c) 2025-2026 Can Bölük
5
+ Copyright (c) 2026 Stencil Labs, Inc.
6
+
7
+ Permission is hereby granted, free of charge, to any person obtaining a copy
8
+ of this software and associated documentation files (the "Software"), to deal
9
+ in the Software without restriction, including without limitation the rights
10
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
11
+ copies of the Software, and to permit persons to whom the Software is
12
+ furnished to do so, subject to the following conditions:
13
+
14
+ The above copyright notice and this permission notice shall be included in all
15
+ copies or substantial portions of the Software.
16
+
17
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
23
+ SOFTWARE.
@@ -0,0 +1,59 @@
1
+ # OAuth model providers
2
+
3
+ This extension registers two opt-in model providers backed by account OAuth:
4
+
5
+ - `google-antigravity` uses Google Cloud Code Assist and supports ordinary Pi
6
+ tool calls.
7
+ - `cursor` uses Cursor AgentService and is experimental, chat-only support.
8
+ It does not advertise or execute Cursor-native coding tools. If the server
9
+ requests one, the request fails explicitly instead of bypassing Pi's tool and
10
+ permission lifecycle.
11
+
12
+ After installing OpenPI, restart Pi or run `/reload`, then authenticate and
13
+ select a model:
14
+
15
+ ```text
16
+ /login google-antigravity
17
+ /login cursor
18
+ /model
19
+ ```
20
+
21
+ For source-checkout testing, follow the repository's
22
+ [development runtime provenance procedure](../../README.md#开发运行时区分-npm-与当前源码).
23
+ Remove any previously installed OpenPI source, install the checkout, and verify
24
+ that `pi list` reports this checkout as the only OpenPI source before reloading
25
+ Pi. Do not mix an installed OpenPI package with an explicitly loaded checkout
26
+ extension, because that does not prove which source owns the runtime behavior.
27
+
28
+ ```sh
29
+ pi list
30
+ OLD_OPENPI_SOURCE=/absolute/path/to/old/openpi
31
+ pi remove "$OLD_OPENPI_SOURCE"
32
+ pi install "$PWD"
33
+ pi list
34
+ ```
35
+
36
+ The model catalog is refreshed from the authenticated account and persisted by
37
+ Pi. A failed refresh retains the last successful account catalog; Antigravity
38
+ also has a validated static baseline, while Cursor keeps the server-side `Auto`
39
+ route as its baseline. No provider is contacted until it is selected.
40
+
41
+ Pi's interactive clipboard flow inserts an image's local path into the editor.
42
+ When Cursor is selected, a supported PNG/JPEG/GIF/WebP path at the start of an
43
+ interactive prompt is converted into an actual image attachment (up to 10 MiB)
44
+ before the request is sent. The absolute path is not exposed to the model.
45
+
46
+ The provider also adds an explicit chat-only rule so the normal Pi coding
47
+ system prompt cannot cause Cursor to attempt unavailable read or shell tools.
48
+
49
+ Cursor's token delta describes generated output only, so the provider does not
50
+ publish it as complete context usage. Pi 0.84.3+ can estimate an all-Cursor
51
+ history and trigger threshold compaction without provider usage. Older Pi hosts
52
+ retain the correct unknown-usage state but cannot automatically threshold-
53
+ compact a session with no usage-backed response. Project-wide Pi baseline
54
+ tracking is kept in [#328](https://github.com/openpi-dev/openpi/issues/328).
55
+
56
+ Cursor model discovery and chat use HTTP/2. They honor `PI_PROXY_CURSOR`, then
57
+ `PI_PROXY`, the standard `HTTPS_PROXY`/`HTTP_PROXY` variables, and `ALL_PROXY`;
58
+ `NO_PROXY` bypass rules apply. The proxy must support HTTP CONNECT and preserve
59
+ HTTP/2 ALPN negotiation to Cursor.
@@ -0,0 +1,52 @@
1
+ /**
2
+ * Credential codec for the Antigravity provider.
3
+ *
4
+ * pi persists whatever object `login` returns into auth.json and hands it back
5
+ * to `refreshToken`/`getApiKey` verbatim, so the Antigravity-specific extras
6
+ * (projectId, email) ride along as additional fields. `getApiKey` then packs
7
+ * everything the stream needs into the single `apiKey` string pi threads into
8
+ * `SimpleStreamOptions.apiKey`.
9
+ */
10
+
11
+ import type { OAuthCredentials } from "@earendil-works/pi-ai/compat";
12
+
13
+ /** Stored credential shape: pi's OAuth fields plus Antigravity extras. */
14
+ export interface AntigravityCredentials extends OAuthCredentials {
15
+ /** Cloud Code Assist project resolved during login provisioning. */
16
+ projectId?: string;
17
+ /** Google account email, best-effort display metadata. */
18
+ email?: string;
19
+ }
20
+
21
+ /** What the stream function needs, packed into the apiKey string. */
22
+ export interface AntigravityApiKeyPayload {
23
+ token: string;
24
+ projectId?: string;
25
+ }
26
+
27
+ export function encodeApiKey(credentials: AntigravityCredentials): string {
28
+ const payload: AntigravityApiKeyPayload = {
29
+ token: credentials.access,
30
+ projectId: credentials.projectId,
31
+ };
32
+ return JSON.stringify(payload);
33
+ }
34
+
35
+ export function decodeApiKey(raw: string): AntigravityApiKeyPayload {
36
+ try {
37
+ const parsed: unknown = JSON.parse(raw);
38
+ if (parsed !== null && typeof parsed === "object" && "token" in parsed) {
39
+ const token = parsed.token;
40
+ if (typeof token === "string") {
41
+ const projectId =
42
+ "projectId" in parsed && typeof parsed.projectId === "string"
43
+ ? parsed.projectId
44
+ : undefined;
45
+ return { token, projectId };
46
+ }
47
+ }
48
+ } catch {
49
+ // Not JSON: tolerate a bare access token (e.g. hand-written auth.json).
50
+ }
51
+ return { token: raw };
52
+ }
@@ -0,0 +1,130 @@
1
+ /**
2
+ * Antigravity model discovery.
3
+ *
4
+ * `POST /v1internal:fetchAvailableModels` returns a map of wire model id to
5
+ * metadata (reference: omp packages/catalog/src/discovery/antigravity.ts).
6
+ * Static models are the provider baseline. Network/protocol failures throw so
7
+ * createProvider can retain the last successfully persisted dynamic catalog.
8
+ */
9
+
10
+ import type { RefreshModelsContext } from "@earendil-works/pi-ai";
11
+ import { Type, type Static } from "typebox";
12
+ import { Value } from "typebox/value";
13
+ import { ensureAntigravityVersion, getAntigravityUserAgent } from "./oauth.ts";
14
+ import {
15
+ ANTIGRAVITY_API_URL,
16
+ type AntigravityProviderModel,
17
+ } from "./models.ts";
18
+ import {
19
+ collapseAntigravityModels,
20
+ type AntigravityModelDefinition,
21
+ } from "./routing.ts";
22
+
23
+ const DISCOVERY_ENDPOINTS = [
24
+ "https://daily-cloudcode-pa.googleapis.com",
25
+ "https://daily-cloudcode-pa.sandbox.googleapis.com",
26
+ ] as const;
27
+ const FETCH_AVAILABLE_MODELS_PATH = "/v1internal:fetchAvailableModels";
28
+
29
+ // Reference: omp ANTIGRAVITY_DISCOVERY_DENYLIST.
30
+ const DISCOVERY_DENYLIST: Record<string, true> = {
31
+ chat_20706: true,
32
+ chat_23310: true,
33
+ "gemini-2.5-pro": true,
34
+ };
35
+
36
+ const DEFAULT_CONTEXT_WINDOW = 200_000;
37
+ const DEFAULT_MAX_TOKENS = 64_000;
38
+
39
+ const DiscoveryModelSchema = Type.Object({
40
+ displayName: Type.Optional(Type.String()),
41
+ supportsImages: Type.Optional(Type.Boolean()),
42
+ supportsThinking: Type.Optional(Type.Boolean()),
43
+ maxTokens: Type.Optional(Type.Number()),
44
+ maxOutputTokens: Type.Optional(Type.Number()),
45
+ isInternal: Type.Optional(Type.Boolean()),
46
+ });
47
+
48
+ const DiscoveryResponseSchema = Type.Object({
49
+ models: Type.Optional(Type.Record(Type.String(), DiscoveryModelSchema)),
50
+ });
51
+
52
+ type DiscoveryResponse = Static<typeof DiscoveryResponseSchema>;
53
+
54
+ type DiscoveredModel = AntigravityProviderModel & AntigravityModelDefinition;
55
+
56
+ function positiveNumber(value: number | undefined, fallback: number): number {
57
+ return value !== undefined && Number.isFinite(value) && value > 0
58
+ ? value
59
+ : fallback;
60
+ }
61
+
62
+ function toModelDefinition(
63
+ id: string,
64
+ meta: Static<typeof DiscoveryModelSchema>,
65
+ ): DiscoveredModel {
66
+ const advertisedMaxTokens = positiveNumber(
67
+ meta.maxOutputTokens,
68
+ DEFAULT_MAX_TOKENS,
69
+ );
70
+ return {
71
+ id,
72
+ name: meta.displayName ?? id,
73
+ api: "antigravity-cloudcode",
74
+ provider: "google-antigravity",
75
+ baseUrl: ANTIGRAVITY_API_URL,
76
+ reasoning: meta.supportsThinking === true,
77
+ input: meta.supportsImages === true ? ["text", "image"] : ["text"],
78
+ cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
79
+ contextWindow: positiveNumber(meta.maxTokens, DEFAULT_CONTEXT_WINDOW),
80
+ maxTokens: id.toLowerCase().includes("claude")
81
+ ? Math.min(advertisedMaxTokens, DEFAULT_MAX_TOKENS)
82
+ : advertisedMaxTokens,
83
+ };
84
+ }
85
+
86
+ export async function fetchAntigravityModels(
87
+ context: RefreshModelsContext,
88
+ ): Promise<DiscoveredModel[]> {
89
+ if (!context.allowNetwork) return [];
90
+ context.signal.throwIfAborted();
91
+ const credential = context.credential;
92
+ if (!credential || credential.type !== "oauth") return [];
93
+ await ensureAntigravityVersion(context.signal);
94
+
95
+ for (const endpoint of DISCOVERY_ENDPOINTS) {
96
+ if (context.signal.aborted) break;
97
+ try {
98
+ const response = await fetch(
99
+ `${endpoint}${FETCH_AVAILABLE_MODELS_PATH}`,
100
+ {
101
+ method: "POST",
102
+ headers: {
103
+ Authorization: `Bearer ${credential.access}`,
104
+ "Content-Type": "application/json",
105
+ "User-Agent": getAntigravityUserAgent(),
106
+ },
107
+ body: "{}",
108
+ signal: context.signal,
109
+ },
110
+ );
111
+ if (!response.ok) continue;
112
+ const parsed = Value.Parse(
113
+ DiscoveryResponseSchema,
114
+ await response.json(),
115
+ ) as DiscoveryResponse;
116
+ if (!parsed.models) continue;
117
+ const discovered = Object.entries(parsed.models)
118
+ .filter(
119
+ ([id, meta]) =>
120
+ !Object.hasOwn(DISCOVERY_DENYLIST, id) && meta.isInternal !== true,
121
+ )
122
+ .map(([id, meta]) => toModelDefinition(id, meta));
123
+ return collapseAntigravityModels(discovered);
124
+ } catch {
125
+ // Try the next endpoint; total failure must preserve the stored catalog.
126
+ }
127
+ }
128
+ context.signal.throwIfAborted();
129
+ throw new Error("Antigravity model discovery failed on all endpoints");
130
+ }