From cbbe445ea563539acd72bd0f0c1349fcac0c18dc Mon Sep 17 00:00:00 2001 From: nulmind Date: Tue, 23 Dec 2025 12:33:38 +0800 Subject: [PATCH 01/14] feat: add provider management CLI commands MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add new 'opencode provider' command suite for managing custom model providers: - provider list: Display all custom providers from opencode.json with details including provider name, base URL, and available models - provider add: Interactive wizard to add new custom providers with prompts for provider ID, name, base URL, SDK package, and models - provider remove: Interactive removal of custom providers with confirmation prompt Features: - Easy management of custom OpenAI-compatible providers (Ollama, LM Studio, etc.) - No need to manually edit opencode.json files - Full integration with existing 'opencode models' and 'opencode run' commands - Supports multiple models per provider - Validates provider IDs and URLs during input Also added opencode.json to .gitignore as it's a local configuration file. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- .gitignore | 1 + packages/opencode/src/cli/cmd/provider.ts | 283 ++++++++++++++++++++++ packages/opencode/src/index.ts | 2 + 3 files changed, 286 insertions(+) create mode 100644 packages/opencode/src/cli/cmd/provider.ts diff --git a/.gitignore b/.gitignore index f69a70796698..fb591e1b8974 100644 --- a/.gitignore +++ b/.gitignore @@ -7,6 +7,7 @@ node_modules .vscode *~ openapi.json +opencode.json playground tmp dist diff --git a/packages/opencode/src/cli/cmd/provider.ts b/packages/opencode/src/cli/cmd/provider.ts new file mode 100644 index 000000000000..8f275a5b40c2 --- /dev/null +++ b/packages/opencode/src/cli/cmd/provider.ts @@ -0,0 +1,283 @@ +import { cmd } from "./cmd" +import * as prompts from "@clack/prompts" +import { UI } from "../ui" +import { Config } from "../../config/config" +import path from "path" +import fs from "fs/promises" +import { Filesystem } from "../../util/filesystem" + +export const ProviderCommand = cmd({ + command: "provider", + describe: "manage custom providers", + builder: (yargs) => + yargs + .command(ProviderAddCommand) + .command(ProviderListCommand) + .command(ProviderRemoveCommand) + .demandCommand(), + async handler() {}, +}) + +export const ProviderListCommand = cmd({ + command: "list", + aliases: ["ls"], + describe: "list custom providers from opencode.json", + async handler() { + UI.empty() + prompts.intro("Custom Providers") + + // Find opencode.json in current directory or up + const configFiles = await Filesystem.findUp("opencode.json", process.cwd()) + const configFile = configFiles[0] + + if (!configFile) { + prompts.log.warn("No opencode.json found in current directory or parent directories") + prompts.outro("No custom providers configured") + return + } + + const configPath = configFile + const displayPath = configPath.replace(process.env.HOME || "", "~") + prompts.log.info(`Config: ${UI.Style.TEXT_DIM}${displayPath}`) + UI.empty() + + try { + const content = await fs.readFile(configPath, "utf-8") + const config = JSON.parse(content) + + if (!config.provider || Object.keys(config.provider).length === 0) { + prompts.log.warn("No custom providers configured in opencode.json") + prompts.outro("0 providers") + return + } + + for (const [providerID, provider] of Object.entries(config.provider as Record)) { + const name = provider.name || providerID + const baseURL = provider.options?.baseURL || "N/A" + const modelCount = Object.keys(provider.models || {}).length + + prompts.log.info(`${name} ${UI.Style.TEXT_DIM}(${providerID})`) + console.log(` ${UI.Style.TEXT_DIM}URL: ${baseURL}`) + console.log(` ${UI.Style.TEXT_DIM}Models: ${modelCount}`) + + // List models + if (provider.models) { + for (const [modelID, model] of Object.entries(provider.models as Record)) { + const modelName = model.name || modelID + console.log(` • ${modelName} ${UI.Style.TEXT_DIM}(${modelID})`) + } + } + UI.empty() + } + + prompts.outro(`${Object.keys(config.provider).length} custom provider(s)`) + } catch (error) { + prompts.log.error(`Failed to read config: ${error}`) + prompts.outro("Failed") + } + }, +}) + +export const ProviderAddCommand = cmd({ + command: "add", + describe: "add a custom provider to opencode.json", + async handler() { + UI.empty() + prompts.intro("Add Custom Provider") + + // Find or create opencode.json + const configFiles = await Filesystem.findUp("opencode.json", process.cwd()) + const configPath = configFiles[0] || path.join(process.cwd(), "opencode.json") + + // Get provider details + const providerID = await prompts.text({ + message: "Provider ID (e.g., bifrost-ollama)", + placeholder: "my-provider", + validate: (x) => (x && x.match(/^[0-9a-z-]+$/) ? undefined : "Use only a-z, 0-9, and hyphens"), + }) + if (prompts.isCancel(providerID)) throw new UI.CancelledError() + + const providerName = await prompts.text({ + message: "Provider display name", + placeholder: "My Custom Provider", + initialValue: providerID + .split("-") + .map((word) => word.charAt(0).toUpperCase() + word.slice(1)) + .join(" "), + }) + if (prompts.isCancel(providerName)) throw new UI.CancelledError() + + const baseURL = await prompts.text({ + message: "Base URL (API endpoint)", + placeholder: "http://localhost:11434/v1", + validate: (x) => { + if (!x) return "Required" + try { + new URL(x) + return undefined + } catch { + return "Must be a valid URL" + } + }, + }) + if (prompts.isCancel(baseURL)) throw new UI.CancelledError() + + const npmPackage = await prompts.select({ + message: "SDK package", + options: [ + { + label: "OpenAI Compatible (@ai-sdk/openai-compatible)", + value: "@ai-sdk/openai-compatible", + hint: "recommended", + }, + { + label: "Other", + value: "other", + }, + ], + }) + if (prompts.isCancel(npmPackage)) throw new UI.CancelledError() + + let finalNpmPackage: string = npmPackage + if (npmPackage === "other") { + const customNpm = await prompts.text({ + message: "NPM package name", + placeholder: "@ai-sdk/openai-compatible", + validate: (x) => (x && x.length > 0 ? undefined : "Required"), + }) + if (prompts.isCancel(customNpm)) throw new UI.CancelledError() + finalNpmPackage = customNpm as string + } + + // Add models + const models: Record = {} + let addMore = true + + while (addMore) { + const modelID = await prompts.text({ + message: "Model ID", + placeholder: "model-name", + validate: (x) => (x && x.length > 0 ? undefined : "Required"), + }) + if (prompts.isCancel(modelID)) throw new UI.CancelledError() + + const modelName = await prompts.text({ + message: "Model display name", + placeholder: "Model Name", + initialValue: modelID + .split("-") + .map((word) => word.charAt(0).toUpperCase() + word.slice(1)) + .join(" "), + }) + if (prompts.isCancel(modelName)) throw new UI.CancelledError() + + const realID = await prompts.text({ + message: "Actual model ID (if different from display ID)", + placeholder: "Leave empty if same as model ID", + }) + if (prompts.isCancel(realID)) { + models[modelID] = { name: modelName } + } else { + models[modelID] = { + name: modelName, + ...(realID && realID.length > 0 ? { id: realID } : {}), + } + } + + const continueAdding = await prompts.confirm({ + message: "Add another model?", + initialValue: false, + }) + if (prompts.isCancel(continueAdding)) throw new UI.CancelledError() + addMore = continueAdding + } + + // Read existing config or create new + let config: any = { + $schema: "https://opencode.ai/config.json", + provider: {}, + } + + try { + const content = await fs.readFile(configPath, "utf-8") + config = JSON.parse(content) + if (!config.provider) config.provider = {} + } catch { + // File doesn't exist, use default + } + + // Add provider + config.provider[providerID] = { + npm: finalNpmPackage, + name: providerName, + options: { + baseURL, + }, + models, + } + + // Write config + await fs.writeFile(configPath, JSON.stringify(config, null, 2) + "\n", "utf-8") + + prompts.log.success(`Provider added to ${configPath}`) + prompts.log.info(`Run 'opencode models ${providerID}' to see available models`) + prompts.outro("Done") + }, +}) + +export const ProviderRemoveCommand = cmd({ + command: "remove", + aliases: ["rm"], + describe: "remove a custom provider from opencode.json", + async handler() { + UI.empty() + prompts.intro("Remove Custom Provider") + + // Find opencode.json + const configFiles = await Filesystem.findUp("opencode.json", process.cwd()) + const configPath = configFiles[0] + + if (!configPath) { + prompts.log.error("No opencode.json found") + prompts.outro("Failed") + return + } + + try { + const content = await fs.readFile(configPath, "utf-8") + const config = JSON.parse(content) + + if (!config.provider || Object.keys(config.provider).length === 0) { + prompts.log.error("No custom providers found in opencode.json") + prompts.outro("Failed") + return + } + + const providerID = await prompts.select({ + message: "Select provider to remove", + options: Object.entries(config.provider).map(([id, provider]: [string, any]) => ({ + label: provider.name || id, + value: id, + hint: provider.options?.baseURL, + })), + }) + if (prompts.isCancel(providerID)) throw new UI.CancelledError() + + const confirm = await prompts.confirm({ + message: `Remove provider '${providerID}'?`, + }) + if (prompts.isCancel(confirm) || !confirm) throw new UI.CancelledError() + + delete config.provider[providerID] + + await fs.writeFile(configPath, JSON.stringify(config, null, 2) + "\n", "utf-8") + + prompts.log.success(`Provider '${providerID}' removed`) + prompts.outro("Done") + } catch (error) { + if (error instanceof UI.CancelledError) throw error + prompts.log.error(`Failed to remove provider: ${error}`) + prompts.outro("Failed") + } + }, +}) diff --git a/packages/opencode/src/index.ts b/packages/opencode/src/index.ts index acd7ee1c099a..437bbaf01d30 100644 --- a/packages/opencode/src/index.ts +++ b/packages/opencode/src/index.ts @@ -7,6 +7,7 @@ import { AuthCommand } from "./cli/cmd/auth" import { AgentCommand } from "./cli/cmd/agent" import { UpgradeCommand } from "./cli/cmd/upgrade" import { ModelsCommand } from "./cli/cmd/models" +import { ProviderCommand } from "./cli/cmd/provider" import { UI } from "./cli/ui" import { Installation } from "./installation" import { NamedError } from "./util/error" @@ -83,6 +84,7 @@ const cli = yargs(hideBin(process.argv)) .command(DebugCommand) .command(AuthCommand) .command(AgentCommand) + .command(ProviderCommand) .command(UpgradeCommand) .command(ServeCommand) .command(WebCommand) From 620d5b75247214efec4120226620b27798641f39 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Tue, 23 Dec 2025 09:06:35 +0000 Subject: [PATCH 02/14] chore: format code --- packages/opencode/src/cli/cmd/provider.ts | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/packages/opencode/src/cli/cmd/provider.ts b/packages/opencode/src/cli/cmd/provider.ts index 8f275a5b40c2..4b357a934bb3 100644 --- a/packages/opencode/src/cli/cmd/provider.ts +++ b/packages/opencode/src/cli/cmd/provider.ts @@ -10,11 +10,7 @@ export const ProviderCommand = cmd({ command: "provider", describe: "manage custom providers", builder: (yargs) => - yargs - .command(ProviderAddCommand) - .command(ProviderListCommand) - .command(ProviderRemoveCommand) - .demandCommand(), + yargs.command(ProviderAddCommand).command(ProviderListCommand).command(ProviderRemoveCommand).demandCommand(), async handler() {}, }) From 8e6da516528bcfe657f0b0e20afb54509bf45271 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Tue, 23 Dec 2025 12:29:13 +0000 Subject: [PATCH 03/14] ignore: update download stats 2025-12-23 --- STATS.md | 281 ++++++++++++++++++++++++++++--------------------------- 1 file changed, 141 insertions(+), 140 deletions(-) diff --git a/STATS.md b/STATS.md index 627771e9b4ed..53cfff730440 100644 --- a/STATS.md +++ b/STATS.md @@ -1,142 +1,143 @@ # Download Stats -| Date | GitHub Downloads | npm Downloads | Total | -| ---------- | ----------------- | ----------------- | ------------------- | -| 2025-06-29 | 18,789 (+0) | 39,420 (+0) | 58,209 (+0) | -| 2025-06-30 | 20,127 (+1,338) | 41,059 (+1,639) | 61,186 (+2,977) | -| 2025-07-01 | 22,108 (+1,981) | 43,745 (+2,686) | 65,853 (+4,667) | -| 2025-07-02 | 24,814 (+2,706) | 46,168 (+2,423) | 70,982 (+5,129) | -| 2025-07-03 | 27,834 (+3,020) | 49,955 (+3,787) | 77,789 (+6,807) | -| 2025-07-04 | 30,608 (+2,774) | 54,758 (+4,803) | 85,366 (+7,577) | -| 2025-07-05 | 32,524 (+1,916) | 58,371 (+3,613) | 90,895 (+5,529) | -| 2025-07-06 | 33,766 (+1,242) | 59,694 (+1,323) | 93,460 (+2,565) | -| 2025-07-08 | 38,052 (+4,286) | 64,468 (+4,774) | 102,520 (+9,060) | -| 2025-07-09 | 40,924 (+2,872) | 67,935 (+3,467) | 108,859 (+6,339) | -| 2025-07-10 | 43,796 (+2,872) | 71,402 (+3,467) | 115,198 (+6,339) | -| 2025-07-11 | 46,982 (+3,186) | 77,462 (+6,060) | 124,444 (+9,246) | -| 2025-07-12 | 49,302 (+2,320) | 82,177 (+4,715) | 131,479 (+7,035) | -| 2025-07-13 | 50,803 (+1,501) | 86,394 (+4,217) | 137,197 (+5,718) | -| 2025-07-14 | 53,283 (+2,480) | 87,860 (+1,466) | 141,143 (+3,946) | -| 2025-07-15 | 57,590 (+4,307) | 91,036 (+3,176) | 148,626 (+7,483) | -| 2025-07-16 | 62,313 (+4,723) | 95,258 (+4,222) | 157,571 (+8,945) | -| 2025-07-17 | 66,684 (+4,371) | 100,048 (+4,790) | 166,732 (+9,161) | -| 2025-07-18 | 70,379 (+3,695) | 102,587 (+2,539) | 172,966 (+6,234) | -| 2025-07-19 | 73,497 (+3,117) | 105,904 (+3,317) | 179,401 (+6,434) | -| 2025-07-20 | 76,453 (+2,956) | 109,044 (+3,140) | 185,497 (+6,096) | -| 2025-07-21 | 80,197 (+3,744) | 113,537 (+4,493) | 193,734 (+8,237) | -| 2025-07-22 | 84,251 (+4,054) | 118,073 (+4,536) | 202,324 (+8,590) | -| 2025-07-23 | 88,589 (+4,338) | 121,436 (+3,363) | 210,025 (+7,701) | -| 2025-07-24 | 92,469 (+3,880) | 124,091 (+2,655) | 216,560 (+6,535) | -| 2025-07-25 | 96,417 (+3,948) | 126,985 (+2,894) | 223,402 (+6,842) | -| 2025-07-26 | 100,646 (+4,229) | 131,411 (+4,426) | 232,057 (+8,655) | -| 2025-07-27 | 102,644 (+1,998) | 134,736 (+3,325) | 237,380 (+5,323) | -| 2025-07-28 | 105,446 (+2,802) | 136,016 (+1,280) | 241,462 (+4,082) | -| 2025-07-29 | 108,998 (+3,552) | 137,542 (+1,526) | 246,540 (+5,078) | -| 2025-07-30 | 113,544 (+4,546) | 140,317 (+2,775) | 253,861 (+7,321) | -| 2025-07-31 | 118,339 (+4,795) | 143,344 (+3,027) | 261,683 (+7,822) | -| 2025-08-01 | 123,539 (+5,200) | 146,680 (+3,336) | 270,219 (+8,536) | -| 2025-08-02 | 127,864 (+4,325) | 149,236 (+2,556) | 277,100 (+6,881) | -| 2025-08-03 | 131,397 (+3,533) | 150,451 (+1,215) | 281,848 (+4,748) | -| 2025-08-04 | 136,266 (+4,869) | 153,260 (+2,809) | 289,526 (+7,678) | -| 2025-08-05 | 141,596 (+5,330) | 155,752 (+2,492) | 297,348 (+7,822) | -| 2025-08-06 | 147,067 (+5,471) | 158,309 (+2,557) | 305,376 (+8,028) | -| 2025-08-07 | 152,591 (+5,524) | 160,889 (+2,580) | 313,480 (+8,104) | -| 2025-08-08 | 158,187 (+5,596) | 163,448 (+2,559) | 321,635 (+8,155) | -| 2025-08-09 | 162,770 (+4,583) | 165,721 (+2,273) | 328,491 (+6,856) | -| 2025-08-10 | 165,695 (+2,925) | 167,109 (+1,388) | 332,804 (+4,313) | -| 2025-08-11 | 169,297 (+3,602) | 167,953 (+844) | 337,250 (+4,446) | -| 2025-08-12 | 176,307 (+7,010) | 171,876 (+3,923) | 348,183 (+10,933) | -| 2025-08-13 | 182,997 (+6,690) | 177,182 (+5,306) | 360,179 (+11,996) | -| 2025-08-14 | 189,063 (+6,066) | 179,741 (+2,559) | 368,804 (+8,625) | -| 2025-08-15 | 193,608 (+4,545) | 181,792 (+2,051) | 375,400 (+6,596) | -| 2025-08-16 | 198,118 (+4,510) | 184,558 (+2,766) | 382,676 (+7,276) | -| 2025-08-17 | 201,299 (+3,181) | 186,269 (+1,711) | 387,568 (+4,892) | -| 2025-08-18 | 204,559 (+3,260) | 187,399 (+1,130) | 391,958 (+4,390) | -| 2025-08-19 | 209,814 (+5,255) | 189,668 (+2,269) | 399,482 (+7,524) | -| 2025-08-20 | 214,497 (+4,683) | 191,481 (+1,813) | 405,978 (+6,496) | -| 2025-08-21 | 220,465 (+5,968) | 194,784 (+3,303) | 415,249 (+9,271) | -| 2025-08-22 | 225,899 (+5,434) | 197,204 (+2,420) | 423,103 (+7,854) | -| 2025-08-23 | 229,005 (+3,106) | 199,238 (+2,034) | 428,243 (+5,140) | -| 2025-08-24 | 232,098 (+3,093) | 201,157 (+1,919) | 433,255 (+5,012) | -| 2025-08-25 | 236,607 (+4,509) | 202,650 (+1,493) | 439,257 (+6,002) | -| 2025-08-26 | 242,783 (+6,176) | 205,242 (+2,592) | 448,025 (+8,768) | -| 2025-08-27 | 248,409 (+5,626) | 205,242 (+0) | 453,651 (+5,626) | -| 2025-08-28 | 252,796 (+4,387) | 205,242 (+0) | 458,038 (+4,387) | -| 2025-08-29 | 256,045 (+3,249) | 211,075 (+5,833) | 467,120 (+9,082) | -| 2025-08-30 | 258,863 (+2,818) | 212,397 (+1,322) | 471,260 (+4,140) | -| 2025-08-31 | 262,004 (+3,141) | 213,944 (+1,547) | 475,948 (+4,688) | -| 2025-09-01 | 265,359 (+3,355) | 215,115 (+1,171) | 480,474 (+4,526) | -| 2025-09-02 | 270,483 (+5,124) | 217,075 (+1,960) | 487,558 (+7,084) | -| 2025-09-03 | 274,793 (+4,310) | 219,755 (+2,680) | 494,548 (+6,990) | -| 2025-09-04 | 280,430 (+5,637) | 222,103 (+2,348) | 502,533 (+7,985) | -| 2025-09-05 | 283,769 (+3,339) | 223,793 (+1,690) | 507,562 (+5,029) | -| 2025-09-06 | 286,245 (+2,476) | 225,036 (+1,243) | 511,281 (+3,719) | -| 2025-09-07 | 288,623 (+2,378) | 225,866 (+830) | 514,489 (+3,208) | -| 2025-09-08 | 293,341 (+4,718) | 227,073 (+1,207) | 520,414 (+5,925) | -| 2025-09-09 | 300,036 (+6,695) | 229,788 (+2,715) | 529,824 (+9,410) | -| 2025-09-10 | 307,287 (+7,251) | 233,435 (+3,647) | 540,722 (+10,898) | -| 2025-09-11 | 314,083 (+6,796) | 237,356 (+3,921) | 551,439 (+10,717) | -| 2025-09-12 | 321,046 (+6,963) | 240,728 (+3,372) | 561,774 (+10,335) | -| 2025-09-13 | 324,894 (+3,848) | 245,539 (+4,811) | 570,433 (+8,659) | -| 2025-09-14 | 328,876 (+3,982) | 248,245 (+2,706) | 577,121 (+6,688) | -| 2025-09-15 | 334,201 (+5,325) | 250,983 (+2,738) | 585,184 (+8,063) | -| 2025-09-16 | 342,609 (+8,408) | 255,264 (+4,281) | 597,873 (+12,689) | -| 2025-09-17 | 351,117 (+8,508) | 260,970 (+5,706) | 612,087 (+14,214) | -| 2025-09-18 | 358,717 (+7,600) | 266,922 (+5,952) | 625,639 (+13,552) | -| 2025-09-19 | 365,401 (+6,684) | 271,859 (+4,937) | 637,260 (+11,621) | -| 2025-09-20 | 372,092 (+6,691) | 276,917 (+5,058) | 649,009 (+11,749) | -| 2025-09-21 | 377,079 (+4,987) | 280,261 (+3,344) | 657,340 (+8,331) | -| 2025-09-22 | 382,492 (+5,413) | 284,009 (+3,748) | 666,501 (+9,161) | -| 2025-09-23 | 387,008 (+4,516) | 289,129 (+5,120) | 676,137 (+9,636) | -| 2025-09-24 | 393,325 (+6,317) | 294,927 (+5,798) | 688,252 (+12,115) | -| 2025-09-25 | 398,879 (+5,554) | 301,663 (+6,736) | 700,542 (+12,290) | -| 2025-09-26 | 404,334 (+5,455) | 306,713 (+5,050) | 711,047 (+10,505) | -| 2025-09-27 | 411,618 (+7,284) | 317,763 (+11,050) | 729,381 (+18,334) | -| 2025-09-28 | 414,910 (+3,292) | 322,522 (+4,759) | 737,432 (+8,051) | -| 2025-09-29 | 419,919 (+5,009) | 328,033 (+5,511) | 747,952 (+10,520) | -| 2025-09-30 | 427,991 (+8,072) | 336,472 (+8,439) | 764,463 (+16,511) | -| 2025-10-01 | 433,591 (+5,600) | 341,742 (+5,270) | 775,333 (+10,870) | -| 2025-10-02 | 440,852 (+7,261) | 348,099 (+6,357) | 788,951 (+13,618) | -| 2025-10-03 | 446,829 (+5,977) | 359,937 (+11,838) | 806,766 (+17,815) | -| 2025-10-04 | 452,561 (+5,732) | 370,386 (+10,449) | 822,947 (+16,181) | -| 2025-10-05 | 455,559 (+2,998) | 374,745 (+4,359) | 830,304 (+7,357) | -| 2025-10-06 | 460,927 (+5,368) | 379,489 (+4,744) | 840,416 (+10,112) | -| 2025-10-07 | 467,336 (+6,409) | 385,438 (+5,949) | 852,774 (+12,358) | -| 2025-10-08 | 474,643 (+7,307) | 394,139 (+8,701) | 868,782 (+16,008) | -| 2025-10-09 | 479,203 (+4,560) | 400,526 (+6,387) | 879,729 (+10,947) | -| 2025-10-10 | 484,374 (+5,171) | 406,015 (+5,489) | 890,389 (+10,660) | -| 2025-10-11 | 488,427 (+4,053) | 414,699 (+8,684) | 903,126 (+12,737) | -| 2025-10-12 | 492,125 (+3,698) | 418,745 (+4,046) | 910,870 (+7,744) | -| 2025-10-14 | 505,130 (+13,005) | 429,286 (+10,541) | 934,416 (+23,546) | -| 2025-10-15 | 512,717 (+7,587) | 439,290 (+10,004) | 952,007 (+17,591) | -| 2025-10-16 | 517,719 (+5,002) | 447,137 (+7,847) | 964,856 (+12,849) | -| 2025-10-17 | 526,239 (+8,520) | 457,467 (+10,330) | 983,706 (+18,850) | -| 2025-10-18 | 531,564 (+5,325) | 465,272 (+7,805) | 996,836 (+13,130) | -| 2025-10-19 | 536,209 (+4,645) | 469,078 (+3,806) | 1,005,287 (+8,451) | -| 2025-10-20 | 541,264 (+5,055) | 472,952 (+3,874) | 1,014,216 (+8,929) | -| 2025-10-21 | 548,721 (+7,457) | 479,703 (+6,751) | 1,028,424 (+14,208) | -| 2025-10-22 | 557,949 (+9,228) | 491,395 (+11,692) | 1,049,344 (+20,920) | -| 2025-10-23 | 564,716 (+6,767) | 498,736 (+7,341) | 1,063,452 (+14,108) | -| 2025-10-24 | 572,692 (+7,976) | 506,905 (+8,169) | 1,079,597 (+16,145) | -| 2025-10-25 | 578,927 (+6,235) | 516,129 (+9,224) | 1,095,056 (+15,459) | -| 2025-10-26 | 584,409 (+5,482) | 521,179 (+5,050) | 1,105,588 (+10,532) | -| 2025-10-27 | 589,999 (+5,590) | 526,001 (+4,822) | 1,116,000 (+10,412) | -| 2025-10-28 | 595,776 (+5,777) | 532,438 (+6,437) | 1,128,214 (+12,214) | -| 2025-10-29 | 606,259 (+10,483) | 542,064 (+9,626) | 1,148,323 (+20,109) | -| 2025-10-30 | 613,746 (+7,487) | 542,064 (+0) | 1,155,810 (+7,487) | -| 2025-10-30 | 617,846 (+4,100) | 555,026 (+12,962) | 1,172,872 (+17,062) | -| 2025-10-31 | 626,612 (+8,766) | 564,579 (+9,553) | 1,191,191 (+18,319) | -| 2025-11-01 | 636,100 (+9,488) | 581,806 (+17,227) | 1,217,906 (+26,715) | -| 2025-11-02 | 644,067 (+7,967) | 590,004 (+8,198) | 1,234,071 (+16,165) | -| 2025-11-03 | 653,130 (+9,063) | 597,139 (+7,135) | 1,250,269 (+16,198) | -| 2025-11-04 | 663,912 (+10,782) | 608,056 (+10,917) | 1,271,968 (+21,699) | -| 2025-11-05 | 675,074 (+11,162) | 619,690 (+11,634) | 1,294,764 (+22,796) | -| 2025-11-06 | 686,252 (+11,178) | 630,885 (+11,195) | 1,317,137 (+22,373) | -| 2025-11-07 | 696,646 (+10,394) | 642,146 (+11,261) | 1,338,792 (+21,655) | -| 2025-11-08 | 706,035 (+9,389) | 653,489 (+11,343) | 1,359,524 (+20,732) | -| 2025-11-09 | 713,462 (+7,427) | 660,459 (+6,970) | 1,373,921 (+14,397) | -| 2025-11-10 | 722,288 (+8,826) | 668,225 (+7,766) | 1,390,513 (+16,592) | -| 2025-11-11 | 729,769 (+7,481) | 677,501 (+9,276) | 1,407,270 (+16,757) | -| 2025-11-12 | 740,180 (+10,411) | 686,454 (+8,953) | 1,426,634 (+19,364) | -| 2025-11-13 | 749,905 (+9,725) | 696,157 (+9,703) | 1,446,062 (+19,428) | -| 2025-11-14 | 759,928 (+10,023) | 705,237 (+9,080) | 1,465,165 (+19,103) | +| Date | GitHub Downloads | npm Downloads | Total | +| ---------- | -------------------- | -------------------- | ---------------------- | +| 2025-06-29 | 18,789 (+0) | 39,420 (+0) | 58,209 (+0) | +| 2025-06-30 | 20,127 (+1,338) | 41,059 (+1,639) | 61,186 (+2,977) | +| 2025-07-01 | 22,108 (+1,981) | 43,745 (+2,686) | 65,853 (+4,667) | +| 2025-07-02 | 24,814 (+2,706) | 46,168 (+2,423) | 70,982 (+5,129) | +| 2025-07-03 | 27,834 (+3,020) | 49,955 (+3,787) | 77,789 (+6,807) | +| 2025-07-04 | 30,608 (+2,774) | 54,758 (+4,803) | 85,366 (+7,577) | +| 2025-07-05 | 32,524 (+1,916) | 58,371 (+3,613) | 90,895 (+5,529) | +| 2025-07-06 | 33,766 (+1,242) | 59,694 (+1,323) | 93,460 (+2,565) | +| 2025-07-08 | 38,052 (+4,286) | 64,468 (+4,774) | 102,520 (+9,060) | +| 2025-07-09 | 40,924 (+2,872) | 67,935 (+3,467) | 108,859 (+6,339) | +| 2025-07-10 | 43,796 (+2,872) | 71,402 (+3,467) | 115,198 (+6,339) | +| 2025-07-11 | 46,982 (+3,186) | 77,462 (+6,060) | 124,444 (+9,246) | +| 2025-07-12 | 49,302 (+2,320) | 82,177 (+4,715) | 131,479 (+7,035) | +| 2025-07-13 | 50,803 (+1,501) | 86,394 (+4,217) | 137,197 (+5,718) | +| 2025-07-14 | 53,283 (+2,480) | 87,860 (+1,466) | 141,143 (+3,946) | +| 2025-07-15 | 57,590 (+4,307) | 91,036 (+3,176) | 148,626 (+7,483) | +| 2025-07-16 | 62,313 (+4,723) | 95,258 (+4,222) | 157,571 (+8,945) | +| 2025-07-17 | 66,684 (+4,371) | 100,048 (+4,790) | 166,732 (+9,161) | +| 2025-07-18 | 70,379 (+3,695) | 102,587 (+2,539) | 172,966 (+6,234) | +| 2025-07-19 | 73,497 (+3,117) | 105,904 (+3,317) | 179,401 (+6,434) | +| 2025-07-20 | 76,453 (+2,956) | 109,044 (+3,140) | 185,497 (+6,096) | +| 2025-07-21 | 80,197 (+3,744) | 113,537 (+4,493) | 193,734 (+8,237) | +| 2025-07-22 | 84,251 (+4,054) | 118,073 (+4,536) | 202,324 (+8,590) | +| 2025-07-23 | 88,589 (+4,338) | 121,436 (+3,363) | 210,025 (+7,701) | +| 2025-07-24 | 92,469 (+3,880) | 124,091 (+2,655) | 216,560 (+6,535) | +| 2025-07-25 | 96,417 (+3,948) | 126,985 (+2,894) | 223,402 (+6,842) | +| 2025-07-26 | 100,646 (+4,229) | 131,411 (+4,426) | 232,057 (+8,655) | +| 2025-07-27 | 102,644 (+1,998) | 134,736 (+3,325) | 237,380 (+5,323) | +| 2025-07-28 | 105,446 (+2,802) | 136,016 (+1,280) | 241,462 (+4,082) | +| 2025-07-29 | 108,998 (+3,552) | 137,542 (+1,526) | 246,540 (+5,078) | +| 2025-07-30 | 113,544 (+4,546) | 140,317 (+2,775) | 253,861 (+7,321) | +| 2025-07-31 | 118,339 (+4,795) | 143,344 (+3,027) | 261,683 (+7,822) | +| 2025-08-01 | 123,539 (+5,200) | 146,680 (+3,336) | 270,219 (+8,536) | +| 2025-08-02 | 127,864 (+4,325) | 149,236 (+2,556) | 277,100 (+6,881) | +| 2025-08-03 | 131,397 (+3,533) | 150,451 (+1,215) | 281,848 (+4,748) | +| 2025-08-04 | 136,266 (+4,869) | 153,260 (+2,809) | 289,526 (+7,678) | +| 2025-08-05 | 141,596 (+5,330) | 155,752 (+2,492) | 297,348 (+7,822) | +| 2025-08-06 | 147,067 (+5,471) | 158,309 (+2,557) | 305,376 (+8,028) | +| 2025-08-07 | 152,591 (+5,524) | 160,889 (+2,580) | 313,480 (+8,104) | +| 2025-08-08 | 158,187 (+5,596) | 163,448 (+2,559) | 321,635 (+8,155) | +| 2025-08-09 | 162,770 (+4,583) | 165,721 (+2,273) | 328,491 (+6,856) | +| 2025-08-10 | 165,695 (+2,925) | 167,109 (+1,388) | 332,804 (+4,313) | +| 2025-08-11 | 169,297 (+3,602) | 167,953 (+844) | 337,250 (+4,446) | +| 2025-08-12 | 176,307 (+7,010) | 171,876 (+3,923) | 348,183 (+10,933) | +| 2025-08-13 | 182,997 (+6,690) | 177,182 (+5,306) | 360,179 (+11,996) | +| 2025-08-14 | 189,063 (+6,066) | 179,741 (+2,559) | 368,804 (+8,625) | +| 2025-08-15 | 193,608 (+4,545) | 181,792 (+2,051) | 375,400 (+6,596) | +| 2025-08-16 | 198,118 (+4,510) | 184,558 (+2,766) | 382,676 (+7,276) | +| 2025-08-17 | 201,299 (+3,181) | 186,269 (+1,711) | 387,568 (+4,892) | +| 2025-08-18 | 204,559 (+3,260) | 187,399 (+1,130) | 391,958 (+4,390) | +| 2025-08-19 | 209,814 (+5,255) | 189,668 (+2,269) | 399,482 (+7,524) | +| 2025-08-20 | 214,497 (+4,683) | 191,481 (+1,813) | 405,978 (+6,496) | +| 2025-08-21 | 220,465 (+5,968) | 194,784 (+3,303) | 415,249 (+9,271) | +| 2025-08-22 | 225,899 (+5,434) | 197,204 (+2,420) | 423,103 (+7,854) | +| 2025-08-23 | 229,005 (+3,106) | 199,238 (+2,034) | 428,243 (+5,140) | +| 2025-08-24 | 232,098 (+3,093) | 201,157 (+1,919) | 433,255 (+5,012) | +| 2025-08-25 | 236,607 (+4,509) | 202,650 (+1,493) | 439,257 (+6,002) | +| 2025-08-26 | 242,783 (+6,176) | 205,242 (+2,592) | 448,025 (+8,768) | +| 2025-08-27 | 248,409 (+5,626) | 205,242 (+0) | 453,651 (+5,626) | +| 2025-08-28 | 252,796 (+4,387) | 205,242 (+0) | 458,038 (+4,387) | +| 2025-08-29 | 256,045 (+3,249) | 211,075 (+5,833) | 467,120 (+9,082) | +| 2025-08-30 | 258,863 (+2,818) | 212,397 (+1,322) | 471,260 (+4,140) | +| 2025-08-31 | 262,004 (+3,141) | 213,944 (+1,547) | 475,948 (+4,688) | +| 2025-09-01 | 265,359 (+3,355) | 215,115 (+1,171) | 480,474 (+4,526) | +| 2025-09-02 | 270,483 (+5,124) | 217,075 (+1,960) | 487,558 (+7,084) | +| 2025-09-03 | 274,793 (+4,310) | 219,755 (+2,680) | 494,548 (+6,990) | +| 2025-09-04 | 280,430 (+5,637) | 222,103 (+2,348) | 502,533 (+7,985) | +| 2025-09-05 | 283,769 (+3,339) | 223,793 (+1,690) | 507,562 (+5,029) | +| 2025-09-06 | 286,245 (+2,476) | 225,036 (+1,243) | 511,281 (+3,719) | +| 2025-09-07 | 288,623 (+2,378) | 225,866 (+830) | 514,489 (+3,208) | +| 2025-09-08 | 293,341 (+4,718) | 227,073 (+1,207) | 520,414 (+5,925) | +| 2025-09-09 | 300,036 (+6,695) | 229,788 (+2,715) | 529,824 (+9,410) | +| 2025-09-10 | 307,287 (+7,251) | 233,435 (+3,647) | 540,722 (+10,898) | +| 2025-09-11 | 314,083 (+6,796) | 237,356 (+3,921) | 551,439 (+10,717) | +| 2025-09-12 | 321,046 (+6,963) | 240,728 (+3,372) | 561,774 (+10,335) | +| 2025-09-13 | 324,894 (+3,848) | 245,539 (+4,811) | 570,433 (+8,659) | +| 2025-09-14 | 328,876 (+3,982) | 248,245 (+2,706) | 577,121 (+6,688) | +| 2025-09-15 | 334,201 (+5,325) | 250,983 (+2,738) | 585,184 (+8,063) | +| 2025-09-16 | 342,609 (+8,408) | 255,264 (+4,281) | 597,873 (+12,689) | +| 2025-09-17 | 351,117 (+8,508) | 260,970 (+5,706) | 612,087 (+14,214) | +| 2025-09-18 | 358,717 (+7,600) | 266,922 (+5,952) | 625,639 (+13,552) | +| 2025-09-19 | 365,401 (+6,684) | 271,859 (+4,937) | 637,260 (+11,621) | +| 2025-09-20 | 372,092 (+6,691) | 276,917 (+5,058) | 649,009 (+11,749) | +| 2025-09-21 | 377,079 (+4,987) | 280,261 (+3,344) | 657,340 (+8,331) | +| 2025-09-22 | 382,492 (+5,413) | 284,009 (+3,748) | 666,501 (+9,161) | +| 2025-09-23 | 387,008 (+4,516) | 289,129 (+5,120) | 676,137 (+9,636) | +| 2025-09-24 | 393,325 (+6,317) | 294,927 (+5,798) | 688,252 (+12,115) | +| 2025-09-25 | 398,879 (+5,554) | 301,663 (+6,736) | 700,542 (+12,290) | +| 2025-09-26 | 404,334 (+5,455) | 306,713 (+5,050) | 711,047 (+10,505) | +| 2025-09-27 | 411,618 (+7,284) | 317,763 (+11,050) | 729,381 (+18,334) | +| 2025-09-28 | 414,910 (+3,292) | 322,522 (+4,759) | 737,432 (+8,051) | +| 2025-09-29 | 419,919 (+5,009) | 328,033 (+5,511) | 747,952 (+10,520) | +| 2025-09-30 | 427,991 (+8,072) | 336,472 (+8,439) | 764,463 (+16,511) | +| 2025-10-01 | 433,591 (+5,600) | 341,742 (+5,270) | 775,333 (+10,870) | +| 2025-10-02 | 440,852 (+7,261) | 348,099 (+6,357) | 788,951 (+13,618) | +| 2025-10-03 | 446,829 (+5,977) | 359,937 (+11,838) | 806,766 (+17,815) | +| 2025-10-04 | 452,561 (+5,732) | 370,386 (+10,449) | 822,947 (+16,181) | +| 2025-10-05 | 455,559 (+2,998) | 374,745 (+4,359) | 830,304 (+7,357) | +| 2025-10-06 | 460,927 (+5,368) | 379,489 (+4,744) | 840,416 (+10,112) | +| 2025-10-07 | 467,336 (+6,409) | 385,438 (+5,949) | 852,774 (+12,358) | +| 2025-10-08 | 474,643 (+7,307) | 394,139 (+8,701) | 868,782 (+16,008) | +| 2025-10-09 | 479,203 (+4,560) | 400,526 (+6,387) | 879,729 (+10,947) | +| 2025-10-10 | 484,374 (+5,171) | 406,015 (+5,489) | 890,389 (+10,660) | +| 2025-10-11 | 488,427 (+4,053) | 414,699 (+8,684) | 903,126 (+12,737) | +| 2025-10-12 | 492,125 (+3,698) | 418,745 (+4,046) | 910,870 (+7,744) | +| 2025-10-14 | 505,130 (+13,005) | 429,286 (+10,541) | 934,416 (+23,546) | +| 2025-10-15 | 512,717 (+7,587) | 439,290 (+10,004) | 952,007 (+17,591) | +| 2025-10-16 | 517,719 (+5,002) | 447,137 (+7,847) | 964,856 (+12,849) | +| 2025-10-17 | 526,239 (+8,520) | 457,467 (+10,330) | 983,706 (+18,850) | +| 2025-10-18 | 531,564 (+5,325) | 465,272 (+7,805) | 996,836 (+13,130) | +| 2025-10-19 | 536,209 (+4,645) | 469,078 (+3,806) | 1,005,287 (+8,451) | +| 2025-10-20 | 541,264 (+5,055) | 472,952 (+3,874) | 1,014,216 (+8,929) | +| 2025-10-21 | 548,721 (+7,457) | 479,703 (+6,751) | 1,028,424 (+14,208) | +| 2025-10-22 | 557,949 (+9,228) | 491,395 (+11,692) | 1,049,344 (+20,920) | +| 2025-10-23 | 564,716 (+6,767) | 498,736 (+7,341) | 1,063,452 (+14,108) | +| 2025-10-24 | 572,692 (+7,976) | 506,905 (+8,169) | 1,079,597 (+16,145) | +| 2025-10-25 | 578,927 (+6,235) | 516,129 (+9,224) | 1,095,056 (+15,459) | +| 2025-10-26 | 584,409 (+5,482) | 521,179 (+5,050) | 1,105,588 (+10,532) | +| 2025-10-27 | 589,999 (+5,590) | 526,001 (+4,822) | 1,116,000 (+10,412) | +| 2025-10-28 | 595,776 (+5,777) | 532,438 (+6,437) | 1,128,214 (+12,214) | +| 2025-10-29 | 606,259 (+10,483) | 542,064 (+9,626) | 1,148,323 (+20,109) | +| 2025-10-30 | 613,746 (+7,487) | 542,064 (+0) | 1,155,810 (+7,487) | +| 2025-10-30 | 617,846 (+4,100) | 555,026 (+12,962) | 1,172,872 (+17,062) | +| 2025-10-31 | 626,612 (+8,766) | 564,579 (+9,553) | 1,191,191 (+18,319) | +| 2025-11-01 | 636,100 (+9,488) | 581,806 (+17,227) | 1,217,906 (+26,715) | +| 2025-11-02 | 644,067 (+7,967) | 590,004 (+8,198) | 1,234,071 (+16,165) | +| 2025-11-03 | 653,130 (+9,063) | 597,139 (+7,135) | 1,250,269 (+16,198) | +| 2025-11-04 | 663,912 (+10,782) | 608,056 (+10,917) | 1,271,968 (+21,699) | +| 2025-11-05 | 675,074 (+11,162) | 619,690 (+11,634) | 1,294,764 (+22,796) | +| 2025-11-06 | 686,252 (+11,178) | 630,885 (+11,195) | 1,317,137 (+22,373) | +| 2025-11-07 | 696,646 (+10,394) | 642,146 (+11,261) | 1,338,792 (+21,655) | +| 2025-11-08 | 706,035 (+9,389) | 653,489 (+11,343) | 1,359,524 (+20,732) | +| 2025-11-09 | 713,462 (+7,427) | 660,459 (+6,970) | 1,373,921 (+14,397) | +| 2025-11-10 | 722,288 (+8,826) | 668,225 (+7,766) | 1,390,513 (+16,592) | +| 2025-11-11 | 729,769 (+7,481) | 677,501 (+9,276) | 1,407,270 (+16,757) | +| 2025-11-12 | 740,180 (+10,411) | 686,454 (+8,953) | 1,426,634 (+19,364) | +| 2025-11-13 | 749,905 (+9,725) | 696,157 (+9,703) | 1,446,062 (+19,428) | +| 2025-11-14 | 759,928 (+10,023) | 705,237 (+9,080) | 1,465,165 (+19,103) | +| 2025-12-23 | 1,286,941 (+527,013) | 1,186,439 (+481,202) | 2,473,380 (+1,008,215) | From b45b7d4b4878721a148ebed4bf4dce89f9acbc71 Mon Sep 17 00:00:00 2001 From: michaelc Date: Wed, 24 Dec 2025 09:33:12 +0800 Subject: [PATCH 04/14] core: add custom build identification and enhanced UX features MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add -mcdev suffix to all build versions for custom build identification - Extend version timestamp to include seconds for better build tracking - Add connection testing and model discovery to provider management - Enhance TUI sidebar with detailed token breakdown and performance metrics - Display context usage as "used / total" with percentage - Show latency and tokens/sec performance stats These changes make it easier to identify custom builds and provide better visibility into model usage and performance during sessions. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- packages/opencode/src/cli/cmd/provider.ts | 340 ++++++++++++++++-- .../src/cli/cmd/tui/routes/session/header.tsx | 23 +- .../cli/cmd/tui/routes/session/sidebar.tsx | 38 +- packages/script/src/index.ts | 8 +- 4 files changed, 365 insertions(+), 44 deletions(-) diff --git a/packages/opencode/src/cli/cmd/provider.ts b/packages/opencode/src/cli/cmd/provider.ts index 4b357a934bb3..9771d8d6c282 100644 --- a/packages/opencode/src/cli/cmd/provider.ts +++ b/packages/opencode/src/cli/cmd/provider.ts @@ -6,11 +6,76 @@ import path from "path" import fs from "fs/promises" import { Filesystem } from "../../util/filesystem" +/** + * Test connection to a provider endpoint + */ +async function testConnection(baseURL: string, modelId?: string): Promise<{ + success: boolean + models?: string[] + error?: string +}> { + try { + const url = baseURL.endsWith('/v1') ? `${baseURL}/models` : `${baseURL}/v1/models` + const response = await fetch(url, { + method: 'GET', + signal: AbortSignal.timeout(5000) + }) + + if (!response.ok) { + return { success: false, error: `HTTP ${response.status}` } + } + + const data = await response.json() + const models = data.data?.map((m: any) => m.id) || [] + + return { success: true, models } + } catch (error: any) { + return { success: false, error: error.message } + } +} + +/** + * Discover available models from endpoint + */ +async function discoverModels(baseURL: string): Promise> { + try { + const url = baseURL.endsWith('/v1') ? `${baseURL}/models` : `${baseURL}/v1/models` + const spinner = prompts.spinner() + spinner.start("Fetching available models...") + + const response = await fetch(url, { + method: 'GET', + signal: AbortSignal.timeout(5000) + }) + + if (!response.ok) { + spinner.stop("Could not fetch models") + return [] + } + + const data = await response.json() + const models = data.data?.map((m: any) => ({ + id: m.id, + name: m.id.split('/').pop()?.split(':')[0] || m.id + })) || [] + + spinner.stop(`Found ${models.length} model(s)`) + return models + } catch (error) { + return [] + } +} + export const ProviderCommand = cmd({ command: "provider", describe: "manage custom providers", builder: (yargs) => - yargs.command(ProviderAddCommand).command(ProviderListCommand).command(ProviderRemoveCommand).demandCommand(), + yargs + .command(ProviderAddCommand) + .command(ProviderListCommand) + .command(ProviderRemoveCommand) + .command(ProviderDoctorCommand) + .demandCommand(), async handler() {}, }) @@ -118,6 +183,25 @@ export const ProviderAddCommand = cmd({ }) if (prompts.isCancel(baseURL)) throw new UI.CancelledError() + // Test connection + UI.empty() + const spinner = prompts.spinner() + spinner.start("Testing connection...") + const testResult = await testConnection(baseURL) + + if (testResult.success) { + spinner.stop("✓ Connection successful") + prompts.log.success(`Detected OpenAI-compatible endpoint`) + if (testResult.models && testResult.models.length > 0) { + prompts.log.info(`Found ${testResult.models.length} model(s) available`) + } + } else { + spinner.stop("⚠ Connection test failed") + prompts.log.warn(`Could not connect: ${testResult.error}`) + prompts.log.info(`Config will be saved anyway. Verify endpoint is correct.`) + } + UI.empty() + const npmPackage = await prompts.select({ message: "SDK package", options: [ @@ -145,47 +229,88 @@ export const ProviderAddCommand = cmd({ finalNpmPackage = customNpm as string } - // Add models + // Add models - offer discovery if connection succeeded const models: Record = {} - let addMore = true + let shouldDiscover = false - while (addMore) { - const modelID = await prompts.text({ - message: "Model ID", - placeholder: "model-name", - validate: (x) => (x && x.length > 0 ? undefined : "Required"), - }) - if (prompts.isCancel(modelID)) throw new UI.CancelledError() - - const modelName = await prompts.text({ - message: "Model display name", - placeholder: "Model Name", - initialValue: modelID - .split("-") - .map((word) => word.charAt(0).toUpperCase() + word.slice(1)) - .join(" "), + if (testResult.success && testResult.models && testResult.models.length > 0) { + const discoverConfirm = await prompts.confirm({ + message: "Fetch available models from server?", + initialValue: true, }) - if (prompts.isCancel(modelName)) throw new UI.CancelledError() + if (prompts.isCancel(discoverConfirm)) throw new UI.CancelledError() + shouldDiscover = Boolean(discoverConfirm) + } - const realID = await prompts.text({ - message: "Actual model ID (if different from display ID)", - placeholder: "Leave empty if same as model ID", - }) - if (prompts.isCancel(realID)) { - models[modelID] = { name: modelName } + if (shouldDiscover) { + const availableModels = await discoverModels(baseURL) + + if (availableModels.length > 0) { + const selectedModels = await prompts.multiselect({ + message: "Select models to add:", + options: availableModels.map(m => ({ + label: m.id, + value: m.id, + hint: m.name, + })), + required: true, + }) + if (prompts.isCancel(selectedModels)) throw new UI.CancelledError() + + // Add selected models + for (const modelFullId of selectedModels) { + const modelKey = String(modelFullId).split('/').pop()?.split(':')[0] || String(modelFullId) + models[modelKey] = { + id: modelFullId, + name: modelKey.charAt(0).toUpperCase() + modelKey.slice(1).replace(/-/g, ' '), + } + } } else { - models[modelID] = { + prompts.log.warn("Could not fetch models. Enter manually.") + } + } + + // Manual model entry if discovery was skipped or failed + if (Object.keys(models).length === 0) { + let addMore = true + + while (addMore) { + const modelID = await prompts.text({ + message: "Model ID (short name for CLI)", + placeholder: "devstral", + validate: (x) => (x && x.length > 0 ? undefined : "Required"), + }) + if (prompts.isCancel(modelID)) throw new UI.CancelledError() + + const realID = await prompts.text({ + message: "Actual model ID (as server expects)", + placeholder: "ollama/devstral-small-2-100k:latest", + initialValue: modelID, + }) + if (prompts.isCancel(realID)) throw new UI.CancelledError() + + const modelName = await prompts.text({ + message: "Model display name", + placeholder: "Devstral Small", + initialValue: String(modelID) + .split("-") + .map((word) => word.charAt(0).toUpperCase() + word.slice(1)) + .join(" "), + }) + if (prompts.isCancel(modelName)) throw new UI.CancelledError() + + models[String(modelID)] = { + id: realID, name: modelName, - ...(realID && realID.length > 0 ? { id: realID } : {}), } - } - const continueAdding = await prompts.confirm({ - message: "Add another model?", - initialValue: false, - }) - if (prompts.isCancel(continueAdding)) throw new UI.CancelledError() - addMore = continueAdding + const continueAdding = await prompts.confirm({ + message: "Add another model?", + initialValue: false, + }) + if (prompts.isCancel(continueAdding)) throw new UI.CancelledError() + addMore = continueAdding + } } // Read existing config or create new @@ -212,12 +337,60 @@ export const ProviderAddCommand = cmd({ models, } + // Show summary before saving + UI.empty() + prompts.log.info("Configuration Summary:") + console.log(` ${UI.Style.TEXT_DIM}Provider ID: ${UI.Style.TEXT_NORMAL}${providerID}`) + console.log(` ${UI.Style.TEXT_DIM}Provider Name: ${UI.Style.TEXT_NORMAL}${providerName}`) + console.log(` ${UI.Style.TEXT_DIM}Base URL: ${UI.Style.TEXT_NORMAL}${baseURL}`) + console.log(` ${UI.Style.TEXT_DIM}NPM Package: ${UI.Style.TEXT_NORMAL}${finalNpmPackage}`) + console.log(` ${UI.Style.TEXT_DIM}Models (${Object.keys(models).length}):`) + for (const [key, model] of Object.entries(models)) { + const modelStr = model.id ? `${key} → ${model.id}` : key + console.log(` • ${modelStr}`) + } + UI.empty() + + const shouldSave = await prompts.confirm({ + message: "Save this configuration?", + initialValue: true, + }) + if (prompts.isCancel(shouldSave) || !shouldSave) { + prompts.outro("Cancelled") + return + } + // Write config await fs.writeFile(configPath, JSON.stringify(config, null, 2) + "\n", "utf-8") - prompts.log.success(`Provider added to ${configPath}`) - prompts.log.info(`Run 'opencode models ${providerID}' to see available models`) - prompts.outro("Done") + UI.empty() + prompts.log.success(`✓ Provider saved to opencode.json`) + + // Validate models if connection was successful + if (testResult.success && testResult.models) { + UI.empty() + prompts.log.info("Validating models...") + for (const [key, model] of Object.entries(models)) { + const modelId = (model as any).id || key + if (testResult.models.includes(modelId)) { + console.log(` ${UI.Style.TEXT_SUCCESS}✓${UI.Style.TEXT_NORMAL} ${key} ${UI.Style.TEXT_DIM}(available)`) + } else { + console.log(` ${UI.Style.TEXT_WARNING}⚠${UI.Style.TEXT_NORMAL} ${key} ${UI.Style.TEXT_DIM}(not found on server)`) + } + } + } + + // Show usage instructions + UI.empty() + prompts.log.step("Test your provider:") + const firstModel = Object.keys(models)[0] + console.log(` ${UI.Style.TEXT_HIGHLIGHT}opencode run --model ${providerID}/${firstModel} "Hello, world!"${UI.Style.TEXT_NORMAL}`) + UI.empty() + prompts.log.step("List all providers:") + console.log(` ${UI.Style.TEXT_HIGHLIGHT}opencode provider list${UI.Style.TEXT_NORMAL}`) + UI.empty() + + prompts.outro("Done! 🚀") }, }) @@ -277,3 +450,96 @@ export const ProviderRemoveCommand = cmd({ } }, }) + +export const ProviderDoctorCommand = cmd({ + command: "doctor", + aliases: ["check", "validate"], + describe: "validate provider configuration and test connections", + async handler() { + UI.empty() + prompts.intro("Provider Configuration Check") + + // Find opencode.json + const configFiles = await Filesystem.findUp("opencode.json", process.cwd()) + const configPath = configFiles[0] + + if (!configPath) { + prompts.log.warn("No opencode.json found") + prompts.log.info("Providers will use built-in configuration only") + prompts.outro("No custom providers") + return + } + + const displayPath = configPath.replace(process.env.HOME || "", "~") + prompts.log.info(`Config: ${UI.Style.TEXT_DIM}${displayPath}`) + UI.empty() + + try { + const content = await fs.readFile(configPath, "utf-8") + const config = JSON.parse(content) + + if (!config.provider || Object.keys(config.provider).length === 0) { + prompts.log.warn("No custom providers configured") + prompts.outro("Nothing to check") + return + } + + const providerCount = Object.keys(config.provider).length + prompts.log.info(`Found ${providerCount} provider(s). Testing connections...\n`) + + let healthyCount = 0 + let unhealthyCount = 0 + + for (const [providerID, provider] of Object.entries(config.provider as Record)) { + const name = provider.name || providerID + console.log(`${UI.Style.TEXT_NORMAL_BOLD}${name}${UI.Style.TEXT_NORMAL} ${UI.Style.TEXT_DIM}(${providerID})`) + console.log(` URL: ${provider.options?.baseURL || 'N/A'}`) + + // Test connection + const testResult = await testConnection(provider.options?.baseURL) + + if (testResult.success) { + console.log(` ${UI.Style.TEXT_SUCCESS}✓ Connection successful${UI.Style.TEXT_NORMAL}`) + healthyCount++ + + // Validate each model + const models = provider.models || {} + const modelCount = Object.keys(models).length + console.log(` Models: ${modelCount}`) + + for (const [key, model] of Object.entries(models)) { + const modelId = (model as any).id || key + if (testResult.models?.includes(modelId)) { + console.log(` ${UI.Style.TEXT_SUCCESS}✓${UI.Style.TEXT_NORMAL} ${key} ${UI.Style.TEXT_DIM}(${modelId})`) + } else { + console.log(` ${UI.Style.TEXT_WARNING}⚠${UI.Style.TEXT_NORMAL} ${key} ${UI.Style.TEXT_DIM}not found on server`) + console.log(` ${UI.Style.TEXT_DIM}Expected: ${modelId}`) + if (testResult.models && testResult.models.length > 0) { + console.log(` ${UI.Style.TEXT_DIM}Available: ${testResult.models.slice(0, 3).join(', ')}${testResult.models.length > 3 ? '...' : ''}`) + } + } + } + } else { + console.log(` ${UI.Style.TEXT_DANGER}✗ Connection failed${UI.Style.TEXT_NORMAL}`) + console.log(` ${UI.Style.TEXT_DIM}Error: ${testResult.error}`) + unhealthyCount++ + } + + UI.empty() + } + + // Summary + if (unhealthyCount === 0) { + prompts.log.success(`All ${healthyCount} provider(s) are healthy ✓`) + prompts.outro("Configuration OK") + } else { + prompts.log.warn(`${unhealthyCount} provider(s) have issues`) + prompts.log.info(`${healthyCount} provider(s) are healthy`) + prompts.outro("Check failed providers") + } + } catch (error) { + prompts.log.error(`Failed to validate config: ${error}`) + prompts.outro("Failed") + } + }, +}) diff --git a/packages/opencode/src/cli/cmd/tui/routes/session/header.tsx b/packages/opencode/src/cli/cmd/tui/routes/session/header.tsx index 9988fbd556e0..4a7d8ef508dc 100644 --- a/packages/opencode/src/cli/cmd/tui/routes/session/header.tsx +++ b/packages/opencode/src/cli/cmd/tui/routes/session/header.tsx @@ -51,10 +51,31 @@ export function Header() { const total = last.tokens.input + last.tokens.output + last.tokens.reasoning + last.tokens.cache.read + last.tokens.cache.write const model = sync.data.provider.find((x) => x.id === last.providerID)?.models[last.modelID] + + // Calculate latency and tokens/sec + const latencyMs = last.time.completed ? last.time.completed - last.time.created : 0 + const latencySec = latencyMs / 1000 + const tokensPerSec = latencySec > 0 ? Math.round(last.tokens.output / latencySec) : 0 + let result = total.toLocaleString() + + // Add max tokens and percentage if (model?.limit.context) { - result += "/" + Math.round((total / model.limit.context) * 100) + "%" + result += `/${model.limit.context.toLocaleString()} (${Math.round((total / model.limit.context) * 100)}%)` + } else { + result += ` tokens` } + + // Add detailed breakdown + result += ` [${last.tokens.input.toLocaleString()}↓/${last.tokens.output.toLocaleString()}↑` + if (last.tokens.cache.read > 0) { + result += ` ${last.tokens.cache.read.toLocaleString()}⚡` + } + if (tokensPerSec > 0) { + result += ` ${tokensPerSec}t/s` + } + result += `]` + return result }) diff --git a/packages/opencode/src/cli/cmd/tui/routes/session/sidebar.tsx b/packages/opencode/src/cli/cmd/tui/routes/session/sidebar.tsx index ee83a3afcb3d..8e005b20155e 100644 --- a/packages/opencode/src/cli/cmd/tui/routes/session/sidebar.tsx +++ b/packages/opencode/src/cli/cmd/tui/routes/session/sidebar.tsx @@ -29,12 +29,27 @@ export function Sidebar(props: { sessionID: string }) { const context = createMemo(() => { const last = messages().findLast((x) => x.role === "assistant" && x.tokens.output > 0) as AssistantMessage if (!last) return + const total = last.tokens.input + last.tokens.output + last.tokens.reasoning + last.tokens.cache.read + last.tokens.cache.write const model = sync.data.provider.find((x) => x.id === last.providerID)?.models[last.modelID] + + // Calculate latency and tokens/sec + const latencyMs = last.time.completed ? last.time.completed - last.time.created : 0 + const latencySec = latencyMs / 1000 + const tokensPerSec = latencySec > 0 ? Math.round(last.tokens.output / latencySec) : 0 + return { tokens: total.toLocaleString(), + maxTokens: model?.limit.context?.toLocaleString() ?? "Unknown", percentage: model?.limit.context ? Math.round((total / model.limit.context) * 100) : null, + input: last.tokens.input.toLocaleString(), + output: last.tokens.output.toLocaleString(), + reasoning: last.tokens.reasoning.toLocaleString(), + cacheRead: last.tokens.cache.read.toLocaleString(), + cacheWrite: last.tokens.cache.write.toLocaleString(), + latency: latencySec.toFixed(2), + tokensPerSec: tokensPerSec.toLocaleString(), } }) @@ -54,10 +69,29 @@ export function Sidebar(props: { sessionID: string }) { Context - {context()?.tokens ?? 0} tokens - {context()?.percentage ?? 0}% used + + {context()?.tokens ?? 0} / {context()?.maxTokens ?? "?"} tokens ({context()?.percentage ?? 0}% used) + + In: {context()?.input ?? 0} | Out: {context()?.output ?? 0} + + Reasoning: {context()?.reasoning ?? 0} + + + + Cache: {context()?.cacheRead ?? 0} read / {context()?.cacheWrite ?? 0} write + + {cost()} spent + + + + Performance + + Latency: {context()?.latency ?? 0}s + Speed: {context()?.tokensPerSec ?? 0} tok/s + + 0}> setMcpExpanded(!mcpExpanded())}> diff --git a/packages/script/src/index.ts b/packages/script/src/index.ts index 141d2b750abe..51a9e0bc0b9e 100644 --- a/packages/script/src/index.ts +++ b/packages/script/src/index.ts @@ -17,7 +17,7 @@ const CHANNEL = process.env["OPENCODE_CHANNEL"] ?? (await $`git branch --show-cu const IS_PREVIEW = CHANNEL !== "latest" const VERSION = await (async () => { if (process.env["OPENCODE_VERSION"]) return process.env["OPENCODE_VERSION"] - if (IS_PREVIEW) return `0.0.0-${CHANNEL}-${new Date().toISOString().slice(0, 16).replace(/[-:T]/g, "")}` + if (IS_PREVIEW) return `0.0.0-${CHANNEL}-${new Date().toISOString().slice(0, 19).replace(/[-:T]/g, "")}-mcdev` const version = await fetch("https://registry.npmjs.org/opencode-ai/latest") .then((res) => { if (!res.ok) throw new Error(res.statusText) @@ -26,9 +26,9 @@ const VERSION = await (async () => { .then((data: any) => data.version) const [major, minor, patch] = version.split(".").map((x: string) => Number(x) || 0) const t = process.env["OPENCODE_BUMP"]?.toLowerCase() - if (t === "major") return `${major + 1}.0.0` - if (t === "minor") return `${major}.${minor + 1}.0` - return `${major}.${minor}.${patch + 1}` + if (t === "major") return `${major + 1}.0.0-mcdev` + if (t === "minor") return `${major}.${minor + 1}.0-mcdev` + return `${major}.${minor}.${patch + 1}-mcdev` })() export const Script = { From 87b9159aedc68b0a7fef642adaf73e0c68c0b6bb Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Wed, 24 Dec 2025 01:51:36 +0000 Subject: [PATCH 05/14] chore: format code --- packages/opencode/src/cli/cmd/provider.ts | 56 ++++++++++++------- .../cli/cmd/tui/routes/session/sidebar.tsx | 4 +- 2 files changed, 38 insertions(+), 22 deletions(-) diff --git a/packages/opencode/src/cli/cmd/provider.ts b/packages/opencode/src/cli/cmd/provider.ts index 9771d8d6c282..3e543a1a585e 100644 --- a/packages/opencode/src/cli/cmd/provider.ts +++ b/packages/opencode/src/cli/cmd/provider.ts @@ -9,16 +9,19 @@ import { Filesystem } from "../../util/filesystem" /** * Test connection to a provider endpoint */ -async function testConnection(baseURL: string, modelId?: string): Promise<{ +async function testConnection( + baseURL: string, + modelId?: string, +): Promise<{ success: boolean models?: string[] error?: string }> { try { - const url = baseURL.endsWith('/v1') ? `${baseURL}/models` : `${baseURL}/v1/models` + const url = baseURL.endsWith("/v1") ? `${baseURL}/models` : `${baseURL}/v1/models` const response = await fetch(url, { - method: 'GET', - signal: AbortSignal.timeout(5000) + method: "GET", + signal: AbortSignal.timeout(5000), }) if (!response.ok) { @@ -37,15 +40,15 @@ async function testConnection(baseURL: string, modelId?: string): Promise<{ /** * Discover available models from endpoint */ -async function discoverModels(baseURL: string): Promise> { +async function discoverModels(baseURL: string): Promise> { try { - const url = baseURL.endsWith('/v1') ? `${baseURL}/models` : `${baseURL}/v1/models` + const url = baseURL.endsWith("/v1") ? `${baseURL}/models` : `${baseURL}/v1/models` const spinner = prompts.spinner() spinner.start("Fetching available models...") const response = await fetch(url, { - method: 'GET', - signal: AbortSignal.timeout(5000) + method: "GET", + signal: AbortSignal.timeout(5000), }) if (!response.ok) { @@ -54,10 +57,11 @@ async function discoverModels(baseURL: string): Promise ({ - id: m.id, - name: m.id.split('/').pop()?.split(':')[0] || m.id - })) || [] + const models = + data.data?.map((m: any) => ({ + id: m.id, + name: m.id.split("/").pop()?.split(":")[0] || m.id, + })) || [] spinner.stop(`Found ${models.length} model(s)`) return models @@ -248,7 +252,7 @@ export const ProviderAddCommand = cmd({ if (availableModels.length > 0) { const selectedModels = await prompts.multiselect({ message: "Select models to add:", - options: availableModels.map(m => ({ + options: availableModels.map((m) => ({ label: m.id, value: m.id, hint: m.name, @@ -259,10 +263,10 @@ export const ProviderAddCommand = cmd({ // Add selected models for (const modelFullId of selectedModels) { - const modelKey = String(modelFullId).split('/').pop()?.split(':')[0] || String(modelFullId) + const modelKey = String(modelFullId).split("/").pop()?.split(":")[0] || String(modelFullId) models[modelKey] = { id: modelFullId, - name: modelKey.charAt(0).toUpperCase() + modelKey.slice(1).replace(/-/g, ' '), + name: modelKey.charAt(0).toUpperCase() + modelKey.slice(1).replace(/-/g, " "), } } } else { @@ -375,7 +379,9 @@ export const ProviderAddCommand = cmd({ if (testResult.models.includes(modelId)) { console.log(` ${UI.Style.TEXT_SUCCESS}✓${UI.Style.TEXT_NORMAL} ${key} ${UI.Style.TEXT_DIM}(available)`) } else { - console.log(` ${UI.Style.TEXT_WARNING}⚠${UI.Style.TEXT_NORMAL} ${key} ${UI.Style.TEXT_DIM}(not found on server)`) + console.log( + ` ${UI.Style.TEXT_WARNING}⚠${UI.Style.TEXT_NORMAL} ${key} ${UI.Style.TEXT_DIM}(not found on server)`, + ) } } } @@ -384,7 +390,9 @@ export const ProviderAddCommand = cmd({ UI.empty() prompts.log.step("Test your provider:") const firstModel = Object.keys(models)[0] - console.log(` ${UI.Style.TEXT_HIGHLIGHT}opencode run --model ${providerID}/${firstModel} "Hello, world!"${UI.Style.TEXT_NORMAL}`) + console.log( + ` ${UI.Style.TEXT_HIGHLIGHT}opencode run --model ${providerID}/${firstModel} "Hello, world!"${UI.Style.TEXT_NORMAL}`, + ) UI.empty() prompts.log.step("List all providers:") console.log(` ${UI.Style.TEXT_HIGHLIGHT}opencode provider list${UI.Style.TEXT_NORMAL}`) @@ -493,7 +501,7 @@ export const ProviderDoctorCommand = cmd({ for (const [providerID, provider] of Object.entries(config.provider as Record)) { const name = provider.name || providerID console.log(`${UI.Style.TEXT_NORMAL_BOLD}${name}${UI.Style.TEXT_NORMAL} ${UI.Style.TEXT_DIM}(${providerID})`) - console.log(` URL: ${provider.options?.baseURL || 'N/A'}`) + console.log(` URL: ${provider.options?.baseURL || "N/A"}`) // Test connection const testResult = await testConnection(provider.options?.baseURL) @@ -510,12 +518,18 @@ export const ProviderDoctorCommand = cmd({ for (const [key, model] of Object.entries(models)) { const modelId = (model as any).id || key if (testResult.models?.includes(modelId)) { - console.log(` ${UI.Style.TEXT_SUCCESS}✓${UI.Style.TEXT_NORMAL} ${key} ${UI.Style.TEXT_DIM}(${modelId})`) + console.log( + ` ${UI.Style.TEXT_SUCCESS}✓${UI.Style.TEXT_NORMAL} ${key} ${UI.Style.TEXT_DIM}(${modelId})`, + ) } else { - console.log(` ${UI.Style.TEXT_WARNING}⚠${UI.Style.TEXT_NORMAL} ${key} ${UI.Style.TEXT_DIM}not found on server`) + console.log( + ` ${UI.Style.TEXT_WARNING}⚠${UI.Style.TEXT_NORMAL} ${key} ${UI.Style.TEXT_DIM}not found on server`, + ) console.log(` ${UI.Style.TEXT_DIM}Expected: ${modelId}`) if (testResult.models && testResult.models.length > 0) { - console.log(` ${UI.Style.TEXT_DIM}Available: ${testResult.models.slice(0, 3).join(', ')}${testResult.models.length > 3 ? '...' : ''}`) + console.log( + ` ${UI.Style.TEXT_DIM}Available: ${testResult.models.slice(0, 3).join(", ")}${testResult.models.length > 3 ? "..." : ""}`, + ) } } } diff --git a/packages/opencode/src/cli/cmd/tui/routes/session/sidebar.tsx b/packages/opencode/src/cli/cmd/tui/routes/session/sidebar.tsx index 8e005b20155e..3b1177ef09af 100644 --- a/packages/opencode/src/cli/cmd/tui/routes/session/sidebar.tsx +++ b/packages/opencode/src/cli/cmd/tui/routes/session/sidebar.tsx @@ -72,7 +72,9 @@ export function Sidebar(props: { sessionID: string }) { {context()?.tokens ?? 0} / {context()?.maxTokens ?? "?"} tokens ({context()?.percentage ?? 0}% used) - In: {context()?.input ?? 0} | Out: {context()?.output ?? 0} + + In: {context()?.input ?? 0} | Out: {context()?.output ?? 0} + Reasoning: {context()?.reasoning ?? 0} From 970b668f03387ce5fb94b640716bfb70b32031f9 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Wed, 24 Dec 2025 12:28:31 +0000 Subject: [PATCH 06/14] ignore: update download stats 2025-12-24 --- STATS.md | 1 + 1 file changed, 1 insertion(+) diff --git a/STATS.md b/STATS.md index 53cfff730440..a19bb837fb4f 100644 --- a/STATS.md +++ b/STATS.md @@ -141,3 +141,4 @@ | 2025-11-13 | 749,905 (+9,725) | 696,157 (+9,703) | 1,446,062 (+19,428) | | 2025-11-14 | 759,928 (+10,023) | 705,237 (+9,080) | 1,465,165 (+19,103) | | 2025-12-23 | 1,286,941 (+527,013) | 1,186,439 (+481,202) | 2,473,380 (+1,008,215) | +| 2025-12-24 | 1,309,579 (+22,638) | 1,203,767 (+17,328) | 2,513,346 (+39,966) | From 8778e16ccec21eba0c015358b4cfc5e2bfe201c8 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Thu, 25 Dec 2025 12:27:51 +0000 Subject: [PATCH 07/14] ignore: update download stats 2025-12-25 --- STATS.md | 1 + 1 file changed, 1 insertion(+) diff --git a/STATS.md b/STATS.md index a19bb837fb4f..a0e5fab95415 100644 --- a/STATS.md +++ b/STATS.md @@ -142,3 +142,4 @@ | 2025-11-14 | 759,928 (+10,023) | 705,237 (+9,080) | 1,465,165 (+19,103) | | 2025-12-23 | 1,286,941 (+527,013) | 1,186,439 (+481,202) | 2,473,380 (+1,008,215) | | 2025-12-24 | 1,309,579 (+22,638) | 1,203,767 (+17,328) | 2,513,346 (+39,966) | +| 2025-12-25 | 1,333,468 (+23,889) | 1,217,283 (+13,516) | 2,550,751 (+37,405) | From 448d6530e3ee809936fe74542cd65c08066a228f Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Fri, 26 Dec 2025 12:27:47 +0000 Subject: [PATCH 08/14] ignore: update download stats 2025-12-26 --- STATS.md | 1 + 1 file changed, 1 insertion(+) diff --git a/STATS.md b/STATS.md index a0e5fab95415..5fc584758b14 100644 --- a/STATS.md +++ b/STATS.md @@ -143,3 +143,4 @@ | 2025-12-23 | 1,286,941 (+527,013) | 1,186,439 (+481,202) | 2,473,380 (+1,008,215) | | 2025-12-24 | 1,309,579 (+22,638) | 1,203,767 (+17,328) | 2,513,346 (+39,966) | | 2025-12-25 | 1,333,468 (+23,889) | 1,217,283 (+13,516) | 2,550,751 (+37,405) | +| 2025-12-26 | 1,352,624 (+19,156) | 1,227,615 (+10,332) | 2,580,239 (+29,488) | From e5f3a28316364eee3fadafcbe0c781fd891a8314 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Sat, 27 Dec 2025 12:25:43 +0000 Subject: [PATCH 09/14] ignore: update download stats 2025-12-27 --- STATS.md | 1 + 1 file changed, 1 insertion(+) diff --git a/STATS.md b/STATS.md index 5fc584758b14..a77d6663f408 100644 --- a/STATS.md +++ b/STATS.md @@ -144,3 +144,4 @@ | 2025-12-24 | 1,309,579 (+22,638) | 1,203,767 (+17,328) | 2,513,346 (+39,966) | | 2025-12-25 | 1,333,468 (+23,889) | 1,217,283 (+13,516) | 2,550,751 (+37,405) | | 2025-12-26 | 1,352,624 (+19,156) | 1,227,615 (+10,332) | 2,580,239 (+29,488) | +| 2025-12-27 | 1,372,012 (+19,388) | 1,238,236 (+10,621) | 2,610,248 (+30,009) | From 84f080cb8128d6747292f8570e53e0acb4746a24 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Sun, 28 Dec 2025 12:26:28 +0000 Subject: [PATCH 10/14] ignore: update download stats 2025-12-28 --- STATS.md | 1 + 1 file changed, 1 insertion(+) diff --git a/STATS.md b/STATS.md index a77d6663f408..6277cd85ac83 100644 --- a/STATS.md +++ b/STATS.md @@ -145,3 +145,4 @@ | 2025-12-25 | 1,333,468 (+23,889) | 1,217,283 (+13,516) | 2,550,751 (+37,405) | | 2025-12-26 | 1,352,624 (+19,156) | 1,227,615 (+10,332) | 2,580,239 (+29,488) | | 2025-12-27 | 1,372,012 (+19,388) | 1,238,236 (+10,621) | 2,610,248 (+30,009) | +| 2025-12-28 | 1,390,676 (+18,664) | 1,245,690 (+7,454) | 2,636,366 (+26,118) | From 5c20607f0f22fa60f8b77a245e3214a21a52330b Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Mon, 29 Dec 2025 12:29:52 +0000 Subject: [PATCH 11/14] ignore: update download stats 2025-12-29 --- STATS.md | 1 + 1 file changed, 1 insertion(+) diff --git a/STATS.md b/STATS.md index 6277cd85ac83..a7b494cf3368 100644 --- a/STATS.md +++ b/STATS.md @@ -146,3 +146,4 @@ | 2025-12-26 | 1,352,624 (+19,156) | 1,227,615 (+10,332) | 2,580,239 (+29,488) | | 2025-12-27 | 1,372,012 (+19,388) | 1,238,236 (+10,621) | 2,610,248 (+30,009) | | 2025-12-28 | 1,390,676 (+18,664) | 1,245,690 (+7,454) | 2,636,366 (+26,118) | +| 2025-12-29 | 1,415,997 (+25,321) | 1,257,101 (+11,411) | 2,673,098 (+36,732) | From 9c35858c508aa0709aee578d9415004b212e140f Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Tue, 30 Dec 2025 12:29:13 +0000 Subject: [PATCH 12/14] ignore: update download stats 2025-12-30 --- STATS.md | 1 + 1 file changed, 1 insertion(+) diff --git a/STATS.md b/STATS.md index a7b494cf3368..cfa5aae8f206 100644 --- a/STATS.md +++ b/STATS.md @@ -147,3 +147,4 @@ | 2025-12-27 | 1,372,012 (+19,388) | 1,238,236 (+10,621) | 2,610,248 (+30,009) | | 2025-12-28 | 1,390,676 (+18,664) | 1,245,690 (+7,454) | 2,636,366 (+26,118) | | 2025-12-29 | 1,415,997 (+25,321) | 1,257,101 (+11,411) | 2,673,098 (+36,732) | +| 2025-12-30 | 1,446,066 (+30,069) | 1,272,689 (+15,588) | 2,718,755 (+45,657) | From 8ae1902adc7f7b45d08b714effa4c42ad574af4a Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Wed, 31 Dec 2025 12:28:27 +0000 Subject: [PATCH 13/14] ignore: update download stats 2025-12-31 --- STATS.md | 1 + 1 file changed, 1 insertion(+) diff --git a/STATS.md b/STATS.md index cfa5aae8f206..1c909d62fde1 100644 --- a/STATS.md +++ b/STATS.md @@ -148,3 +148,4 @@ | 2025-12-28 | 1,390,676 (+18,664) | 1,245,690 (+7,454) | 2,636,366 (+26,118) | | 2025-12-29 | 1,415,997 (+25,321) | 1,257,101 (+11,411) | 2,673,098 (+36,732) | | 2025-12-30 | 1,446,066 (+30,069) | 1,272,689 (+15,588) | 2,718,755 (+45,657) | +| 2025-12-31 | 1,479,945 (+33,879) | 1,293,235 (+20,546) | 2,773,180 (+54,425) | From c37d9a76121f4b9e35d406f30dbbdcd69bc9e4d3 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Thu, 1 Jan 2026 12:28:33 +0000 Subject: [PATCH 14/14] ignore: update download stats 2026-01-01 --- STATS.md | 1 + 1 file changed, 1 insertion(+) diff --git a/STATS.md b/STATS.md index 1c909d62fde1..b45b1fa07c87 100644 --- a/STATS.md +++ b/STATS.md @@ -149,3 +149,4 @@ | 2025-12-29 | 1,415,997 (+25,321) | 1,257,101 (+11,411) | 2,673,098 (+36,732) | | 2025-12-30 | 1,446,066 (+30,069) | 1,272,689 (+15,588) | 2,718,755 (+45,657) | | 2025-12-31 | 1,479,945 (+33,879) | 1,293,235 (+20,546) | 2,773,180 (+54,425) | +| 2026-01-01 | 1,510,088 (+30,143) | 1,309,874 (+16,639) | 2,819,962 (+46,782) |