@contractspec/lib.ai-providers 3.0.0 → 3.1.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.
@@ -288,22 +288,30 @@ function getDefaultModel(provider) {
288
288
  }
289
289
 
290
290
  // src/factory.ts
291
- import { anthropic } from "@ai-sdk/anthropic";
292
- import { google } from "@ai-sdk/google";
293
- import { mistral } from "@ai-sdk/mistral";
294
- import { openai } from "@ai-sdk/openai";
295
- import { ollama } from "ollama-ai-provider";
291
+ import { createAnthropic } from "@ai-sdk/anthropic";
292
+ import { createGoogleGenerativeAI } from "@ai-sdk/google";
293
+ import { createMistral } from "@ai-sdk/mistral";
294
+ import { createOpenAI } from "@ai-sdk/openai";
295
+ import { createOllama } from "ollama-ai-provider";
296
296
  class BaseProvider {
297
297
  name;
298
298
  model;
299
299
  mode;
300
300
  config;
301
+ transport;
302
+ authMethod;
303
+ apiVersion;
304
+ customHeaders;
301
305
  cachedModel = null;
302
306
  constructor(config) {
303
307
  this.name = config.provider;
304
308
  this.model = config.model ?? DEFAULT_MODELS[config.provider];
305
309
  this.mode = this.determineMode(config);
306
310
  this.config = config;
311
+ this.transport = config.transport;
312
+ this.authMethod = config.authMethod;
313
+ this.apiVersion = config.apiVersion;
314
+ this.customHeaders = config.customHeaders;
307
315
  }
308
316
  getModel() {
309
317
  if (!this.cachedModel) {
@@ -343,81 +351,33 @@ class BaseProvider {
343
351
  return "managed";
344
352
  }
345
353
  createModel() {
346
- const { baseUrl, proxyUrl } = this.config;
354
+ const { baseUrl, proxyUrl, apiKey } = this.config;
355
+ const headers = this.customHeaders;
356
+ if (this.name === "ollama") {
357
+ const provider = createOllama({ baseURL: baseUrl, headers });
358
+ return provider(this.model);
359
+ }
360
+ if (this.mode === "managed" && proxyUrl) {
361
+ const provider = createOpenAI({ baseURL: proxyUrl, apiKey, headers });
362
+ return provider(this.model);
363
+ }
347
364
  switch (this.name) {
348
- case "ollama": {
349
- const originalBaseUrl = process.env.OLLAMA_BASE_URL;
350
- if (baseUrl && baseUrl !== "http://localhost:11434") {
351
- process.env.OLLAMA_BASE_URL = baseUrl;
352
- }
353
- const ollamaModel = ollama(this.model);
354
- if (originalBaseUrl !== undefined) {
355
- process.env.OLLAMA_BASE_URL = originalBaseUrl;
356
- } else if (baseUrl && baseUrl !== "http://localhost:11434") {
357
- delete process.env.OLLAMA_BASE_URL;
358
- }
359
- return ollamaModel;
365
+ case "openai": {
366
+ const provider = createOpenAI({ apiKey, headers });
367
+ return provider(this.model);
368
+ }
369
+ case "anthropic": {
370
+ const provider = createAnthropic({ apiKey, headers });
371
+ return provider(this.model);
372
+ }
373
+ case "mistral": {
374
+ const provider = createMistral({ apiKey, headers });
375
+ return provider(this.model);
376
+ }
377
+ case "gemini": {
378
+ const provider = createGoogleGenerativeAI({ apiKey, headers });
379
+ return provider(this.model);
360
380
  }
361
- case "openai":
362
- if (this.mode === "managed") {
363
- const originalBaseUrl = process.env.OPENAI_BASE_URL;
364
- if (proxyUrl) {
365
- process.env.OPENAI_BASE_URL = proxyUrl;
366
- }
367
- const model = openai(this.model);
368
- if (originalBaseUrl !== undefined) {
369
- process.env.OPENAI_BASE_URL = originalBaseUrl;
370
- } else if (proxyUrl) {
371
- delete process.env.OPENAI_BASE_URL;
372
- }
373
- return model;
374
- }
375
- return openai(this.model);
376
- case "anthropic":
377
- if (this.mode === "managed") {
378
- const originalBaseUrl = process.env.OPENAI_BASE_URL;
379
- if (proxyUrl) {
380
- process.env.OPENAI_BASE_URL = proxyUrl;
381
- }
382
- const model = openai(this.model);
383
- if (originalBaseUrl !== undefined) {
384
- process.env.OPENAI_BASE_URL = originalBaseUrl;
385
- } else if (proxyUrl) {
386
- delete process.env.OPENAI_BASE_URL;
387
- }
388
- return model;
389
- }
390
- return anthropic(this.model);
391
- case "mistral":
392
- if (this.mode === "managed") {
393
- const originalBaseUrl = process.env.OPENAI_BASE_URL;
394
- if (proxyUrl) {
395
- process.env.OPENAI_BASE_URL = proxyUrl;
396
- }
397
- const model = openai(this.model);
398
- if (originalBaseUrl !== undefined) {
399
- process.env.OPENAI_BASE_URL = originalBaseUrl;
400
- } else if (proxyUrl) {
401
- delete process.env.OPENAI_BASE_URL;
402
- }
403
- return model;
404
- }
405
- return mistral(this.model);
406
- case "gemini":
407
- if (this.mode === "managed") {
408
- const originalBaseUrl = process.env.OPENAI_BASE_URL;
409
- if (proxyUrl) {
410
- process.env.OPENAI_BASE_URL = proxyUrl;
411
- }
412
- const model = openai(this.model);
413
- if (originalBaseUrl !== undefined) {
414
- process.env.OPENAI_BASE_URL = originalBaseUrl;
415
- } else if (proxyUrl) {
416
- delete process.env.OPENAI_BASE_URL;
417
- }
418
- return model;
419
- }
420
- return google(this.model);
421
381
  default:
422
382
  throw new Error(`Unknown provider: ${this.name}`);
423
383
  }
@@ -499,13 +459,17 @@ function createProviderFromEnv() {
499
459
  case "ollama":
500
460
  break;
501
461
  }
462
+ const transport = process.env.CONTRACTSPEC_AI_TRANSPORT;
463
+ const apiVersion = process.env.CONTRACTSPEC_AI_API_VERSION;
502
464
  return createProvider({
503
465
  provider,
504
466
  model,
505
467
  apiKey,
506
468
  baseUrl: process.env.OLLAMA_BASE_URL,
507
469
  proxyUrl: process.env.CONTRACTSPEC_AI_PROXY_URL,
508
- organizationId: process.env.CONTRACTSPEC_ORG_ID
470
+ organizationId: process.env.CONTRACTSPEC_ORG_ID,
471
+ transport,
472
+ apiVersion
509
473
  });
510
474
  }
511
475
  function getAvailableProviders() {
@@ -513,35 +477,45 @@ function getAvailableProviders() {
513
477
  providers.push({
514
478
  provider: "ollama",
515
479
  available: true,
516
- mode: "local"
480
+ mode: "local",
481
+ transports: ["rest", "sdk"],
482
+ authMethods: []
517
483
  });
518
484
  const openaiKey = process.env.OPENAI_API_KEY;
519
485
  providers.push({
520
486
  provider: "openai",
521
487
  available: Boolean(openaiKey) || Boolean(process.env.CONTRACTSPEC_AI_PROXY_URL),
522
488
  mode: openaiKey ? "byok" : "managed",
523
- reason: !openaiKey ? "Set OPENAI_API_KEY for BYOK mode" : undefined
489
+ reason: !openaiKey ? "Set OPENAI_API_KEY for BYOK mode" : undefined,
490
+ transports: ["rest", "sdk"],
491
+ authMethods: ["api-key"]
524
492
  });
525
493
  const anthropicKey = process.env.ANTHROPIC_API_KEY;
526
494
  providers.push({
527
495
  provider: "anthropic",
528
496
  available: Boolean(anthropicKey) || Boolean(process.env.CONTRACTSPEC_AI_PROXY_URL),
529
497
  mode: anthropicKey ? "byok" : "managed",
530
- reason: !anthropicKey ? "Set ANTHROPIC_API_KEY for BYOK mode" : undefined
498
+ reason: !anthropicKey ? "Set ANTHROPIC_API_KEY for BYOK mode" : undefined,
499
+ transports: ["rest", "sdk"],
500
+ authMethods: ["api-key"]
531
501
  });
532
502
  const mistralKey = process.env.MISTRAL_API_KEY;
533
503
  providers.push({
534
504
  provider: "mistral",
535
505
  available: Boolean(mistralKey) || Boolean(process.env.CONTRACTSPEC_AI_PROXY_URL),
536
506
  mode: mistralKey ? "byok" : "managed",
537
- reason: !mistralKey ? "Set MISTRAL_API_KEY for BYOK mode" : undefined
507
+ reason: !mistralKey ? "Set MISTRAL_API_KEY for BYOK mode" : undefined,
508
+ transports: ["rest", "sdk"],
509
+ authMethods: ["api-key"]
538
510
  });
539
511
  const geminiKey = process.env.GOOGLE_API_KEY ?? process.env.GEMINI_API_KEY;
540
512
  providers.push({
541
513
  provider: "gemini",
542
514
  available: Boolean(geminiKey) || Boolean(process.env.CONTRACTSPEC_AI_PROXY_URL),
543
515
  mode: geminiKey ? "byok" : "managed",
544
- reason: !geminiKey ? "Set GOOGLE_API_KEY for BYOK mode" : undefined
516
+ reason: !geminiKey ? "Set GOOGLE_API_KEY for BYOK mode" : undefined,
517
+ transports: ["rest", "sdk"],
518
+ authMethods: ["api-key"]
545
519
  });
546
520
  return providers;
547
521
  }
@@ -288,22 +288,30 @@ function getDefaultModel(provider) {
288
288
  }
289
289
 
290
290
  // src/factory.ts
291
- import { anthropic } from "@ai-sdk/anthropic";
292
- import { google } from "@ai-sdk/google";
293
- import { mistral } from "@ai-sdk/mistral";
294
- import { openai } from "@ai-sdk/openai";
295
- import { ollama } from "ollama-ai-provider";
291
+ import { createAnthropic } from "@ai-sdk/anthropic";
292
+ import { createGoogleGenerativeAI } from "@ai-sdk/google";
293
+ import { createMistral } from "@ai-sdk/mistral";
294
+ import { createOpenAI } from "@ai-sdk/openai";
295
+ import { createOllama } from "ollama-ai-provider";
296
296
  class BaseProvider {
297
297
  name;
298
298
  model;
299
299
  mode;
300
300
  config;
301
+ transport;
302
+ authMethod;
303
+ apiVersion;
304
+ customHeaders;
301
305
  cachedModel = null;
302
306
  constructor(config) {
303
307
  this.name = config.provider;
304
308
  this.model = config.model ?? DEFAULT_MODELS[config.provider];
305
309
  this.mode = this.determineMode(config);
306
310
  this.config = config;
311
+ this.transport = config.transport;
312
+ this.authMethod = config.authMethod;
313
+ this.apiVersion = config.apiVersion;
314
+ this.customHeaders = config.customHeaders;
307
315
  }
308
316
  getModel() {
309
317
  if (!this.cachedModel) {
@@ -343,81 +351,33 @@ class BaseProvider {
343
351
  return "managed";
344
352
  }
345
353
  createModel() {
346
- const { baseUrl, proxyUrl } = this.config;
354
+ const { baseUrl, proxyUrl, apiKey } = this.config;
355
+ const headers = this.customHeaders;
356
+ if (this.name === "ollama") {
357
+ const provider = createOllama({ baseURL: baseUrl, headers });
358
+ return provider(this.model);
359
+ }
360
+ if (this.mode === "managed" && proxyUrl) {
361
+ const provider = createOpenAI({ baseURL: proxyUrl, apiKey, headers });
362
+ return provider(this.model);
363
+ }
347
364
  switch (this.name) {
348
- case "ollama": {
349
- const originalBaseUrl = process.env.OLLAMA_BASE_URL;
350
- if (baseUrl && baseUrl !== "http://localhost:11434") {
351
- process.env.OLLAMA_BASE_URL = baseUrl;
352
- }
353
- const ollamaModel = ollama(this.model);
354
- if (originalBaseUrl !== undefined) {
355
- process.env.OLLAMA_BASE_URL = originalBaseUrl;
356
- } else if (baseUrl && baseUrl !== "http://localhost:11434") {
357
- delete process.env.OLLAMA_BASE_URL;
358
- }
359
- return ollamaModel;
365
+ case "openai": {
366
+ const provider = createOpenAI({ apiKey, headers });
367
+ return provider(this.model);
368
+ }
369
+ case "anthropic": {
370
+ const provider = createAnthropic({ apiKey, headers });
371
+ return provider(this.model);
372
+ }
373
+ case "mistral": {
374
+ const provider = createMistral({ apiKey, headers });
375
+ return provider(this.model);
376
+ }
377
+ case "gemini": {
378
+ const provider = createGoogleGenerativeAI({ apiKey, headers });
379
+ return provider(this.model);
360
380
  }
361
- case "openai":
362
- if (this.mode === "managed") {
363
- const originalBaseUrl = process.env.OPENAI_BASE_URL;
364
- if (proxyUrl) {
365
- process.env.OPENAI_BASE_URL = proxyUrl;
366
- }
367
- const model = openai(this.model);
368
- if (originalBaseUrl !== undefined) {
369
- process.env.OPENAI_BASE_URL = originalBaseUrl;
370
- } else if (proxyUrl) {
371
- delete process.env.OPENAI_BASE_URL;
372
- }
373
- return model;
374
- }
375
- return openai(this.model);
376
- case "anthropic":
377
- if (this.mode === "managed") {
378
- const originalBaseUrl = process.env.OPENAI_BASE_URL;
379
- if (proxyUrl) {
380
- process.env.OPENAI_BASE_URL = proxyUrl;
381
- }
382
- const model = openai(this.model);
383
- if (originalBaseUrl !== undefined) {
384
- process.env.OPENAI_BASE_URL = originalBaseUrl;
385
- } else if (proxyUrl) {
386
- delete process.env.OPENAI_BASE_URL;
387
- }
388
- return model;
389
- }
390
- return anthropic(this.model);
391
- case "mistral":
392
- if (this.mode === "managed") {
393
- const originalBaseUrl = process.env.OPENAI_BASE_URL;
394
- if (proxyUrl) {
395
- process.env.OPENAI_BASE_URL = proxyUrl;
396
- }
397
- const model = openai(this.model);
398
- if (originalBaseUrl !== undefined) {
399
- process.env.OPENAI_BASE_URL = originalBaseUrl;
400
- } else if (proxyUrl) {
401
- delete process.env.OPENAI_BASE_URL;
402
- }
403
- return model;
404
- }
405
- return mistral(this.model);
406
- case "gemini":
407
- if (this.mode === "managed") {
408
- const originalBaseUrl = process.env.OPENAI_BASE_URL;
409
- if (proxyUrl) {
410
- process.env.OPENAI_BASE_URL = proxyUrl;
411
- }
412
- const model = openai(this.model);
413
- if (originalBaseUrl !== undefined) {
414
- process.env.OPENAI_BASE_URL = originalBaseUrl;
415
- } else if (proxyUrl) {
416
- delete process.env.OPENAI_BASE_URL;
417
- }
418
- return model;
419
- }
420
- return google(this.model);
421
381
  default:
422
382
  throw new Error(`Unknown provider: ${this.name}`);
423
383
  }
@@ -499,13 +459,17 @@ function createProviderFromEnv() {
499
459
  case "ollama":
500
460
  break;
501
461
  }
462
+ const transport = process.env.CONTRACTSPEC_AI_TRANSPORT;
463
+ const apiVersion = process.env.CONTRACTSPEC_AI_API_VERSION;
502
464
  return createProvider({
503
465
  provider,
504
466
  model,
505
467
  apiKey,
506
468
  baseUrl: process.env.OLLAMA_BASE_URL,
507
469
  proxyUrl: process.env.CONTRACTSPEC_AI_PROXY_URL,
508
- organizationId: process.env.CONTRACTSPEC_ORG_ID
470
+ organizationId: process.env.CONTRACTSPEC_ORG_ID,
471
+ transport,
472
+ apiVersion
509
473
  });
510
474
  }
511
475
  function getAvailableProviders() {
@@ -513,35 +477,45 @@ function getAvailableProviders() {
513
477
  providers.push({
514
478
  provider: "ollama",
515
479
  available: true,
516
- mode: "local"
480
+ mode: "local",
481
+ transports: ["rest", "sdk"],
482
+ authMethods: []
517
483
  });
518
484
  const openaiKey = process.env.OPENAI_API_KEY;
519
485
  providers.push({
520
486
  provider: "openai",
521
487
  available: Boolean(openaiKey) || Boolean(process.env.CONTRACTSPEC_AI_PROXY_URL),
522
488
  mode: openaiKey ? "byok" : "managed",
523
- reason: !openaiKey ? "Set OPENAI_API_KEY for BYOK mode" : undefined
489
+ reason: !openaiKey ? "Set OPENAI_API_KEY for BYOK mode" : undefined,
490
+ transports: ["rest", "sdk"],
491
+ authMethods: ["api-key"]
524
492
  });
525
493
  const anthropicKey = process.env.ANTHROPIC_API_KEY;
526
494
  providers.push({
527
495
  provider: "anthropic",
528
496
  available: Boolean(anthropicKey) || Boolean(process.env.CONTRACTSPEC_AI_PROXY_URL),
529
497
  mode: anthropicKey ? "byok" : "managed",
530
- reason: !anthropicKey ? "Set ANTHROPIC_API_KEY for BYOK mode" : undefined
498
+ reason: !anthropicKey ? "Set ANTHROPIC_API_KEY for BYOK mode" : undefined,
499
+ transports: ["rest", "sdk"],
500
+ authMethods: ["api-key"]
531
501
  });
532
502
  const mistralKey = process.env.MISTRAL_API_KEY;
533
503
  providers.push({
534
504
  provider: "mistral",
535
505
  available: Boolean(mistralKey) || Boolean(process.env.CONTRACTSPEC_AI_PROXY_URL),
536
506
  mode: mistralKey ? "byok" : "managed",
537
- reason: !mistralKey ? "Set MISTRAL_API_KEY for BYOK mode" : undefined
507
+ reason: !mistralKey ? "Set MISTRAL_API_KEY for BYOK mode" : undefined,
508
+ transports: ["rest", "sdk"],
509
+ authMethods: ["api-key"]
538
510
  });
539
511
  const geminiKey = process.env.GOOGLE_API_KEY ?? process.env.GEMINI_API_KEY;
540
512
  providers.push({
541
513
  provider: "gemini",
542
514
  available: Boolean(geminiKey) || Boolean(process.env.CONTRACTSPEC_AI_PROXY_URL),
543
515
  mode: geminiKey ? "byok" : "managed",
544
- reason: !geminiKey ? "Set GOOGLE_API_KEY for BYOK mode" : undefined
516
+ reason: !geminiKey ? "Set GOOGLE_API_KEY for BYOK mode" : undefined,
517
+ transports: ["rest", "sdk"],
518
+ authMethods: ["api-key"]
545
519
  });
546
520
  return providers;
547
521
  }
@@ -601,6 +575,174 @@ async function listOllamaModels(baseUrl = "http://localhost:11434") {
601
575
  return [];
602
576
  }
603
577
  }
578
+ // src/selector.ts
579
+ function createModelSelector(options) {
580
+ const { store, fallbackModels, defaultConstraints } = options;
581
+ const catalog = fallbackModels ?? MODELS;
582
+ return {
583
+ async select(context) {
584
+ const merged = mergeConstraints(defaultConstraints, context.constraints);
585
+ if (context.priorities?.length) {
586
+ return selectMultiObjective(store, catalog, context.priorities, merged);
587
+ }
588
+ const dimension = context.taskDimension ?? "reasoning";
589
+ return selectByDimension(store, catalog, dimension, merged);
590
+ },
591
+ async selectAndCreate(context) {
592
+ const selection = await this.select(context);
593
+ const model = createProvider({
594
+ provider: selection.providerKey,
595
+ model: selection.modelId
596
+ }).getModel();
597
+ return { model, selection };
598
+ }
599
+ };
600
+ }
601
+ async function selectByDimension(store, catalog, dimension, constraints) {
602
+ const { rankings } = await store.listModelRankings({ dimension, limit: 50 });
603
+ const eligible = filterRankings(rankings, catalog, constraints);
604
+ const topCandidate = eligible[0];
605
+ if (topCandidate) {
606
+ const dimScore = topCandidate.dimensionScores[dimension]?.score ?? topCandidate.compositeScore;
607
+ return {
608
+ modelId: topCandidate.modelId,
609
+ providerKey: topCandidate.providerKey,
610
+ score: dimScore,
611
+ reason: `Top-ranked for "${dimension}" (score ${Math.round(dimScore)})`,
612
+ alternatives: eligible.slice(1, 4).map((r) => ({
613
+ modelId: r.modelId,
614
+ providerKey: r.providerKey,
615
+ score: r.dimensionScores[dimension]?.score ?? r.compositeScore
616
+ }))
617
+ };
618
+ }
619
+ return fallbackFromCatalog(catalog, constraints, dimension);
620
+ }
621
+ async function selectMultiObjective(store, catalog, priorities, constraints) {
622
+ const { rankings } = await store.listModelRankings({ limit: 100 });
623
+ const eligible = filterRankings(rankings, catalog, constraints);
624
+ if (eligible.length === 0) {
625
+ const primaryDim = priorities.reduce((a, b) => b.weight > a.weight ? b : a).dimension;
626
+ return fallbackFromCatalog(catalog, constraints, primaryDim);
627
+ }
628
+ const totalWeight = priorities.reduce((sum, p) => sum + p.weight, 0) || 1;
629
+ const scored = eligible.map((r) => {
630
+ let weightedScore = 0;
631
+ for (const p of priorities) {
632
+ const dimScore = r.dimensionScores[p.dimension]?.score ?? 0;
633
+ weightedScore += dimScore * (p.weight / totalWeight);
634
+ }
635
+ return { ranking: r, weightedScore };
636
+ });
637
+ scored.sort((a, b) => b.weightedScore - a.weightedScore);
638
+ const best = scored[0];
639
+ if (!best) {
640
+ const primaryDim = priorities.reduce((a, b) => b.weight > a.weight ? b : a).dimension;
641
+ return fallbackFromCatalog(catalog, constraints, primaryDim);
642
+ }
643
+ const dims = priorities.map((p) => p.dimension).join(", ");
644
+ return {
645
+ modelId: best.ranking.modelId,
646
+ providerKey: best.ranking.providerKey,
647
+ score: Math.round(best.weightedScore * 100) / 100,
648
+ reason: `Multi-objective optimum across [${dims}]`,
649
+ alternatives: scored.slice(1, 4).map((s) => ({
650
+ modelId: s.ranking.modelId,
651
+ providerKey: s.ranking.providerKey,
652
+ score: Math.round(s.weightedScore * 100) / 100
653
+ }))
654
+ };
655
+ }
656
+ function filterRankings(rankings, catalog, constraints) {
657
+ return rankings.filter((r) => {
658
+ if (constraints.allowedProviders?.length) {
659
+ if (!constraints.allowedProviders.includes(r.providerKey))
660
+ return false;
661
+ }
662
+ if (constraints.excludeModels?.length) {
663
+ if (constraints.excludeModels.includes(r.modelId))
664
+ return false;
665
+ }
666
+ const info = getModelInfo(r.modelId) ?? catalog.find((m) => m.id === r.modelId);
667
+ if (!info)
668
+ return true;
669
+ if (constraints.minContextWindow && info.contextWindow < constraints.minContextWindow) {
670
+ return false;
671
+ }
672
+ if (constraints.maxCostPerMillionInput && info.costPerMillion) {
673
+ if (info.costPerMillion.input > constraints.maxCostPerMillionInput)
674
+ return false;
675
+ }
676
+ if (constraints.maxCostPerMillionOutput && info.costPerMillion) {
677
+ if (info.costPerMillion.output > constraints.maxCostPerMillionOutput)
678
+ return false;
679
+ }
680
+ if (constraints.requiredCapabilities?.length) {
681
+ for (const cap of constraints.requiredCapabilities) {
682
+ if (!info.capabilities[cap])
683
+ return false;
684
+ }
685
+ }
686
+ return true;
687
+ });
688
+ }
689
+ function fallbackFromCatalog(catalog, constraints, dimension) {
690
+ let eligible = catalog.filter((m) => m.costPerMillion != null);
691
+ const {
692
+ allowedProviders,
693
+ excludeModels,
694
+ minContextWindow,
695
+ requiredCapabilities
696
+ } = constraints;
697
+ if (allowedProviders?.length) {
698
+ eligible = eligible.filter((m) => allowedProviders.includes(m.provider));
699
+ }
700
+ if (excludeModels?.length) {
701
+ eligible = eligible.filter((m) => !excludeModels.includes(m.id));
702
+ }
703
+ if (minContextWindow) {
704
+ eligible = eligible.filter((m) => m.contextWindow >= minContextWindow);
705
+ }
706
+ if (requiredCapabilities?.length) {
707
+ eligible = eligible.filter((m) => requiredCapabilities.every((cap) => m.capabilities[cap]));
708
+ }
709
+ if (eligible.length === 0) {
710
+ eligible = catalog.slice(0, 5);
711
+ }
712
+ eligible.sort((a, b) => {
713
+ const costA = a.costPerMillion ? (a.costPerMillion.input + a.costPerMillion.output) / 2 : 999;
714
+ const costB = b.costPerMillion ? (b.costPerMillion.input + b.costPerMillion.output) / 2 : 999;
715
+ return b.contextWindow / 1e5 - costB - (a.contextWindow / 1e5 - costA);
716
+ });
717
+ const best = eligible[0];
718
+ if (!best) {
719
+ return {
720
+ modelId: "unknown",
721
+ providerKey: "openai",
722
+ score: 0,
723
+ reason: `No eligible models found for "${dimension}"`,
724
+ alternatives: []
725
+ };
726
+ }
727
+ return {
728
+ modelId: best.id,
729
+ providerKey: best.provider,
730
+ score: 0,
731
+ reason: `Fallback from catalog (no ranking data for "${dimension}")`,
732
+ alternatives: eligible.slice(1, 4).map((m) => ({
733
+ modelId: m.id,
734
+ providerKey: m.provider,
735
+ score: 0
736
+ }))
737
+ };
738
+ }
739
+ function mergeConstraints(defaults, overrides) {
740
+ if (!defaults)
741
+ return overrides ?? {};
742
+ if (!overrides)
743
+ return defaults;
744
+ return { ...defaults, ...overrides };
745
+ }
604
746
 
605
747
  // src/legacy.ts
606
748
  function mapLegacyProvider(legacy) {
@@ -684,6 +826,7 @@ export {
684
826
  getAIProvider,
685
827
  createProviderFromEnv,
686
828
  createProvider,
829
+ createModelSelector,
687
830
  MODELS,
688
831
  DEFAULT_MODELS
689
832
  };