@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.
- package/README.md +185 -0
- package/dist/cli/commands/add.js +29 -0
- package/dist/cli/commands/context.js +29 -0
- package/dist/cli/commands/doctor.js +62 -0
- package/dist/cli/commands/edit.js +85 -0
- package/dist/cli/commands/help.js +16 -0
- package/dist/cli/commands/list.js +25 -0
- package/dist/cli/commands/log.js +63 -0
- package/dist/cli/commands/memory.js +241 -0
- package/dist/cli/commands/report.js +35 -0
- package/dist/cli/commands/session.js +74 -0
- package/dist/cli/commands/setup.js +86 -0
- package/dist/cli/commands/status.js +50 -0
- package/dist/cli/commands/telegram.js +701 -0
- package/dist/cli/commands/use.js +108 -0
- package/dist/cli/commands/version.js +12 -0
- package/dist/cli/index.js +276 -0
- package/dist/cli/output/table.js +22 -0
- package/dist/cli/prompts/prompt.js +37 -0
- package/dist/cli/registry.js +16 -0
- package/dist/core/config/cache-store.js +193 -0
- package/dist/core/config/json-store.js +114 -0
- package/dist/core/config/paths.js +85 -0
- package/dist/core/config/providers-store.js +89 -0
- package/dist/core/config/schema.js +60 -0
- package/dist/core/config/session-store.js +30 -0
- package/dist/core/config/usage-store.js +18 -0
- package/dist/core/context/context-service.js +186 -0
- package/dist/core/integrations/integration-state.js +105 -0
- package/dist/core/logs/log-service.js +56 -0
- package/dist/core/memory/embedding-check.js +121 -0
- package/dist/core/memory/memory-config.js +122 -0
- package/dist/core/models/model-discovery.js +430 -0
- package/dist/core/models/model-filter.js +13 -0
- package/dist/core/providers/provider-service.js +212 -0
- package/dist/core/reports/report-service.js +166 -0
- package/dist/core/runner/command-resolver.js +60 -0
- package/dist/core/runner/engine-registry.js +93 -0
- package/dist/core/runner/fallback.js +114 -0
- package/dist/core/runner/openai-usage-http.js +82 -0
- package/dist/core/runner/openai-usage-proxy.js +1 -0
- package/dist/core/runner/openai-usage-recording.js +172 -0
- package/dist/core/runner/openai-usage-responses.js +469 -0
- package/dist/core/runner/openai-usage-server.js +319 -0
- package/dist/core/runner/openai-usage-types.js +1 -0
- package/dist/core/runner/tool-runner.js +138 -0
- package/dist/core/sessions/session-service.js +47 -0
- package/dist/core/status/doctor-service.js +391 -0
- package/dist/core/status/status-service.js +60 -0
- package/dist/core/types.js +1 -0
- package/dist/core/usage/pricing.js +113 -0
- package/dist/core/usage/usage-service.js +30 -0
- package/dist/core/utils/is-record.js +3 -0
- package/dist/server/index.js +28 -0
- package/dist/server/runtime-server.js +430 -0
- package/dist/telegram/bot-registry.js +80 -0
- package/dist/telegram/bot.js +128 -0
- package/dist/telegram/config.js +235 -0
- package/dist/telegram/engine/claude-engine.js +240 -0
- package/dist/telegram/engine/codex-engine.js +437 -0
- package/dist/telegram/engine/engine-utils.js +67 -0
- package/dist/telegram/engine/process-utils.js +132 -0
- package/dist/telegram/engine/registry.js +31 -0
- package/dist/telegram/engine/types.js +1 -0
- package/dist/telegram/handler-registry.js +28 -0
- package/dist/telegram/handlers/callback.js +311 -0
- package/dist/telegram/handlers/command.js +272 -0
- package/dist/telegram/handlers/document.js +108 -0
- package/dist/telegram/handlers/memory.js +305 -0
- package/dist/telegram/handlers/message.js +701 -0
- package/dist/telegram/handlers/provider.js +332 -0
- package/dist/telegram/handlers/setup.js +527 -0
- package/dist/telegram/handlers/usage.js +124 -0
- package/dist/telegram/index.js +93 -0
- package/dist/telegram/interaction/approval.js +108 -0
- package/dist/telegram/interaction/command-menu.js +253 -0
- package/dist/telegram/interaction/formatter.js +487 -0
- package/dist/telegram/interaction/keyboards.js +145 -0
- package/dist/telegram/interaction/progress-reporter.js +160 -0
- package/dist/telegram/interaction/prompt-middleware.js +168 -0
- package/dist/telegram/interaction/result-store.js +41 -0
- package/dist/telegram/interaction/token-budget.js +21 -0
- package/dist/telegram/interaction/tool-name.js +41 -0
- package/dist/telegram/lifecycle-registry.js +47 -0
- package/dist/telegram/log.js +46 -0
- package/dist/telegram/memory/memory-inject.js +52 -0
- package/dist/telegram/memory/memory-service.js +413 -0
- package/dist/telegram/memory/memory-store.js +216 -0
- package/dist/telegram/memory/types.js +1 -0
- package/dist/telegram/network-retry.js +22 -0
- package/dist/telegram/network.js +53 -0
- package/dist/telegram/session/manager.js +229 -0
- package/dist/telegram/session/store.js +363 -0
- package/dist/telegram/session/types.js +1 -0
- package/dist/telegram/supervisor.js +57 -0
- package/dist/telegram/templates/messages.js +1 -0
- package/docs/README.md +98 -0
- package/docs/USAGE.html +853 -0
- 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
|
+
}
|