llmist 15.2.2 → 15.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -1326,24 +1326,23 @@ function isKnownModelPattern(model) {
1326
1326
  return KNOWN_MODEL_PATTERNS.some((pattern) => pattern.test(model));
1327
1327
  }
1328
1328
  function resolveModel(model, options = {}) {
1329
- if (model.includes(":")) {
1330
- return model;
1331
- }
1332
1329
  const normalized = model.toLowerCase();
1333
1330
  if (MODEL_ALIASES[normalized]) {
1334
1331
  return MODEL_ALIASES[normalized];
1335
1332
  }
1336
- const modelLower = model.toLowerCase();
1337
- if (modelLower.startsWith("gpt")) {
1333
+ if (model.includes(":")) {
1334
+ return model;
1335
+ }
1336
+ if (normalized.startsWith("gpt")) {
1338
1337
  return `openai:${model}`;
1339
1338
  }
1340
- if (modelLower.startsWith("claude")) {
1339
+ if (normalized.startsWith("claude")) {
1341
1340
  return `anthropic:${model}`;
1342
1341
  }
1343
- if (modelLower.startsWith("gemini")) {
1342
+ if (normalized.startsWith("gemini")) {
1344
1343
  return `gemini:${model}`;
1345
1344
  }
1346
- if (modelLower.match(/^o\d/)) {
1345
+ if (normalized.match(/^o\d/)) {
1347
1346
  return `openai:${model}`;
1348
1347
  }
1349
1348
  if (!isKnownModelPattern(model)) {
@@ -1408,7 +1407,16 @@ var init_model_shortcuts = __esm({
1408
1407
  "gemini-flash": "gemini:gemini-2.5-flash",
1409
1408
  "flash-lite": "gemini:gemini-2.5-flash-lite",
1410
1409
  "gemini-pro": "gemini:gemini-3-pro-preview",
1411
- pro: "gemini:gemini-3-pro-preview"
1410
+ pro: "gemini:gemini-3-pro-preview",
1411
+ // OpenRouter aliases (or: prefix for short)
1412
+ "or:sonnet": "openrouter:anthropic/claude-sonnet-4-5",
1413
+ "or:opus": "openrouter:anthropic/claude-opus-4-5",
1414
+ "or:haiku": "openrouter:anthropic/claude-haiku-4-5",
1415
+ "or:gpt4o": "openrouter:openai/gpt-4o",
1416
+ "or:gpt5": "openrouter:openai/gpt-5.2",
1417
+ "or:flash": "openrouter:google/gemini-2.5-flash",
1418
+ "or:llama": "openrouter:meta-llama/llama-3.3-70b-instruct",
1419
+ "or:deepseek": "openrouter:deepseek/deepseek-r1"
1412
1420
  };
1413
1421
  KNOWN_MODEL_PATTERNS = [
1414
1422
  /^gpt-?\d/i,
@@ -1738,6 +1746,9 @@ function isRetryableError(error) {
1738
1746
  if (message.includes("overloaded_error") || message.includes("api_error")) {
1739
1747
  return true;
1740
1748
  }
1749
+ if (message.includes("hf bad request")) {
1750
+ return true;
1751
+ }
1741
1752
  if (message.includes("401") || message.includes("403") || message.includes("400") || message.includes("404") || message.includes("authentication") || message.includes("unauthorized") || message.includes("forbidden") || message.includes("invalid") || message.includes("content policy") || name === "AuthenticationError" || name === "BadRequestError" || name === "NotFoundError" || name === "PermissionDeniedError") {
1742
1753
  return false;
1743
1754
  }
@@ -6259,84 +6270,111 @@ var init_huggingface_models = __esm({
6259
6270
  }
6260
6271
  });
6261
6272
 
6262
- // src/providers/huggingface.ts
6263
- function createHuggingFaceProviderFromEnv() {
6264
- const token = readEnvVar("HF_TOKEN") || readEnvVar("HUGGING_FACE_API_KEY");
6265
- if (!isNonEmpty(token)) {
6266
- return null;
6267
- }
6268
- if (!token.startsWith("hf_")) {
6269
- console.warn(
6270
- "Warning: HF token should start with 'hf_'. Authentication may fail if token format is incorrect."
6271
- );
6272
- }
6273
- const endpointUrl = readEnvVar("HF_ENDPOINT_URL");
6274
- const baseURL = endpointUrl || "https://router.huggingface.co/v1";
6275
- const endpointType = endpointUrl ? "dedicated" : "serverless";
6276
- const client = new import_openai.default({
6277
- apiKey: token.trim(),
6278
- baseURL,
6279
- timeout: 6e4,
6280
- // 60s timeout - HF free tier can be slower than OpenAI
6281
- maxRetries: 0
6282
- // Disable SDK retries - llmist handles all retries at application level
6283
- });
6284
- return new HuggingFaceProvider(client, endpointType);
6285
- }
6286
- var import_openai, ROLE_MAP, HuggingFaceProvider;
6287
- var init_huggingface = __esm({
6288
- "src/providers/huggingface.ts"() {
6273
+ // src/providers/openai-compatible-provider.ts
6274
+ var import_openai, ROLE_MAP, OpenAICompatibleProvider;
6275
+ var init_openai_compatible_provider = __esm({
6276
+ "src/providers/openai-compatible-provider.ts"() {
6289
6277
  "use strict";
6290
6278
  import_openai = __toESM(require("openai"), 1);
6291
6279
  init_messages();
6292
6280
  init_base_provider();
6293
6281
  init_constants2();
6294
- init_huggingface_models();
6295
- init_utils();
6296
6282
  ROLE_MAP = {
6297
6283
  system: "system",
6298
6284
  user: "user",
6299
6285
  assistant: "assistant"
6300
6286
  };
6301
- HuggingFaceProvider = class extends BaseProviderAdapter {
6302
- providerId = "huggingface";
6303
- endpointType;
6304
- constructor(client, endpointType = "serverless") {
6287
+ OpenAICompatibleProvider = class extends BaseProviderAdapter {
6288
+ /**
6289
+ * Short alias for the provider (e.g., "or" for openrouter, "hf" for huggingface).
6290
+ * If not set, only the full providerId is accepted.
6291
+ */
6292
+ providerAlias;
6293
+ config;
6294
+ constructor(client, config) {
6305
6295
  super(client);
6306
- this.endpointType = endpointType;
6296
+ this.config = config;
6307
6297
  }
6298
+ /**
6299
+ * Check if this provider supports the given model descriptor.
6300
+ * Accepts both the full providerId and the short alias.
6301
+ */
6308
6302
  supports(descriptor) {
6309
- return descriptor.provider === this.providerId || descriptor.provider === "hf";
6303
+ return descriptor.provider === this.providerId || this.providerAlias !== void 0 && descriptor.provider === this.providerAlias;
6310
6304
  }
6311
- getModelSpecs() {
6312
- return HUGGINGFACE_MODELS;
6305
+ /**
6306
+ * Get custom headers to include in requests.
6307
+ * Override in subclasses for provider-specific headers.
6308
+ */
6309
+ getCustomHeaders() {
6310
+ return this.config.customHeaders ?? {};
6311
+ }
6312
+ /**
6313
+ * Enhance error messages with provider-specific guidance.
6314
+ * Override in subclasses for better error messages.
6315
+ */
6316
+ enhanceError(error) {
6317
+ if (error instanceof Error) {
6318
+ return error;
6319
+ }
6320
+ return new Error(String(error));
6321
+ }
6322
+ /**
6323
+ * Build provider-specific request parameters.
6324
+ * Override in subclasses to add custom parameters from `extra`.
6325
+ *
6326
+ * @param extra - The extra options from LLMGenerationOptions
6327
+ * @returns Object with provider-specific params to merge into the request
6328
+ */
6329
+ buildProviderSpecificParams(_extra) {
6330
+ return {};
6313
6331
  }
6314
6332
  buildApiRequest(options, descriptor, _spec, messages) {
6315
6333
  const { maxTokens, temperature, topP, stopSequences, extra } = options;
6316
- return {
6334
+ const request = {
6317
6335
  model: descriptor.name,
6318
- messages: messages.map((message) => this.convertToHuggingFaceMessage(message)),
6319
- // HF accepts max_tokens (like many providers), though OpenAI uses max_completion_tokens
6320
- ...maxTokens !== void 0 ? { max_tokens: maxTokens } : {},
6321
- temperature,
6322
- top_p: topP,
6323
- stop: stopSequences,
6336
+ messages: messages.map((message) => this.convertMessage(message)),
6324
6337
  stream: true,
6325
- stream_options: { include_usage: true },
6326
- ...extra ?? {}
6338
+ stream_options: { include_usage: true }
6327
6339
  };
6340
+ if (maxTokens !== void 0) {
6341
+ request.max_tokens = maxTokens;
6342
+ }
6343
+ if (temperature !== void 0) {
6344
+ request.temperature = temperature;
6345
+ }
6346
+ if (topP !== void 0) {
6347
+ request.top_p = topP;
6348
+ }
6349
+ if (stopSequences) {
6350
+ request.stop = stopSequences;
6351
+ }
6352
+ const providerParams = this.buildProviderSpecificParams(extra);
6353
+ Object.assign(request, providerParams);
6354
+ if (extra) {
6355
+ const handledKeys = Object.keys(providerParams);
6356
+ for (const [key, value] of Object.entries(extra)) {
6357
+ if (!handledKeys.includes(key) && !this.isProviderSpecificKey(key)) {
6358
+ request[key] = value;
6359
+ }
6360
+ }
6361
+ }
6362
+ return request;
6328
6363
  }
6329
6364
  /**
6330
- * Convert an LLMMessage to HuggingFace's ChatCompletionMessageParam.
6331
- * HF uses OpenAI-compatible format.
6332
- * Handles role-specific content type requirements:
6333
- * - system/assistant: string content only
6334
- * - user: string or multimodal array content (for vision models)
6365
+ * Check if a key should be filtered from passthrough.
6366
+ * Override in subclasses to filter provider-specific keys from extra.
6335
6367
  */
6336
- convertToHuggingFaceMessage(message) {
6368
+ isProviderSpecificKey(_key) {
6369
+ return false;
6370
+ }
6371
+ /**
6372
+ * Convert an LLMMessage to OpenAI's ChatCompletionMessageParam format.
6373
+ */
6374
+ convertMessage(message) {
6337
6375
  const role = ROLE_MAP[message.role];
6338
6376
  if (role === "user") {
6339
- const content = this.convertToHuggingFaceContent(message.content);
6377
+ const content = this.convertContent(message.content);
6340
6378
  return {
6341
6379
  role: "user",
6342
6380
  content,
@@ -6358,11 +6396,9 @@ var init_huggingface = __esm({
6358
6396
  };
6359
6397
  }
6360
6398
  /**
6361
- * Convert llmist content to HuggingFace's content format.
6362
- * Optimizes by returning string for text-only content, array for multimodal.
6363
- * Note: Multimodal support will be added in Phase 2.
6399
+ * Convert llmist content to OpenAI's content format.
6364
6400
  */
6365
- convertToHuggingFaceContent(content) {
6401
+ convertContent(content) {
6366
6402
  if (typeof content === "string") {
6367
6403
  return content;
6368
6404
  }
@@ -6375,16 +6411,14 @@ var init_huggingface = __esm({
6375
6411
  }
6376
6412
  if (part.type === "audio") {
6377
6413
  throw new Error(
6378
- "Hugging Face chat completions do not currently support audio input in llmist. Audio support will be added in Phase 2."
6414
+ `${this.providerId} does not support audio input through llmist. Check provider docs for model-specific audio support.`
6379
6415
  );
6380
6416
  }
6381
6417
  throw new Error(`Unsupported content type: ${part.type}`);
6382
6418
  });
6383
6419
  }
6384
6420
  /**
6385
- * Convert an image content part to HuggingFace's image_url format.
6386
- * Supports both URLs and base64 data URLs (OpenAI-compatible format).
6387
- * Note: Image support requires vision-capable models on HF.
6421
+ * Convert an image content part to OpenAI's image_url format.
6388
6422
  */
6389
6423
  convertImagePart(part) {
6390
6424
  if (part.source.type === "url") {
@@ -6402,28 +6436,22 @@ var init_huggingface = __esm({
6402
6436
  }
6403
6437
  async executeStreamRequest(payload, signal) {
6404
6438
  const client = this.client;
6439
+ const headers = this.getCustomHeaders();
6440
+ const requestOptions = {};
6441
+ if (signal) {
6442
+ requestOptions.signal = signal;
6443
+ }
6444
+ if (Object.keys(headers).length > 0) {
6445
+ requestOptions.headers = headers;
6446
+ }
6405
6447
  try {
6406
- const stream2 = await client.chat.completions.create(payload, signal ? { signal } : void 0);
6448
+ const stream2 = await client.chat.completions.create(
6449
+ payload,
6450
+ Object.keys(requestOptions).length > 0 ? requestOptions : void 0
6451
+ );
6407
6452
  return stream2;
6408
6453
  } catch (error) {
6409
- if (error instanceof Error) {
6410
- if (error.message.includes("rate limit") || error.message.includes("429")) {
6411
- throw new Error(
6412
- `HF rate limit exceeded. Free tier has limits. Consider upgrading or using a dedicated endpoint. Original error: ${error.message}`
6413
- );
6414
- }
6415
- if (error.message.includes("model not found") || error.message.includes("404")) {
6416
- throw new Error(
6417
- `Model not available on HF ${this.endpointType} inference. Check model name or try a different endpoint type. Original error: ${error.message}`
6418
- );
6419
- }
6420
- if (error.message.includes("401") || error.message.includes("unauthorized")) {
6421
- throw new Error(
6422
- `HF authentication failed. Check that HF_TOKEN or HUGGING_FACE_API_KEY is set correctly and starts with 'hf_'. Original error: ${error.message}`
6423
- );
6424
- }
6425
- }
6426
- throw error;
6454
+ throw this.enhanceError(error);
6427
6455
  }
6428
6456
  }
6429
6457
  async *normalizeProviderStream(iterable) {
@@ -6438,7 +6466,6 @@ var init_huggingface = __esm({
6438
6466
  inputTokens: chunk.usage.prompt_tokens,
6439
6467
  outputTokens: chunk.usage.completion_tokens,
6440
6468
  totalTokens: chunk.usage.total_tokens,
6441
- // HF doesn't currently support prompt caching, but structure is ready
6442
6469
  cachedInputTokens: 0
6443
6470
  } : void 0;
6444
6471
  if (finishReason || usage) {
@@ -6447,21 +6474,8 @@ var init_huggingface = __esm({
6447
6474
  }
6448
6475
  }
6449
6476
  /**
6450
- * Count tokens in messages using character-based fallback estimation.
6451
- *
6452
- * Hugging Face doesn't provide a native token counting API yet, so we use
6453
- * a simple character-based heuristic (4 chars per token) which is reasonably
6454
- * accurate for most models.
6455
- *
6456
- * Future enhancement: Could integrate tiktoken for common model families
6457
- * (Llama, Mistral) that use known tokenizers.
6458
- *
6459
- * @param messages - The messages to count tokens for
6460
- * @param descriptor - Model descriptor containing the model name
6461
- * @param _spec - Optional model specification (currently unused)
6462
- * @returns Promise resolving to the estimated input token count
6463
- *
6464
- * @throws Never throws - returns 0 on error with warning
6477
+ * Count tokens using character-based fallback estimation.
6478
+ * Most meta-providers don't have a native token counting API.
6465
6479
  */
6466
6480
  async countTokens(messages, descriptor, _spec) {
6467
6481
  try {
@@ -6484,6 +6498,87 @@ var init_huggingface = __esm({
6484
6498
  }
6485
6499
  });
6486
6500
 
6501
+ // src/providers/huggingface.ts
6502
+ function createHuggingFaceProviderFromEnv() {
6503
+ const token = readEnvVar("HF_TOKEN") || readEnvVar("HUGGING_FACE_API_KEY");
6504
+ if (!isNonEmpty(token)) {
6505
+ return null;
6506
+ }
6507
+ if (!token.startsWith("hf_")) {
6508
+ console.warn(
6509
+ "Warning: HF token should start with 'hf_'. Authentication may fail if token format is incorrect."
6510
+ );
6511
+ }
6512
+ const endpointUrl = readEnvVar("HF_ENDPOINT_URL");
6513
+ const baseURL = endpointUrl || "https://router.huggingface.co/v1";
6514
+ const endpointType = endpointUrl ? "dedicated" : "serverless";
6515
+ const config = {
6516
+ endpointType
6517
+ };
6518
+ const client = new import_openai2.default({
6519
+ apiKey: token.trim(),
6520
+ baseURL,
6521
+ timeout: 6e4,
6522
+ // 60s timeout - HF free tier can be slower than OpenAI
6523
+ maxRetries: 0
6524
+ // Disable SDK retries - llmist handles all retries at application level
6525
+ });
6526
+ return new HuggingFaceProvider(client, config);
6527
+ }
6528
+ var import_openai2, HuggingFaceProvider;
6529
+ var init_huggingface = __esm({
6530
+ "src/providers/huggingface.ts"() {
6531
+ "use strict";
6532
+ import_openai2 = __toESM(require("openai"), 1);
6533
+ init_huggingface_models();
6534
+ init_openai_compatible_provider();
6535
+ init_utils();
6536
+ HuggingFaceProvider = class extends OpenAICompatibleProvider {
6537
+ providerId = "huggingface";
6538
+ providerAlias = "hf";
6539
+ constructor(client, config = {}) {
6540
+ super(client, { endpointType: "serverless", ...config });
6541
+ }
6542
+ getModelSpecs() {
6543
+ return HUGGINGFACE_MODELS;
6544
+ }
6545
+ /**
6546
+ * Enhance error messages with HuggingFace-specific guidance.
6547
+ */
6548
+ enhanceError(error) {
6549
+ if (!(error instanceof Error)) {
6550
+ return new Error(String(error));
6551
+ }
6552
+ const message = error.message.toLowerCase();
6553
+ if (message.includes("rate limit") || message.includes("429")) {
6554
+ return new Error(
6555
+ `HF rate limit exceeded. Free tier has limits. Consider upgrading or using a dedicated endpoint.
6556
+ Original error: ${error.message}`
6557
+ );
6558
+ }
6559
+ if (message.includes("model not found") || message.includes("404")) {
6560
+ return new Error(
6561
+ `Model not available on HF ${this.config.endpointType} inference. Check model name or try a different endpoint type.
6562
+ Original error: ${error.message}`
6563
+ );
6564
+ }
6565
+ if (message.includes("401") || message.includes("unauthorized")) {
6566
+ return new Error(
6567
+ `HF authentication failed. Check that HF_TOKEN or HUGGING_FACE_API_KEY is set correctly and starts with 'hf_'.
6568
+ Original error: ${error.message}`
6569
+ );
6570
+ }
6571
+ if (message.includes("400") || message.includes("bad request")) {
6572
+ return new Error(
6573
+ `HF bad request (often transient on serverless). Original error: ${error.message}`
6574
+ );
6575
+ }
6576
+ return error;
6577
+ }
6578
+ };
6579
+ }
6580
+ });
6581
+
6487
6582
  // src/providers/openai-image-models.ts
6488
6583
  function getOpenAIImageModelSpec(modelId) {
6489
6584
  return openaiImageModels.find((m) => m.modelId === modelId);
@@ -7330,13 +7425,13 @@ function sanitizeExtra(extra, allowTemperature) {
7330
7425
  return Object.fromEntries(Object.entries(extra).filter(([key]) => key !== "temperature"));
7331
7426
  }
7332
7427
  function createOpenAIProviderFromEnv() {
7333
- return createProviderFromEnv("OPENAI_API_KEY", import_openai2.default, OpenAIChatProvider);
7428
+ return createProviderFromEnv("OPENAI_API_KEY", import_openai3.default, OpenAIChatProvider);
7334
7429
  }
7335
- var import_openai2, import_tiktoken, ROLE_MAP2, OpenAIChatProvider;
7430
+ var import_openai3, import_tiktoken, ROLE_MAP2, OpenAIChatProvider;
7336
7431
  var init_openai = __esm({
7337
7432
  "src/providers/openai.ts"() {
7338
7433
  "use strict";
7339
- import_openai2 = __toESM(require("openai"), 1);
7434
+ import_openai3 = __toESM(require("openai"), 1);
7340
7435
  import_tiktoken = require("tiktoken");
7341
7436
  init_messages();
7342
7437
  init_base_provider();
@@ -7635,6 +7730,475 @@ var init_openai = __esm({
7635
7730
  }
7636
7731
  });
7637
7732
 
7733
+ // src/providers/openrouter-models.ts
7734
+ var OPENROUTER_MODELS;
7735
+ var init_openrouter_models = __esm({
7736
+ "src/providers/openrouter-models.ts"() {
7737
+ "use strict";
7738
+ OPENROUTER_MODELS = [
7739
+ // ============================================================
7740
+ // Anthropic Claude Models (via OpenRouter)
7741
+ // ============================================================
7742
+ {
7743
+ provider: "openrouter",
7744
+ modelId: "anthropic/claude-sonnet-4-5",
7745
+ displayName: "Claude Sonnet 4.5 (OpenRouter)",
7746
+ contextWindow: 2e5,
7747
+ maxOutputTokens: 64e3,
7748
+ pricing: {
7749
+ input: 3,
7750
+ output: 15
7751
+ },
7752
+ knowledgeCutoff: "2025-01",
7753
+ features: {
7754
+ streaming: true,
7755
+ functionCalling: true,
7756
+ vision: true,
7757
+ reasoning: true
7758
+ },
7759
+ metadata: {
7760
+ family: "Claude 4",
7761
+ notes: "Anthropic Claude via OpenRouter. Pricing may vary."
7762
+ }
7763
+ },
7764
+ {
7765
+ provider: "openrouter",
7766
+ modelId: "anthropic/claude-opus-4-5",
7767
+ displayName: "Claude Opus 4.5 (OpenRouter)",
7768
+ contextWindow: 2e5,
7769
+ maxOutputTokens: 64e3,
7770
+ pricing: {
7771
+ input: 15,
7772
+ output: 75
7773
+ },
7774
+ knowledgeCutoff: "2025-01",
7775
+ features: {
7776
+ streaming: true,
7777
+ functionCalling: true,
7778
+ vision: true,
7779
+ reasoning: true
7780
+ },
7781
+ metadata: {
7782
+ family: "Claude 4",
7783
+ notes: "Anthropic Claude Opus via OpenRouter. Most capable Claude model."
7784
+ }
7785
+ },
7786
+ {
7787
+ provider: "openrouter",
7788
+ modelId: "anthropic/claude-haiku-4-5",
7789
+ displayName: "Claude Haiku 4.5 (OpenRouter)",
7790
+ contextWindow: 2e5,
7791
+ maxOutputTokens: 64e3,
7792
+ pricing: {
7793
+ input: 0.8,
7794
+ output: 4
7795
+ },
7796
+ knowledgeCutoff: "2025-02",
7797
+ features: {
7798
+ streaming: true,
7799
+ functionCalling: true,
7800
+ vision: true,
7801
+ reasoning: true
7802
+ },
7803
+ metadata: {
7804
+ family: "Claude 4",
7805
+ notes: "Anthropic Claude Haiku via OpenRouter. Fast and efficient."
7806
+ }
7807
+ },
7808
+ // ============================================================
7809
+ // OpenAI GPT Models (via OpenRouter)
7810
+ // ============================================================
7811
+ {
7812
+ provider: "openrouter",
7813
+ modelId: "openai/gpt-4o",
7814
+ displayName: "GPT-4o (OpenRouter)",
7815
+ contextWindow: 128e3,
7816
+ maxOutputTokens: 16384,
7817
+ pricing: {
7818
+ input: 2.5,
7819
+ output: 10
7820
+ },
7821
+ knowledgeCutoff: "2024-10",
7822
+ features: {
7823
+ streaming: true,
7824
+ functionCalling: true,
7825
+ vision: true
7826
+ },
7827
+ metadata: {
7828
+ family: "GPT-4",
7829
+ notes: "OpenAI GPT-4o via OpenRouter."
7830
+ }
7831
+ },
7832
+ {
7833
+ provider: "openrouter",
7834
+ modelId: "openai/gpt-4o-mini",
7835
+ displayName: "GPT-4o Mini (OpenRouter)",
7836
+ contextWindow: 128e3,
7837
+ maxOutputTokens: 16384,
7838
+ pricing: {
7839
+ input: 0.15,
7840
+ output: 0.6
7841
+ },
7842
+ knowledgeCutoff: "2024-10",
7843
+ features: {
7844
+ streaming: true,
7845
+ functionCalling: true,
7846
+ vision: true
7847
+ },
7848
+ metadata: {
7849
+ family: "GPT-4",
7850
+ notes: "OpenAI GPT-4o Mini via OpenRouter. Cost-effective option."
7851
+ }
7852
+ },
7853
+ {
7854
+ provider: "openrouter",
7855
+ modelId: "openai/gpt-5.2",
7856
+ displayName: "GPT-5.2 (OpenRouter)",
7857
+ contextWindow: 1e6,
7858
+ maxOutputTokens: 128e3,
7859
+ pricing: {
7860
+ input: 5,
7861
+ output: 20
7862
+ },
7863
+ knowledgeCutoff: "2025-03",
7864
+ features: {
7865
+ streaming: true,
7866
+ functionCalling: true,
7867
+ vision: true,
7868
+ reasoning: true
7869
+ },
7870
+ metadata: {
7871
+ family: "GPT-5",
7872
+ notes: "OpenAI GPT-5.2 via OpenRouter. Latest flagship model."
7873
+ }
7874
+ },
7875
+ // ============================================================
7876
+ // Google Gemini Models (via OpenRouter)
7877
+ // ============================================================
7878
+ {
7879
+ provider: "openrouter",
7880
+ modelId: "google/gemini-2.5-flash",
7881
+ displayName: "Gemini 2.5 Flash (OpenRouter)",
7882
+ contextWindow: 1e6,
7883
+ maxOutputTokens: 65536,
7884
+ pricing: {
7885
+ input: 0.15,
7886
+ output: 0.6
7887
+ },
7888
+ knowledgeCutoff: "2025-01",
7889
+ features: {
7890
+ streaming: true,
7891
+ functionCalling: true,
7892
+ vision: true,
7893
+ reasoning: true
7894
+ },
7895
+ metadata: {
7896
+ family: "Gemini 2.5",
7897
+ notes: "Google Gemini 2.5 Flash via OpenRouter. Fast and cost-effective."
7898
+ }
7899
+ },
7900
+ {
7901
+ provider: "openrouter",
7902
+ modelId: "google/gemini-2.5-pro",
7903
+ displayName: "Gemini 2.5 Pro (OpenRouter)",
7904
+ contextWindow: 1e6,
7905
+ maxOutputTokens: 65536,
7906
+ pricing: {
7907
+ input: 2.5,
7908
+ output: 10
7909
+ },
7910
+ knowledgeCutoff: "2025-01",
7911
+ features: {
7912
+ streaming: true,
7913
+ functionCalling: true,
7914
+ vision: true,
7915
+ reasoning: true
7916
+ },
7917
+ metadata: {
7918
+ family: "Gemini 2.5",
7919
+ notes: "Google Gemini 2.5 Pro via OpenRouter."
7920
+ }
7921
+ },
7922
+ // ============================================================
7923
+ // Meta Llama Models (via OpenRouter)
7924
+ // ============================================================
7925
+ {
7926
+ provider: "openrouter",
7927
+ modelId: "meta-llama/llama-3.3-70b-instruct",
7928
+ displayName: "Llama 3.3 70B Instruct (OpenRouter)",
7929
+ contextWindow: 128e3,
7930
+ maxOutputTokens: 8192,
7931
+ pricing: {
7932
+ input: 0.4,
7933
+ output: 0.4
7934
+ },
7935
+ knowledgeCutoff: "2024-12",
7936
+ features: {
7937
+ streaming: true,
7938
+ functionCalling: true,
7939
+ vision: false
7940
+ },
7941
+ metadata: {
7942
+ family: "Llama 3.3",
7943
+ notes: "Meta Llama 3.3 70B via OpenRouter. Excellent open-source model."
7944
+ }
7945
+ },
7946
+ {
7947
+ provider: "openrouter",
7948
+ modelId: "meta-llama/llama-4-maverick",
7949
+ displayName: "Llama 4 Maverick (OpenRouter)",
7950
+ contextWindow: 1e6,
7951
+ maxOutputTokens: 128e3,
7952
+ pricing: {
7953
+ input: 0.2,
7954
+ output: 0.6
7955
+ },
7956
+ knowledgeCutoff: "2025-04",
7957
+ features: {
7958
+ streaming: true,
7959
+ functionCalling: true,
7960
+ vision: true
7961
+ },
7962
+ metadata: {
7963
+ family: "Llama 4",
7964
+ notes: "Meta Llama 4 Maverick via OpenRouter. Latest Llama generation."
7965
+ }
7966
+ },
7967
+ // ============================================================
7968
+ // DeepSeek Models (via OpenRouter)
7969
+ // ============================================================
7970
+ {
7971
+ provider: "openrouter",
7972
+ modelId: "deepseek/deepseek-r1",
7973
+ displayName: "DeepSeek R1 (OpenRouter)",
7974
+ contextWindow: 64e3,
7975
+ maxOutputTokens: 8192,
7976
+ pricing: {
7977
+ input: 0.55,
7978
+ output: 2.19
7979
+ },
7980
+ knowledgeCutoff: "2025-01",
7981
+ features: {
7982
+ streaming: true,
7983
+ functionCalling: true,
7984
+ vision: false,
7985
+ reasoning: true
7986
+ },
7987
+ metadata: {
7988
+ family: "DeepSeek R1",
7989
+ notes: "DeepSeek R1 via OpenRouter. Strong reasoning capabilities."
7990
+ }
7991
+ },
7992
+ {
7993
+ provider: "openrouter",
7994
+ modelId: "deepseek/deepseek-chat",
7995
+ displayName: "DeepSeek Chat (OpenRouter)",
7996
+ contextWindow: 64e3,
7997
+ maxOutputTokens: 8192,
7998
+ pricing: {
7999
+ input: 0.14,
8000
+ output: 0.28
8001
+ },
8002
+ knowledgeCutoff: "2025-01",
8003
+ features: {
8004
+ streaming: true,
8005
+ functionCalling: true,
8006
+ vision: false
8007
+ },
8008
+ metadata: {
8009
+ family: "DeepSeek V3",
8010
+ notes: "DeepSeek Chat via OpenRouter. Very cost-effective."
8011
+ }
8012
+ },
8013
+ // ============================================================
8014
+ // Mistral Models (via OpenRouter)
8015
+ // ============================================================
8016
+ {
8017
+ provider: "openrouter",
8018
+ modelId: "mistralai/mistral-large",
8019
+ displayName: "Mistral Large (OpenRouter)",
8020
+ contextWindow: 128e3,
8021
+ maxOutputTokens: 8192,
8022
+ pricing: {
8023
+ input: 2,
8024
+ output: 6
8025
+ },
8026
+ knowledgeCutoff: "2024-11",
8027
+ features: {
8028
+ streaming: true,
8029
+ functionCalling: true,
8030
+ vision: false
8031
+ },
8032
+ metadata: {
8033
+ family: "Mistral Large",
8034
+ notes: "Mistral Large via OpenRouter. Strong multilingual capabilities."
8035
+ }
8036
+ },
8037
+ {
8038
+ provider: "openrouter",
8039
+ modelId: "mistralai/mixtral-8x22b-instruct",
8040
+ displayName: "Mixtral 8x22B Instruct (OpenRouter)",
8041
+ contextWindow: 65536,
8042
+ maxOutputTokens: 8192,
8043
+ pricing: {
8044
+ input: 0.9,
8045
+ output: 0.9
8046
+ },
8047
+ knowledgeCutoff: "2024-04",
8048
+ features: {
8049
+ streaming: true,
8050
+ functionCalling: true,
8051
+ vision: false
8052
+ },
8053
+ metadata: {
8054
+ family: "Mixtral",
8055
+ notes: "Mixtral 8x22B via OpenRouter. Sparse MoE architecture."
8056
+ }
8057
+ },
8058
+ // ============================================================
8059
+ // Qwen Models (via OpenRouter)
8060
+ // ============================================================
8061
+ {
8062
+ provider: "openrouter",
8063
+ modelId: "qwen/qwen-2.5-72b-instruct",
8064
+ displayName: "Qwen 2.5 72B Instruct (OpenRouter)",
8065
+ contextWindow: 131072,
8066
+ maxOutputTokens: 8192,
8067
+ pricing: {
8068
+ input: 0.35,
8069
+ output: 0.4
8070
+ },
8071
+ knowledgeCutoff: "2024-09",
8072
+ features: {
8073
+ streaming: true,
8074
+ functionCalling: true,
8075
+ vision: false
8076
+ },
8077
+ metadata: {
8078
+ family: "Qwen 2.5",
8079
+ notes: "Qwen 2.5 72B via OpenRouter. Strong coding and math."
8080
+ }
8081
+ }
8082
+ ];
8083
+ }
8084
+ });
8085
+
8086
+ // src/providers/openrouter.ts
8087
+ function createOpenRouterProviderFromEnv() {
8088
+ const apiKey = readEnvVar("OPENROUTER_API_KEY");
8089
+ if (!isNonEmpty(apiKey)) {
8090
+ return null;
8091
+ }
8092
+ const config = {
8093
+ siteUrl: readEnvVar("OPENROUTER_SITE_URL"),
8094
+ appName: readEnvVar("OPENROUTER_APP_NAME") || "llmist"
8095
+ };
8096
+ const client = new import_openai4.default({
8097
+ apiKey: apiKey.trim(),
8098
+ baseURL: "https://openrouter.ai/api/v1",
8099
+ timeout: 12e4,
8100
+ // 2 minute timeout
8101
+ maxRetries: 0
8102
+ // Disable SDK retries - llmist handles all retries at application level
8103
+ });
8104
+ return new OpenRouterProvider(client, config);
8105
+ }
8106
+ var import_openai4, OpenRouterProvider;
8107
+ var init_openrouter = __esm({
8108
+ "src/providers/openrouter.ts"() {
8109
+ "use strict";
8110
+ import_openai4 = __toESM(require("openai"), 1);
8111
+ init_openai_compatible_provider();
8112
+ init_openrouter_models();
8113
+ init_utils();
8114
+ OpenRouterProvider = class extends OpenAICompatibleProvider {
8115
+ providerId = "openrouter";
8116
+ providerAlias = "or";
8117
+ constructor(client, config = {}) {
8118
+ super(client, config);
8119
+ }
8120
+ getModelSpecs() {
8121
+ return OPENROUTER_MODELS;
8122
+ }
8123
+ /**
8124
+ * Get custom headers for OpenRouter analytics.
8125
+ */
8126
+ getCustomHeaders() {
8127
+ const headers = {};
8128
+ if (this.config.siteUrl) {
8129
+ headers["HTTP-Referer"] = this.config.siteUrl;
8130
+ }
8131
+ if (this.config.appName) {
8132
+ headers["X-Title"] = this.config.appName;
8133
+ }
8134
+ return headers;
8135
+ }
8136
+ /**
8137
+ * Build OpenRouter-specific request parameters from `extra.routing`.
8138
+ */
8139
+ buildProviderSpecificParams(extra) {
8140
+ const routing = extra?.routing;
8141
+ if (!routing) {
8142
+ return {};
8143
+ }
8144
+ const params = {};
8145
+ if (routing.models && routing.models.length > 0) {
8146
+ params.models = routing.models;
8147
+ }
8148
+ if (routing.route) {
8149
+ params.route = routing.route;
8150
+ }
8151
+ if (routing.provider) {
8152
+ params.provider = { order: [routing.provider] };
8153
+ } else if (routing.order && routing.order.length > 0) {
8154
+ params.provider = { order: routing.order };
8155
+ }
8156
+ return params;
8157
+ }
8158
+ /**
8159
+ * Filter out the 'routing' key from extra passthrough.
8160
+ */
8161
+ isProviderSpecificKey(key) {
8162
+ return key === "routing";
8163
+ }
8164
+ /**
8165
+ * Enhance error messages with OpenRouter-specific guidance.
8166
+ */
8167
+ enhanceError(error) {
8168
+ if (!(error instanceof Error)) {
8169
+ return new Error(String(error));
8170
+ }
8171
+ const message = error.message.toLowerCase();
8172
+ if (message.includes("402") || message.includes("insufficient")) {
8173
+ return new Error(
8174
+ `OpenRouter: Insufficient credits. Add funds at https://openrouter.ai/credits
8175
+ Original error: ${error.message}`
8176
+ );
8177
+ }
8178
+ if (message.includes("429") || message.includes("rate limit")) {
8179
+ return new Error(
8180
+ `OpenRouter: Rate limit exceeded. Consider upgrading your plan or reducing request frequency.
8181
+ Original error: ${error.message}`
8182
+ );
8183
+ }
8184
+ if (message.includes("503") || message.includes("unavailable")) {
8185
+ return new Error(
8186
+ `OpenRouter: Model temporarily unavailable. Try a different model or use the 'models' fallback option for automatic retry.
8187
+ Original error: ${error.message}`
8188
+ );
8189
+ }
8190
+ if (message.includes("401") || message.includes("unauthorized") || message.includes("invalid")) {
8191
+ return new Error(
8192
+ `OpenRouter: Authentication failed. Check that OPENROUTER_API_KEY is set correctly.
8193
+ Original error: ${error.message}`
8194
+ );
8195
+ }
8196
+ return error;
8197
+ }
8198
+ };
8199
+ }
8200
+ });
8201
+
7638
8202
  // src/providers/discovery.ts
7639
8203
  function discoverProviderAdapters() {
7640
8204
  const adapters = [];
@@ -7654,11 +8218,13 @@ var init_discovery = __esm({
7654
8218
  init_gemini();
7655
8219
  init_huggingface();
7656
8220
  init_openai();
8221
+ init_openrouter();
7657
8222
  DISCOVERERS = [
7658
8223
  createOpenAIProviderFromEnv,
7659
8224
  createAnthropicProviderFromEnv,
7660
8225
  createGeminiProviderFromEnv,
7661
- createHuggingFaceProviderFromEnv
8226
+ createHuggingFaceProviderFromEnv,
8227
+ createOpenRouterProviderFromEnv
7662
8228
  ];
7663
8229
  }
7664
8230
  });
@@ -13118,6 +13684,8 @@ __export(index_exports, {
13118
13684
  ModelIdentifierParser: () => ModelIdentifierParser,
13119
13685
  ModelRegistry: () => ModelRegistry,
13120
13686
  OpenAIChatProvider: () => OpenAIChatProvider,
13687
+ OpenAICompatibleProvider: () => OpenAICompatibleProvider,
13688
+ OpenRouterProvider: () => OpenRouterProvider,
13121
13689
  RateLimitTracker: () => RateLimitTracker,
13122
13690
  SimpleSessionManager: () => SimpleSessionManager,
13123
13691
  SlidingWindowStrategy: () => SlidingWindowStrategy,
@@ -13139,6 +13707,7 @@ __export(index_exports, {
13139
13707
  createLogger: () => createLogger,
13140
13708
  createMediaOutput: () => createMediaOutput,
13141
13709
  createOpenAIProviderFromEnv: () => createOpenAIProviderFromEnv,
13710
+ createOpenRouterProviderFromEnv: () => createOpenRouterProviderFromEnv,
13142
13711
  createSubagent: () => createSubagent,
13143
13712
  defaultLogger: () => defaultLogger,
13144
13713
  detectAudioMimeType: () => detectAudioMimeType,
@@ -14489,6 +15058,8 @@ init_discovery();
14489
15058
  init_gemini();
14490
15059
  init_huggingface();
14491
15060
  init_openai();
15061
+ init_openai_compatible_provider();
15062
+ init_openrouter();
14492
15063
 
14493
15064
  // src/session/manager.ts
14494
15065
  var BaseSessionManager = class {
@@ -14763,6 +15334,8 @@ function getHostExports2(ctx) {
14763
15334
  ModelIdentifierParser,
14764
15335
  ModelRegistry,
14765
15336
  OpenAIChatProvider,
15337
+ OpenAICompatibleProvider,
15338
+ OpenRouterProvider,
14766
15339
  RateLimitTracker,
14767
15340
  SimpleSessionManager,
14768
15341
  SlidingWindowStrategy,
@@ -14784,6 +15357,7 @@ function getHostExports2(ctx) {
14784
15357
  createLogger,
14785
15358
  createMediaOutput,
14786
15359
  createOpenAIProviderFromEnv,
15360
+ createOpenRouterProviderFromEnv,
14787
15361
  createSubagent,
14788
15362
  defaultLogger,
14789
15363
  detectAudioMimeType,