@contractspec/lib.ai-providers 2.9.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.
@@ -159,11 +159,23 @@ var MODELS = [
159
159
  },
160
160
  costPerMillion: { input: 2, output: 6 }
161
161
  },
162
+ {
163
+ id: "mistral-medium-latest",
164
+ name: "Mistral Medium",
165
+ provider: "mistral",
166
+ contextWindow: 128000,
167
+ capabilities: {
168
+ vision: false,
169
+ tools: true,
170
+ reasoning: false,
171
+ streaming: true
172
+ }
173
+ },
162
174
  {
163
175
  id: "codestral-latest",
164
176
  name: "Codestral",
165
177
  provider: "mistral",
166
- contextWindow: 32000,
178
+ contextWindow: 256000,
167
179
  capabilities: {
168
180
  vision: false,
169
181
  tools: true,
@@ -172,6 +184,42 @@ var MODELS = [
172
184
  },
173
185
  costPerMillion: { input: 0.2, output: 0.6 }
174
186
  },
187
+ {
188
+ id: "devstral-small-latest",
189
+ name: "Devstral Small",
190
+ provider: "mistral",
191
+ contextWindow: 128000,
192
+ capabilities: {
193
+ vision: false,
194
+ tools: true,
195
+ reasoning: true,
196
+ streaming: true
197
+ }
198
+ },
199
+ {
200
+ id: "magistral-medium-latest",
201
+ name: "Magistral Medium",
202
+ provider: "mistral",
203
+ contextWindow: 128000,
204
+ capabilities: {
205
+ vision: false,
206
+ tools: true,
207
+ reasoning: true,
208
+ streaming: true
209
+ }
210
+ },
211
+ {
212
+ id: "pixtral-large-latest",
213
+ name: "Pixtral Large",
214
+ provider: "mistral",
215
+ contextWindow: 128000,
216
+ capabilities: {
217
+ vision: true,
218
+ tools: true,
219
+ reasoning: false,
220
+ streaming: true
221
+ }
222
+ },
175
223
  {
176
224
  id: "mistral-small-latest",
177
225
  name: "Mistral Small",
@@ -240,22 +288,30 @@ function getDefaultModel(provider) {
240
288
  }
241
289
 
242
290
  // src/factory.ts
243
- import { anthropic } from "@ai-sdk/anthropic";
244
- import { google } from "@ai-sdk/google";
245
- import { mistral } from "@ai-sdk/mistral";
246
- import { openai } from "@ai-sdk/openai";
247
- 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";
248
296
  class BaseProvider {
249
297
  name;
250
298
  model;
251
299
  mode;
252
300
  config;
301
+ transport;
302
+ authMethod;
303
+ apiVersion;
304
+ customHeaders;
253
305
  cachedModel = null;
254
306
  constructor(config) {
255
307
  this.name = config.provider;
256
308
  this.model = config.model ?? DEFAULT_MODELS[config.provider];
257
309
  this.mode = this.determineMode(config);
258
310
  this.config = config;
311
+ this.transport = config.transport;
312
+ this.authMethod = config.authMethod;
313
+ this.apiVersion = config.apiVersion;
314
+ this.customHeaders = config.customHeaders;
259
315
  }
260
316
  getModel() {
261
317
  if (!this.cachedModel) {
@@ -295,81 +351,33 @@ class BaseProvider {
295
351
  return "managed";
296
352
  }
297
353
  createModel() {
298
- 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
+ }
299
364
  switch (this.name) {
300
- case "ollama": {
301
- const originalBaseUrl = process.env.OLLAMA_BASE_URL;
302
- if (baseUrl && baseUrl !== "http://localhost:11434") {
303
- process.env.OLLAMA_BASE_URL = baseUrl;
304
- }
305
- const ollamaModel = ollama(this.model);
306
- if (originalBaseUrl !== undefined) {
307
- process.env.OLLAMA_BASE_URL = originalBaseUrl;
308
- } else if (baseUrl && baseUrl !== "http://localhost:11434") {
309
- delete process.env.OLLAMA_BASE_URL;
310
- }
311
- 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);
312
380
  }
313
- case "openai":
314
- if (this.mode === "managed") {
315
- const originalBaseUrl = process.env.OPENAI_BASE_URL;
316
- if (proxyUrl) {
317
- process.env.OPENAI_BASE_URL = proxyUrl;
318
- }
319
- const model = openai(this.model);
320
- if (originalBaseUrl !== undefined) {
321
- process.env.OPENAI_BASE_URL = originalBaseUrl;
322
- } else if (proxyUrl) {
323
- delete process.env.OPENAI_BASE_URL;
324
- }
325
- return model;
326
- }
327
- return openai(this.model);
328
- case "anthropic":
329
- if (this.mode === "managed") {
330
- const originalBaseUrl = process.env.OPENAI_BASE_URL;
331
- if (proxyUrl) {
332
- process.env.OPENAI_BASE_URL = proxyUrl;
333
- }
334
- const model = openai(this.model);
335
- if (originalBaseUrl !== undefined) {
336
- process.env.OPENAI_BASE_URL = originalBaseUrl;
337
- } else if (proxyUrl) {
338
- delete process.env.OPENAI_BASE_URL;
339
- }
340
- return model;
341
- }
342
- return anthropic(this.model);
343
- case "mistral":
344
- if (this.mode === "managed") {
345
- const originalBaseUrl = process.env.OPENAI_BASE_URL;
346
- if (proxyUrl) {
347
- process.env.OPENAI_BASE_URL = proxyUrl;
348
- }
349
- const model = openai(this.model);
350
- if (originalBaseUrl !== undefined) {
351
- process.env.OPENAI_BASE_URL = originalBaseUrl;
352
- } else if (proxyUrl) {
353
- delete process.env.OPENAI_BASE_URL;
354
- }
355
- return model;
356
- }
357
- return mistral(this.model);
358
- case "gemini":
359
- if (this.mode === "managed") {
360
- const originalBaseUrl = process.env.OPENAI_BASE_URL;
361
- if (proxyUrl) {
362
- process.env.OPENAI_BASE_URL = proxyUrl;
363
- }
364
- const model = openai(this.model);
365
- if (originalBaseUrl !== undefined) {
366
- process.env.OPENAI_BASE_URL = originalBaseUrl;
367
- } else if (proxyUrl) {
368
- delete process.env.OPENAI_BASE_URL;
369
- }
370
- return model;
371
- }
372
- return google(this.model);
373
381
  default:
374
382
  throw new Error(`Unknown provider: ${this.name}`);
375
383
  }
@@ -451,13 +459,17 @@ function createProviderFromEnv() {
451
459
  case "ollama":
452
460
  break;
453
461
  }
462
+ const transport = process.env.CONTRACTSPEC_AI_TRANSPORT;
463
+ const apiVersion = process.env.CONTRACTSPEC_AI_API_VERSION;
454
464
  return createProvider({
455
465
  provider,
456
466
  model,
457
467
  apiKey,
458
468
  baseUrl: process.env.OLLAMA_BASE_URL,
459
469
  proxyUrl: process.env.CONTRACTSPEC_AI_PROXY_URL,
460
- organizationId: process.env.CONTRACTSPEC_ORG_ID
470
+ organizationId: process.env.CONTRACTSPEC_ORG_ID,
471
+ transport,
472
+ apiVersion
461
473
  });
462
474
  }
463
475
  function getAvailableProviders() {
@@ -465,35 +477,45 @@ function getAvailableProviders() {
465
477
  providers.push({
466
478
  provider: "ollama",
467
479
  available: true,
468
- mode: "local"
480
+ mode: "local",
481
+ transports: ["rest", "sdk"],
482
+ authMethods: []
469
483
  });
470
484
  const openaiKey = process.env.OPENAI_API_KEY;
471
485
  providers.push({
472
486
  provider: "openai",
473
487
  available: Boolean(openaiKey) || Boolean(process.env.CONTRACTSPEC_AI_PROXY_URL),
474
488
  mode: openaiKey ? "byok" : "managed",
475
- 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"]
476
492
  });
477
493
  const anthropicKey = process.env.ANTHROPIC_API_KEY;
478
494
  providers.push({
479
495
  provider: "anthropic",
480
496
  available: Boolean(anthropicKey) || Boolean(process.env.CONTRACTSPEC_AI_PROXY_URL),
481
497
  mode: anthropicKey ? "byok" : "managed",
482
- 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"]
483
501
  });
484
502
  const mistralKey = process.env.MISTRAL_API_KEY;
485
503
  providers.push({
486
504
  provider: "mistral",
487
505
  available: Boolean(mistralKey) || Boolean(process.env.CONTRACTSPEC_AI_PROXY_URL),
488
506
  mode: mistralKey ? "byok" : "managed",
489
- 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"]
490
510
  });
491
511
  const geminiKey = process.env.GOOGLE_API_KEY ?? process.env.GEMINI_API_KEY;
492
512
  providers.push({
493
513
  provider: "gemini",
494
514
  available: Boolean(geminiKey) || Boolean(process.env.CONTRACTSPEC_AI_PROXY_URL),
495
515
  mode: geminiKey ? "byok" : "managed",
496
- 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"]
497
519
  });
498
520
  return providers;
499
521
  }
@@ -553,6 +575,174 @@ async function listOllamaModels(baseUrl = "http://localhost:11434") {
553
575
  return [];
554
576
  }
555
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
+ }
556
746
 
557
747
  // src/legacy.ts
558
748
  function mapLegacyProvider(legacy) {
@@ -636,6 +826,7 @@ export {
636
826
  getAIProvider,
637
827
  createProviderFromEnv,
638
828
  createProvider,
829
+ createModelSelector,
639
830
  MODELS,
640
831
  DEFAULT_MODELS
641
832
  };