clawmoney 0.12.2 → 0.13.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.
@@ -0,0 +1,24 @@
1
+ /**
2
+ * `clawmoney antigravity login` — OAuth browser flow for Google Antigravity IDE.
3
+ *
4
+ * Antigravity is Google's agentic IDE. Its quota pool is separate from Gemini
5
+ * CLI's, so a provider who pairs an Antigravity daemon with a Gemini CLI
6
+ * daemon on the same Google account gets 2× Gemini capacity. More importantly,
7
+ * Antigravity is the only path that exposes Claude models to non-Anthropic-
8
+ * subscribed Google Ultra users.
9
+ *
10
+ * Flow:
11
+ * 1. Generate PKCE verifier + challenge.
12
+ * 2. Start a short-lived HTTP server on localhost:51121.
13
+ * 3. Print the Google consent URL (and try to open it in the browser).
14
+ * 4. Wait for Google to redirect back with ?code=....
15
+ * 5. Exchange the code for refresh + access tokens.
16
+ * 6. Resolve the cloudaicompanionProject via loadCodeAssist.
17
+ * 7. Persist to ~/.clawmoney/antigravity-accounts.json.
18
+ *
19
+ * The implementation borrows heavily from the opencode-antigravity-auth
20
+ * reference project — same client id, same scopes, same PKCE + state payload
21
+ * format, so tokens issued by this flow are interchangeable with that plugin.
22
+ */
23
+ export declare function antigravityLoginCommand(): Promise<void>;
24
+ export declare function antigravityStatusCommand(): Promise<void>;
@@ -0,0 +1,218 @@
1
+ /**
2
+ * `clawmoney antigravity login` — OAuth browser flow for Google Antigravity IDE.
3
+ *
4
+ * Antigravity is Google's agentic IDE. Its quota pool is separate from Gemini
5
+ * CLI's, so a provider who pairs an Antigravity daemon with a Gemini CLI
6
+ * daemon on the same Google account gets 2× Gemini capacity. More importantly,
7
+ * Antigravity is the only path that exposes Claude models to non-Anthropic-
8
+ * subscribed Google Ultra users.
9
+ *
10
+ * Flow:
11
+ * 1. Generate PKCE verifier + challenge.
12
+ * 2. Start a short-lived HTTP server on localhost:51121.
13
+ * 3. Print the Google consent URL (and try to open it in the browser).
14
+ * 4. Wait for Google to redirect back with ?code=....
15
+ * 5. Exchange the code for refresh + access tokens.
16
+ * 6. Resolve the cloudaicompanionProject via loadCodeAssist.
17
+ * 7. Persist to ~/.clawmoney/antigravity-accounts.json.
18
+ *
19
+ * The implementation borrows heavily from the opencode-antigravity-auth
20
+ * reference project — same client id, same scopes, same PKCE + state payload
21
+ * format, so tokens issued by this flow are interchangeable with that plugin.
22
+ */
23
+ import { createServer } from "node:http";
24
+ import { createHash, randomBytes } from "node:crypto";
25
+ import { exec } from "node:child_process";
26
+ import chalk from "chalk";
27
+ import ora from "ora";
28
+ import { ANTIGRAVITY_CLIENT_ID, ANTIGRAVITY_SCOPES, ANTIGRAVITY_REDIRECT_URI, exchangeAntigravityAuthCode, fetchAntigravityUserEmail, storeNewAntigravityAccount, ANTIGRAVITY_ACCOUNTS_FILE, loadAccounts, } from "../relay/upstream/antigravity-api.js";
29
+ const CALLBACK_PORT = 51121;
30
+ const CALLBACK_PATH = "/oauth-callback";
31
+ const CALLBACK_TIMEOUT_MS = 5 * 60 * 1000;
32
+ function base64Url(input) {
33
+ return input
34
+ .toString("base64")
35
+ .replace(/=/g, "")
36
+ .replace(/\+/g, "-")
37
+ .replace(/\//g, "_");
38
+ }
39
+ function generatePkce() {
40
+ // RFC 7636: verifier is 43–128 chars of [A-Z / a-z / 0-9 / "-" / "." / "_" / "~"].
41
+ // We use 32 random bytes → 43-char base64url, matching @openauthjs/openauth.
42
+ const verifier = base64Url(randomBytes(32));
43
+ const challenge = base64Url(createHash("sha256").update(verifier).digest());
44
+ return { verifier, challenge };
45
+ }
46
+ function encodeState(verifier) {
47
+ return Buffer.from(JSON.stringify({ verifier, projectId: "" }), "utf8").toString("base64url");
48
+ }
49
+ function buildAuthUrl(challenge, state) {
50
+ const url = new URL("https://accounts.google.com/o/oauth2/v2/auth");
51
+ url.searchParams.set("client_id", ANTIGRAVITY_CLIENT_ID);
52
+ url.searchParams.set("response_type", "code");
53
+ url.searchParams.set("redirect_uri", ANTIGRAVITY_REDIRECT_URI);
54
+ url.searchParams.set("scope", ANTIGRAVITY_SCOPES.join(" "));
55
+ url.searchParams.set("code_challenge", challenge);
56
+ url.searchParams.set("code_challenge_method", "S256");
57
+ url.searchParams.set("state", state);
58
+ url.searchParams.set("access_type", "offline");
59
+ // `prompt=consent` forces Google to re-issue a refresh_token even if the
60
+ // user has already granted the Antigravity scopes before. Without this,
61
+ // Google silently drops refresh_token from the token response and we'd
62
+ // store a dead account.
63
+ url.searchParams.set("prompt", "consent");
64
+ return url.toString();
65
+ }
66
+ function openInBrowser(url) {
67
+ const cmd = process.platform === "darwin"
68
+ ? `open "${url}"`
69
+ : process.platform === "win32"
70
+ ? `start "" "${url}"`
71
+ : `xdg-open "${url}"`;
72
+ exec(cmd, (err) => {
73
+ if (err) {
74
+ // Non-fatal — we already printed the URL for the user to copy.
75
+ }
76
+ });
77
+ }
78
+ function waitForCallback() {
79
+ return new Promise((resolve, reject) => {
80
+ const server = createServer((req, res) => {
81
+ if (!req.url || !req.url.startsWith(CALLBACK_PATH)) {
82
+ res.writeHead(404);
83
+ res.end();
84
+ return;
85
+ }
86
+ const parsed = new URL(req.url, `http://localhost:${CALLBACK_PORT}`);
87
+ const error = parsed.searchParams.get("error");
88
+ if (error) {
89
+ res.writeHead(400, { "content-type": "text/html; charset=utf-8" });
90
+ res.end(`<html><body><h1>Login failed</h1><p>${error}</p><p>You can close this tab and re-run <code>clawmoney antigravity login</code>.</p></body></html>`);
91
+ reject(new Error(`OAuth error: ${error}`));
92
+ return;
93
+ }
94
+ const code = parsed.searchParams.get("code");
95
+ const state = parsed.searchParams.get("state");
96
+ if (!code || !state) {
97
+ res.writeHead(400, { "content-type": "text/html; charset=utf-8" });
98
+ res.end("<html><body><h1>Missing code or state</h1></body></html>");
99
+ reject(new Error("OAuth callback missing code/state"));
100
+ return;
101
+ }
102
+ res.writeHead(200, { "content-type": "text/html; charset=utf-8" });
103
+ res.end(`<html><body style="font-family:system-ui;padding:40px;text-align:center;">
104
+ <h1>✓ ClawMoney is linked to Antigravity</h1>
105
+ <p>You can close this tab and return to your terminal.</p>
106
+ </body></html>`);
107
+ resolve({ result: { code, state }, server });
108
+ });
109
+ server.on("error", (err) => reject(err));
110
+ server.listen(CALLBACK_PORT, "127.0.0.1");
111
+ setTimeout(() => {
112
+ reject(new Error(`Timed out waiting for OAuth callback after ${Math.round(CALLBACK_TIMEOUT_MS / 60_000)} minutes.`));
113
+ }, CALLBACK_TIMEOUT_MS).unref();
114
+ });
115
+ }
116
+ export async function antigravityLoginCommand() {
117
+ console.log(chalk.bold("\n Antigravity OAuth login\n"));
118
+ console.log(chalk.dim(" This links a Google account's Antigravity IDE quota to your ClawMoney daemon."));
119
+ console.log(chalk.dim(" Antigravity has a SEPARATE quota pool from Gemini CLI — use both to double your capacity."));
120
+ console.log("");
121
+ const { verifier, challenge } = generatePkce();
122
+ const state = encodeState(verifier);
123
+ const authUrl = buildAuthUrl(challenge, state);
124
+ const spinner = ora("Starting local callback server on 127.0.0.1:51121...").start();
125
+ let callbackPromise;
126
+ try {
127
+ callbackPromise = waitForCallback();
128
+ spinner.succeed("Callback server ready");
129
+ }
130
+ catch (err) {
131
+ spinner.fail(`Failed to bind localhost:${CALLBACK_PORT} — is another clawmoney login already running?`);
132
+ throw err;
133
+ }
134
+ console.log("");
135
+ console.log(chalk.bold(" Open this URL in your browser to authorize:"));
136
+ console.log("");
137
+ console.log(" " + chalk.cyan(authUrl));
138
+ console.log("");
139
+ console.log(chalk.dim(" (Attempting to open it automatically. If nothing opens, copy it manually.)"));
140
+ openInBrowser(authUrl);
141
+ const waitSpinner = ora("Waiting for Google to redirect back...").start();
142
+ let code;
143
+ let server;
144
+ try {
145
+ const callback = await callbackPromise;
146
+ code = callback.result.code;
147
+ server = callback.server;
148
+ waitSpinner.succeed("Received authorization code");
149
+ }
150
+ catch (err) {
151
+ waitSpinner.fail(err.message);
152
+ throw err;
153
+ }
154
+ finally {
155
+ // The HTTP server is single-use; close it regardless of success.
156
+ }
157
+ server.close();
158
+ const exchangeSpinner = ora("Exchanging code for tokens...").start();
159
+ let tokens;
160
+ try {
161
+ tokens = await exchangeAntigravityAuthCode({
162
+ code,
163
+ code_verifier: verifier,
164
+ });
165
+ exchangeSpinner.succeed("Tokens received");
166
+ }
167
+ catch (err) {
168
+ exchangeSpinner.fail(err.message);
169
+ throw err;
170
+ }
171
+ const emailSpinner = ora("Fetching account email...").start();
172
+ const email = await fetchAntigravityUserEmail(tokens.access_token);
173
+ if (email) {
174
+ emailSpinner.succeed(`Linked account: ${email}`);
175
+ }
176
+ else {
177
+ emailSpinner.warn("Could not fetch account email (not fatal)");
178
+ }
179
+ const storeSpinner = ora("Resolving project ID and persisting account...").start();
180
+ try {
181
+ const account = await storeNewAntigravityAccount({
182
+ refresh_token: tokens.refresh_token,
183
+ access_token: tokens.access_token,
184
+ expiry_ms: tokens.expiry_ms,
185
+ email,
186
+ });
187
+ storeSpinner.succeed(`Saved to ${ANTIGRAVITY_ACCOUNTS_FILE} (project=${account.project_id})`);
188
+ }
189
+ catch (err) {
190
+ storeSpinner.fail(err.message);
191
+ throw err;
192
+ }
193
+ console.log("");
194
+ console.log(chalk.green(" Antigravity login complete."));
195
+ console.log("");
196
+ console.log(chalk.dim(" Next: register a model and start the daemon. Example:\n" +
197
+ " clawmoney relay register --cli antigravity --model antigravity-gemini-3-pro\n" +
198
+ " clawmoney relay start --cli antigravity"));
199
+ }
200
+ export async function antigravityStatusCommand() {
201
+ const file = loadAccounts();
202
+ if (file.accounts.length === 0) {
203
+ console.log(chalk.dim("No Antigravity accounts stored."));
204
+ console.log(chalk.dim(` Run "clawmoney antigravity login" to add one.`));
205
+ return;
206
+ }
207
+ console.log(chalk.bold("\n Antigravity accounts\n"));
208
+ for (let i = 0; i < file.accounts.length; i++) {
209
+ const a = file.accounts[i];
210
+ const expiryStr = a.expiry_ms
211
+ ? new Date(a.expiry_ms).toISOString().replace("T", " ").slice(0, 19)
212
+ : "unknown";
213
+ console.log(` ${i === 0 ? chalk.bold("●") : " "} ${a.email ?? "(email unknown)"}`);
214
+ console.log(` project: ${a.project_id ?? "-"}`);
215
+ console.log(` expires: ${expiryStr}`);
216
+ console.log("");
217
+ }
218
+ }
@@ -6,26 +6,63 @@ import ora from "ora";
6
6
  import { requireConfig } from "../utils/config.js";
7
7
  import { apiGet, apiPost } from "../utils/api.js";
8
8
  import { readRelayPid, isRelayPidAlive, removeRelayPid } from "../relay/provider.js";
9
+ import { API_PRICES, RELAY_DISCOUNT } from "../relay/pricing.js";
9
10
  const LOG_FILE = join(homedir(), ".clawmoney", "relay.log");
10
11
  export async function relayRegisterCommand(options) {
11
12
  const config = requireConfig();
12
13
  // Validate CLI type
13
- const validClis = ["claude", "codex", "gemini"];
14
+ const validClis = ["claude", "codex", "gemini", "antigravity"];
14
15
  if (!validClis.includes(options.cli)) {
15
16
  console.error(chalk.red(`Invalid CLI type "${options.cli}". Must be one of: ${validClis.join(", ")}`));
16
17
  process.exit(1);
17
18
  }
18
- // Verify CLI is installed
19
- const spinner = ora(`Checking if ${options.cli} is installed...`).start();
20
- try {
21
- execSync(`which ${options.cli}`, { stdio: "pipe" });
22
- spinner.succeed(`${options.cli} is available`);
19
+ // Antigravity is api-only — there is no local CLI binary. For the other
20
+ // types we still probe `which` so misconfigured boxes fail fast.
21
+ if (options.cli === "antigravity") {
22
+ const spinner = ora("Checking Antigravity OAuth token...").start();
23
+ try {
24
+ const { loadAccounts } = await import("../relay/upstream/antigravity-api.js");
25
+ const file = loadAccounts();
26
+ if (file.accounts.length === 0) {
27
+ spinner.fail(chalk.red("No Antigravity accounts found."));
28
+ console.log(chalk.dim(` Run "clawmoney antigravity login" first to link a Google account.`));
29
+ process.exit(1);
30
+ }
31
+ spinner.succeed(`Antigravity linked (${file.accounts[0].email ?? "email unknown"})`);
32
+ }
33
+ catch (err) {
34
+ spinner.fail(chalk.red(`Antigravity token check failed: ${err.message}`));
35
+ process.exit(1);
36
+ }
37
+ }
38
+ else {
39
+ const spinner = ora(`Checking if ${options.cli} is installed...`).start();
40
+ try {
41
+ execSync(`which ${options.cli}`, { stdio: "pipe" });
42
+ spinner.succeed(`${options.cli} is available`);
43
+ }
44
+ catch {
45
+ spinner.fail(chalk.red(`${options.cli} is not installed or not in PATH`));
46
+ console.log(chalk.dim(` Make sure ${options.cli} CLI is installed and accessible.`));
47
+ process.exit(1);
48
+ }
23
49
  }
24
- catch {
25
- spinner.fail(chalk.red(`${options.cli} is not installed or not in PATH`));
26
- console.log(chalk.dim(` Make sure ${options.cli} CLI is installed and accessible.`));
50
+ // Auto-populate prices from the LiteLLM-sourced pricing table. Providers
51
+ // register at the FULL official API price; the Hub applies RELAY_DISCOUNT
52
+ // at charge time so buyers pay a fixed fraction across all platforms.
53
+ const known = API_PRICES[options.model];
54
+ if (!known && (options.priceInput == null || options.priceOutput == null)) {
55
+ console.error(chalk.red(`Unknown model "${options.model}". Pricing table has no entry.`));
56
+ console.log(chalk.dim(` Either add it to clawmoney-cli/src/relay/pricing.ts, or pass both ` +
57
+ `--price-input and --price-output explicitly.`));
27
58
  process.exit(1);
28
59
  }
60
+ const priceInput = options.priceInput != null
61
+ ? parseFloat(options.priceInput)
62
+ : known.input;
63
+ const priceOutput = options.priceOutput != null
64
+ ? parseFloat(options.priceOutput)
65
+ : known.output;
29
66
  const regSpinner = ora("Registering as relay provider...").start();
30
67
  try {
31
68
  const body = {
@@ -34,8 +71,8 @@ export async function relayRegisterCommand(options) {
34
71
  mode: options.mode ?? "chat",
35
72
  concurrency: parseInt(options.concurrency ?? "5", 10),
36
73
  daily_limit_usd: parseFloat(options.dailyLimit ?? "20"),
37
- price_input_per_m: parseFloat(options.priceInput ?? "5"),
38
- price_output_per_m: parseFloat(options.priceOutput ?? "25"),
74
+ price_input_per_m: priceInput,
75
+ price_output_per_m: priceOutput,
39
76
  };
40
77
  const resp = await apiPost("/api/v1/relay/providers", body, config.api_key);
41
78
  if (!resp.ok) {
@@ -55,8 +92,10 @@ export async function relayRegisterCommand(options) {
55
92
  console.log(` ${chalk.bold("Mode:")} ${options.mode ?? "chat"}`);
56
93
  console.log(` ${chalk.bold("Concurrency:")} ${body.concurrency}`);
57
94
  console.log(` ${chalk.bold("Daily Limit:")} $${body.daily_limit_usd}`);
58
- console.log(` ${chalk.bold("Input Price:")} $${body.price_input_per_m}/1M tokens`);
59
- console.log(` ${chalk.bold("Output Price:")} $${body.price_output_per_m}/1M tokens`);
95
+ console.log(` ${chalk.bold("Input Price:")} $${body.price_input_per_m}/1M tokens (official API)`);
96
+ console.log(` ${chalk.bold("Output Price:")} $${body.price_output_per_m}/1M tokens (official API)`);
97
+ const discountPct = Math.round(RELAY_DISCOUNT * 100);
98
+ console.log(chalk.dim(` Buyers pay ${discountPct}% of the official API price — a ${100 - discountPct}% discount applied by the Hub.`));
60
99
  console.log("");
61
100
  console.log(chalk.dim(` Next: run "clawmoney relay start" to begin accepting requests.`));
62
101
  }
package/dist/index.js CHANGED
@@ -9,6 +9,7 @@ import { tweetCommand } from './commands/tweet.js';
9
9
  import { gigCreateCommand, gigBrowseCommand, gigDetailCommand, gigAcceptCommand, gigDeliverCommand, gigApproveCommand, gigDisputeCommand, } from './commands/gig.js';
10
10
  import { hubStartCommand, hubStopCommand, hubStatusCommand, hubSearchCommand, hubCallCommand, hubRegisterCommand, hubSkillsCommand, hubOrderCommand, hubHistoryCommand, } from './commands/hub.js';
11
11
  import { relayRegisterCommand, relayStartCommand, relayStopCommand, relayStatusCommand, relayModelsCommand, relayCreditsCommand, } from './commands/relay.js';
12
+ import { antigravityLoginCommand, antigravityStatusCommand, } from './commands/antigravity.js';
12
13
  import { createRequire } from 'node:module';
13
14
  const require = createRequire(import.meta.url);
14
15
  const pkg = require('../package.json');
@@ -389,7 +390,7 @@ const relay = program
389
390
  relay
390
391
  .command('register')
391
392
  .description('Register as a relay provider')
392
- .requiredOption('--cli <type>', 'Backend CLI: claude, codex, gemini')
393
+ .requiredOption('--cli <type>', 'Backend CLI: claude, codex, gemini, antigravity')
393
394
  .requiredOption('--model <model>', 'Model to offer (e.g., claude-opus-4-6)')
394
395
  .option('--mode <mode>', 'Safety mode: chat, search, code, full', 'chat')
395
396
  .option('--concurrency <n>', 'Max concurrent requests', '5')
@@ -408,7 +409,7 @@ relay
408
409
  relay
409
410
  .command('start')
410
411
  .description('Start accepting relay requests')
411
- .option('--cli <type>', 'Override CLI type (claude, codex, gemini)')
412
+ .option('--cli <type>', 'Override CLI type (claude, codex, gemini, antigravity)')
412
413
  .action(async (options) => {
413
414
  try {
414
415
  await relayStartCommand(options);
@@ -466,4 +467,32 @@ relay
466
467
  process.exit(1);
467
468
  }
468
469
  });
470
+ // antigravity (Google Antigravity IDE OAuth — separate quota pool + Claude access)
471
+ const antigravity = program
472
+ .command('antigravity')
473
+ .description('Google Antigravity IDE OAuth: link a Google account so the relay daemon can serve Claude + Gemini via the Antigravity quota pool');
474
+ antigravity
475
+ .command('login')
476
+ .description('OAuth browser flow to link a Google account')
477
+ .action(async () => {
478
+ try {
479
+ await antigravityLoginCommand();
480
+ }
481
+ catch (err) {
482
+ console.error(err.message);
483
+ process.exit(1);
484
+ }
485
+ });
486
+ antigravity
487
+ .command('status')
488
+ .description('Show linked Antigravity accounts')
489
+ .action(async () => {
490
+ try {
491
+ await antigravityStatusCommand();
492
+ }
493
+ catch (err) {
494
+ console.error(err.message);
495
+ process.exit(1);
496
+ }
497
+ });
469
498
  program.parse();
@@ -5,6 +5,16 @@
5
5
  * It runs the relay provider main loop (WS + Executor).
6
6
  */
7
7
  import { runRelayProvider } from "./provider.js";
8
+ import { relayLogger as logger } from "./logger.js";
9
+ // Process-level safety net: an unhandled promise rejection anywhere in the
10
+ // async reconnect / request path must NOT silently kill the daemon. Log it
11
+ // loudly and keep running — the reconnect loop will self-heal any broken WS.
12
+ process.on("unhandledRejection", (reason) => {
13
+ logger.error("Unhandled promise rejection (daemon continues running):", reason instanceof Error ? reason.stack ?? reason.message : reason);
14
+ });
15
+ process.on("uncaughtException", (err) => {
16
+ logger.error("Uncaught exception (daemon continues running):", err.stack ?? err.message);
17
+ });
8
18
  // Parse CLI args passed from the parent
9
19
  let cliType;
10
20
  const args = process.argv.slice(2);
@@ -1,16 +1,26 @@
1
1
  /**
2
2
  * API pricing per million tokens (USD).
3
- * Source: Official pricing pages (April 2026)
4
- * - Anthropic: https://platform.claude.com/docs/en/docs/about-claude/pricing
5
- * - OpenAI: https://developers.openai.com/api/docs/pricing
6
- * - Google: https://ai.google.dev/gemini-api/docs/pricing
3
+ *
4
+ * Source: cross-referenced against the LiteLLM community pricing database
5
+ * (`backend/resources/model-pricing/model_prices_and_context_window.json`
6
+ * inside sub2api at commit ~April 2026). LiteLLM is the most actively
7
+ * maintained open pricing source for AI models and stays in sync with
8
+ * Anthropic / OpenAI / Google public pricing pages.
9
+ *
10
+ * When a model appears in a CLI's supported list but is absent from LiteLLM
11
+ * (typically pre-release or deprecated models), we either fall back to the
12
+ * closest known variant (documented in `resolveFallback()`) or keep the
13
+ * last manually-verified number and mark it with a comment.
14
+ *
15
+ * If you update these values, also update the LiteLLM reference timestamp
16
+ * in the header above so future ops know when the last sync happened.
7
17
  */
8
18
  export interface ModelPricing {
9
19
  input: number;
10
20
  output: number;
11
21
  }
12
22
  export declare const API_PRICES: Record<string, ModelPricing>;
13
- export declare const RELAY_DISCOUNT = 0.3;
23
+ export declare const RELAY_DISCOUNT = 0.2;
14
24
  export declare const PLATFORM_FEE = 0.05;
15
25
  export declare function getModelPricing(model: string): ModelPricing;
16
26
  export interface CostBreakdown {
@@ -1,37 +1,109 @@
1
1
  /**
2
2
  * API pricing per million tokens (USD).
3
- * Source: Official pricing pages (April 2026)
4
- * - Anthropic: https://platform.claude.com/docs/en/docs/about-claude/pricing
5
- * - OpenAI: https://developers.openai.com/api/docs/pricing
6
- * - Google: https://ai.google.dev/gemini-api/docs/pricing
3
+ *
4
+ * Source: cross-referenced against the LiteLLM community pricing database
5
+ * (`backend/resources/model-pricing/model_prices_and_context_window.json`
6
+ * inside sub2api at commit ~April 2026). LiteLLM is the most actively
7
+ * maintained open pricing source for AI models and stays in sync with
8
+ * Anthropic / OpenAI / Google public pricing pages.
9
+ *
10
+ * When a model appears in a CLI's supported list but is absent from LiteLLM
11
+ * (typically pre-release or deprecated models), we either fall back to the
12
+ * closest known variant (documented in `resolveFallback()`) or keep the
13
+ * last manually-verified number and mark it with a comment.
14
+ *
15
+ * If you update these values, also update the LiteLLM reference timestamp
16
+ * in the header above so future ops know when the last sync happened.
7
17
  */
8
18
  export const API_PRICES = {
9
19
  // ── Anthropic (Claude) ──
20
+ // Verified against LiteLLM pricing DB. cache_read = 0.1x input,
21
+ // cache_write = 1.25x input (Anthropic ephemeral cache).
10
22
  "claude-opus-4-6": { input: 5, output: 25 },
11
23
  "claude-opus-4-5": { input: 5, output: 25 },
12
24
  "claude-sonnet-4-6": { input: 3, output: 15 },
13
25
  "claude-sonnet-4-5": { input: 3, output: 15 },
14
26
  "claude-haiku-4-5": { input: 1, output: 5 },
15
- // ── OpenAI ──
27
+ // ── OpenAI (ChatGPT Plus / Codex subscriptions) ──
28
+ // Verified against LiteLLM. Codex CLI may expose any of these depending
29
+ // on the user's subscription tier and which models ChatGPT enables.
16
30
  "gpt-5.4": { input: 2.50, output: 15 },
17
31
  "gpt-5.4-mini": { input: 0.75, output: 4.50 },
18
32
  "gpt-5.4-nano": { input: 0.20, output: 1.25 },
33
+ // gpt-5.4-pro is not in LiteLLM as of April 2026. Keeping the manually
34
+ // verified values from OpenAI's enterprise pricing page.
19
35
  "gpt-5.4-pro": { input: 30, output: 180 },
20
36
  "gpt-5.3-codex": { input: 1.75, output: 14 },
21
- "o3": { input: 5, output: 20 },
22
- "o4-mini": { input: 4, output: 16 },
37
+ // gpt-5.3-codex-spark is not in LiteLLM — sub2api falls back to
38
+ // gpt-5.1-codex pricing (see pricing_service.go SparkBilling handling).
39
+ "gpt-5.3-codex-spark": { input: 1.25, output: 10 },
40
+ "gpt-5.2": { input: 1.75, output: 14 },
41
+ "gpt-5.2-codex": { input: 1.75, output: 14 },
42
+ "gpt-5.1": { input: 1.25, output: 10 },
43
+ "gpt-5.1-codex": { input: 1.25, output: 10 },
44
+ "gpt-5.1-codex-mini": { input: 0.25, output: 2 },
45
+ "gpt-5.1-codex-max": { input: 1.25, output: 10 },
46
+ "gpt-5": { input: 1.25, output: 10 },
47
+ // Reasoning models (o-series). Previously had incorrect values — LiteLLM
48
+ // confirms o3 is $2/$8 (not $5/$20) and o4-mini is $1.1/$4.4 (not $4/$16).
49
+ "o3": { input: 2, output: 8 },
50
+ "o4-mini": { input: 1.1, output: 4.4 },
51
+ // ── Google Antigravity (Ultra-bundled IDE quota pool) ──
52
+ // Antigravity is the only path that exposes Claude to Google-OAuth users.
53
+ // We price these at the public Anthropic / Google API rates (providers earn
54
+ // the same per-request as if they were serving via Anthropic/Google direct),
55
+ // even though their real cost is zero — the quota is a sunk cost of the
56
+ // Ultra subscription.
57
+ "antigravity-gemini-3-pro": { input: 2, output: 12 },
58
+ "antigravity-gemini-3.1-pro": { input: 2, output: 12 },
59
+ "antigravity-gemini-3-flash": { input: 0.50, output: 3 },
60
+ "antigravity-claude-sonnet-4-6": { input: 3, output: 15 },
61
+ "antigravity-claude-opus-4-6-thinking": { input: 5, output: 25 },
23
62
  // ── Google (Gemini) ──
63
+ // Verified against LiteLLM pricing DB.
64
+ "gemini-3.1-pro-preview": { input: 2, output: 12 },
65
+ "gemini-3-pro-preview": { input: 2, output: 12 },
66
+ // gemini-3.1-flash-preview is not in LiteLLM — fall back to
67
+ // gemini-3-flash-preview pricing.
68
+ "gemini-3.1-flash-preview": { input: 0.50, output: 3 },
69
+ "gemini-3-flash-preview": { input: 0.50, output: 3 },
24
70
  "gemini-2.5-pro": { input: 1.25, output: 10 },
25
71
  "gemini-2.5-flash": { input: 0.30, output: 2.50 },
26
72
  "gemini-2.5-flash-lite": { input: 0.10, output: 0.40 },
27
- "gemini-3-flash-preview": { input: 0.50, output: 3 },
28
- "gemini-3.1-pro-preview": { input: 2, output: 12 },
73
+ "gemini-2.0-flash": { input: 0.10, output: 0.40 },
29
74
  };
30
- // Default fallback for unknown models
75
+ // Default fallback for unknown models. Priced at the Claude Opus rate
76
+ // intentionally — better to over-charge an unknown model than under-charge
77
+ // and lose money. If a Provider sees a real model billed at this rate,
78
+ // they should file a bug to add the model to API_PRICES.
31
79
  const DEFAULT_PRICING = { input: 5, output: 25 };
80
+ // ── Relay economics ──────────────────────────────────────────────────────
81
+ //
82
+ // Pricing strategy (April 2026):
83
+ // 1. Buyer pays RELAY_DISCOUNT × API_price (currently 20% of official
84
+ // Anthropic / OpenAI / Google API prices — i.e. an 80% discount).
85
+ // 2. ClawMoney platform takes PLATFORM_FEE of what the buyer pays.
86
+ // 3. Provider keeps the rest.
87
+ //
88
+ // Concretely, for a 1M-token Claude Sonnet input+output at default rates:
89
+ // API price = 1M × ($3 input + $15 output) = $18
90
+ // Buyer pays (20%) = $3.60
91
+ // Platform fee (5%) = $0.18
92
+ // Provider earns = $3.42
93
+ //
94
+ // Why 20% flat across all platforms:
95
+ // - The Provider's subscription fee is a sunk cost; their marginal cost
96
+ // per relayed request is zero (modulo rate-limit guards).
97
+ // - 80% off the official API is a strong enough discount that most buyers
98
+ // would rather route through ClawMoney than pay Anthropic/OpenAI/Google
99
+ // direct.
100
+ // - Simpler to explain to both sides than a per-platform discount table.
101
+ //
102
+ // If we later see sustained demand-side hesitation or supply-side complaints
103
+ // we can revisit and split into per-cli_type rates.
32
104
  // Relay discount: consumers pay this fraction of API price
33
- export const RELAY_DISCOUNT = 0.30; // 30% of API price
34
- // Platform fee: this fraction goes to the platform
105
+ export const RELAY_DISCOUNT = 0.20; // 20% of API price (buyer saves 80%)
106
+ // Platform fee: this fraction of what the buyer pays goes to the platform
35
107
  export const PLATFORM_FEE = 0.05; // 5%
36
108
  export function getModelPricing(model) {
37
109
  return API_PRICES[model] ?? DEFAULT_PRICING;
@@ -7,6 +7,7 @@ import { spawnCli, buildCliArgs, parseCliOutput, ensureEmptyMcpConfig, ensureSan
7
7
  import { callClaudeApi, preflightClaudeApi, getRateGuardSnapshot } from "./upstream/claude-api.js";
8
8
  import { callCodexApi, preflightCodexApi } from "./upstream/codex-api.js";
9
9
  import { callGeminiApi, preflightGeminiApi } from "./upstream/gemini-api.js";
10
+ import { callAntigravityApi, preflightAntigravityApi, } from "./upstream/antigravity-api.js";
10
11
  import { calculateCost } from "./pricing.js";
11
12
  import { relayLogger as logger } from "./logger.js";
12
13
  const CONFIG_DIR = join(homedir(), ".clawmoney");
@@ -108,10 +109,13 @@ async function executeRelayRequest(request, config) {
108
109
  const model = request.model ?? config.relay.model;
109
110
  const stateful = request.stateful ?? false;
110
111
  const cliSessionId = request.cli_session_id ?? undefined;
111
- // api mode is supported for claude / codex / gemini; anything else falls
112
- // back to spawning the local CLI subprocess.
113
- const useApiMode = config.relay.execution_mode === "api" &&
114
- (cliType === "claude" || cliType === "codex" || cliType === "gemini");
112
+ // api mode is supported for claude / codex / gemini / antigravity; anything
113
+ // else falls back to spawning the local CLI subprocess. Antigravity is
114
+ // api-only (there is no local CLI to spawn) so execution_mode is ignored
115
+ // for it and we always route through the direct upstream handler.
116
+ const useApiMode = (config.relay.execution_mode === "api" &&
117
+ (cliType === "claude" || cliType === "codex" || cliType === "gemini")) ||
118
+ cliType === "antigravity";
115
119
  // Build prompt from messages
116
120
  const prompt = request.messages
117
121
  ? messagesToPrompt(request.messages)
@@ -153,6 +157,13 @@ async function executeRelayRequest(request, config) {
153
157
  maxTokens: max_budget_usd ? undefined : 8192,
154
158
  });
155
159
  }
160
+ else if (cliType === "antigravity") {
161
+ parsed = await callAntigravityApi({
162
+ prompt,
163
+ model,
164
+ maxTokens: max_budget_usd ? undefined : 8192,
165
+ });
166
+ }
156
167
  else {
157
168
  parsed = await callClaudeApi({
158
169
  prompt,
@@ -235,6 +246,11 @@ export function runRelayProvider(cliOverride) {
235
246
  // up-front so we fail fast instead of on the first inbound request. Each
236
247
  // cli_type has its own preflight path (different credential file, different
237
248
  // fingerprint schema, different rate-guard instance).
249
+ // Antigravity is always api mode (no CLI to spawn), so force api execution
250
+ // even if config.yaml says "cli". For the other CLIs, api mode is opt-in.
251
+ if (config.relay.cli_type === "antigravity") {
252
+ config.relay.execution_mode = "api";
253
+ }
238
254
  if (config.relay.execution_mode === "api") {
239
255
  const preflightFn = config.relay.cli_type === "codex"
240
256
  ? preflightCodexApi
@@ -242,7 +258,9 @@ export function runRelayProvider(cliOverride) {
242
258
  ? preflightGeminiApi
243
259
  : config.relay.cli_type === "claude"
244
260
  ? preflightClaudeApi
245
- : null;
261
+ : config.relay.cli_type === "antigravity"
262
+ ? preflightAntigravityApi
263
+ : null;
246
264
  if (preflightFn) {
247
265
  preflightFn(config.relay.rate_guard).catch((err) => {
248
266
  logger.error(`${config.relay.cli_type} API preflight failed — falling back to CLI mode: ${err.message}`);