zelari-code 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (146) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +156 -0
  3. package/bin/zelari-code.js +64 -0
  4. package/dist/agents/advancedTools.js +115 -0
  5. package/dist/agents/advancedTools.js.map +1 -0
  6. package/dist/agents/councilApi.js +375 -0
  7. package/dist/agents/councilApi.js.map +1 -0
  8. package/dist/agents/councilDirectives.js +100 -0
  9. package/dist/agents/councilDirectives.js.map +1 -0
  10. package/dist/agents/promoteMember.js +210 -0
  11. package/dist/agents/promoteMember.js.map +1 -0
  12. package/dist/agents/promptModules.js +139 -0
  13. package/dist/agents/promptModules.js.map +1 -0
  14. package/dist/agents/roles.js +281 -0
  15. package/dist/agents/roles.js.map +1 -0
  16. package/dist/agents/skills/builtin/debugging.js +248 -0
  17. package/dist/agents/skills/builtin/debugging.js.map +1 -0
  18. package/dist/agents/skills/builtin/docs.js +301 -0
  19. package/dist/agents/skills/builtin/docs.js.map +1 -0
  20. package/dist/agents/skills/builtin/git-ops.js +301 -0
  21. package/dist/agents/skills/builtin/git-ops.js.map +1 -0
  22. package/dist/agents/skills/builtin/planning.js +335 -0
  23. package/dist/agents/skills/builtin/planning.js.map +1 -0
  24. package/dist/agents/skills/builtin/refactoring.js +261 -0
  25. package/dist/agents/skills/builtin/refactoring.js.map +1 -0
  26. package/dist/agents/skills/builtin/review.js +302 -0
  27. package/dist/agents/skills/builtin/review.js.map +1 -0
  28. package/dist/agents/skills/builtin/testing.js +245 -0
  29. package/dist/agents/skills/builtin/testing.js.map +1 -0
  30. package/dist/agents/skills.js +272 -0
  31. package/dist/agents/skills.js.map +1 -0
  32. package/dist/agents/systemPromptBuilder.js +151 -0
  33. package/dist/agents/systemPromptBuilder.js.map +1 -0
  34. package/dist/agents/toolSchemas.js +249 -0
  35. package/dist/agents/toolSchemas.js.map +1 -0
  36. package/dist/agents/tools.js +196 -0
  37. package/dist/agents/tools.js.map +1 -0
  38. package/dist/agents/vaultTools.js +134 -0
  39. package/dist/agents/vaultTools.js.map +1 -0
  40. package/dist/cli/app.js +1823 -0
  41. package/dist/cli/app.js.map +1 -0
  42. package/dist/cli/branchManager.js +280 -0
  43. package/dist/cli/branchManager.js.map +1 -0
  44. package/dist/cli/compaction.js +63 -0
  45. package/dist/cli/compaction.js.map +1 -0
  46. package/dist/cli/components/ChatStream.js +23 -0
  47. package/dist/cli/components/ChatStream.js.map +1 -0
  48. package/dist/cli/components/CollapsibleToolOutput.js +38 -0
  49. package/dist/cli/components/CollapsibleToolOutput.js.map +1 -0
  50. package/dist/cli/components/Header.js +31 -0
  51. package/dist/cli/components/Header.js.map +1 -0
  52. package/dist/cli/components/InputBar.js +9 -0
  53. package/dist/cli/components/InputBar.js.map +1 -0
  54. package/dist/cli/components/Sidebar.js +17 -0
  55. package/dist/cli/components/Sidebar.js.map +1 -0
  56. package/dist/cli/councilCost.js +81 -0
  57. package/dist/cli/councilCost.js.map +1 -0
  58. package/dist/cli/councilDispatcher.js +56 -0
  59. package/dist/cli/councilDispatcher.js.map +1 -0
  60. package/dist/cli/councilFeedback.js +164 -0
  61. package/dist/cli/councilFeedback.js.map +1 -0
  62. package/dist/cli/crossProviderFailover.js +83 -0
  63. package/dist/cli/crossProviderFailover.js.map +1 -0
  64. package/dist/cli/gitOps.js +96 -0
  65. package/dist/cli/gitOps.js.map +1 -0
  66. package/dist/cli/grokOAuth.js +375 -0
  67. package/dist/cli/grokOAuth.js.map +1 -0
  68. package/dist/cli/keyStore.js +263 -0
  69. package/dist/cli/keyStore.js.map +1 -0
  70. package/dist/cli/keyValidator.js +83 -0
  71. package/dist/cli/keyValidator.js.map +1 -0
  72. package/dist/cli/main.bundled.js +21428 -0
  73. package/dist/cli/main.bundled.js.map +7 -0
  74. package/dist/cli/main.js +72 -0
  75. package/dist/cli/main.js.map +1 -0
  76. package/dist/cli/metrics.js +131 -0
  77. package/dist/cli/metrics.js.map +1 -0
  78. package/dist/cli/modelDiscovery.js +234 -0
  79. package/dist/cli/modelDiscovery.js.map +1 -0
  80. package/dist/cli/modelPricing.js +112 -0
  81. package/dist/cli/modelPricing.js.map +1 -0
  82. package/dist/cli/oauthCallbackServer.js +207 -0
  83. package/dist/cli/oauthCallbackServer.js.map +1 -0
  84. package/dist/cli/provider/openai-compatible.js +236 -0
  85. package/dist/cli/provider/openai-compatible.js.map +1 -0
  86. package/dist/cli/providerConfig.js +210 -0
  87. package/dist/cli/providerConfig.js.map +1 -0
  88. package/dist/cli/providerFailover.js +90 -0
  89. package/dist/cli/providerFailover.js.map +1 -0
  90. package/dist/cli/refreshRegistry.js +77 -0
  91. package/dist/cli/refreshRegistry.js.map +1 -0
  92. package/dist/cli/safety/auditLogger.js +111 -0
  93. package/dist/cli/safety/auditLogger.js.map +1 -0
  94. package/dist/cli/safety/sandboxPath.js +62 -0
  95. package/dist/cli/safety/sandboxPath.js.map +1 -0
  96. package/dist/cli/safety/shellBlocklist.js +71 -0
  97. package/dist/cli/safety/shellBlocklist.js.map +1 -0
  98. package/dist/cli/sessionManager.js +149 -0
  99. package/dist/cli/sessionManager.js.map +1 -0
  100. package/dist/cli/skillCache.js +205 -0
  101. package/dist/cli/skillCache.js.map +1 -0
  102. package/dist/cli/skillHistory.js +178 -0
  103. package/dist/cli/skillHistory.js.map +1 -0
  104. package/dist/cli/skillSuggest.js +132 -0
  105. package/dist/cli/skillSuggest.js.map +1 -0
  106. package/dist/cli/slashCommands.js +431 -0
  107. package/dist/cli/slashCommands.js.map +1 -0
  108. package/dist/cli/toolRegistry.js +187 -0
  109. package/dist/cli/toolRegistry.js.map +1 -0
  110. package/dist/cli/updater.js +157 -0
  111. package/dist/cli/updater.js.map +1 -0
  112. package/dist/main/core/AgentHarness.js +345 -0
  113. package/dist/main/core/AgentHarness.js.map +1 -0
  114. package/dist/main/core/providerStream.js +99 -0
  115. package/dist/main/core/providerStream.js.map +1 -0
  116. package/dist/main/core/sessionJsonl.js +99 -0
  117. package/dist/main/core/sessionJsonl.js.map +1 -0
  118. package/dist/main/core/tools/auditLog.js +26 -0
  119. package/dist/main/core/tools/auditLog.js.map +1 -0
  120. package/dist/main/core/tools/builtin/filesystem.js +110 -0
  121. package/dist/main/core/tools/builtin/filesystem.js.map +1 -0
  122. package/dist/main/core/tools/builtin/search.js +56 -0
  123. package/dist/main/core/tools/builtin/search.js.map +1 -0
  124. package/dist/main/core/tools/builtin/shell.js +51 -0
  125. package/dist/main/core/tools/builtin/shell.js.map +1 -0
  126. package/dist/main/core/tools/registry.js +71 -0
  127. package/dist/main/core/tools/registry.js.map +1 -0
  128. package/dist/main/core/tools/toolTypes.js +8 -0
  129. package/dist/main/core/tools/toolTypes.js.map +1 -0
  130. package/dist/main/core/tools/zodBridge.js +48 -0
  131. package/dist/main/core/tools/zodBridge.js.map +1 -0
  132. package/dist/shared/eventBus.js +104 -0
  133. package/dist/shared/eventBus.js.map +1 -0
  134. package/dist/shared/events.js +73 -0
  135. package/dist/shared/events.js.map +1 -0
  136. package/dist/shared/index.js +8 -0
  137. package/dist/shared/index.js.map +1 -0
  138. package/dist/types/context.js +3 -0
  139. package/dist/types/context.js.map +1 -0
  140. package/dist/types/index.js +2 -0
  141. package/dist/types/index.js.map +1 -0
  142. package/dist/types/knowledge.js +2 -0
  143. package/dist/types/knowledge.js.map +1 -0
  144. package/dist/types/systemTypes.js +10 -0
  145. package/dist/types/systemTypes.js.map +1 -0
  146. package/package.json +63 -0
@@ -0,0 +1,72 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * zelari-code — CLI coding agent on top of AnathemaBrain.
4
+ * Phase 14 Task 14.3 + 14.4: multi-panel TUI + slash command wiring.
5
+ */
6
+ import React from 'react';
7
+ import { render } from 'ink';
8
+ // @ts-ignore
9
+ import { App } from './app.js';
10
+ import { getMetricsLogger } from './metrics.js';
11
+ export const VERSION = '0.1.0';
12
+ /**
13
+ * Silent background update check (Task N.6, v3-N).
14
+ *
15
+ * Runs ~3s after startup. If a newer version exists on npm, prints a
16
+ * one-line hint to stderr (so it doesn't pollute the TUI). Failures
17
+ * are swallowed silently — registry outages must NEVER block the CLI.
18
+ *
19
+ * Disabled in dev mode (`ANATHEMA_DEV=1`) to avoid noise during local
20
+ * development where the bundled version is the source repo.
21
+ */
22
+ async function backgroundUpdateCheck() {
23
+ if (process.env.ANATHEMA_DEV === '1')
24
+ return;
25
+ await new Promise((resolve) => setTimeout(resolve, 3000));
26
+ try {
27
+ const { checkForUpdate } = await import('./updater.js');
28
+ const info = await checkForUpdate();
29
+ if (info.updateAvailable && !info.error) {
30
+ // eslint-disable-next-line no-console
31
+ console.error(`[zelari-coder] 🆕 v${info.latestVersion} available (current: v${info.currentVersion}). ` +
32
+ `Run \`zelari-coder\` then \`/update --yes\` to upgrade.`);
33
+ }
34
+ }
35
+ catch {
36
+ // Swallow — network failures, malformed responses, etc.
37
+ // The CLI is fully usable without update awareness.
38
+ }
39
+ }
40
+ async function shutdown() {
41
+ // Flush the process-wide MetricsLogger (Task G.3.3, carryover from v3-B
42
+ // B.5.2). The chat session in `app.tsx` writes via fire-and-forget
43
+ // queue — if we just `process.exit(0)` on SIGINT, the last few records
44
+ // (often the most interesting: agent_end + tool_execution_end) never
45
+ // land in `~/.tmp/anathema-coder/metrics.jsonl`. Awaiting `flush()`
46
+ // before exit guarantees the file is fully written.
47
+ try {
48
+ await getMetricsLogger().flush();
49
+ }
50
+ catch {
51
+ // Best-effort — never block shutdown on a metrics write error.
52
+ }
53
+ process.exit(0);
54
+ }
55
+ function main() {
56
+ const { waitUntilExit, unmount } = render(React.createElement(App));
57
+ process.on('SIGINT', () => {
58
+ unmount();
59
+ void shutdown();
60
+ });
61
+ process.on('SIGTERM', () => {
62
+ unmount();
63
+ void shutdown();
64
+ });
65
+ // Fire-and-forget — the CLI works regardless of the update check result.
66
+ void backgroundUpdateCheck();
67
+ waitUntilExit().then(() => {
68
+ void shutdown();
69
+ });
70
+ }
71
+ main();
72
+ //# sourceMappingURL=main.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"main.js","sourceRoot":"","sources":["../../src/cli/main.ts"],"names":[],"mappings":";AACA;;;GAGG;AAEH,OAAO,KAAK,MAAM,OAAO,CAAC;AAC1B,OAAO,EAAE,MAAM,EAAE,MAAM,KAAK,CAAC;AAC7B,aAAa;AACb,OAAO,EAAE,GAAG,EAAE,MAAM,UAAU,CAAC;AAC/B,OAAO,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAEhD,MAAM,CAAC,MAAM,OAAO,GAAG,OAAO,CAAC;AAE/B;;;;;;;;;GASG;AACH,KAAK,UAAU,qBAAqB;IAClC,IAAI,OAAO,CAAC,GAAG,CAAC,YAAY,KAAK,GAAG;QAAE,OAAO;IAC7C,MAAM,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,UAAU,CAAC,OAAO,EAAE,IAAI,CAAC,CAAC,CAAC;IAC1D,IAAI,CAAC;QACH,MAAM,EAAE,cAAc,EAAE,GAAG,MAAM,MAAM,CAAC,cAAc,CAAC,CAAC;QACxD,MAAM,IAAI,GAAG,MAAM,cAAc,EAAE,CAAC;QACpC,IAAI,IAAI,CAAC,eAAe,IAAI,CAAC,IAAI,CAAC,KAAK,EAAE,CAAC;YACxC,sCAAsC;YACtC,OAAO,CAAC,KAAK,CACX,sBAAsB,IAAI,CAAC,aAAa,yBAAyB,IAAI,CAAC,cAAc,KAAK;gBACvF,yDAAyD,CAC5D,CAAC;QACJ,CAAC;IACH,CAAC;IAAC,MAAM,CAAC;QACP,wDAAwD;QACxD,oDAAoD;IACtD,CAAC;AACH,CAAC;AAED,KAAK,UAAU,QAAQ;IACrB,wEAAwE;IACxE,mEAAmE;IACnE,uEAAuE;IACvE,qEAAqE;IACrE,oEAAoE;IACpE,oDAAoD;IACpD,IAAI,CAAC;QACH,MAAM,gBAAgB,EAAE,CAAC,KAAK,EAAE,CAAC;IACnC,CAAC;IAAC,MAAM,CAAC;QACP,+DAA+D;IACjE,CAAC;IACD,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AAClB,CAAC;AAED,SAAS,IAAI;IACX,MAAM,EAAE,aAAa,EAAE,OAAO,EAAE,GAAG,MAAM,CAAC,KAAK,CAAC,aAAa,CAAC,GAAG,CAAC,CAAC,CAAC;IAEpE,OAAO,CAAC,EAAE,CAAC,QAAQ,EAAE,GAAG,EAAE;QACxB,OAAO,EAAE,CAAC;QACV,KAAK,QAAQ,EAAE,CAAC;IAClB,CAAC,CAAC,CAAC;IACH,OAAO,CAAC,EAAE,CAAC,SAAS,EAAE,GAAG,EAAE;QACzB,OAAO,EAAE,CAAC;QACV,KAAK,QAAQ,EAAE,CAAC;IAClB,CAAC,CAAC,CAAC;IAEH,yEAAyE;IACzE,KAAK,qBAAqB,EAAE,CAAC;IAE7B,aAAa,EAAE,CAAC,IAAI,CAAC,GAAG,EAAE;QACxB,KAAK,QAAQ,EAAE,CAAC;IAClB,CAAC,CAAC,CAAC;AACL,CAAC;AAED,IAAI,EAAE,CAAC"}
@@ -0,0 +1,131 @@
1
+ /**
2
+ * metrics — JSONL metrics logger (Task B.5).
3
+ *
4
+ * Append-only NDJSON writer for runtime telemetry. One event per line,
5
+ * flushed via fire-and-forget queue (matches auditLogger pattern from
6
+ * v3-A).
7
+ *
8
+ * Schema (per record):
9
+ * {
10
+ * ts: number, // epoch ms
11
+ * kind: 'run' | 'message' | 'error',
12
+ * sessionId?: string,
13
+ * provider?: string,
14
+ * model?: string,
15
+ * latencyMs?: number,
16
+ * tokens?: number,
17
+ * costUsd?: number,
18
+ * ok?: boolean,
19
+ * error?: string,
20
+ * }
21
+ *
22
+ * Storage: `~/.tmp/anathema-coder/metrics.jsonl` (override via
23
+ * `ANATHEMA_METRICS_FILE` env var, useful for tests).
24
+ *
25
+ * Rotation: when the file exceeds 10MB, it's renamed to
26
+ * `metrics.1.jsonl` (existing `.1.jsonl` is overwritten). Only one
27
+ * rotation step — older data is dropped. This matches typical
28
+ * logrotate setups.
29
+ */
30
+ import { promises as fs, existsSync, statSync, renameSync, appendFileSync, mkdirSync } from 'node:fs';
31
+ import path from 'node:path';
32
+ import os from 'node:os';
33
+ export const METRICS_ROTATE_BYTES = 10 * 1024 * 1024; // 10 MB
34
+ export class MetricsLogger {
35
+ file;
36
+ writeQueue = Promise.resolve();
37
+ constructor(file) {
38
+ this.file = file ?? process.env.ANATHEMA_METRICS_FILE
39
+ ?? path.join(os.homedir(), '.tmp', 'zelari-code', 'metrics.jsonl');
40
+ mkdirSync(path.dirname(this.file), { recursive: true });
41
+ }
42
+ /** Fire-and-forget record append. */
43
+ record(rec) {
44
+ const full = { ts: rec.ts ?? Date.now(), ...rec };
45
+ this.writeQueue = this.writeQueue.then(() => this.append(full));
46
+ void this.writeQueue.catch(() => { });
47
+ }
48
+ /** Append a single record synchronously (file I/O). */
49
+ append(rec) {
50
+ return new Promise((resolve, reject) => {
51
+ try {
52
+ this.maybeRotate();
53
+ const line = JSON.stringify(rec) + '\n';
54
+ appendFileSync(this.file, line, { encoding: 'utf-8' });
55
+ resolve();
56
+ }
57
+ catch (err) {
58
+ reject(err);
59
+ }
60
+ });
61
+ }
62
+ /**
63
+ * Force the write queue to flush — used by tests + on graceful shutdown.
64
+ * Returns the underlying promise so callers can await it.
65
+ */
66
+ async flush() {
67
+ await this.writeQueue;
68
+ }
69
+ /** If the file is over the rotation threshold, rotate it. */
70
+ maybeRotate() {
71
+ if (!existsSync(this.file))
72
+ return;
73
+ try {
74
+ const stat = statSync(this.file);
75
+ if (stat.size >= METRICS_ROTATE_BYTES) {
76
+ const rotated = this.file.replace(/\.jsonl$/, '.1.jsonl');
77
+ renameSync(this.file, rotated);
78
+ }
79
+ }
80
+ catch {
81
+ // Best-effort.
82
+ }
83
+ }
84
+ }
85
+ /** Convenience helper for tests: read all records from a metrics file. */
86
+ export async function readMetrics(file) {
87
+ let raw = '';
88
+ try {
89
+ raw = await fs.readFile(file, 'utf-8');
90
+ }
91
+ catch {
92
+ return [];
93
+ }
94
+ const out = [];
95
+ for (const line of raw.split('\n')) {
96
+ if (!line.trim())
97
+ continue;
98
+ try {
99
+ out.push(JSON.parse(line));
100
+ }
101
+ catch {
102
+ // Skip malformed lines.
103
+ }
104
+ }
105
+ return out;
106
+ }
107
+ /**
108
+ * Process-wide singleton MetricsLogger (Task G.3.3). Multiple consumers
109
+ * (the chat session in `app.tsx`, the shutdown handler in `main.ts`)
110
+ * need to coordinate on a single writer so SIGINT/SIGTERM can flush
111
+ * the pending fire-and-forget queue before `process.exit(0)` discards
112
+ * anything still in memory.
113
+ *
114
+ * Lazy-instantiated on first access. Respects the same env override as
115
+ * the constructor (`ANATHEMA_METRICS_FILE`), so tests can point it at
116
+ * a temp file before any consumer imports it.
117
+ *
118
+ * @see docs/plans/2026-06-29-anathema-coder-v3-G.md (Task G.3.3)
119
+ */
120
+ let _singleton = null;
121
+ export function getMetricsLogger() {
122
+ if (!_singleton) {
123
+ _singleton = new MetricsLogger();
124
+ }
125
+ return _singleton;
126
+ }
127
+ /** Reset the singleton (test-only). Forces a fresh instance on next getMetricsLogger(). */
128
+ export function resetMetricsLogger() {
129
+ _singleton = null;
130
+ }
131
+ //# sourceMappingURL=metrics.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"metrics.js","sourceRoot":"","sources":["../../src/cli/metrics.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,234 @@
1
+ /**
2
+ * modelDiscovery — auto-discover available models for each provider.
3
+ *
4
+ * Triggers:
5
+ * - After `/login <provider> <key>` succeeds (keyStore write)
6
+ * - After `/login grok` (OAuth) succeeds
7
+ * - On user request via `/model refresh` or `/models refresh`
8
+ * - On startup if cache is older than `staleAfterMs` (default 6h)
9
+ *
10
+ * Why dynamic discovery? Providers (xAI, Z.AI, MiniMax, OpenAI) add/remove
11
+ * models frequently — hardcoding a list in `providerDefaults` makes the user
12
+ * see stale choices. After auth we hit the provider's `/v1/models` endpoint
13
+ * and cache the IDs in `~/.tmp/anathema-coder/models.json`.
14
+ *
15
+ * Pure node:fs + fetch — no Electron deps, browser-importable for jsdom tests.
16
+ * Env override: `ANATHEMA_MODELS_FILE` (useful for tests + CI).
17
+ *
18
+ * @see docs/plans/2026-06-30-anathema-coder-v3-U.md
19
+ */
20
+ import { promises as fs, existsSync, readFileSync } from 'node:fs';
21
+ import { homedir } from 'node:os';
22
+ import path from 'node:path';
23
+ // ---------------------------------------------------------------------------
24
+ // Provider endpoint map
25
+ // ---------------------------------------------------------------------------
26
+ /** Default base URL per provider for `/v1/models` discovery. */
27
+ const PROVIDER_BASE_URLS = {
28
+ 'grok': 'https://api.x.ai/v1',
29
+ 'glm': 'https://api.z.ai/api/paas/v4',
30
+ 'minimax': 'https://api.minimaxi.chat/v1',
31
+ 'openai-compatible': 'https://api.openai.com/v1',
32
+ };
33
+ // ---------------------------------------------------------------------------
34
+ // Cache file location
35
+ // ---------------------------------------------------------------------------
36
+ function defaultModelsFilePath() {
37
+ return process.env.ANATHEMA_MODELS_FILE
38
+ ?? path.join(homedir(), '.tmp', 'anathema-coder', 'models.json');
39
+ }
40
+ export function getModelsFilePath() {
41
+ return defaultModelsFilePath();
42
+ }
43
+ // ---------------------------------------------------------------------------
44
+ // Sync read (used at app start)
45
+ // ---------------------------------------------------------------------------
46
+ /** Read the on-disk models registry. Returns empty object if file missing/corrupt. */
47
+ export function loadModelsRegistry(file = getModelsFilePath()) {
48
+ if (!existsSync(file))
49
+ return {};
50
+ try {
51
+ const raw = readFileSync(file, 'utf-8');
52
+ const parsed = JSON.parse(raw);
53
+ if (!parsed || typeof parsed !== 'object')
54
+ return {};
55
+ return parsed;
56
+ }
57
+ catch {
58
+ return {};
59
+ }
60
+ }
61
+ /** Read cached models for a single provider. Returns undefined if not cached. */
62
+ export function getCachedModels(provider, file = getModelsFilePath()) {
63
+ const registry = loadModelsRegistry(file);
64
+ return registry[provider];
65
+ }
66
+ /** Returns true if the cache exists and is older than `maxAgeMs`. */
67
+ export function isModelsCacheStale(provider, maxAgeMs = 6 * 60 * 60 * 1000, // 6 hours
68
+ file = getModelsFilePath(), now = Date.now()) {
69
+ const entry = getCachedModels(provider, file);
70
+ if (!entry)
71
+ return true;
72
+ return (now - entry.fetchedAt) > maxAgeMs;
73
+ }
74
+ // ---------------------------------------------------------------------------
75
+ // Async write (atomic: write to .tmp, rename) — serialized via in-memory mutex
76
+ // to avoid concurrent discoveries clobbering each other (race on rename).
77
+ // Also wraps the read-modify-write cycle so two parallel calls don't both
78
+ // observe an empty registry and then clobber each other.
79
+ // ---------------------------------------------------------------------------
80
+ let writeChain = Promise.resolve();
81
+ async function withRegistryLock(fn) {
82
+ const next = writeChain.then(async () => fn());
83
+ writeChain = next.catch(() => undefined);
84
+ return next;
85
+ }
86
+ async function readModifyWriteRegistry(mutator, file = getModelsFilePath()) {
87
+ return withRegistryLock(async () => {
88
+ // Read latest on-disk (other concurrent calls may have written since
89
+ // we last read).
90
+ const onDisk = loadModelsRegistry(file);
91
+ const merged = mutator(onDisk);
92
+ await fs.mkdir(path.dirname(file), { recursive: true });
93
+ const tmp = `${file}.tmp-${process.pid}-${Date.now()}`;
94
+ await fs.writeFile(tmp, JSON.stringify(merged, null, 2), 'utf-8');
95
+ await fs.rename(tmp, file);
96
+ return merged;
97
+ });
98
+ }
99
+ // ---------------------------------------------------------------------------
100
+ // Auth resolution
101
+ // ---------------------------------------------------------------------------
102
+ /** Synchronous auth resolution — reads keyStore via dynamic import to keep this module browser-friendly. */
103
+ async function resolveAuthToken(provider, options) {
104
+ if (options.authToken)
105
+ return options.authToken;
106
+ // Late import: keyStore uses node:fs/promises, no Electron deps.
107
+ const { resolveApiKeyWithMeta, getOAuthToken } = await import('./keyStore.js');
108
+ if (provider === 'grok') {
109
+ const oauth = getOAuthToken('grok');
110
+ if (oauth?.apiKey)
111
+ return oauth.apiKey;
112
+ }
113
+ const resolved = await resolveApiKeyWithMeta(provider);
114
+ return resolved?.apiKey;
115
+ }
116
+ function parseOpenAIModelsResponse(json, baseUrl) {
117
+ if (!json || typeof json !== 'object')
118
+ return [];
119
+ const obj = json;
120
+ if (!Array.isArray(obj.data))
121
+ return [];
122
+ const models = [];
123
+ for (const m of obj.data) {
124
+ if (!m || typeof m.id !== 'string')
125
+ continue;
126
+ const out = { id: m.id };
127
+ if (typeof m.created === 'number')
128
+ out.created = m.created;
129
+ if (typeof m.owned_by === 'string')
130
+ out.ownedBy = m.owned_by;
131
+ // Optional context length (some providers include it as extra field)
132
+ const ctx = m.context_length;
133
+ if (typeof ctx === 'number')
134
+ out.contextLength = ctx;
135
+ models.push(out);
136
+ }
137
+ models.sort((a, b) => a.id.localeCompare(b.id));
138
+ return models;
139
+ }
140
+ // ---------------------------------------------------------------------------
141
+ // Main: discoverModelsForProvider
142
+ // ---------------------------------------------------------------------------
143
+ /**
144
+ * Fetch available models for a provider and persist the result to disk.
145
+ * Throws on network errors but never corrupts the on-disk cache.
146
+ */
147
+ export async function discoverModelsForProvider(provider, options = {}) {
148
+ const baseUrl = options.baseUrl ?? PROVIDER_BASE_URLS[provider];
149
+ const url = `${baseUrl.replace(/\/$/, '')}/models`;
150
+ const authToken = await resolveAuthToken(provider, options);
151
+ const fetchImpl = options.fetchImpl ?? fetch;
152
+ let response;
153
+ try {
154
+ const headers = { Accept: 'application/json' };
155
+ if (authToken)
156
+ headers.Authorization = `Bearer ${authToken}`;
157
+ response = await fetchImpl(url, { method: 'GET', headers });
158
+ }
159
+ catch (err) {
160
+ throw new ModelDiscoveryError(`Network error contacting ${url}: ${err instanceof Error ? err.message : String(err)}`, 'network_error');
161
+ }
162
+ if (!response.ok) {
163
+ const body = await response.text().catch(() => '');
164
+ throw new ModelDiscoveryError(`HTTP ${response.status} from ${url}: ${body.slice(0, 200)}`, `http_${response.status}`);
165
+ }
166
+ let json;
167
+ try {
168
+ json = await response.json();
169
+ }
170
+ catch (err) {
171
+ throw new ModelDiscoveryError(`Invalid JSON from ${url}: ${err instanceof Error ? err.message : String(err)}`, 'invalid_json');
172
+ }
173
+ const models = parseOpenAIModelsResponse(json, baseUrl);
174
+ if (models.length === 0) {
175
+ throw new ModelDiscoveryError(`Provider ${provider} returned 0 models — refusing to overwrite cache`, 'empty_response');
176
+ }
177
+ const entry = {
178
+ models,
179
+ fetchedAt: Date.now(),
180
+ baseUrl,
181
+ };
182
+ if (!options.skipCacheWrite) {
183
+ const file = getModelsFilePath();
184
+ await readModifyWriteRegistry((current) => {
185
+ current[provider] = entry;
186
+ return current;
187
+ }, file);
188
+ }
189
+ return entry;
190
+ }
191
+ /**
192
+ * Fire-and-forget variant: kicks off discovery in the background, logs errors
193
+ * to the `onError` callback instead of throwing. Used by `/login` handlers
194
+ * that must not block the user response on a slow `/v1/models` call.
195
+ */
196
+ export function discoverModelsInBackground(provider, options = {}) {
197
+ discoverModelsForProvider(provider, options).catch((err) => {
198
+ if (err instanceof ModelDiscoveryError && options.onError) {
199
+ options.onError(err);
200
+ }
201
+ });
202
+ }
203
+ // ---------------------------------------------------------------------------
204
+ // Error
205
+ // ---------------------------------------------------------------------------
206
+ export class ModelDiscoveryError extends Error {
207
+ code;
208
+ constructor(message, code) {
209
+ super(message);
210
+ this.code = code;
211
+ this.name = 'ModelDiscoveryError';
212
+ }
213
+ }
214
+ // ---------------------------------------------------------------------------
215
+ // Suggestion helper: filter /model tab-completion against discovered models
216
+ // ---------------------------------------------------------------------------
217
+ /** Returns the discovered model ids for a provider, or undefined if no cache. */
218
+ export function getDiscoveredModelIds(provider, file = getModelsFilePath()) {
219
+ const entry = getCachedModels(provider, file);
220
+ return entry?.models.map((m) => m.id);
221
+ }
222
+ /** Returns the "best" default model id: discovered (most recent) → providerDefaults fallback. */
223
+ export function pickDefaultModel(provider, providerDefaults, file = getModelsFilePath()) {
224
+ const cached = getCachedModels(provider, file);
225
+ if (cached && cached.models.length > 0) {
226
+ // Prefer the first model alphabetically that contains "latest" or matches the default name
227
+ const preferred = cached.models.find((m) => /latest|default|chat/i.test(m.id));
228
+ if (preferred)
229
+ return preferred.id;
230
+ return cached.models[0]?.id;
231
+ }
232
+ return providerDefaults[provider];
233
+ }
234
+ //# sourceMappingURL=modelDiscovery.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"modelDiscovery.js","sourceRoot":"","sources":["../../src/cli/modelDiscovery.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAEH,OAAO,EAAE,QAAQ,IAAI,EAAE,EAAE,UAAU,EAAE,YAAY,EAA4B,MAAM,SAAS,CAAC;AAC7F,OAAO,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AAClC,OAAO,IAAI,MAAM,WAAW,CAAC;AAkD7B,8EAA8E;AAC9E,wBAAwB;AACxB,8EAA8E;AAE9E,gEAAgE;AAChE,MAAM,kBAAkB,GAA+B;IACrD,MAAM,EAAE,qBAAqB;IAC7B,KAAK,EAAE,8BAA8B;IACrC,SAAS,EAAE,8BAA8B;IACzC,mBAAmB,EAAE,2BAA2B;CACjD,CAAC;AAEF,8EAA8E;AAC9E,sBAAsB;AACtB,8EAA8E;AAE9E,SAAS,qBAAqB;IAC5B,OAAO,OAAO,CAAC,GAAG,CAAC,oBAAoB;WAClC,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,EAAE,MAAM,EAAE,gBAAgB,EAAE,aAAa,CAAC,CAAC;AACrE,CAAC;AAED,MAAM,UAAU,iBAAiB;IAC/B,OAAO,qBAAqB,EAAE,CAAC;AACjC,CAAC;AAED,8EAA8E;AAC9E,gCAAgC;AAChC,8EAA8E;AAE9E,sFAAsF;AACtF,MAAM,UAAU,kBAAkB,CAAC,OAAe,iBAAiB,EAAE;IACnE,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC;QAAE,OAAO,EAAE,CAAC;IACjC,IAAI,CAAC;QACH,MAAM,GAAG,GAAG,YAAY,CAAC,IAAI,EAAE,OAAO,CAAC,CAAC;QACxC,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;QAC/B,IAAI,CAAC,MAAM,IAAI,OAAO,MAAM,KAAK,QAAQ;YAAE,OAAO,EAAE,CAAC;QACrD,OAAO,MAAwB,CAAC;IAClC,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,EAAE,CAAC;IACZ,CAAC;AACH,CAAC;AAED,iFAAiF;AACjF,MAAM,UAAU,eAAe,CAC7B,QAAoB,EACpB,OAAe,iBAAiB,EAAE;IAElC,MAAM,QAAQ,GAAG,kBAAkB,CAAC,IAAI,CAAC,CAAC;IAC1C,OAAO,QAAQ,CAAC,QAAQ,CAAC,CAAC;AAC5B,CAAC;AAED,qEAAqE;AACrE,MAAM,UAAU,kBAAkB,CAChC,QAAoB,EACpB,WAAmB,CAAC,GAAG,EAAE,GAAG,EAAE,GAAG,IAAI,EAAE,UAAU;AACjD,OAAe,iBAAiB,EAAE,EAClC,MAAc,IAAI,CAAC,GAAG,EAAE;IAExB,MAAM,KAAK,GAAG,eAAe,CAAC,QAAQ,EAAE,IAAI,CAAC,CAAC;IAC9C,IAAI,CAAC,KAAK;QAAE,OAAO,IAAI,CAAC;IACxB,OAAO,CAAC,GAAG,GAAG,KAAK,CAAC,SAAS,CAAC,GAAG,QAAQ,CAAC;AAC5C,CAAC;AAED,8EAA8E;AAC9E,+EAA+E;AAC/E,0EAA0E;AAC1E,0EAA0E;AAC1E,yDAAyD;AACzD,8EAA8E;AAE9E,IAAI,UAAU,GAAqB,OAAO,CAAC,OAAO,EAAE,CAAC;AAErD,KAAK,UAAU,gBAAgB,CAAI,EAAoB;IACrD,MAAM,IAAI,GAAG,UAAU,CAAC,IAAI,CAAC,KAAK,IAAI,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC;IAC/C,UAAU,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,SAAS,CAAC,CAAC;IACzC,OAAO,IAAI,CAAC;AACd,CAAC;AAED,KAAK,UAAU,uBAAuB,CACpC,OAAoD,EACpD,OAAe,iBAAiB,EAAE;IAElC,OAAO,gBAAgB,CAAC,KAAK,IAAI,EAAE;QACjC,qEAAqE;QACrE,iBAAiB;QACjB,MAAM,MAAM,GAAG,kBAAkB,CAAC,IAAI,CAAC,CAAC;QACxC,MAAM,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;QAC/B,MAAM,EAAE,CAAC,KAAK,CAAC,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;QACxD,MAAM,GAAG,GAAG,GAAG,IAAI,QAAQ,OAAO,CAAC,GAAG,IAAI,IAAI,CAAC,GAAG,EAAE,EAAE,CAAC;QACvD,MAAM,EAAE,CAAC,SAAS,CAAC,GAAG,EAAE,IAAI,CAAC,SAAS,CAAC,MAAM,EAAE,IAAI,EAAE,CAAC,CAAC,EAAE,OAAO,CAAC,CAAC;QAClE,MAAM,EAAE,CAAC,MAAM,CAAC,GAAG,EAAE,IAAI,CAAC,CAAC;QAC3B,OAAO,MAAM,CAAC;IAChB,CAAC,CAAC,CAAC;AACL,CAAC;AAED,8EAA8E;AAC9E,kBAAkB;AAClB,8EAA8E;AAE9E,4GAA4G;AAC5G,KAAK,UAAU,gBAAgB,CAC7B,QAAoB,EACpB,OAA+B;IAE/B,IAAI,OAAO,CAAC,SAAS;QAAE,OAAO,OAAO,CAAC,SAAS,CAAC;IAChD,iEAAiE;IACjE,MAAM,EAAE,qBAAqB,EAAE,aAAa,EAAE,GAAG,MAAM,MAAM,CAAC,eAAe,CAAC,CAAC;IAC/E,IAAI,QAAQ,KAAK,MAAM,EAAE,CAAC;QACxB,MAAM,KAAK,GAAG,aAAa,CAAC,MAAM,CAAC,CAAC;QACpC,IAAI,KAAK,EAAE,MAAM;YAAE,OAAO,KAAK,CAAC,MAAM,CAAC;IACzC,CAAC;IACD,MAAM,QAAQ,GAAG,MAAM,qBAAqB,CAAC,QAAQ,CAAC,CAAC;IACvD,OAAO,QAAQ,EAAE,MAAM,CAAC;AAC1B,CAAC;AAiBD,SAAS,yBAAyB,CAAC,IAAa,EAAE,OAAe;IAC/D,IAAI,CAAC,IAAI,IAAI,OAAO,IAAI,KAAK,QAAQ;QAAE,OAAO,EAAE,CAAC;IACjD,MAAM,GAAG,GAAG,IAAqC,CAAC;IAClD,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC;QAAE,OAAO,EAAE,CAAC;IACxC,MAAM,MAAM,GAAsB,EAAE,CAAC;IACrC,KAAK,MAAM,CAAC,IAAI,GAAG,CAAC,IAAI,EAAE,CAAC;QACzB,IAAI,CAAC,CAAC,IAAI,OAAO,CAAC,CAAC,EAAE,KAAK,QAAQ;YAAE,SAAS;QAC7C,MAAM,GAAG,GAAoB,EAAE,EAAE,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC;QAC1C,IAAI,OAAO,CAAC,CAAC,OAAO,KAAK,QAAQ;YAAE,GAAG,CAAC,OAAO,GAAG,CAAC,CAAC,OAAO,CAAC;QAC3D,IAAI,OAAO,CAAC,CAAC,QAAQ,KAAK,QAAQ;YAAE,GAAG,CAAC,OAAO,GAAG,CAAC,CAAC,QAAQ,CAAC;QAC7D,qEAAqE;QACrE,MAAM,GAAG,GAAI,CAA6B,CAAC,cAAc,CAAC;QAC1D,IAAI,OAAO,GAAG,KAAK,QAAQ;YAAE,GAAG,CAAC,aAAa,GAAG,GAAG,CAAC;QACrD,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;IACnB,CAAC;IACD,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,aAAa,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;IAChD,OAAO,MAAM,CAAC;AAChB,CAAC;AAED,8EAA8E;AAC9E,kCAAkC;AAClC,8EAA8E;AAE9E;;;GAGG;AACH,MAAM,CAAC,KAAK,UAAU,yBAAyB,CAC7C,QAAoB,EACpB,UAA2B,EAAE;IAE7B,MAAM,OAAO,GAAG,OAAO,CAAC,OAAO,IAAI,kBAAkB,CAAC,QAAQ,CAAC,CAAC;IAChE,MAAM,GAAG,GAAG,GAAG,OAAO,CAAC,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,SAAS,CAAC;IACnD,MAAM,SAAS,GAAG,MAAM,gBAAgB,CAAC,QAAQ,EAAE,OAAO,CAAC,CAAC;IAE5D,MAAM,SAAS,GAAG,OAAO,CAAC,SAAS,IAAI,KAAK,CAAC;IAC7C,IAAI,QAAkB,CAAC;IACvB,IAAI,CAAC;QACH,MAAM,OAAO,GAA2B,EAAE,MAAM,EAAE,kBAAkB,EAAE,CAAC;QACvE,IAAI,SAAS;YAAE,OAAO,CAAC,aAAa,GAAG,UAAU,SAAS,EAAE,CAAC;QAC7D,QAAQ,GAAG,MAAM,SAAS,CAAC,GAAG,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,EAAE,CAAC,CAAC;IAC9D,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,MAAM,IAAI,mBAAmB,CAC3B,4BAA4B,GAAG,KAAK,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,EAAE,EACtF,eAAe,CAChB,CAAC;IACJ,CAAC;IAED,IAAI,CAAC,QAAQ,CAAC,EAAE,EAAE,CAAC;QACjB,MAAM,IAAI,GAAG,MAAM,QAAQ,CAAC,IAAI,EAAE,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,EAAE,CAAC,CAAC;QACnD,MAAM,IAAI,mBAAmB,CAC3B,QAAQ,QAAQ,CAAC,MAAM,SAAS,GAAG,KAAK,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,CAAC,EAAE,EAC5D,QAAQ,QAAQ,CAAC,MAAM,EAAE,CAC1B,CAAC;IACJ,CAAC;IAED,IAAI,IAAa,CAAC;IAClB,IAAI,CAAC;QACH,IAAI,GAAG,MAAM,QAAQ,CAAC,IAAI,EAAE,CAAC;IAC/B,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,MAAM,IAAI,mBAAmB,CAC3B,qBAAqB,GAAG,KAAK,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,EAAE,EAC/E,cAAc,CACf,CAAC;IACJ,CAAC;IAED,MAAM,MAAM,GAAG,yBAAyB,CAAC,IAAI,EAAE,OAAO,CAAC,CAAC;IACxD,IAAI,MAAM,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACxB,MAAM,IAAI,mBAAmB,CAC3B,YAAY,QAAQ,kDAAkD,EACtE,gBAAgB,CACjB,CAAC;IACJ,CAAC;IAED,MAAM,KAAK,GAAwB;QACjC,MAAM;QACN,SAAS,EAAE,IAAI,CAAC,GAAG,EAAE;QACrB,OAAO;KACR,CAAC;IAEF,IAAI,CAAC,OAAO,CAAC,cAAc,EAAE,CAAC;QAC5B,MAAM,IAAI,GAAG,iBAAiB,EAAE,CAAC;QACjC,MAAM,uBAAuB,CAAC,CAAC,OAAO,EAAE,EAAE;YACxC,OAAO,CAAC,QAAQ,CAAC,GAAG,KAAK,CAAC;YAC1B,OAAO,OAAO,CAAC;QACjB,CAAC,EAAE,IAAI,CAAC,CAAC;IACX,CAAC;IAED,OAAO,KAAK,CAAC;AACf,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,0BAA0B,CACxC,QAAoB,EACpB,UAA8E,EAAE;IAEhF,yBAAyB,CAAC,QAAQ,EAAE,OAAO,CAAC,CAAC,KAAK,CAAC,CAAC,GAAG,EAAE,EAAE;QACzD,IAAI,GAAG,YAAY,mBAAmB,IAAI,OAAO,CAAC,OAAO,EAAE,CAAC;YAC1D,OAAO,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;QACvB,CAAC;IACH,CAAC,CAAC,CAAC;AACL,CAAC;AAED,8EAA8E;AAC9E,QAAQ;AACR,8EAA8E;AAE9E,MAAM,OAAO,mBAAoB,SAAQ,KAAK;IACC;IAA7C,YAAY,OAAe,EAAkB,IAAY;QACvD,KAAK,CAAC,OAAO,CAAC,CAAC;QAD4B,SAAI,GAAJ,IAAI,CAAQ;QAEvD,IAAI,CAAC,IAAI,GAAG,qBAAqB,CAAC;IACpC,CAAC;CACF;AAED,8EAA8E;AAC9E,4EAA4E;AAC5E,8EAA8E;AAE9E,iFAAiF;AACjF,MAAM,UAAU,qBAAqB,CACnC,QAAoB,EACpB,OAAe,iBAAiB,EAAE;IAElC,MAAM,KAAK,GAAG,eAAe,CAAC,QAAQ,EAAE,IAAI,CAAC,CAAC;IAC9C,OAAO,KAAK,EAAE,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC;AACxC,CAAC;AAED,iGAAiG;AACjG,MAAM,UAAU,gBAAgB,CAC9B,QAAoB,EACpB,gBAAqD,EACrD,OAAe,iBAAiB,EAAE;IAElC,MAAM,MAAM,GAAG,eAAe,CAAC,QAAQ,EAAE,IAAI,CAAC,CAAC;IAC/C,IAAI,MAAM,IAAI,MAAM,CAAC,MAAM,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QACvC,2FAA2F;QAC3F,MAAM,SAAS,GAAG,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,sBAAsB,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;QAC/E,IAAI,SAAS;YAAE,OAAO,SAAS,CAAC,EAAE,CAAC;QACnC,OAAO,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC;IAC9B,CAAC;IACD,OAAO,gBAAgB,CAAC,QAAQ,CAAC,CAAC;AACpC,CAAC"}
@@ -0,0 +1,112 @@
1
+ /**
2
+ * modelPricing — USD cost calculator for LLM tokens.
3
+ *
4
+ * Pricing table covers the providers wired in `keyStore.PROVIDERS`. Costs
5
+ * are in **USD per 1M tokens** (industry standard). Numbers are conservative
6
+ * defaults — the user can override per-model via env vars (see below).
7
+ *
8
+ * Env overrides (1M-token rate):
9
+ * - ANATHEMA_PRICE_GROK4
10
+ * - ANATHEMA_PRICE_GROK4_FAST
11
+ * - ANATHEMA_PRICE_GLM46
12
+ * - ANATHEMA_PRICE_MINIMAX_M25
13
+ * - ANATHEMA_PRICE_DEFAULT (fallback for unknown models)
14
+ *
15
+ * Costs returned are precise to 6 decimals. Round to cents at the UI layer.
16
+ */
17
+ /** Pricing table (USD per 1M tokens) — conservative public list prices. */
18
+ const PRICES_PER_MILLION = {
19
+ // xAI Grok
20
+ 'grok-4': { input: 3, output: 15 },
21
+ 'grok-4-fast': { input: 0.20, output: 0.50 },
22
+ 'grok-3': { input: 3, output: 15 },
23
+ 'grok-3-mini': { input: 0.30, output: 0.50 },
24
+ 'grok-2-vision': { input: 2, output: 10 },
25
+ // GLM / Z.AI
26
+ 'glm-4.6': { input: 0.60, output: 2.20 },
27
+ 'glm-4.5': { input: 0.50, output: 2.00 },
28
+ 'glm-4.5-air': { input: 0.10, output: 0.60 },
29
+ 'glm-z1': { input: 0.50, output: 2.00 },
30
+ // MiniMax
31
+ 'MiniMax-M2.5': { input: 0.20, output: 1.10 },
32
+ 'MiniMax-M2': { input: 0.20, output: 1.10 },
33
+ 'MiniMax-M2-her': { input: 0.30, output: 1.20 },
34
+ // OpenAI (for openai-compatible fallback)
35
+ 'gpt-4o': { input: 2.50, output: 10 },
36
+ 'gpt-4o-mini': { input: 0.15, output: 0.60 },
37
+ 'gpt-4-turbo': { input: 10, output: 30 },
38
+ 'o1-preview': { input: 15, output: 60 },
39
+ 'o1-mini': { input: 3, output: 12 },
40
+ };
41
+ /** Default rate for unknown models — assume a mid-tier provider. */
42
+ const DEFAULT_RATE = { input: 1.0, output: 3.0 };
43
+ /** Read an env-var override for a given model, if present. */
44
+ function envOverride(model) {
45
+ const key = `ANATHEMA_PRICE_${model.toUpperCase().replace(/[^A-Z0-9]/g, '_')}`;
46
+ const raw = process.env[key] ?? process.env.ANATHEMA_PRICE_DEFAULT;
47
+ return parseRateOverride(raw);
48
+ }
49
+ /**
50
+ * Parse a price-override string in the format `INPUT[/OUTPUT]`.
51
+ * Exported for testing (env-var-based tests are subject to platform
52
+ * content filters that rewrite numeric literals in test files).
53
+ *
54
+ * @returns null if `raw` is undefined/empty or if either number is invalid.
55
+ */
56
+ export function parseRateOverride(raw) {
57
+ if (!raw)
58
+ return null;
59
+ const [inStr, outStr] = raw.split('/');
60
+ const input = Number.parseFloat(inStr);
61
+ const output = outStr !== undefined ? Number.parseFloat(outStr) : input;
62
+ if (!Number.isFinite(input) || !Number.isFinite(output))
63
+ return null;
64
+ return { input, output };
65
+ }
66
+ /** Look up pricing for a model. Falls back to DEFAULT_RATE. */
67
+ export function getModelRate(model) {
68
+ if (!model)
69
+ return DEFAULT_RATE;
70
+ return envOverride(model)
71
+ ?? PRICES_PER_MILLION[model]
72
+ ?? DEFAULT_RATE;
73
+ }
74
+ /**
75
+ * Compute the USD cost for a given number of prompt + completion tokens.
76
+ * Rates are per 1M tokens, so we divide the token count by 1_000_000.
77
+ *
78
+ * @returns cost in USD (0 if either token count is negative or NaN).
79
+ */
80
+ export function calculateCost(model, promptTokens, completionTokens) {
81
+ if (!Number.isFinite(promptTokens) || promptTokens < 0)
82
+ return 0;
83
+ if (!Number.isFinite(completionTokens) || completionTokens < 0)
84
+ return 0;
85
+ const rate = getModelRate(model);
86
+ const inputCost = (promptTokens / 1_000_000) * rate.input;
87
+ const outputCost = (completionTokens / 1_000_000) * rate.output;
88
+ return Number((inputCost + outputCost).toFixed(6));
89
+ }
90
+ /** Format a USD cost as `$0.0234` style (4 decimals). */
91
+ export function formatCost(usd) {
92
+ if (!Number.isFinite(usd) || usd < 0)
93
+ return '$0.0000';
94
+ if (usd === 0)
95
+ return '$0.0000';
96
+ if (usd < 0.0001)
97
+ return '<$0.0001';
98
+ return `$${usd.toFixed(4)}`;
99
+ }
100
+ /** Format a token count compactly: `1.2k`, `3.4M`, etc. */
101
+ export function formatTokens(tokens) {
102
+ if (!Number.isFinite(tokens) || tokens < 0)
103
+ return '0';
104
+ if (tokens < 1_000)
105
+ return `${tokens}`;
106
+ if (tokens < 1_000_000)
107
+ return `${(tokens / 1_000).toFixed(1)}k`;
108
+ if (tokens < 1_000_000_000)
109
+ return `${(tokens / 1_000_000).toFixed(2)}M`;
110
+ return `${(tokens / 1_000_000_000).toFixed(2)}B`;
111
+ }
112
+ //# sourceMappingURL=modelPricing.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"modelPricing.js","sourceRoot":"","sources":["../../src/cli/modelPricing.ts"],"names":[],"mappings":"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"}