From 74d02a971f7d5e29be0baec0eaf11b502b600908 Mon Sep 17 00:00:00 2001 From: Bo Lu Date: Sat, 25 Apr 2026 16:40:26 +0800 Subject: [PATCH] feat(pricing): add pricing-sync subcommand for manual sync (Task #15) New `pricing-sync` CLI subcommand bootstraps a temporary server and runs a one-off pricing sync against Azure/Anthropic sources via the LLM extraction pipeline. Co-Authored-By: Claude Opus 4.6 --- src/main.ts | 9 ++- src/pricing-sync-cmd.ts | 153 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 161 insertions(+), 1 deletion(-) create mode 100644 src/pricing-sync-cmd.ts diff --git a/src/main.ts b/src/main.ts index 4f6ca784b..c21a76b41 100644 --- a/src/main.ts +++ b/src/main.ts @@ -5,6 +5,7 @@ import { defineCommand, runMain } from "citty" import { auth } from "./auth" import { checkUsage } from "./check-usage" import { debug } from "./debug" +import { pricingSyncCmd } from "./pricing-sync-cmd" import { start } from "./start" const main = defineCommand({ @@ -13,7 +14,13 @@ const main = defineCommand({ description: "A wrapper around GitHub Copilot API to make it OpenAI compatible, making it usable for other tools.", }, - subCommands: { auth, start, "check-usage": checkUsage, debug }, + subCommands: { + auth, + start, + "check-usage": checkUsage, + "pricing-sync": pricingSyncCmd, + debug, + }, }) await runMain(main) diff --git a/src/pricing-sync-cmd.ts b/src/pricing-sync-cmd.ts new file mode 100644 index 000000000..55faa6cc4 --- /dev/null +++ b/src/pricing-sync-cmd.ts @@ -0,0 +1,153 @@ +import { defineCommand } from "citty" +import consola from "consola" +import { serve, type ServerHandler } from "srvx" + +import { loadAccounts } from "./lib/accounts-loader" +import { initDb } from "./lib/db" +import { ensurePaths, PATHS } from "./lib/paths" +import { runPricingSync } from "./lib/pricing-sync-runner" +import { initProxyFromEnv } from "./lib/proxy" +import { state } from "./lib/state" +import { setupCopilotTokenFor, setupGitHubToken } from "./lib/token" +import { cacheModels, cacheVSCodeVersion } from "./lib/utils" +import { server } from "./server" + +interface RunPricingSyncCmdOptions { + port: number + syncModel?: string + githubToken?: string + accountsFile?: string + accountType: string + dbPath: string + proxyEnv: boolean + verbose: boolean +} + +async function bootstrapServer( + options: RunPricingSyncCmdOptions, +): Promise { + if (options.proxyEnv) { + initProxyFromEnv() + } + if (options.verbose) { + consola.level = 5 + } + + state.accountType = options.accountType + await ensurePaths() + initDb(options.dbPath) + await cacheVSCodeVersion() + + let legacyToken = options.githubToken + if (!options.accountsFile && !legacyToken) { + legacyToken = await setupGitHubToken() + } + + const loaded = await loadAccounts({ + accountsFile: options.accountsFile, + legacyToken, + defaultAccountType: options.accountType, + }) + + if (loaded.length === 0) { + throw new Error("No accounts available.") + } + + state.pool = undefined as never // not needed for sync + await Promise.all(loaded.map((a) => setupCopilotTokenFor(a))) + await cacheModels() +} + +function startTempServer(port: number): void { + serve({ + fetch: server.fetch as ServerHandler, + port, + }) +} + +export async function runPricingSyncCmd( + options: RunPricingSyncCmdOptions, +): Promise { + await bootstrapServer(options) + startTempServer(options.port) + + consola.info("Running one-off pricing sync…") + const result = await runPricingSync({ + port: options.port, + syncModel: options.syncModel, + }) + + if (result.status === "ok") { + consola.success(`Pricing sync complete: ${result.updated} model(s) updated`) + } else if (result.status === "rejected") { + consola.warn( + `Pricing sync rejected (sanity check): ${result.rejected} model(s)`, + ) + } else { + consola.error(`Pricing sync failed: ${result.error ?? "unknown error"}`) + } + + process.exit(result.status === "ok" ? 0 : 1) +} + +export const pricingSyncCmd = defineCommand({ + meta: { + name: "pricing-sync", + description: "Run a one-off pricing sync against Azure and Anthropic", + }, + args: { + port: { + alias: "p", + type: "string", + default: "4141", + description: "Port for the temporary server (needed for LLM self-call)", + }, + "sync-model": { + type: "string", + description: "Model to use for the LLM extraction step", + }, + "github-token": { + alias: "g", + type: "string", + description: "GitHub token", + }, + "accounts-file": { + type: "string", + description: "Path to accounts JSON file", + }, + "account-type": { + alias: "a", + type: "string", + default: "individual", + description: "Account type", + }, + "db-path": { + type: "string", + default: PATHS.USAGE_DB_PATH, + description: "Path to the usage SQLite database", + }, + "proxy-env": { + type: "boolean", + default: false, + description: "Initialize proxy from environment variables", + }, + verbose: { + alias: "v", + type: "boolean", + default: false, + description: "Enable verbose logging", + }, + }, + run({ args }) { + return runPricingSyncCmd({ + port: Number.parseInt(args.port, 10), + syncModel: args["sync-model"], + githubToken: args["github-token"], + accountsFile: args["accounts-file"], + accountType: args["account-type"], + dbPath: args["db-path"], + proxyEnv: args["proxy-env"], + verbose: args.verbose, + }) + }, +})