@offerpilot/axiomruntime 0.0.1

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 (99) hide show
  1. package/README.md +185 -0
  2. package/dist/cli/commands/add.js +29 -0
  3. package/dist/cli/commands/context.js +29 -0
  4. package/dist/cli/commands/doctor.js +62 -0
  5. package/dist/cli/commands/edit.js +85 -0
  6. package/dist/cli/commands/help.js +16 -0
  7. package/dist/cli/commands/list.js +25 -0
  8. package/dist/cli/commands/log.js +63 -0
  9. package/dist/cli/commands/memory.js +241 -0
  10. package/dist/cli/commands/report.js +35 -0
  11. package/dist/cli/commands/session.js +74 -0
  12. package/dist/cli/commands/setup.js +86 -0
  13. package/dist/cli/commands/status.js +50 -0
  14. package/dist/cli/commands/telegram.js +701 -0
  15. package/dist/cli/commands/use.js +108 -0
  16. package/dist/cli/commands/version.js +12 -0
  17. package/dist/cli/index.js +276 -0
  18. package/dist/cli/output/table.js +22 -0
  19. package/dist/cli/prompts/prompt.js +37 -0
  20. package/dist/cli/registry.js +16 -0
  21. package/dist/core/config/cache-store.js +193 -0
  22. package/dist/core/config/json-store.js +114 -0
  23. package/dist/core/config/paths.js +85 -0
  24. package/dist/core/config/providers-store.js +89 -0
  25. package/dist/core/config/schema.js +60 -0
  26. package/dist/core/config/session-store.js +30 -0
  27. package/dist/core/config/usage-store.js +18 -0
  28. package/dist/core/context/context-service.js +186 -0
  29. package/dist/core/integrations/integration-state.js +105 -0
  30. package/dist/core/logs/log-service.js +56 -0
  31. package/dist/core/memory/embedding-check.js +121 -0
  32. package/dist/core/memory/memory-config.js +122 -0
  33. package/dist/core/models/model-discovery.js +430 -0
  34. package/dist/core/models/model-filter.js +13 -0
  35. package/dist/core/providers/provider-service.js +212 -0
  36. package/dist/core/reports/report-service.js +166 -0
  37. package/dist/core/runner/command-resolver.js +60 -0
  38. package/dist/core/runner/engine-registry.js +93 -0
  39. package/dist/core/runner/fallback.js +114 -0
  40. package/dist/core/runner/openai-usage-http.js +82 -0
  41. package/dist/core/runner/openai-usage-proxy.js +1 -0
  42. package/dist/core/runner/openai-usage-recording.js +172 -0
  43. package/dist/core/runner/openai-usage-responses.js +469 -0
  44. package/dist/core/runner/openai-usage-server.js +319 -0
  45. package/dist/core/runner/openai-usage-types.js +1 -0
  46. package/dist/core/runner/tool-runner.js +138 -0
  47. package/dist/core/sessions/session-service.js +47 -0
  48. package/dist/core/status/doctor-service.js +391 -0
  49. package/dist/core/status/status-service.js +60 -0
  50. package/dist/core/types.js +1 -0
  51. package/dist/core/usage/pricing.js +113 -0
  52. package/dist/core/usage/usage-service.js +30 -0
  53. package/dist/core/utils/is-record.js +3 -0
  54. package/dist/server/index.js +28 -0
  55. package/dist/server/runtime-server.js +430 -0
  56. package/dist/telegram/bot-registry.js +80 -0
  57. package/dist/telegram/bot.js +128 -0
  58. package/dist/telegram/config.js +235 -0
  59. package/dist/telegram/engine/claude-engine.js +240 -0
  60. package/dist/telegram/engine/codex-engine.js +437 -0
  61. package/dist/telegram/engine/engine-utils.js +67 -0
  62. package/dist/telegram/engine/process-utils.js +132 -0
  63. package/dist/telegram/engine/registry.js +31 -0
  64. package/dist/telegram/engine/types.js +1 -0
  65. package/dist/telegram/handler-registry.js +28 -0
  66. package/dist/telegram/handlers/callback.js +311 -0
  67. package/dist/telegram/handlers/command.js +272 -0
  68. package/dist/telegram/handlers/document.js +108 -0
  69. package/dist/telegram/handlers/memory.js +305 -0
  70. package/dist/telegram/handlers/message.js +701 -0
  71. package/dist/telegram/handlers/provider.js +332 -0
  72. package/dist/telegram/handlers/setup.js +527 -0
  73. package/dist/telegram/handlers/usage.js +124 -0
  74. package/dist/telegram/index.js +93 -0
  75. package/dist/telegram/interaction/approval.js +108 -0
  76. package/dist/telegram/interaction/command-menu.js +253 -0
  77. package/dist/telegram/interaction/formatter.js +487 -0
  78. package/dist/telegram/interaction/keyboards.js +145 -0
  79. package/dist/telegram/interaction/progress-reporter.js +160 -0
  80. package/dist/telegram/interaction/prompt-middleware.js +168 -0
  81. package/dist/telegram/interaction/result-store.js +41 -0
  82. package/dist/telegram/interaction/token-budget.js +21 -0
  83. package/dist/telegram/interaction/tool-name.js +41 -0
  84. package/dist/telegram/lifecycle-registry.js +47 -0
  85. package/dist/telegram/log.js +46 -0
  86. package/dist/telegram/memory/memory-inject.js +52 -0
  87. package/dist/telegram/memory/memory-service.js +413 -0
  88. package/dist/telegram/memory/memory-store.js +216 -0
  89. package/dist/telegram/memory/types.js +1 -0
  90. package/dist/telegram/network-retry.js +22 -0
  91. package/dist/telegram/network.js +53 -0
  92. package/dist/telegram/session/manager.js +229 -0
  93. package/dist/telegram/session/store.js +363 -0
  94. package/dist/telegram/session/types.js +1 -0
  95. package/dist/telegram/supervisor.js +57 -0
  96. package/dist/telegram/templates/messages.js +1 -0
  97. package/docs/README.md +98 -0
  98. package/docs/USAGE.html +853 -0
  99. package/package.json +57 -0
@@ -0,0 +1,391 @@
1
+ import fs from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { getCachePath, getConfigRoot, getProvidersPath, getSessionsPath, getUsagePath } from "../config/paths.js";
4
+ import { readCache } from "../config/cache-store.js";
5
+ import { writeJsonFile } from "../config/json-store.js";
6
+ import { appendUsageEvent } from "../config/usage-store.js";
7
+ import { readProviders } from "../config/providers-store.js";
8
+ import { checkProvider, runDeepCheck } from "../models/model-discovery.js";
9
+ import { writeLog } from "../logs/log-service.js";
10
+ import { getKnownCandidates, resolveToolCommand } from "../runner/command-resolver.js";
11
+ import { resolveProviderCandidatesForTool } from "../runner/fallback.js";
12
+ import { buildUsageEvent } from "../usage/usage-service.js";
13
+ const MIN_NODE_VERSION = "22.19.0";
14
+ export async function runDoctor(options = {}) {
15
+ const items = [];
16
+ const fix = options.fix ?? false;
17
+ const checkProviders = options.checkProviders ?? true;
18
+ checkNodeVersion(items);
19
+ if (options.checkNativeDependencies ?? true) {
20
+ await checkNativeDependencies(items);
21
+ }
22
+ await checkConfigRoot(items, fix);
23
+ await checkRuntimeFiles(items, fix);
24
+ await checkProvidersAndModels(items, checkProviders);
25
+ checkCommand("Local CLIs", "claude", items);
26
+ checkCommand("Local CLIs", "codex", items);
27
+ return items;
28
+ }
29
+ export async function runDeepDoctor(tool = "codex", options = {}) {
30
+ const items = await runDoctor(options);
31
+ try {
32
+ const candidates = await resolveProviderCandidatesForTool(tool);
33
+ const success = await runDeepCandidates(tool, candidates, items);
34
+ if (!success) {
35
+ items.push({
36
+ group: "Deep",
37
+ status: "error",
38
+ message: "No provider passed deep diagnostics.",
39
+ suggestion: "Check the provider URL, API key, model, quota, and upstream service status."
40
+ });
41
+ }
42
+ }
43
+ catch (error) {
44
+ items.push({
45
+ group: "Deep",
46
+ status: "warn",
47
+ message: `No usable provider for the ${tool} deep check: ${formatError(error)}`,
48
+ suggestion: "Run `ai setup` to configure a provider, then retry the deep check."
49
+ });
50
+ }
51
+ return items;
52
+ }
53
+ function checkNodeVersion(items) {
54
+ const current = process.versions.node;
55
+ if (compareVersions(current, MIN_NODE_VERSION) >= 0) {
56
+ items.push({ group: "Installation", status: "ok", message: `Node.js ${current} (required: >=${MIN_NODE_VERSION}).` });
57
+ return;
58
+ }
59
+ items.push({
60
+ group: "Installation",
61
+ status: "error",
62
+ message: `Node.js ${current} is too old (required: >=${MIN_NODE_VERSION}).`,
63
+ suggestion: "Upgrade Node.js, then reinstall `@offerpilot/axiomruntime`."
64
+ });
65
+ }
66
+ async function checkNativeDependencies(items) {
67
+ try {
68
+ await import("better-sqlite3");
69
+ items.push({ group: "Installation", status: "ok", message: "Native SQLite dependency loaded successfully." });
70
+ }
71
+ catch (error) {
72
+ items.push({
73
+ group: "Installation",
74
+ status: "error",
75
+ message: `Native SQLite dependency failed to load: ${formatError(error)}`,
76
+ suggestion: "Reinstall with `npm install --global @offerpilot/axiomruntime`, then run `ai doctor` again."
77
+ });
78
+ }
79
+ }
80
+ async function checkConfigRoot(items, fix) {
81
+ const configRoot = getConfigRoot();
82
+ try {
83
+ await fs.access(configRoot, fs.constants.R_OK | fs.constants.W_OK);
84
+ items.push({ group: "Runtime Home", status: "ok", message: `${configRoot} is readable and writable.` });
85
+ }
86
+ catch (error) {
87
+ if (error.code === "ENOENT" && fix) {
88
+ try {
89
+ await fs.mkdir(configRoot, { recursive: true, mode: 0o700 });
90
+ items.push({ group: "Runtime Home", status: "ok", message: `Created ${configRoot}.`, fixed: true });
91
+ return;
92
+ }
93
+ catch (repairError) {
94
+ items.push({
95
+ group: "Runtime Home",
96
+ status: "error",
97
+ message: `Could not create ${configRoot}: ${formatError(repairError)}`,
98
+ suggestion: "Set `AI_GATEWAY_HOME` to a writable directory and retry."
99
+ });
100
+ return;
101
+ }
102
+ }
103
+ items.push({
104
+ group: "Runtime Home",
105
+ status: error.code === "ENOENT" ? "warn" : "error",
106
+ message: `${configRoot} is not ready: ${formatError(error)}`,
107
+ suggestion: fix
108
+ ? "Set `AI_GATEWAY_HOME` to a writable directory and retry."
109
+ : "Run `ai doctor --fix` or `ai setup` to create it."
110
+ });
111
+ }
112
+ }
113
+ async function checkRuntimeFiles(items, fix) {
114
+ const specs = [
115
+ {
116
+ group: "Providers",
117
+ filePath: getProvidersPath(),
118
+ defaultValue: [],
119
+ validate: Array.isArray,
120
+ repairInvalid: false
121
+ },
122
+ {
123
+ group: "Cache",
124
+ filePath: getCachePath(),
125
+ defaultValue: { providers: {} },
126
+ validate: (value) => isRecord(value) && isRecord(value.providers),
127
+ repairInvalid: true
128
+ },
129
+ {
130
+ group: "Usage",
131
+ filePath: getUsagePath(),
132
+ defaultValue: { events: [] },
133
+ validate: (value) => isRecord(value) && Array.isArray(value.events),
134
+ repairInvalid: true
135
+ },
136
+ {
137
+ group: "Sessions",
138
+ filePath: getSessionsPath(),
139
+ defaultValue: { sessions: [] },
140
+ validate: (value) => isRecord(value) && Array.isArray(value.sessions),
141
+ repairInvalid: true
142
+ }
143
+ ];
144
+ for (const spec of specs) {
145
+ await checkJsonFile(spec, items, fix);
146
+ }
147
+ }
148
+ async function checkJsonFile(spec, items, fix) {
149
+ try {
150
+ await fs.access(spec.filePath, fs.constants.R_OK | fs.constants.W_OK);
151
+ const raw = await fs.readFile(spec.filePath, "utf8");
152
+ const value = JSON.parse(raw);
153
+ if (!spec.validate(value)) {
154
+ throw new Error("unexpected JSON structure");
155
+ }
156
+ items.push({ group: spec.group, status: "ok", message: `${spec.filePath} is valid.` });
157
+ }
158
+ catch (error) {
159
+ const code = error.code;
160
+ if (code === "ENOENT") {
161
+ if (fix) {
162
+ try {
163
+ await writeJsonFile(spec.filePath, spec.defaultValue);
164
+ items.push({ group: spec.group, status: "ok", message: `Created ${spec.filePath}.`, fixed: true });
165
+ }
166
+ catch (repairError) {
167
+ items.push({
168
+ group: spec.group,
169
+ status: "error",
170
+ message: `Could not create ${spec.filePath}: ${formatError(repairError)}`,
171
+ suggestion: "Check the Runtime home directory permissions."
172
+ });
173
+ }
174
+ }
175
+ else {
176
+ items.push({
177
+ group: spec.group,
178
+ status: "warn",
179
+ message: `${spec.filePath} does not exist yet.`,
180
+ suggestion: "Run `ai doctor --fix` or `ai setup` to create it."
181
+ });
182
+ }
183
+ return;
184
+ }
185
+ if (fix && spec.repairInvalid && isRepairableJsonError(error)) {
186
+ try {
187
+ const backupPath = await backupInvalidFile(spec.filePath);
188
+ await writeJsonFile(spec.filePath, spec.defaultValue);
189
+ items.push({
190
+ group: spec.group,
191
+ status: "ok",
192
+ message: `Rebuilt ${spec.filePath}; invalid data was backed up to ${backupPath}.`,
193
+ fixed: true
194
+ });
195
+ return;
196
+ }
197
+ catch (repairError) {
198
+ items.push({
199
+ group: spec.group,
200
+ status: "error",
201
+ message: `Could not repair ${spec.filePath}: ${formatError(repairError)}`,
202
+ suggestion: "Check file permissions and restore the latest valid backup."
203
+ });
204
+ return;
205
+ }
206
+ }
207
+ items.push({
208
+ group: spec.group,
209
+ status: "error",
210
+ message: `${spec.filePath} is invalid: ${formatError(error)}`,
211
+ suggestion: spec.repairInvalid
212
+ ? "Run `ai doctor --fix` to back up and rebuild this derived file."
213
+ : "Provider configuration is never overwritten automatically. Correct or restore providers.json, then retry."
214
+ });
215
+ }
216
+ }
217
+ async function checkProvidersAndModels(items, checkConnectivity) {
218
+ let providers;
219
+ try {
220
+ providers = await readProviders();
221
+ }
222
+ catch (error) {
223
+ items.push({
224
+ group: "Providers",
225
+ status: "error",
226
+ message: `Provider configuration could not be loaded: ${formatError(error)}`,
227
+ suggestion: "Correct or restore providers.json; `ai doctor` will not overwrite provider credentials."
228
+ });
229
+ return;
230
+ }
231
+ if (!providers.length) {
232
+ items.push({
233
+ group: "Providers",
234
+ status: "warn",
235
+ message: "No providers are configured.",
236
+ suggestion: "Run `ai setup` or `ai provider add` to configure the first provider."
237
+ });
238
+ return;
239
+ }
240
+ items.push({ group: "Providers", status: "ok", message: `${providers.length} provider(s) configured.` });
241
+ let cache = null;
242
+ try {
243
+ cache = await readCache();
244
+ }
245
+ catch (error) {
246
+ items.push({
247
+ group: "Cache",
248
+ status: "error",
249
+ message: `Provider cache could not be loaded: ${formatError(error)}`,
250
+ suggestion: "Run `ai doctor --fix` to back up and rebuild the cache."
251
+ });
252
+ }
253
+ for (const provider of providers) {
254
+ const cached = cache?.providers[provider.name];
255
+ if (provider.model && cached?.models?.length && !cached.models.includes(provider.model)) {
256
+ items.push({
257
+ group: "Models",
258
+ status: "warn",
259
+ message: `${provider.name} default model is not in the last discovered model list: ${provider.model}`,
260
+ suggestion: `Run \`ai status\` to refresh models, then edit ${provider.name} if necessary.`
261
+ });
262
+ }
263
+ if (!checkConnectivity)
264
+ continue;
265
+ const result = await checkProvider(provider);
266
+ const healthy = result.status === "ok" && result.models.length > 0;
267
+ items.push({
268
+ group: "Providers",
269
+ status: healthy ? "ok" : "error",
270
+ message: healthy
271
+ ? `${provider.name}: reachable; discovered ${result.models.length} model(s).`
272
+ : `${provider.name}: ${result.error ?? "provider returned no models"}`,
273
+ suggestion: healthy ? undefined : diagnoseProviderError(provider.name, result.error)
274
+ });
275
+ }
276
+ }
277
+ async function runDeepCandidates(tool, candidates, items) {
278
+ for (const candidate of candidates) {
279
+ const result = await runDeepCheck(candidate.provider, candidate.model);
280
+ if (result.status === "ok") {
281
+ if (result.usage) {
282
+ await recordDeepUsage(tool, candidate, result.usage);
283
+ }
284
+ items.push({
285
+ group: "Deep",
286
+ status: "ok",
287
+ message: `${candidate.provider.name}/${candidate.model}: ok${formatUsage(result)}`
288
+ });
289
+ return true;
290
+ }
291
+ items.push({
292
+ group: "Deep",
293
+ status: "warn",
294
+ message: `${candidate.provider.name}/${candidate.model}: failed, trying next provider (${result.error})`,
295
+ suggestion: diagnoseProviderError(candidate.provider.name, result.error)
296
+ });
297
+ await writeLog({
298
+ level: "warn",
299
+ category: "strategy",
300
+ action: "deep_fallback",
301
+ message: `${candidate.provider.name}/${candidate.model} failed deep check; trying next provider.`,
302
+ metadata: { provider: candidate.provider.name, model: candidate.model, tool, error: result.error }
303
+ });
304
+ }
305
+ return false;
306
+ }
307
+ async function recordDeepUsage(tool, candidate, usage) {
308
+ await appendUsageEvent(buildUsageEvent({
309
+ provider: candidate.provider.name,
310
+ tool,
311
+ model: candidate.model,
312
+ source: "doctor_deep",
313
+ inputTokens: usage.inputTokens,
314
+ outputTokens: usage.outputTokens,
315
+ totalTokens: usage.totalTokens
316
+ }));
317
+ await writeLog({
318
+ category: "usage",
319
+ action: "token_usage_recorded",
320
+ message: `Recorded ${usage.totalTokens} token(s) from doctor deep check.`,
321
+ metadata: {
322
+ provider: candidate.provider.name,
323
+ tool,
324
+ model: candidate.model,
325
+ source: "doctor_deep",
326
+ inputTokens: usage.inputTokens,
327
+ outputTokens: usage.outputTokens,
328
+ totalTokens: usage.totalTokens
329
+ }
330
+ });
331
+ }
332
+ function checkCommand(group, command, items) {
333
+ const resolved = resolveToolCommand(command);
334
+ if (resolved) {
335
+ items.push({ group, status: "ok", message: `${command}: ${resolved}` });
336
+ return;
337
+ }
338
+ const packageName = command === "claude" ? "@anthropic-ai/claude-code" : "@openai/codex";
339
+ items.push({
340
+ group,
341
+ status: "warn",
342
+ message: `${command} is not installed or failed its version check. Checked: ${getKnownCandidates(command).join(", ")}`,
343
+ suggestion: `Run \`ai setup\` or install it with \`npm install --global ${packageName}\`.`
344
+ });
345
+ }
346
+ function diagnoseProviderError(providerName, error) {
347
+ const message = (error ?? "").toLowerCase();
348
+ if (/401|403|unauthor|forbidden|api.?key|credential/.test(message)) {
349
+ return `Check the API key for ${providerName} with \`ai provider edit ${providerName}\`.`;
350
+ }
351
+ if (/404|not found|base url|non-json/.test(message)) {
352
+ return `Check the Base URL for ${providerName}; it may need or may already include /v1.`;
353
+ }
354
+ if (/429|quota|rate.?limit|credit|billing/.test(message)) {
355
+ return `Check quota, billing, and rate limits for ${providerName}.`;
356
+ }
357
+ if (/timeout|abort|network|fetch|enotfound|econn/.test(message)) {
358
+ return `Check network access, proxy settings, DNS, and the Base URL for ${providerName}.`;
359
+ }
360
+ return `Run \`ai provider edit ${providerName}\` to verify its URL, API key, and default model.`;
361
+ }
362
+ function formatUsage(result) {
363
+ if (!result.usage)
364
+ return " (usage not returned)";
365
+ return ` (${result.usage.totalTokens} tokens: ${result.usage.inputTokens} input, ${result.usage.outputTokens} output)`;
366
+ }
367
+ function compareVersions(left, right) {
368
+ const leftParts = left.split(".").map(Number);
369
+ const rightParts = right.split(".").map(Number);
370
+ for (let index = 0; index < Math.max(leftParts.length, rightParts.length); index += 1) {
371
+ const difference = (leftParts[index] ?? 0) - (rightParts[index] ?? 0);
372
+ if (difference !== 0)
373
+ return difference;
374
+ }
375
+ return 0;
376
+ }
377
+ function isRecord(value) {
378
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
379
+ }
380
+ function isRepairableJsonError(error) {
381
+ return error instanceof SyntaxError || (error instanceof Error && error.message === "unexpected JSON structure");
382
+ }
383
+ async function backupInvalidFile(filePath) {
384
+ const stamp = new Date().toISOString().replace(/[:.]/g, "-");
385
+ const backupPath = path.join(path.dirname(filePath), `${path.basename(filePath)}.invalid-${stamp}`);
386
+ await fs.rename(filePath, backupPath);
387
+ return backupPath;
388
+ }
389
+ function formatError(error) {
390
+ return error instanceof Error ? error.message : String(error);
391
+ }
@@ -0,0 +1,60 @@
1
+ import { readCache } from "../config/cache-store.js";
2
+ import { readProviders } from "../config/providers-store.js";
3
+ import { isModelCompatible, pickCompatibleModel } from "../models/model-filter.js";
4
+ import { refreshAllProviders } from "../providers/provider-service.js";
5
+ import { getToolBaseUrl } from "../runner/tool-runner.js";
6
+ import { getUsageShare, getWeeklyTokenMap } from "../usage/usage-service.js";
7
+ export async function getProviderStatuses(options = {}) {
8
+ if (options.refresh) {
9
+ await refreshAllProviders();
10
+ }
11
+ const [providers, cache, weeklyTokens] = await Promise.all([readProviders(), readCache(), getWeeklyTokenMap()]);
12
+ const allTokens = [...weeklyTokens.values()].reduce((sum, value) => sum + value, 0);
13
+ return providers.map((provider) => {
14
+ const cached = cache.providers[provider.name];
15
+ const tokens = weeklyTokens.get(provider.name) ?? 0;
16
+ return {
17
+ ...provider,
18
+ health: cached?.status ?? "unknown",
19
+ models: cached?.models ?? [],
20
+ lastCheckedAt: cached?.lastCheckedAt ?? null,
21
+ lastError: cached?.lastError ?? null,
22
+ weeklyTokens: tokens,
23
+ usageShare: getUsageShare(tokens, allTokens),
24
+ claude: buildToolStatus("claude", provider, cached?.status ?? "unknown", cached?.models ?? []),
25
+ codex: buildToolStatus("codex", provider, cached?.status ?? "unknown", cached?.models ?? [])
26
+ };
27
+ });
28
+ }
29
+ function buildToolStatus(tool, provider, health, models) {
30
+ const baseUrl = getToolBaseUrl(tool, provider);
31
+ if (health === "error") {
32
+ return {
33
+ state: "provider-error",
34
+ baseUrl,
35
+ model: null,
36
+ note: "provider health error"
37
+ };
38
+ }
39
+ const model = resolveToolModel(tool, provider.model, models);
40
+ if (!model) {
41
+ return {
42
+ state: "no-model",
43
+ baseUrl,
44
+ model: null,
45
+ note: "no compatible cached model"
46
+ };
47
+ }
48
+ return {
49
+ state: "ready",
50
+ baseUrl,
51
+ model,
52
+ note: models.includes(model) ? "model listed" : "default model not verified"
53
+ };
54
+ }
55
+ function resolveToolModel(tool, defaultModel, models) {
56
+ if (defaultModel && isModelCompatible(tool, defaultModel) && (!models.length || models.includes(defaultModel))) {
57
+ return defaultModel;
58
+ }
59
+ return pickCompatibleModel(tool, "", models);
60
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,113 @@
1
+ import { getModelPricingPath } from "../config/paths.js";
2
+ import { readJsonFile, updateJsonFile } from "../config/json-store.js";
3
+ import { isRecord } from "../utils/is-record.js";
4
+ export async function readModelPricing() {
5
+ const raw = await readJsonFile(getModelPricingPath(), {});
6
+ return normalizePricingTable(raw);
7
+ }
8
+ export async function estimateTokenCostUsdFromConfig(providerName, model, usage) {
9
+ return estimateTokenCostUsd(providerName, model, usage, await readModelPricing());
10
+ }
11
+ export async function upsertModelPrice(providerName, model, price) {
12
+ const key = getProviderModelPricingKey(providerName, model);
13
+ return updateJsonFile(getModelPricingPath(), {}, (raw) => ({
14
+ ...normalizePricingTable(raw),
15
+ [key]: price
16
+ }));
17
+ }
18
+ export function getProviderModelPricingKey(providerName, model) {
19
+ return `${providerName.trim()}/${model.trim()}`;
20
+ }
21
+ export function estimateTokenCostUsd(providerName, model, usage, pricing) {
22
+ const price = resolveModelPrice(providerName, model, pricing);
23
+ if (!price)
24
+ return undefined;
25
+ const cacheReadInputTokens = usage.cacheReadInputTokens ?? 0;
26
+ const regularInputTokens = Math.max(0, usage.inputTokens - cacheReadInputTokens);
27
+ const cacheReadPrice = price.cacheReadPerMillionUsd ?? price.inputPerMillionUsd;
28
+ const cost = (regularInputTokens * price.inputPerMillionUsd
29
+ + cacheReadInputTokens * cacheReadPrice
30
+ + usage.outputTokens * price.outputPerMillionUsd) / 1_000_000;
31
+ return Number(cost.toFixed(8));
32
+ }
33
+ export function normalizePricingTable(value) {
34
+ const source = isRecord(value) && isRecord(value.models) ? value.models : value;
35
+ if (!isRecord(source))
36
+ return {};
37
+ const table = {};
38
+ for (const [key, rawPrice] of Object.entries(source)) {
39
+ const price = normalizeModelPrice(rawPrice);
40
+ if (key.trim() && price) {
41
+ table[key.trim()] = price;
42
+ }
43
+ }
44
+ return table;
45
+ }
46
+ function resolveModelPrice(providerName, model, pricing) {
47
+ const keys = [
48
+ `${providerName}/${model}`,
49
+ `${providerName}:${model}`,
50
+ model
51
+ ];
52
+ for (const key of keys) {
53
+ const exact = pricing[key];
54
+ if (exact)
55
+ return exact;
56
+ const lower = Object.entries(pricing).find(([candidate]) => candidate.toLowerCase() === key.toLowerCase());
57
+ if (lower)
58
+ return lower[1];
59
+ }
60
+ return undefined;
61
+ }
62
+ function normalizeModelPrice(value) {
63
+ if (!isRecord(value))
64
+ return null;
65
+ const inputPerMillionUsd = readFiniteNumber(value, [
66
+ "inputPerMillionUsd",
67
+ "input_per_million_usd",
68
+ "input",
69
+ "promptPerMillionUsd",
70
+ "prompt_per_million_usd"
71
+ ]);
72
+ const outputPerMillionUsd = readFiniteNumber(value, [
73
+ "outputPerMillionUsd",
74
+ "output_per_million_usd",
75
+ "output",
76
+ "completionPerMillionUsd",
77
+ "completion_per_million_usd"
78
+ ]);
79
+ if (inputPerMillionUsd === undefined || outputPerMillionUsd === undefined)
80
+ return null;
81
+ const cacheReadPerMillionUsd = readFiniteNumber(value, [
82
+ "cacheReadPerMillionUsd",
83
+ "cache_read_per_million_usd",
84
+ "cacheRead",
85
+ "cache_read",
86
+ "cachedInputPerMillionUsd",
87
+ "cached_input_per_million_usd",
88
+ "cachedInput",
89
+ "cached_input"
90
+ ]);
91
+ if (inputPerMillionUsd < 0 || outputPerMillionUsd < 0)
92
+ return null;
93
+ if (cacheReadPerMillionUsd !== undefined && cacheReadPerMillionUsd < 0)
94
+ return null;
95
+ return {
96
+ inputPerMillionUsd,
97
+ outputPerMillionUsd,
98
+ ...(cacheReadPerMillionUsd !== undefined ? { cacheReadPerMillionUsd } : {})
99
+ };
100
+ }
101
+ function readFiniteNumber(data, keys) {
102
+ for (const key of keys) {
103
+ const value = data[key];
104
+ if (typeof value === "number" && Number.isFinite(value))
105
+ return value;
106
+ if (typeof value === "string" && value.trim()) {
107
+ const numeric = Number(value);
108
+ if (Number.isFinite(numeric))
109
+ return numeric;
110
+ }
111
+ }
112
+ return undefined;
113
+ }
@@ -0,0 +1,30 @@
1
+ import { readUsage } from "../config/usage-store.js";
2
+ export async function getWeeklyTokenMap() {
3
+ const usage = await readUsage();
4
+ const start = getWeekStart(new Date());
5
+ const map = new Map();
6
+ for (const event of usage.events) {
7
+ if (new Date(event.createdAt) >= start) {
8
+ map.set(event.provider, (map.get(event.provider) ?? 0) + event.totalTokens);
9
+ }
10
+ }
11
+ return map;
12
+ }
13
+ export function getUsageShare(providerTokens, allTokens) {
14
+ if (!allTokens)
15
+ return 0;
16
+ return Number(((providerTokens / allTokens) * 100).toFixed(2));
17
+ }
18
+ export function buildUsageEvent(input) {
19
+ return {
20
+ ...input,
21
+ createdAt: new Date().toISOString()
22
+ };
23
+ }
24
+ function getWeekStart(date) {
25
+ const start = new Date(date);
26
+ const day = start.getDay() || 7;
27
+ start.setHours(0, 0, 0, 0);
28
+ start.setDate(start.getDate() - day + 1);
29
+ return start;
30
+ }
@@ -0,0 +1,3 @@
1
+ export function isRecord(value) {
2
+ return typeof value === "object" && value !== null && !Array.isArray(value);
3
+ }
@@ -0,0 +1,28 @@
1
+ #!/usr/bin/env node
2
+ import fs from "node:fs";
3
+ import { startRuntimeServer } from "./runtime-server.js";
4
+ function readToken() {
5
+ const inline = process.env.AI_GATEWAY_SERVER_TOKEN?.trim();
6
+ if (inline)
7
+ return inline;
8
+ const file = process.env.AI_GATEWAY_SERVER_TOKEN_FILE?.trim();
9
+ if (file)
10
+ return fs.readFileSync(file, "utf8").trim();
11
+ throw new Error("Set AI_GATEWAY_SERVER_TOKEN or AI_GATEWAY_SERVER_TOKEN_FILE.");
12
+ }
13
+ const server = await startRuntimeServer({
14
+ host: process.env.AI_GATEWAY_SERVER_HOST || "127.0.0.1",
15
+ port: Number(process.env.AI_GATEWAY_SERVER_PORT || "8080"),
16
+ token: readToken(),
17
+ maxRequestBytes: Number(process.env.AI_GATEWAY_MAX_REQUEST_BYTES || "16777216"),
18
+ upstreamHeadersTimeoutMs: Number(process.env.AI_GATEWAY_UPSTREAM_HEADERS_TIMEOUT_MS || "45000"),
19
+ agentWorkspaceRoot: process.env.AI_GATEWAY_AGENT_WORKSPACE_ROOT,
20
+ agentMaxConcurrentRuns: Number(process.env.AI_GATEWAY_AGENT_MAX_CONCURRENT_RUNS || "2"),
21
+ agentTimeoutMs: Number(process.env.AI_GATEWAY_AGENT_TIMEOUT_MS || "600000")
22
+ });
23
+ console.log(`AI Gateway runtime server listening on ${server.baseUrl}`);
24
+ for (const signal of ["SIGINT", "SIGTERM"]) {
25
+ process.once(signal, () => {
26
+ void server.close().finally(() => process.exit(0));
27
+ });
28
+ }