aistatus 0.0.2

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/dist/index.js ADDED
@@ -0,0 +1,1057 @@
1
+ // src/errors.ts
2
+ var AIStatusError = class extends Error {
3
+ constructor(message) {
4
+ super(message);
5
+ this.name = new.target.name;
6
+ }
7
+ };
8
+ var AllProvidersDown = class extends AIStatusError {
9
+ tried;
10
+ constructor(tried) {
11
+ super(
12
+ `All providers unavailable. Tried: ${tried.join(", ")}. Check https://aistatus.cc for current status.`
13
+ );
14
+ this.tried = tried;
15
+ }
16
+ };
17
+ var ProviderCallFailed = class extends AIStatusError {
18
+ provider;
19
+ model;
20
+ cause;
21
+ constructor(provider, model, cause) {
22
+ super(`${provider} (${model}) call failed: ${String(cause)}`);
23
+ this.provider = provider;
24
+ this.model = model;
25
+ this.cause = cause;
26
+ }
27
+ };
28
+ var NoBudgetMatch = class extends AIStatusError {
29
+ maxCost;
30
+ tier;
31
+ constructor(maxCost, tier) {
32
+ super(`No operational model in tier '${tier}' under $${maxCost}/M tokens.`);
33
+ this.maxCost = maxCost;
34
+ this.tier = tier;
35
+ }
36
+ };
37
+ var ProviderNotConfigured = class extends AIStatusError {
38
+ provider;
39
+ envName;
40
+ constructor(provider, envName) {
41
+ super(
42
+ envName ? `Provider '${provider}' is not configured. Set ${envName} or pass apiKey explicitly.` : `Provider '${provider}' is not configured. Pass apiKey explicitly.`
43
+ );
44
+ this.provider = provider;
45
+ this.envName = envName;
46
+ }
47
+ };
48
+ var ProviderNotInstalled = class extends AIStatusError {
49
+ provider;
50
+ packageName;
51
+ constructor(provider, packageName) {
52
+ super(
53
+ packageName ? `Provider '${provider}' requires package '${packageName}'.` : `Provider '${provider}' is not available in this runtime.`
54
+ );
55
+ this.provider = provider;
56
+ this.packageName = packageName;
57
+ }
58
+ };
59
+ var CheckAPIUnreachable = class extends AIStatusError {
60
+ constructor() {
61
+ super(
62
+ "Could not reach aistatus.cc API. Proceeding with provider inference only."
63
+ );
64
+ }
65
+ };
66
+
67
+ // src/http.ts
68
+ var HTTPStatusError = class extends Error {
69
+ status;
70
+ body;
71
+ constructor(status, body) {
72
+ super(`HTTP ${status}`);
73
+ this.name = "HTTPStatusError";
74
+ this.status = status;
75
+ this.body = body;
76
+ }
77
+ };
78
+ function readEnv(name) {
79
+ if (!name) {
80
+ return void 0;
81
+ }
82
+ if (typeof process === "undefined" || !process.env) {
83
+ return void 0;
84
+ }
85
+ return process.env[name];
86
+ }
87
+ function requireApiKey(provider, apiKey, envName) {
88
+ if (!apiKey) {
89
+ throw new ProviderNotConfigured(provider, envName);
90
+ }
91
+ return apiKey;
92
+ }
93
+ function joinUrl(baseUrl, path) {
94
+ return `${baseUrl.replace(/\/+$/, "")}/${path.replace(/^\/+/, "")}`;
95
+ }
96
+ function extractText(value) {
97
+ if (typeof value === "string") {
98
+ return value;
99
+ }
100
+ if (Array.isArray(value)) {
101
+ return value.map((item) => extractText(item)).filter(Boolean).join("\n");
102
+ }
103
+ if (isRecord(value)) {
104
+ if (typeof value.text === "string") {
105
+ return value.text;
106
+ }
107
+ if ("content" in value) {
108
+ return extractText(value.content);
109
+ }
110
+ }
111
+ return "";
112
+ }
113
+ function isRecord(value) {
114
+ return typeof value === "object" && value !== null && !Array.isArray(value);
115
+ }
116
+ async function fetchJson(url, init = {}) {
117
+ const controller = new AbortController();
118
+ const timeoutMs = init.timeoutMs ?? 3e3;
119
+ const timeoutId = setTimeout(() => {
120
+ controller.abort(new Error(`Request timed out after ${timeoutMs}ms`));
121
+ }, timeoutMs);
122
+ const externalSignal = init.signal;
123
+ const abortFromExternal = () => {
124
+ controller.abort(externalSignal?.reason);
125
+ };
126
+ if (externalSignal) {
127
+ if (externalSignal.aborted) {
128
+ controller.abort(externalSignal.reason);
129
+ } else {
130
+ externalSignal.addEventListener("abort", abortFromExternal, {
131
+ once: true
132
+ });
133
+ }
134
+ }
135
+ try {
136
+ const headers = new Headers(init.headers ?? {});
137
+ let body = init.body;
138
+ if (body !== void 0 && body !== null && typeof body !== "string" && !(body instanceof URLSearchParams) && !(body instanceof FormData) && !(body instanceof Blob) && !(body instanceof ArrayBuffer)) {
139
+ body = JSON.stringify(body);
140
+ }
141
+ if (typeof body === "string" && !headers.has("content-type")) {
142
+ headers.set("content-type", "application/json");
143
+ }
144
+ const response = await fetch(url, {
145
+ ...init,
146
+ headers,
147
+ body,
148
+ signal: controller.signal
149
+ });
150
+ const text = await response.text();
151
+ const payload = text ? tryParseJson(text) : null;
152
+ if (!response.ok) {
153
+ throw new HTTPStatusError(response.status, payload);
154
+ }
155
+ return payload;
156
+ } finally {
157
+ clearTimeout(timeoutId);
158
+ externalSignal?.removeEventListener("abort", abortFromExternal);
159
+ }
160
+ }
161
+ function tryParseJson(text) {
162
+ try {
163
+ return JSON.parse(text);
164
+ } catch {
165
+ return text;
166
+ }
167
+ }
168
+
169
+ // src/models.ts
170
+ var Status = /* @__PURE__ */ ((Status2) => {
171
+ Status2["OPERATIONAL"] = "operational";
172
+ Status2["DEGRADED"] = "degraded";
173
+ Status2["DOWN"] = "down";
174
+ Status2["UNKNOWN"] = "unknown";
175
+ return Status2;
176
+ })(Status || {});
177
+ var CheckResult = class {
178
+ provider;
179
+ status;
180
+ statusDetail;
181
+ model;
182
+ alternatives;
183
+ constructor(init) {
184
+ this.provider = init.provider;
185
+ this.status = init.status;
186
+ this.statusDetail = init.statusDetail ?? null;
187
+ this.model = init.model ?? null;
188
+ this.alternatives = init.alternatives ?? [];
189
+ }
190
+ get isAvailable() {
191
+ return this.status === "operational" /* OPERATIONAL */;
192
+ }
193
+ };
194
+ var RouteResponse = class {
195
+ content;
196
+ modelUsed;
197
+ providerUsed;
198
+ wasFallback;
199
+ fallbackReason;
200
+ inputTokens;
201
+ outputTokens;
202
+ costUsd;
203
+ raw;
204
+ constructor(init) {
205
+ this.content = init.content;
206
+ this.modelUsed = init.modelUsed;
207
+ this.providerUsed = init.providerUsed;
208
+ this.wasFallback = init.wasFallback;
209
+ this.fallbackReason = init.fallbackReason ?? null;
210
+ this.inputTokens = init.inputTokens ?? 0;
211
+ this.outputTokens = init.outputTokens ?? 0;
212
+ this.costUsd = init.costUsd ?? 0;
213
+ this.raw = init.raw ?? null;
214
+ }
215
+ toString() {
216
+ return this.content;
217
+ }
218
+ };
219
+
220
+ // src/defaults.ts
221
+ var AUTO_PROVIDERS = {
222
+ anthropic: {
223
+ envVar: "ANTHROPIC_API_KEY",
224
+ adapterType: "anthropic"
225
+ },
226
+ openai: {
227
+ envVar: "OPENAI_API_KEY",
228
+ adapterType: "openai"
229
+ },
230
+ google: {
231
+ envVar: "GEMINI_API_KEY",
232
+ adapterType: "google"
233
+ },
234
+ openrouter: {
235
+ envVar: "OPENROUTER_API_KEY",
236
+ adapterType: "openrouter"
237
+ },
238
+ deepseek: {
239
+ envVar: "DEEPSEEK_API_KEY",
240
+ adapterType: "deepseek"
241
+ },
242
+ mistral: {
243
+ envVar: "MISTRAL_API_KEY",
244
+ adapterType: "mistral",
245
+ aliases: ["mistralai"]
246
+ },
247
+ xai: {
248
+ envVar: "XAI_API_KEY",
249
+ adapterType: "xai",
250
+ aliases: ["x-ai"]
251
+ },
252
+ groq: {
253
+ envVar: "GROQ_API_KEY",
254
+ adapterType: "groq"
255
+ },
256
+ together: {
257
+ envVar: "TOGETHER_API_KEY",
258
+ adapterType: "together"
259
+ },
260
+ moonshot: {
261
+ envVar: "MOONSHOT_API_KEY",
262
+ adapterType: "moonshot",
263
+ aliases: ["moonshotai"]
264
+ },
265
+ qwen: {
266
+ envVar: "DASHSCOPE_API_KEY",
267
+ adapterType: "qwen"
268
+ }
269
+ };
270
+ var PROVIDER_ALIASES = {
271
+ anthropic: "anthropic",
272
+ openai: "openai",
273
+ google: "google",
274
+ openrouter: "openrouter",
275
+ deepseek: "deepseek",
276
+ mistral: "mistral",
277
+ mistralai: "mistral",
278
+ xai: "xai",
279
+ "x-ai": "xai",
280
+ groq: "groq",
281
+ together: "together",
282
+ moonshot: "moonshot",
283
+ moonshotai: "moonshot",
284
+ qwen: "qwen"
285
+ };
286
+ var MODEL_PREFIX_MAP = {
287
+ claude: "anthropic",
288
+ gpt: "openai",
289
+ o1: "openai",
290
+ o3: "openai",
291
+ o4: "openai",
292
+ chatgpt: "openai",
293
+ gemini: "google",
294
+ deepseek: "deepseek",
295
+ mistral: "mistral",
296
+ codestral: "mistral",
297
+ pixtral: "mistral",
298
+ grok: "xai",
299
+ llama: "groq",
300
+ qwen: "qwen",
301
+ moonshot: "moonshot"
302
+ };
303
+ function normalizeProviderSlug(slug) {
304
+ const value = (slug ?? "").trim().toLowerCase();
305
+ return PROVIDER_ALIASES[value] ?? value;
306
+ }
307
+ function extractProviderSlug(modelId) {
308
+ const value = (modelId ?? "").trim();
309
+ if (!value.includes("/")) {
310
+ return null;
311
+ }
312
+ return normalizeProviderSlug(value.split("/", 1)[0]);
313
+ }
314
+
315
+ // src/providers/base.ts
316
+ var ProviderAdapter = class {
317
+ config;
318
+ constructor(config) {
319
+ this.config = {
320
+ ...config,
321
+ slug: normalizeProviderSlug(config.slug),
322
+ aliases: (config.aliases ?? []).map((alias) => normalizeProviderSlug(alias))
323
+ };
324
+ }
325
+ get slug() {
326
+ return this.config.slug;
327
+ }
328
+ get aliases() {
329
+ return this.config.aliases ?? [];
330
+ }
331
+ supportsProvider(slug) {
332
+ const normalized = normalizeProviderSlug(slug);
333
+ return normalized === this.slug || this.aliases.includes(normalized);
334
+ }
335
+ stripProvider(modelId) {
336
+ if (!modelId.includes("/")) {
337
+ return modelId;
338
+ }
339
+ return modelId.slice(modelId.indexOf("/") + 1);
340
+ }
341
+ };
342
+ var ADAPTER_TYPES = /* @__PURE__ */ new Map();
343
+ function registerAdapterType(typeName, ctor) {
344
+ ADAPTER_TYPES.set(typeName.toLowerCase(), ctor);
345
+ }
346
+ function createAdapter(config) {
347
+ const ctor = ADAPTER_TYPES.get(config.adapterType.toLowerCase());
348
+ if (!ctor) {
349
+ throw new Error(`Unknown adapter type: ${config.adapterType}`);
350
+ }
351
+ return new ctor(config);
352
+ }
353
+
354
+ // src/providers/openai.ts
355
+ var OpenAIAdapter = class extends ProviderAdapter {
356
+ defaultBaseUrl = "https://api.openai.com/v1";
357
+ defaultEnvVar = "OPENAI_API_KEY";
358
+ getBaseUrl() {
359
+ return this.config.baseUrl ?? this.defaultBaseUrl;
360
+ }
361
+ getApiKey() {
362
+ return requireApiKey(
363
+ this.slug,
364
+ this.config.apiKey ?? readEnv(this.config.env ?? this.defaultEnvVar),
365
+ this.config.env ?? this.defaultEnvVar
366
+ );
367
+ }
368
+ getDefaultHeaders() {
369
+ return {};
370
+ }
371
+ buildPayload(modelId, messages, options) {
372
+ const payload = {
373
+ ...options.providerOptions ?? {},
374
+ model: this.stripProvider(modelId),
375
+ messages
376
+ };
377
+ if (options.maxTokens !== void 0 && payload.max_tokens === void 0 && payload.max_completion_tokens === void 0) {
378
+ payload.max_tokens = options.maxTokens;
379
+ }
380
+ if (options.temperature !== void 0 && payload.temperature === void 0) {
381
+ payload.temperature = options.temperature;
382
+ }
383
+ if (options.topP !== void 0 && payload.top_p === void 0) {
384
+ payload.top_p = options.topP;
385
+ }
386
+ return payload;
387
+ }
388
+ async call(modelId, messages, timeoutSeconds, options) {
389
+ const response = await fetchJson(
390
+ joinUrl(this.getBaseUrl(), "chat/completions"),
391
+ {
392
+ method: "POST",
393
+ headers: {
394
+ authorization: `Bearer ${this.getApiKey()}`,
395
+ ...this.getDefaultHeaders(),
396
+ ...this.config.headers ?? {},
397
+ ...options.headers ?? {}
398
+ },
399
+ body: this.buildPayload(modelId, messages, options),
400
+ timeoutMs: timeoutSeconds * 1e3,
401
+ signal: options.signal
402
+ }
403
+ );
404
+ return this.toResponse(response, modelId);
405
+ }
406
+ toResponse(response, modelId) {
407
+ return new RouteResponse({
408
+ content: extractText(response.choices?.[0]?.message?.content),
409
+ modelUsed: modelId,
410
+ providerUsed: this.slug,
411
+ wasFallback: false,
412
+ inputTokens: response.usage?.prompt_tokens ?? 0,
413
+ outputTokens: response.usage?.completion_tokens ?? 0,
414
+ raw: response
415
+ });
416
+ }
417
+ };
418
+ registerAdapterType("openai", OpenAIAdapter);
419
+
420
+ // src/providers/openrouter.ts
421
+ var OpenRouterAdapter = class extends OpenAIAdapter {
422
+ defaultBaseUrl = "https://openrouter.ai/api/v1";
423
+ defaultEnvVar = "OPENROUTER_API_KEY";
424
+ getDefaultHeaders() {
425
+ return {
426
+ "HTTP-Referer": "https://aistatus.cc",
427
+ "X-Title": "aistatus"
428
+ };
429
+ }
430
+ };
431
+ registerAdapterType("openrouter", OpenRouterAdapter);
432
+
433
+ // src/providers/anthropic.ts
434
+ var AnthropicAdapter = class extends ProviderAdapter {
435
+ defaultBaseUrl = "https://api.anthropic.com/v1";
436
+ defaultEnvVar = "ANTHROPIC_API_KEY";
437
+ async call(modelId, messages, timeoutSeconds, options) {
438
+ const systemParts = [];
439
+ const anthropicMessages = messages.filter((message) => {
440
+ if (message.role === "system") {
441
+ systemParts.push(message.content);
442
+ return false;
443
+ }
444
+ return true;
445
+ }).map((message) => ({
446
+ role: message.role === "assistant" ? "assistant" : "user",
447
+ content: message.content
448
+ }));
449
+ const payload = {
450
+ ...options.providerOptions ?? {},
451
+ model: this.stripProvider(modelId),
452
+ messages: anthropicMessages
453
+ };
454
+ if (systemParts.length > 0 && payload.system === void 0) {
455
+ payload.system = systemParts.join("\n\n");
456
+ }
457
+ if (payload.max_tokens === void 0) {
458
+ payload.max_tokens = options.maxTokens ?? 4096;
459
+ }
460
+ if (options.temperature !== void 0 && payload.temperature === void 0) {
461
+ payload.temperature = options.temperature;
462
+ }
463
+ if (options.topP !== void 0 && payload.top_p === void 0) {
464
+ payload.top_p = options.topP;
465
+ }
466
+ const response = await fetchJson(
467
+ joinUrl(this.config.baseUrl ?? this.defaultBaseUrl, "messages"),
468
+ {
469
+ method: "POST",
470
+ headers: {
471
+ "x-api-key": requireApiKey(
472
+ this.slug,
473
+ this.config.apiKey ?? readEnv(this.config.env ?? this.defaultEnvVar),
474
+ this.config.env ?? this.defaultEnvVar
475
+ ),
476
+ "anthropic-version": "2023-06-01",
477
+ ...this.config.headers ?? {},
478
+ ...options.headers ?? {}
479
+ },
480
+ body: payload,
481
+ timeoutMs: timeoutSeconds * 1e3,
482
+ signal: options.signal
483
+ }
484
+ );
485
+ return new RouteResponse({
486
+ content: (response.content ?? []).filter((block) => block.type === "text" && typeof block.text === "string").map((block) => block.text).join("\n"),
487
+ modelUsed: modelId,
488
+ providerUsed: this.slug,
489
+ wasFallback: false,
490
+ inputTokens: response.usage?.input_tokens ?? 0,
491
+ outputTokens: response.usage?.output_tokens ?? 0,
492
+ raw: response
493
+ });
494
+ }
495
+ };
496
+ registerAdapterType("anthropic", AnthropicAdapter);
497
+
498
+ // src/providers/google.ts
499
+ var GoogleAdapter = class extends ProviderAdapter {
500
+ defaultBaseUrl = "https://generativelanguage.googleapis.com/v1beta";
501
+ defaultEnvVar = "GEMINI_API_KEY";
502
+ async call(modelId, messages, timeoutSeconds, options) {
503
+ const providerOptions = { ...options.providerOptions ?? {} };
504
+ const generationConfig = isRecord(providerOptions.generationConfig) ? { ...providerOptions.generationConfig } : {};
505
+ const systemParts = [];
506
+ const contents = messages.filter((message) => {
507
+ if (message.role === "system") {
508
+ systemParts.push(message.content);
509
+ return false;
510
+ }
511
+ return true;
512
+ }).map((message) => ({
513
+ role: message.role === "assistant" ? "model" : "user",
514
+ parts: [{ text: message.content }]
515
+ }));
516
+ if (options.maxTokens !== void 0 && generationConfig.maxOutputTokens === void 0) {
517
+ generationConfig.maxOutputTokens = options.maxTokens;
518
+ }
519
+ if (options.temperature !== void 0 && generationConfig.temperature === void 0) {
520
+ generationConfig.temperature = options.temperature;
521
+ }
522
+ if (options.topP !== void 0 && generationConfig.topP === void 0) {
523
+ generationConfig.topP = options.topP;
524
+ }
525
+ const body = {
526
+ ...providerOptions,
527
+ contents
528
+ };
529
+ if (Object.keys(generationConfig).length > 0) {
530
+ body.generationConfig = generationConfig;
531
+ }
532
+ if (systemParts.length > 0 && body.systemInstruction === void 0) {
533
+ body.systemInstruction = {
534
+ parts: [{ text: systemParts.join("\n\n") }]
535
+ };
536
+ }
537
+ const modelName = normalizeGoogleModelName(this.stripProvider(modelId));
538
+ const apiKey = requireApiKey(
539
+ this.slug,
540
+ this.config.apiKey ?? readEnv(this.config.env ?? this.defaultEnvVar),
541
+ this.config.env ?? this.defaultEnvVar
542
+ );
543
+ const url = new URL(
544
+ `${this.config.baseUrl ?? this.defaultBaseUrl}/models/${modelName}:generateContent`
545
+ );
546
+ url.searchParams.set("key", apiKey);
547
+ const response = await fetchJson(url.toString(), {
548
+ method: "POST",
549
+ headers: {
550
+ ...this.config.headers ?? {},
551
+ ...options.headers ?? {}
552
+ },
553
+ body,
554
+ timeoutMs: timeoutSeconds * 1e3,
555
+ signal: options.signal
556
+ });
557
+ return new RouteResponse({
558
+ content: (response.candidates?.[0]?.content?.parts ?? []).map((part) => part.text ?? "").filter(Boolean).join("\n"),
559
+ modelUsed: modelId,
560
+ providerUsed: this.slug,
561
+ wasFallback: false,
562
+ inputTokens: response.usageMetadata?.promptTokenCount ?? 0,
563
+ outputTokens: response.usageMetadata?.candidatesTokenCount ?? 0,
564
+ raw: response
565
+ });
566
+ }
567
+ };
568
+ function normalizeGoogleModelName(modelName) {
569
+ return modelName.startsWith("models/") ? modelName.slice(7) : modelName;
570
+ }
571
+ registerAdapterType("google", GoogleAdapter);
572
+
573
+ // src/providers/compatible.ts
574
+ var DeepSeekAdapter = class extends OpenAIAdapter {
575
+ defaultBaseUrl = "https://api.deepseek.com";
576
+ defaultEnvVar = "DEEPSEEK_API_KEY";
577
+ };
578
+ var MistralAdapter = class extends OpenAIAdapter {
579
+ defaultBaseUrl = "https://api.mistral.ai/v1";
580
+ defaultEnvVar = "MISTRAL_API_KEY";
581
+ };
582
+ var XAIAdapter = class extends OpenAIAdapter {
583
+ defaultBaseUrl = "https://api.x.ai/v1";
584
+ defaultEnvVar = "XAI_API_KEY";
585
+ };
586
+ var GroqAdapter = class extends OpenAIAdapter {
587
+ defaultBaseUrl = "https://api.groq.com/openai/v1";
588
+ defaultEnvVar = "GROQ_API_KEY";
589
+ };
590
+ var TogetherAdapter = class extends OpenAIAdapter {
591
+ defaultBaseUrl = "https://api.together.xyz/v1";
592
+ defaultEnvVar = "TOGETHER_API_KEY";
593
+ };
594
+ var MoonshotAdapter = class extends OpenAIAdapter {
595
+ defaultBaseUrl = "https://api.moonshot.cn/v1";
596
+ defaultEnvVar = "MOONSHOT_API_KEY";
597
+ };
598
+ var QwenAdapter = class extends OpenAIAdapter {
599
+ defaultBaseUrl = "https://dashscope.aliyuncs.com/compatible-mode/v1";
600
+ defaultEnvVar = "DASHSCOPE_API_KEY";
601
+ };
602
+ registerAdapterType("deepseek", DeepSeekAdapter);
603
+ registerAdapterType("mistral", MistralAdapter);
604
+ registerAdapterType("xai", XAIAdapter);
605
+ registerAdapterType("groq", GroqAdapter);
606
+ registerAdapterType("together", TogetherAdapter);
607
+ registerAdapterType("moonshot", MoonshotAdapter);
608
+ registerAdapterType("qwen", QwenAdapter);
609
+
610
+ // src/api.ts
611
+ var BASE_URL = "https://aistatus.cc";
612
+ var StatusAPI = class {
613
+ baseUrl;
614
+ timeoutMs;
615
+ constructor(baseUrl = BASE_URL, timeoutSeconds = 3) {
616
+ this.baseUrl = baseUrl.replace(/\/+$/, "");
617
+ this.timeoutMs = timeoutSeconds * 1e3;
618
+ }
619
+ async checkProvider(slug) {
620
+ const data = await this.get("/api/check", {
621
+ provider: slug
622
+ });
623
+ return this.parseCheck(data);
624
+ }
625
+ async checkModel(modelId) {
626
+ const data = await this.get("/api/check", {
627
+ model: modelId
628
+ });
629
+ return this.parseCheck(data);
630
+ }
631
+ async acheckProvider(slug) {
632
+ return this.checkProvider(slug);
633
+ }
634
+ async acheckModel(modelId) {
635
+ return this.checkModel(modelId);
636
+ }
637
+ async providers() {
638
+ const data = await this.get("/api/providers");
639
+ const providers = Array.isArray(data.providers) ? data.providers : [];
640
+ return providers.map((provider) => this.parseProviderStatus(provider)).filter((provider) => provider !== null);
641
+ }
642
+ async model(modelId) {
643
+ try {
644
+ const data = await this.get(`/api/models/${encodeURI(modelId)}`);
645
+ return this.parseModel(data);
646
+ } catch (error) {
647
+ if (error instanceof HTTPStatusError && error.status === 404) {
648
+ return null;
649
+ }
650
+ throw error;
651
+ }
652
+ }
653
+ async searchModels(query) {
654
+ const data = await this.get("/api/models", {
655
+ q: query
656
+ });
657
+ const models = Array.isArray(data.models) ? data.models : [];
658
+ return models.map((model) => this.parseModel(model)).filter((model) => model !== null);
659
+ }
660
+ async get(path, params) {
661
+ const url = new URL(`${this.baseUrl}${path}`);
662
+ if (params) {
663
+ for (const [key, value] of Object.entries(params)) {
664
+ if (value !== void 0) {
665
+ url.searchParams.set(key, value);
666
+ }
667
+ }
668
+ }
669
+ return fetchJson(url.toString(), {
670
+ timeoutMs: this.timeoutMs
671
+ });
672
+ }
673
+ parseCheck(data) {
674
+ const model = asString(data.model) ?? null;
675
+ const provider = normalizeProviderSlug(
676
+ asString(data.provider) ?? asString(data.slug) ?? extractProviderSlug(model) ?? ""
677
+ ) || "";
678
+ const status = parseStatus(
679
+ asString(data.status) ?? asString(data.providerStatus) ?? availableToStatus(data.available)
680
+ );
681
+ const alternatives = Array.isArray(data.alternatives) ? data.alternatives.map((alternative) => parseAlternative(alternative)).filter((alternative) => alternative !== null) : [];
682
+ return new CheckResult({
683
+ provider,
684
+ status,
685
+ statusDetail: asString(data.statusDetail) ?? asString(data.providerStatusDetail) ?? null,
686
+ model,
687
+ alternatives
688
+ });
689
+ }
690
+ parseProviderStatus(value) {
691
+ if (!isRecord(value)) {
692
+ return null;
693
+ }
694
+ return {
695
+ slug: normalizeProviderSlug(asString(value.slug) ?? ""),
696
+ name: asString(value.name) ?? asString(value.slug) ?? "",
697
+ status: parseStatus(asString(value.status)),
698
+ statusDetail: asString(value.statusDetail) ?? null,
699
+ modelCount: asNumber(value.modelCount)
700
+ };
701
+ }
702
+ parseModel(value) {
703
+ if (!isRecord(value)) {
704
+ return null;
705
+ }
706
+ const pricing = isRecord(value.pricing) ? value.pricing : {};
707
+ const provider = isRecord(value.provider) ? value.provider : {};
708
+ return {
709
+ id: asString(value.id) ?? "",
710
+ name: asString(value.name) ?? "",
711
+ providerSlug: normalizeProviderSlug(
712
+ asString(provider.slug) ?? extractProviderSlug(asString(value.id) ?? "") ?? ""
713
+ ),
714
+ contextLength: asNumber(value.context_length),
715
+ modality: asString(value.modality) ?? "text->text",
716
+ promptPrice: asFloat(pricing.prompt),
717
+ completionPrice: asFloat(pricing.completion)
718
+ };
719
+ }
720
+ };
721
+ function parseAlternative(value) {
722
+ if (!isRecord(value)) {
723
+ return null;
724
+ }
725
+ const suggestedModel = asString(value.suggestedModel) ?? asString(value.model) ?? asString(value.id) ?? "";
726
+ const slug = normalizeProviderSlug(
727
+ asString(value.slug) ?? asString(value.provider) ?? extractProviderSlug(suggestedModel) ?? ""
728
+ );
729
+ return {
730
+ slug,
731
+ name: asString(value.name) ?? slug,
732
+ status: parseStatus(
733
+ asString(value.status) ?? asString(value.providerStatus) ?? availableToStatus(value.available)
734
+ ),
735
+ suggestedModel
736
+ };
737
+ }
738
+ function availableToStatus(value) {
739
+ if (value === true) {
740
+ return "operational" /* OPERATIONAL */;
741
+ }
742
+ if (value === false) {
743
+ return "down" /* DOWN */;
744
+ }
745
+ return void 0;
746
+ }
747
+ function parseStatus(value) {
748
+ switch (value) {
749
+ case "operational" /* OPERATIONAL */:
750
+ return "operational" /* OPERATIONAL */;
751
+ case "degraded" /* DEGRADED */:
752
+ return "degraded" /* DEGRADED */;
753
+ case "down" /* DOWN */:
754
+ return "down" /* DOWN */;
755
+ default:
756
+ return "unknown" /* UNKNOWN */;
757
+ }
758
+ }
759
+ function asString(value) {
760
+ return typeof value === "string" ? value : void 0;
761
+ }
762
+ function asNumber(value) {
763
+ if (typeof value === "number" && Number.isFinite(value)) {
764
+ return value;
765
+ }
766
+ if (typeof value === "string") {
767
+ const parsed = Number(value);
768
+ return Number.isFinite(parsed) ? parsed : 0;
769
+ }
770
+ return 0;
771
+ }
772
+ function asFloat(value) {
773
+ return asNumber(value);
774
+ }
775
+
776
+ // src/router.ts
777
+ var Router = class {
778
+ api;
779
+ adapters = /* @__PURE__ */ new Map();
780
+ adapterIndex = /* @__PURE__ */ new Map();
781
+ tiers = /* @__PURE__ */ new Map();
782
+ constructor(options = {}) {
783
+ this.api = new StatusAPI(
784
+ options.baseUrl,
785
+ options.checkTimeout ?? 3
786
+ );
787
+ if (options.autoDiscover !== false) {
788
+ this.autoDiscover(options.providers);
789
+ }
790
+ }
791
+ registerProvider(config) {
792
+ const adapter = createAdapter(config);
793
+ this.adapters.set(adapter.slug, adapter);
794
+ this.indexAdapter(adapter);
795
+ }
796
+ addTier(name, models) {
797
+ this.tiers.set(name, [...models]);
798
+ }
799
+ async route(messages, options = {}) {
800
+ const normalizedMessages = this.normalizeMessages(messages, options.system);
801
+ const callOptions = this.extractCallOptions(options);
802
+ if (!options.model && !options.tier) {
803
+ throw new Error("Either 'model' or 'tier' must be specified");
804
+ }
805
+ if (options.tier) {
806
+ return this.routeTier(normalizedMessages, options.tier, options, callOptions);
807
+ }
808
+ return this.routeModel(
809
+ normalizedMessages,
810
+ options.model,
811
+ options,
812
+ callOptions
813
+ );
814
+ }
815
+ async aroute(messages, options = {}) {
816
+ return this.route(messages, options);
817
+ }
818
+ autoDiscover(only) {
819
+ for (const [slug, spec] of Object.entries(AUTO_PROVIDERS)) {
820
+ if (only && !only.includes(slug)) {
821
+ continue;
822
+ }
823
+ if (!readEnv(spec.envVar)) {
824
+ continue;
825
+ }
826
+ this.registerProvider({
827
+ slug,
828
+ adapterType: spec.adapterType,
829
+ env: spec.envVar,
830
+ aliases: spec.aliases
831
+ });
832
+ }
833
+ }
834
+ indexAdapter(adapter) {
835
+ this.adapterIndex.set(adapter.slug, adapter.slug);
836
+ for (const alias of adapter.aliases) {
837
+ this.adapterIndex.set(alias, adapter.slug);
838
+ }
839
+ }
840
+ normalizeMessages(messages, system) {
841
+ const normalized = typeof messages === "string" ? [{ role: "user", content: messages }] : [...messages];
842
+ if (system) {
843
+ normalized.unshift({ role: "system", content: system });
844
+ }
845
+ return normalized;
846
+ }
847
+ extractCallOptions(options) {
848
+ const {
849
+ model: _model,
850
+ tier: _tier,
851
+ system: _system,
852
+ allowFallback: _allowFallback,
853
+ timeout: _timeout,
854
+ prefer: _prefer,
855
+ maxTokens,
856
+ temperature,
857
+ topP,
858
+ providerOptions,
859
+ headers,
860
+ signal,
861
+ ...extraProviderOptions
862
+ } = options;
863
+ return {
864
+ maxTokens,
865
+ temperature,
866
+ topP,
867
+ providerOptions: {
868
+ ...extraProviderOptions,
869
+ ...providerOptions ?? {}
870
+ },
871
+ headers,
872
+ signal
873
+ };
874
+ }
875
+ async resolveModel(model, prefer) {
876
+ try {
877
+ const check = await this.api.checkModel(model);
878
+ const primaryProvider = check.provider || this.guessProvider(model)[0]?.providerSlug;
879
+ const primary = {
880
+ providerSlug: primaryProvider,
881
+ modelId: check.model ?? model
882
+ };
883
+ const alternatives = check.alternatives.filter((alternative) => alternative.status === "operational").map((alternative) => ({
884
+ providerSlug: alternative.slug,
885
+ modelId: alternative.suggestedModel || model
886
+ }));
887
+ const ordered = check.isAvailable ? [primary, ...alternatives] : [...alternatives, primary];
888
+ return this.sortAndBindCandidates(ordered, prefer);
889
+ } catch {
890
+ return this.sortAndBindCandidates(this.guessProvider(model), prefer);
891
+ }
892
+ }
893
+ guessProvider(model) {
894
+ const normalizedModel = model.toLowerCase();
895
+ const directProvider = normalizeProviderSlug(model.split("/", 1)[0]);
896
+ if (model.includes("/") && this.adapterIndex.has(directProvider)) {
897
+ return [{ providerSlug: directProvider, modelId: model }];
898
+ }
899
+ for (const [prefix, providerSlug] of Object.entries(MODEL_PREFIX_MAP)) {
900
+ if (normalizedModel.startsWith(prefix)) {
901
+ return [{ providerSlug, modelId: model }];
902
+ }
903
+ }
904
+ return Array.from(this.adapters.keys()).map((adapterKey) => ({
905
+ providerSlug: adapterKey,
906
+ modelId: model
907
+ }));
908
+ }
909
+ sortAndBindCandidates(candidates, prefer) {
910
+ const preferOrder = (prefer ?? []).map((item) => normalizeProviderSlug(item));
911
+ const resolved = candidates.map((candidate) => {
912
+ const providerSlug = normalizeProviderSlug(candidate.providerSlug);
913
+ const adapterKey = this.adapterIndex.get(providerSlug);
914
+ if (!adapterKey) {
915
+ return null;
916
+ }
917
+ return {
918
+ providerSlug,
919
+ adapterKey,
920
+ modelId: candidate.modelId
921
+ };
922
+ }).filter((candidate) => candidate !== null);
923
+ resolved.sort((left, right) => {
924
+ if (preferOrder.length === 0) {
925
+ return 0;
926
+ }
927
+ return preferenceScore(left, preferOrder) - preferenceScore(right, preferOrder);
928
+ });
929
+ return dedupeCandidates(resolved);
930
+ }
931
+ async routeModel(messages, model, options, callOptions) {
932
+ const candidates = await this.resolveModel(model, options.prefer);
933
+ if (candidates.length === 0) {
934
+ throw new AllProvidersDown([`no adapter for model '${model}'`]);
935
+ }
936
+ const tried = [];
937
+ const first = candidates[0];
938
+ for (const candidate of candidates) {
939
+ const adapter = this.adapters.get(candidate.adapterKey);
940
+ if (!adapter) {
941
+ continue;
942
+ }
943
+ try {
944
+ const response = await adapter.call(
945
+ candidate.modelId,
946
+ messages,
947
+ options.timeout ?? 30,
948
+ callOptions
949
+ );
950
+ const usedModel = response.modelUsed.includes("/") ? response.modelUsed : `${candidate.providerSlug}/${response.modelUsed}`;
951
+ const isFallback = candidate.providerSlug !== first.providerSlug || candidate.modelId !== first.modelId;
952
+ return new RouteResponse({
953
+ content: response.content,
954
+ modelUsed: usedModel,
955
+ providerUsed: candidate.providerSlug,
956
+ wasFallback: isFallback,
957
+ fallbackReason: isFallback ? `${first.providerSlug} unavailable` : null,
958
+ inputTokens: response.inputTokens,
959
+ outputTokens: response.outputTokens,
960
+ costUsd: response.costUsd,
961
+ raw: response.raw
962
+ });
963
+ } catch (error) {
964
+ tried.push(`${candidate.providerSlug}[error]`);
965
+ if (options.allowFallback === false) {
966
+ throw new ProviderCallFailed(
967
+ candidate.providerSlug,
968
+ candidate.modelId,
969
+ error
970
+ );
971
+ }
972
+ }
973
+ }
974
+ throw new AllProvidersDown(tried);
975
+ }
976
+ async routeTier(messages, tier, options, callOptions) {
977
+ const models = this.tiers.get(tier);
978
+ if (!models) {
979
+ throw new Error(
980
+ `Tier '${tier}' not configured. Use router.addTier('${tier}', ['model-a']) first.`
981
+ );
982
+ }
983
+ const tried = [];
984
+ for (const model of models) {
985
+ try {
986
+ return await this.routeModel(messages, model, options, callOptions);
987
+ } catch (error) {
988
+ if (error instanceof AllProvidersDown) {
989
+ tried.push(...error.tried);
990
+ continue;
991
+ }
992
+ if (error instanceof ProviderCallFailed && options.allowFallback === false) {
993
+ throw error;
994
+ }
995
+ }
996
+ }
997
+ throw new AllProvidersDown(tried);
998
+ }
999
+ };
1000
+ function preferenceScore(candidate, preferOrder) {
1001
+ const providerIndex = preferOrder.indexOf(candidate.providerSlug);
1002
+ if (providerIndex !== -1) {
1003
+ return providerIndex;
1004
+ }
1005
+ const adapterIndex = preferOrder.indexOf(candidate.adapterKey);
1006
+ if (adapterIndex !== -1) {
1007
+ return adapterIndex;
1008
+ }
1009
+ return preferOrder.length;
1010
+ }
1011
+ function dedupeCandidates(candidates) {
1012
+ const seen = /* @__PURE__ */ new Set();
1013
+ const result = [];
1014
+ for (const candidate of candidates) {
1015
+ const key = `${candidate.adapterKey}:${candidate.modelId}`;
1016
+ if (seen.has(key)) {
1017
+ continue;
1018
+ }
1019
+ seen.add(key);
1020
+ result.push(candidate);
1021
+ }
1022
+ return result;
1023
+ }
1024
+
1025
+ // src/index.ts
1026
+ var VERSION = "0.0.2";
1027
+ var defaultRouter = null;
1028
+ function getDefaultRouter() {
1029
+ defaultRouter ??= new Router();
1030
+ return defaultRouter;
1031
+ }
1032
+ async function route(messages, options = {}) {
1033
+ return getDefaultRouter().route(messages, options);
1034
+ }
1035
+ async function aroute(messages, options = {}) {
1036
+ return route(messages, options);
1037
+ }
1038
+ export {
1039
+ AIStatusError,
1040
+ AllProvidersDown,
1041
+ CheckAPIUnreachable,
1042
+ CheckResult,
1043
+ NoBudgetMatch,
1044
+ ProviderAdapter,
1045
+ ProviderCallFailed,
1046
+ ProviderNotConfigured,
1047
+ ProviderNotInstalled,
1048
+ RouteResponse,
1049
+ Router,
1050
+ Status,
1051
+ StatusAPI,
1052
+ VERSION,
1053
+ aroute,
1054
+ createAdapter,
1055
+ registerAdapterType,
1056
+ route
1057
+ };