@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,430 @@
1
+ const RETRY_DELAYS_MS = [0, 200, 600];
2
+ const RETRYABLE_STATUS_CODES = new Set([408, 409, 425, 429, 500, 502, 503, 504]);
3
+ export async function checkProvider(provider) {
4
+ const checkedAt = new Date().toISOString();
5
+ const startedAt = Date.now();
6
+ const errors = [];
7
+ const retryErrors = [];
8
+ let attempts = 0;
9
+ try {
10
+ for (const url of buildModelUrlCandidates(provider.baseUrl)) {
11
+ const result = await fetchTextWithRetry(url, {
12
+ method: "GET",
13
+ headers: {
14
+ Authorization: `Bearer ${provider.apiKey}`,
15
+ Accept: "application/json"
16
+ }
17
+ });
18
+ attempts += result.attempts;
19
+ retryErrors.push(...result.retryErrors);
20
+ if (result.error || !result.response) {
21
+ errors.push(`GET ${url} failed: ${result.error ?? "empty response"}`);
22
+ continue;
23
+ }
24
+ const { response, text } = result;
25
+ if (!response.ok) {
26
+ errors.push(`GET ${url} failed with ${response.status} ${response.statusText}`);
27
+ continue;
28
+ }
29
+ const contentType = response.headers.get("content-type") ?? "";
30
+ if (!looksLikeJson(contentType, text)) {
31
+ errors.push(`GET ${url} returned ${contentType || "non-JSON response"}. Base URL may need /v1.`);
32
+ continue;
33
+ }
34
+ const models = extractModels(JSON.parse(text));
35
+ return {
36
+ status: "ok",
37
+ models,
38
+ checkedAt,
39
+ error: models.length ? null : "Provider responded but no models were found.",
40
+ attempts,
41
+ durationMs: Date.now() - startedAt,
42
+ retryErrors
43
+ };
44
+ }
45
+ return {
46
+ status: "error",
47
+ models: [],
48
+ checkedAt,
49
+ error: errors.join("; ") || "No model discovery endpoint worked.",
50
+ attempts,
51
+ durationMs: Date.now() - startedAt,
52
+ retryErrors
53
+ };
54
+ }
55
+ catch (error) {
56
+ return {
57
+ status: "error",
58
+ models: [],
59
+ checkedAt,
60
+ error: error instanceof Error ? error.message : String(error),
61
+ attempts,
62
+ durationMs: Date.now() - startedAt,
63
+ retryErrors
64
+ };
65
+ }
66
+ }
67
+ export async function runDeepCheck(provider, model) {
68
+ const checkedAt = new Date().toISOString();
69
+ const startedAt = Date.now();
70
+ try {
71
+ const url = buildChatCompletionsUrl(provider.baseUrl);
72
+ const result = await fetchTextWithRetry(url, {
73
+ method: "POST",
74
+ headers: {
75
+ Authorization: `Bearer ${provider.apiKey}`,
76
+ "Content-Type": "application/json",
77
+ Accept: "application/json"
78
+ },
79
+ body: JSON.stringify({
80
+ model,
81
+ messages: [{ role: "user", content: "ping" }],
82
+ max_tokens: 1,
83
+ temperature: 0
84
+ })
85
+ });
86
+ if (result.error || !result.response) {
87
+ return {
88
+ status: "error",
89
+ models: [],
90
+ checkedAt,
91
+ error: result.error ?? "empty response",
92
+ attempts: result.attempts,
93
+ durationMs: Date.now() - startedAt,
94
+ retryErrors: result.retryErrors
95
+ };
96
+ }
97
+ const { response, text } = result;
98
+ if (!response.ok) {
99
+ return {
100
+ status: "error",
101
+ models: [],
102
+ checkedAt,
103
+ error: `POST ${url} failed with ${response.status} ${response.statusText}`,
104
+ attempts: result.attempts,
105
+ durationMs: Date.now() - startedAt,
106
+ retryErrors: result.retryErrors
107
+ };
108
+ }
109
+ const json = looksLikeJson(response.headers.get("content-type") ?? "", text) ? JSON.parse(text) : {};
110
+ return {
111
+ status: "ok",
112
+ models: [model],
113
+ checkedAt,
114
+ error: null,
115
+ usage: extractTokenUsage(json),
116
+ attempts: result.attempts,
117
+ durationMs: Date.now() - startedAt,
118
+ retryErrors: result.retryErrors
119
+ };
120
+ }
121
+ catch (error) {
122
+ return {
123
+ status: "error",
124
+ models: [],
125
+ checkedAt,
126
+ error: error instanceof Error ? error.message : String(error),
127
+ attempts: 1,
128
+ durationMs: Date.now() - startedAt,
129
+ retryErrors: []
130
+ };
131
+ }
132
+ }
133
+ export async function checkCallableModels(provider, models) {
134
+ const uniqueModels = [...new Set(models.filter(Boolean))];
135
+ return mapWithConcurrency(uniqueModels, 3, (model) => checkCallableModel(provider, model));
136
+ }
137
+ async function checkCallableModel(provider, model) {
138
+ const lower = model.toLowerCase();
139
+ return lower.includes("claude")
140
+ ? checkAnthropicMessagesModel(provider, model)
141
+ : checkChatCompletionsModel(provider, model);
142
+ }
143
+ async function checkAnthropicMessagesModel(provider, model) {
144
+ const baseUrl = provider.baseUrl.replace(/\/+$/, "").replace(/\/v1$/i, "");
145
+ const url = `${baseUrl}/v1/messages`;
146
+ const payload = {
147
+ model,
148
+ max_tokens: 1,
149
+ messages: [{ role: "user", content: "." }]
150
+ };
151
+ const headers = {
152
+ "x-api-key": provider.apiKey,
153
+ "anthropic-version": "2023-06-01",
154
+ "Content-Type": "application/json",
155
+ Accept: "application/json"
156
+ };
157
+ return postModelProbe(url, headers, payload, model);
158
+ }
159
+ async function checkChatCompletionsModel(provider, model) {
160
+ const payload = {
161
+ model,
162
+ messages: [{ role: "user", content: "." }],
163
+ max_tokens: 1,
164
+ temperature: 0
165
+ };
166
+ const headers = {
167
+ Authorization: `Bearer ${provider.apiKey}`,
168
+ "Content-Type": "application/json",
169
+ Accept: "application/json"
170
+ };
171
+ let lastResult = null;
172
+ for (const url of buildChatCompletionsUrlCandidates(provider.baseUrl)) {
173
+ const result = await postModelProbe(url, headers, payload, model);
174
+ if (result.status === "ok")
175
+ return result;
176
+ lastResult = result;
177
+ }
178
+ return lastResult ?? { model, status: "error", error: "No chat completions endpoint worked." };
179
+ }
180
+ async function postModelProbe(url, headers, payload, model) {
181
+ try {
182
+ const result = await fetchTextWithRetry(url, {
183
+ method: "POST",
184
+ headers,
185
+ body: JSON.stringify(payload)
186
+ });
187
+ if (result.error || !result.response) {
188
+ return {
189
+ model,
190
+ status: "error",
191
+ error: result.error ?? "empty response",
192
+ attempts: result.attempts,
193
+ durationMs: result.durationMs,
194
+ retryErrors: result.retryErrors
195
+ };
196
+ }
197
+ const { response, text } = result;
198
+ const json = looksLikeJson(response.headers.get("content-type") ?? "", text) ? JSON.parse(text) : {};
199
+ if (!response.ok) {
200
+ return {
201
+ model,
202
+ status: "error",
203
+ error: `POST ${url} failed with ${response.status} ${response.statusText}${extractErrorMessage(json)}`,
204
+ attempts: result.attempts,
205
+ durationMs: result.durationMs,
206
+ retryErrors: result.retryErrors
207
+ };
208
+ }
209
+ return {
210
+ model,
211
+ status: "ok",
212
+ error: null,
213
+ usage: extractTokenUsage(json),
214
+ attempts: result.attempts,
215
+ durationMs: result.durationMs,
216
+ retryErrors: result.retryErrors
217
+ };
218
+ }
219
+ catch (error) {
220
+ return {
221
+ model,
222
+ status: "error",
223
+ error: error instanceof Error ? error.message : String(error),
224
+ attempts: 1,
225
+ durationMs: 0,
226
+ retryErrors: []
227
+ };
228
+ }
229
+ }
230
+ export function extractTokenUsage(value) {
231
+ const usage = value?.usage;
232
+ if (!usage || typeof usage !== "object") {
233
+ return undefined;
234
+ }
235
+ const data = usage;
236
+ const rawInputTokens = readNumber(data, ["input_tokens", "prompt_tokens", "inputTokens", "promptTokens"]);
237
+ const outputTokens = readNumber(data, ["output_tokens", "completion_tokens", "outputTokens", "completionTokens"]);
238
+ const totalTokens = readNumber(data, ["total_tokens", "totalTokens"]);
239
+ const cacheReadInputTokens = readCacheReadInputTokens(data);
240
+ const cacheCreationInputTokens = readNumber(data, [
241
+ "cache_creation_input_tokens",
242
+ "cacheCreationInputTokens",
243
+ "cache_creation_tokens",
244
+ "cacheCreationTokens"
245
+ ]) ?? 0;
246
+ if (rawInputTokens === undefined
247
+ && outputTokens === undefined
248
+ && totalTokens === undefined
249
+ && cacheReadInputTokens === undefined) {
250
+ return undefined;
251
+ }
252
+ const input = normalizeInputTokens(data, rawInputTokens ?? 0, cacheReadInputTokens ?? 0, cacheCreationInputTokens);
253
+ const output = outputTokens ?? 0;
254
+ return {
255
+ inputTokens: input,
256
+ outputTokens: output,
257
+ totalTokens: totalTokens ?? input + output,
258
+ ...(cacheReadInputTokens !== undefined ? { cacheReadInputTokens } : {})
259
+ };
260
+ }
261
+ function normalizeInputTokens(data, rawInputTokens, cacheReadInputTokens, cacheCreationInputTokens) {
262
+ return hasNestedCachedTokens(data)
263
+ ? rawInputTokens
264
+ : rawInputTokens + cacheReadInputTokens + cacheCreationInputTokens;
265
+ }
266
+ function readCacheReadInputTokens(data) {
267
+ return readNumber(data, [
268
+ "cache_read_input_tokens",
269
+ "cacheReadInputTokens",
270
+ "cache_read_tokens",
271
+ "cacheReadTokens",
272
+ "cached_tokens",
273
+ "cachedTokens"
274
+ ])
275
+ ?? readNumberFromRecord(data.prompt_tokens_details, ["cached_tokens", "cachedTokens"])
276
+ ?? readNumberFromRecord(data.input_tokens_details, ["cached_tokens", "cachedTokens", "cache_read_tokens", "cacheReadTokens"]);
277
+ }
278
+ function hasNestedCachedTokens(data) {
279
+ return readNumberFromRecord(data.prompt_tokens_details, ["cached_tokens", "cachedTokens"]) !== undefined
280
+ || readNumberFromRecord(data.input_tokens_details, ["cached_tokens", "cachedTokens", "cache_read_tokens", "cacheReadTokens"]) !== undefined;
281
+ }
282
+ function readNumberFromRecord(value, keys) {
283
+ if (!value || typeof value !== "object" || Array.isArray(value))
284
+ return undefined;
285
+ return readNumber(value, keys);
286
+ }
287
+ function readNumber(data, keys) {
288
+ for (const key of keys) {
289
+ const value = data[key];
290
+ if (typeof value === "number" && Number.isFinite(value)) {
291
+ return value;
292
+ }
293
+ }
294
+ return undefined;
295
+ }
296
+ function buildModelUrlCandidates(baseUrl) {
297
+ const normalized = baseUrl.replace(/\/+$/, "");
298
+ const candidates = [`${normalized}/models`];
299
+ if (!normalized.endsWith("/v1")) {
300
+ candidates.push(`${normalized}/v1/models`);
301
+ }
302
+ return candidates;
303
+ }
304
+ function buildChatCompletionsUrl(baseUrl) {
305
+ return `${baseUrl.replace(/\/+$/, "")}/chat/completions`;
306
+ }
307
+ function buildChatCompletionsUrlCandidates(baseUrl) {
308
+ const normalized = baseUrl.replace(/\/+$/, "");
309
+ const candidates = [`${normalized}/chat/completions`];
310
+ if (!normalized.endsWith("/v1")) {
311
+ candidates.push(`${normalized}/v1/chat/completions`);
312
+ }
313
+ return candidates;
314
+ }
315
+ async function fetchTextWithRetry(url, init) {
316
+ const startedAt = Date.now();
317
+ const retryErrors = [];
318
+ for (const [index, delayMs] of RETRY_DELAYS_MS.entries()) {
319
+ if (delayMs > 0) {
320
+ await sleep(delayMs);
321
+ }
322
+ const attempt = index + 1;
323
+ try {
324
+ const response = await fetchWithTimeout(url, init);
325
+ const text = await response.text();
326
+ if (!shouldRetryResponse(response) || attempt >= RETRY_DELAYS_MS.length) {
327
+ return {
328
+ response,
329
+ text,
330
+ attempts: attempt,
331
+ durationMs: Date.now() - startedAt,
332
+ retryErrors
333
+ };
334
+ }
335
+ retryErrors.push(`attempt ${attempt}: ${response.status} ${response.statusText}`);
336
+ }
337
+ catch (error) {
338
+ const message = error instanceof Error ? error.message : String(error);
339
+ if (attempt >= RETRY_DELAYS_MS.length) {
340
+ return {
341
+ text: "",
342
+ error: message,
343
+ attempts: attempt,
344
+ durationMs: Date.now() - startedAt,
345
+ retryErrors
346
+ };
347
+ }
348
+ retryErrors.push(`attempt ${attempt}: ${message}`);
349
+ }
350
+ }
351
+ return {
352
+ text: "",
353
+ error: "Request failed after retries.",
354
+ attempts: RETRY_DELAYS_MS.length,
355
+ durationMs: Date.now() - startedAt,
356
+ retryErrors
357
+ };
358
+ }
359
+ function shouldRetryResponse(response) {
360
+ return RETRYABLE_STATUS_CODES.has(response.status);
361
+ }
362
+ function sleep(ms) {
363
+ return new Promise((resolve) => setTimeout(resolve, ms));
364
+ }
365
+ async function fetchWithTimeout(url, init, timeoutMs = 12000) {
366
+ const controller = new AbortController();
367
+ const timeout = setTimeout(() => controller.abort(), timeoutMs);
368
+ try {
369
+ return await fetch(url, { ...init, signal: controller.signal });
370
+ }
371
+ finally {
372
+ clearTimeout(timeout);
373
+ }
374
+ }
375
+ function looksLikeJson(contentType, text) {
376
+ const trimmed = text.trim();
377
+ return contentType.includes("application/json") || trimmed.startsWith("{") || trimmed.startsWith("[");
378
+ }
379
+ function extractModels(value) {
380
+ const payload = value;
381
+ const source = Array.isArray(payload.data)
382
+ ? payload.data
383
+ : Array.isArray(payload.models)
384
+ ? payload.models
385
+ : Array.isArray(value)
386
+ ? value
387
+ : [];
388
+ const models = source
389
+ .map((entry) => {
390
+ if (typeof entry === "string")
391
+ return entry;
392
+ if (entry && typeof entry === "object" && "id" in entry) {
393
+ return String(entry.id);
394
+ }
395
+ if (entry && typeof entry === "object" && "name" in entry) {
396
+ return String(entry.name);
397
+ }
398
+ return "";
399
+ })
400
+ .filter(Boolean);
401
+ return [...new Set(models)].sort((a, b) => a.localeCompare(b));
402
+ }
403
+ function extractErrorMessage(value) {
404
+ if (!value || typeof value !== "object")
405
+ return "";
406
+ const data = value;
407
+ const error = data.error;
408
+ if (typeof error === "string")
409
+ return `: ${error}`;
410
+ if (error && typeof error === "object") {
411
+ const message = error.message;
412
+ if (typeof message === "string")
413
+ return `: ${message}`;
414
+ }
415
+ const message = data.message;
416
+ return typeof message === "string" ? `: ${message}` : "";
417
+ }
418
+ async function mapWithConcurrency(items, limit, mapper) {
419
+ const results = new Array(items.length);
420
+ let nextIndex = 0;
421
+ const workers = Array.from({ length: Math.min(limit, items.length) }, async () => {
422
+ while (nextIndex < items.length) {
423
+ const currentIndex = nextIndex;
424
+ nextIndex += 1;
425
+ results[currentIndex] = await mapper(items[currentIndex]);
426
+ }
427
+ });
428
+ await Promise.all(workers);
429
+ return results;
430
+ }
@@ -0,0 +1,13 @@
1
+ export function isModelCompatible(tool, model) {
2
+ const value = model.toLowerCase();
3
+ if (tool === "claude") {
4
+ return value.includes("claude");
5
+ }
6
+ return value.includes("gpt") || value.includes("codex") || value.includes("grok") || /^o\d/.test(value) || value.includes("openai");
7
+ }
8
+ export function pickCompatibleModel(tool, preferredModel, models) {
9
+ if (preferredModel && isModelCompatible(tool, preferredModel)) {
10
+ return preferredModel;
11
+ }
12
+ return models.find((model) => isModelCompatible(tool, model)) ?? null;
13
+ }
@@ -0,0 +1,212 @@
1
+ import { buildProviderHealthCache, deleteProviderCache, readCache, renameProviderCache, upsertProviderCache } from "../config/cache-store.js";
2
+ import { appendUsageEvent } from "../config/usage-store.js";
3
+ import { recordProviderChange } from "../integrations/integration-state.js";
4
+ import { writeLog } from "../logs/log-service.js";
5
+ import { readProviders, swapProviderLevel, updateProviders } from "../config/providers-store.js";
6
+ import { checkCallableModels, checkProvider } from "../models/model-discovery.js";
7
+ import { buildUsageEvent } from "../usage/usage-service.js";
8
+ export async function listProviders() {
9
+ return readProviders();
10
+ }
11
+ export async function addProvider(input) {
12
+ const providers = await readProviders();
13
+ if (providers.some((provider) => provider.name === input.name.trim())) {
14
+ throw new Error(`Provider already exists: ${input.name}`);
15
+ }
16
+ const draft = {
17
+ name: input.name.trim(),
18
+ baseUrl: input.baseUrl.trim().replace(/\/+$/, ""),
19
+ apiKey: input.apiKey.trim(),
20
+ mode: input.mode ?? "auto",
21
+ level: providers.length + 1,
22
+ model: input.model?.trim() ?? ""
23
+ };
24
+ const result = await checkProvider(draft);
25
+ const model = draft.model || result.models[0] || "";
26
+ const normalizedProviders = await updateProviders((current) => {
27
+ if (current.some((provider) => provider.name === draft.name)) {
28
+ throw new Error(`Provider already exists: ${draft.name}`);
29
+ }
30
+ return [...current, { ...draft, level: current.length + 1, model }];
31
+ });
32
+ const provider = normalizedProviders.find((item) => item.name === draft.name);
33
+ if (!provider) {
34
+ throw new Error("Provider add failed.");
35
+ }
36
+ await upsertProviderCache(provider.name, buildProviderHealthCache(undefined, {
37
+ status: result.status,
38
+ models: result.models,
39
+ lastCheckedAt: result.checkedAt,
40
+ lastError: result.error
41
+ }));
42
+ await recordProviderChange({ type: "added", provider: provider.name });
43
+ return provider;
44
+ }
45
+ export async function updateProvider(providerName, patch) {
46
+ const targetName = patch.name?.trim() || providerName;
47
+ const normalizedProviders = await updateProviders((providers) => {
48
+ const current = providers.find((provider) => provider.name === providerName);
49
+ if (!current) {
50
+ throw new Error(`Provider not found: ${providerName}`);
51
+ }
52
+ if (targetName !== providerName && providers.some((provider) => provider.name === targetName)) {
53
+ throw new Error(`Provider already exists: ${targetName}`);
54
+ }
55
+ let nextProviders = providers.map((provider) => provider.name === providerName
56
+ ? {
57
+ ...provider,
58
+ ...patch,
59
+ name: targetName,
60
+ baseUrl: patch.baseUrl?.trim().replace(/\/+$/, "") || provider.baseUrl,
61
+ apiKey: patch.apiKey?.trim() || provider.apiKey,
62
+ model: patch.model?.trim() ?? provider.model
63
+ }
64
+ : provider);
65
+ if (patch.level) {
66
+ nextProviders = swapProviderLevel(nextProviders, targetName, patch.level);
67
+ }
68
+ return nextProviders;
69
+ });
70
+ const updated = normalizedProviders.find((provider) => provider.name === targetName);
71
+ if (!updated) {
72
+ throw new Error("Provider update failed.");
73
+ }
74
+ if (patch.name && patch.name !== providerName) {
75
+ await renameProviderCache(providerName, patch.name);
76
+ }
77
+ if (patch.name || patch.baseUrl || patch.apiKey) {
78
+ await refreshProvider(updated);
79
+ }
80
+ await recordProviderChange({
81
+ type: "updated",
82
+ provider: updated.name,
83
+ previousProvider: updated.name !== providerName ? providerName : undefined
84
+ });
85
+ return updated;
86
+ }
87
+ export async function deleteProvider(providerName) {
88
+ const deleted = {};
89
+ await updateProviders((providers) => {
90
+ deleted.value = providers.find((item) => item.name === providerName);
91
+ if (!deleted.value) {
92
+ throw new Error(`Provider not found: ${providerName}`);
93
+ }
94
+ return providers.filter((item) => item.name !== providerName);
95
+ });
96
+ if (!deleted.value) {
97
+ throw new Error(`Provider not found: ${providerName}`);
98
+ }
99
+ await deleteProviderCache(providerName);
100
+ await recordProviderChange({ type: "deleted", provider: deleted.value.name });
101
+ return deleted.value;
102
+ }
103
+ export async function refreshProvider(provider, options = {}) {
104
+ const result = await checkProvider(provider);
105
+ const callable = options.verifyCallableModels && result.models.length
106
+ ? await filterCallableModels(provider, result.models)
107
+ : { models: result.models, failed: 0, attempts: 0, durationMs: 0, retryErrors: [] };
108
+ const models = callable.models;
109
+ const status = result.status === "ok" && options.verifyCallableModels && !models.length ? "error" : result.status;
110
+ const lastError = status === "error" && options.verifyCallableModels && !models.length
111
+ ? `No callable models found. ${result.error ?? ""}`.trim()
112
+ : result.error;
113
+ const cache = await readCache();
114
+ await upsertProviderCache(provider.name, buildProviderHealthCache(cache.providers[provider.name], {
115
+ status,
116
+ models,
117
+ lastCheckedAt: result.checkedAt,
118
+ lastError
119
+ }));
120
+ await writeLog({
121
+ level: status === "ok" ? "info" : "error",
122
+ category: "provider",
123
+ action: "provider_health_check",
124
+ message: `${provider.name}: ${status}${lastError ? ` (${lastError})` : ""}`,
125
+ metadata: {
126
+ provider: provider.name,
127
+ status,
128
+ models: models.length,
129
+ discoveredModels: result.models.length,
130
+ failedCallableModels: callable.failed,
131
+ checkedAt: result.checkedAt,
132
+ verifyCallableModels: Boolean(options.verifyCallableModels),
133
+ attempts: (result.attempts ?? 0) + callable.attempts,
134
+ discoveryAttempts: result.attempts ?? null,
135
+ callableAttempts: callable.attempts,
136
+ durationMs: (result.durationMs ?? 0) + callable.durationMs,
137
+ discoveryDurationMs: result.durationMs ?? null,
138
+ callableDurationMs: callable.durationMs,
139
+ retryErrors: [...(result.retryErrors ?? []), ...callable.retryErrors].slice(-8)
140
+ }
141
+ });
142
+ }
143
+ export async function refreshAllProviders(options = {}) {
144
+ const providers = await readProviders();
145
+ if (options.verifyCallableModels) {
146
+ await mapWithConcurrency(providers, 2, (provider) => refreshProvider(provider, options));
147
+ return;
148
+ }
149
+ await Promise.all(providers.map((provider) => refreshProvider(provider, options)));
150
+ }
151
+ export async function getCachedModels(providerName) {
152
+ const cache = await readCache();
153
+ return cache.providers[providerName]?.models ?? [];
154
+ }
155
+ async function filterCallableModels(provider, models) {
156
+ const results = await checkCallableModels(provider, models);
157
+ for (const result of results) {
158
+ if (result.status === "ok" && result.usage) {
159
+ await recordModelProbeUsage(provider, result.model, result.usage);
160
+ }
161
+ }
162
+ const callableModels = results.filter((result) => result.status === "ok").map((result) => result.model);
163
+ const retryErrors = results.flatMap((result) => result.retryErrors ?? []);
164
+ await writeLog({
165
+ level: callableModels.length ? "info" : "warn",
166
+ category: "provider",
167
+ action: "provider_model_probe",
168
+ message: `${provider.name}: ${callableModels.length}/${models.length} callable model(s).`,
169
+ metadata: {
170
+ provider: provider.name,
171
+ callableModels,
172
+ failedModels: results.filter((result) => result.status === "error").map((result) => ({
173
+ model: result.model,
174
+ error: result.error,
175
+ attempts: result.attempts ?? null,
176
+ durationMs: result.durationMs ?? null
177
+ })),
178
+ attempts: results.reduce((sum, result) => sum + (result.attempts ?? 0), 0),
179
+ durationMs: results.reduce((sum, result) => sum + (result.durationMs ?? 0), 0),
180
+ retryErrors: retryErrors.slice(-8)
181
+ }
182
+ });
183
+ return {
184
+ models: callableModels,
185
+ failed: results.length - callableModels.length,
186
+ attempts: results.reduce((sum, result) => sum + (result.attempts ?? 0), 0),
187
+ durationMs: results.reduce((sum, result) => sum + (result.durationMs ?? 0), 0),
188
+ retryErrors
189
+ };
190
+ }
191
+ async function recordModelProbeUsage(provider, model, usage) {
192
+ await appendUsageEvent(buildUsageEvent({
193
+ provider: provider.name,
194
+ tool: "provider",
195
+ model,
196
+ source: "model_probe",
197
+ inputTokens: usage.inputTokens,
198
+ outputTokens: usage.outputTokens,
199
+ totalTokens: usage.totalTokens
200
+ }));
201
+ }
202
+ async function mapWithConcurrency(items, limit, mapper) {
203
+ let nextIndex = 0;
204
+ const workers = Array.from({ length: Math.min(limit, items.length) }, async () => {
205
+ while (nextIndex < items.length) {
206
+ const currentIndex = nextIndex;
207
+ nextIndex += 1;
208
+ await mapper(items[currentIndex]);
209
+ }
210
+ });
211
+ await Promise.all(workers);
212
+ }