language-models 2.1.1 → 2.3.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.
Files changed (53) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/CHANGELOG.md +36 -0
  3. package/README.md +106 -43
  4. package/dist/index.d.ts +3 -1
  5. package/dist/index.d.ts.map +1 -1
  6. package/dist/index.js +13 -1
  7. package/dist/index.js.map +1 -1
  8. package/dist/models.d.ts +1 -1
  9. package/dist/models.d.ts.map +1 -1
  10. package/dist/models.js +8 -10
  11. package/dist/models.js.map +1 -1
  12. package/dist/policy.d.ts +127 -0
  13. package/dist/policy.d.ts.map +1 -0
  14. package/dist/policy.js +246 -0
  15. package/dist/policy.js.map +1 -0
  16. package/dist/pricing/index.d.ts +19 -0
  17. package/dist/pricing/index.d.ts.map +1 -0
  18. package/dist/pricing/index.js +18 -0
  19. package/dist/pricing/index.js.map +1 -0
  20. package/dist/pricing/lookup.d.ts +46 -0
  21. package/dist/pricing/lookup.d.ts.map +1 -0
  22. package/dist/pricing/lookup.js +94 -0
  23. package/dist/pricing/lookup.js.map +1 -0
  24. package/dist/pricing/table.d.ts +46 -0
  25. package/dist/pricing/table.d.ts.map +1 -0
  26. package/dist/pricing/table.js +214 -0
  27. package/dist/pricing/table.js.map +1 -0
  28. package/dist/pricing/types.d.ts +84 -0
  29. package/dist/pricing/types.d.ts.map +1 -0
  30. package/dist/pricing/types.js +32 -0
  31. package/dist/pricing/types.js.map +1 -0
  32. package/package.json +6 -2
  33. package/src/index.ts +42 -1
  34. package/src/models.ts +8 -12
  35. package/src/policy.ts +343 -0
  36. package/src/pricing/index.ts +29 -0
  37. package/src/pricing/lookup.ts +124 -0
  38. package/src/pricing/table.ts +235 -0
  39. package/src/pricing/types.ts +90 -0
  40. package/{src → test}/aliases.test.ts +20 -22
  41. package/{src → test}/index.test.ts +9 -9
  42. package/{src → test}/models.test.ts +8 -6
  43. package/test/policy.test.ts +203 -0
  44. package/test/pricing.test.ts +279 -0
  45. package/vitest.config.ts +21 -1
  46. package/.turbo/turbo-test.log +0 -7
  47. package/src/aliases.js +0 -40
  48. package/src/aliases.test.js +0 -264
  49. package/src/index.js +0 -9
  50. package/src/index.test.js +0 -320
  51. package/src/models.js +0 -108
  52. package/src/models.test.js +0 -335
  53. package/vitest.config.js +0 -10
package/src/models.js DELETED
@@ -1,108 +0,0 @@
1
- /**
2
- * Model listing and resolution
3
- */
4
- import { createRequire } from 'module';
5
- import { ALIASES } from './aliases.js';
6
- const require = createRequire(import.meta.url);
7
- // Load models from JSON
8
- let modelsCache = null;
9
- function loadModels() {
10
- if (modelsCache)
11
- return modelsCache;
12
- try {
13
- modelsCache = require('../data/models.json');
14
- return modelsCache;
15
- }
16
- catch {
17
- return [];
18
- }
19
- }
20
- /**
21
- * List all available models
22
- */
23
- export function list() {
24
- return loadModels();
25
- }
26
- /**
27
- * Get a model by exact ID
28
- */
29
- export function get(id) {
30
- return loadModels().find(m => m.id === id);
31
- }
32
- /**
33
- * Search models by query string
34
- * Searches in id and name fields
35
- */
36
- export function search(query) {
37
- const q = query.toLowerCase();
38
- return loadModels().filter(m => m.id.toLowerCase().includes(q) ||
39
- m.name.toLowerCase().includes(q));
40
- }
41
- /**
42
- * Resolve a model alias or partial name to a full model ID
43
- *
44
- * Resolution order:
45
- * 1. Check aliases (e.g., 'opus' -> 'anthropic/claude-opus-4.5')
46
- * 2. Check if it's already a full ID (contains '/')
47
- * 3. Search for first matching model
48
- *
49
- * @example
50
- * resolve('opus') // 'anthropic/claude-opus-4.5'
51
- * resolve('gpt-4o') // 'openai/gpt-4o'
52
- * resolve('claude-sonnet') // 'anthropic/claude-sonnet-4.5'
53
- * resolve('llama-70b') // 'meta-llama/llama-3.3-70b-instruct'
54
- */
55
- export function resolve(input) {
56
- const normalized = input.toLowerCase().trim();
57
- // Check aliases first
58
- if (ALIASES[normalized]) {
59
- return ALIASES[normalized];
60
- }
61
- // Already a full ID with provider prefix
62
- if (input.includes('/')) {
63
- // Verify it exists or return as-is
64
- const model = get(input);
65
- return model?.id || input;
66
- }
67
- // Search for matching model
68
- const matches = search(normalized);
69
- const firstMatch = matches[0];
70
- if (firstMatch) {
71
- return firstMatch.id;
72
- }
73
- // Return as-is if nothing found
74
- return input;
75
- }
76
- /**
77
- * Providers that support direct SDK access (not via OpenRouter)
78
- * These providers have special capabilities like MCP, extended thinking, etc.
79
- */
80
- export const DIRECT_PROVIDERS = ['openai', 'anthropic', 'google'];
81
- /**
82
- * Resolve a model alias and get full routing information
83
- *
84
- * @example
85
- * const info = resolveWithProvider('opus')
86
- * // {
87
- * // id: 'anthropic/claude-opus-4.5',
88
- * // provider: 'anthropic',
89
- * // providerModelId: 'claude-opus-4-5-20251101',
90
- * // supportsDirectRouting: true,
91
- * // model: { ... }
92
- * // }
93
- */
94
- export function resolveWithProvider(input) {
95
- const id = resolve(input);
96
- const model = get(id);
97
- // Extract provider from ID (e.g., 'anthropic' from 'anthropic/claude-opus-4.5')
98
- const slashIndex = id.indexOf('/');
99
- const provider = slashIndex > 0 ? id.substring(0, slashIndex) : 'unknown';
100
- const supportsDirectRouting = DIRECT_PROVIDERS.includes(provider);
101
- return {
102
- id,
103
- provider,
104
- providerModelId: model?.provider_model_id,
105
- supportsDirectRouting,
106
- model
107
- };
108
- }
@@ -1,335 +0,0 @@
1
- /**
2
- * Tests for model listing, resolution, and search
3
- *
4
- * These are pure unit tests - no external API calls needed.
5
- */
6
- import { describe, it, expect, beforeEach } from 'vitest';
7
- import { list, get, search, resolve, resolveWithProvider, DIRECT_PROVIDERS, } from './models.js';
8
- import { ALIASES } from './aliases.js';
9
- describe('list', () => {
10
- it('returns an array of models', () => {
11
- const models = list();
12
- expect(Array.isArray(models)).toBe(true);
13
- });
14
- it('returns models with required properties', () => {
15
- const models = list();
16
- if (models.length > 0) {
17
- const model = models[0];
18
- expect(model).toHaveProperty('id');
19
- expect(model).toHaveProperty('name');
20
- expect(model).toHaveProperty('context_length');
21
- expect(model).toHaveProperty('pricing');
22
- expect(model.pricing).toHaveProperty('prompt');
23
- expect(model.pricing).toHaveProperty('completion');
24
- }
25
- });
26
- it('caches results on subsequent calls', () => {
27
- const models1 = list();
28
- const models2 = list();
29
- expect(models1).toBe(models2); // Same reference
30
- });
31
- it('returns empty array if models.json does not exist', () => {
32
- // This test verifies graceful handling of missing data file
33
- const models = list();
34
- expect(Array.isArray(models)).toBe(true);
35
- });
36
- });
37
- describe('get', () => {
38
- it('returns undefined for non-existent model', () => {
39
- const model = get('non-existent/model-id');
40
- expect(model).toBeUndefined();
41
- });
42
- it('returns model info for valid model ID', () => {
43
- const models = list();
44
- if (models.length > 0) {
45
- const firstModel = models[0];
46
- const retrieved = get(firstModel.id);
47
- expect(retrieved).toBeDefined();
48
- expect(retrieved?.id).toBe(firstModel.id);
49
- expect(retrieved?.name).toBe(firstModel.name);
50
- }
51
- });
52
- it('performs exact match only', () => {
53
- const models = list();
54
- if (models.length > 0) {
55
- const model = models[0];
56
- const partialId = model.id.split('/')[0]; // Just the provider
57
- const result = get(partialId);
58
- // Should not match partial ID
59
- if (result) {
60
- expect(result.id).toBe(partialId); // Only matches if there's an exact model with this ID
61
- }
62
- }
63
- });
64
- });
65
- describe('search', () => {
66
- it('returns empty array for no matches', () => {
67
- const results = search('this-should-not-match-anything-12345');
68
- expect(results).toEqual([]);
69
- });
70
- it('searches by model ID', () => {
71
- const models = list();
72
- if (models.length > 0) {
73
- const model = models[0];
74
- const idPart = model.id.split('/')[0]; // Provider name
75
- const results = search(idPart);
76
- expect(results.length).toBeGreaterThan(0);
77
- expect(results.some(m => m.id.includes(idPart))).toBe(true);
78
- }
79
- });
80
- it('searches by model name', () => {
81
- const models = list();
82
- if (models.length > 0) {
83
- const model = models[0];
84
- const namePart = model.name.split(' ')[0].toLowerCase();
85
- const results = search(namePart);
86
- expect(results.length).toBeGreaterThan(0);
87
- }
88
- });
89
- it('is case-insensitive', () => {
90
- const models = list();
91
- if (models.length > 0) {
92
- const model = models[0];
93
- const idLower = model.id.toLowerCase();
94
- const idUpper = model.id.toUpperCase();
95
- const resultsLower = search(idLower);
96
- const resultsUpper = search(idUpper);
97
- expect(resultsLower).toEqual(resultsUpper);
98
- }
99
- });
100
- it('searches in both id and name fields', () => {
101
- const models = list();
102
- if (models.length > 0) {
103
- // Find a model and search for part of its name
104
- const model = models.find(m => m.name.includes(' '));
105
- if (model) {
106
- const namePart = model.name.split(' ')[0].toLowerCase();
107
- const results = search(namePart);
108
- expect(results.some(m => m.id === model.id || m.name.toLowerCase().includes(namePart))).toBe(true);
109
- }
110
- }
111
- });
112
- it('returns multiple matches', () => {
113
- const models = list();
114
- if (models.length > 1) {
115
- // Search for a common term that should match multiple models
116
- const commonProviders = ['anthropic', 'openai', 'google', 'meta'];
117
- for (const provider of commonProviders) {
118
- const results = search(provider);
119
- if (results.length > 1) {
120
- expect(results.length).toBeGreaterThan(1);
121
- break;
122
- }
123
- }
124
- }
125
- });
126
- });
127
- describe('resolve', () => {
128
- beforeEach(() => {
129
- // Ensure we have fresh data
130
- list();
131
- });
132
- describe('alias resolution', () => {
133
- it('resolves known aliases', () => {
134
- const result = resolve('opus');
135
- expect(result).toBe(ALIASES['opus']);
136
- });
137
- it('resolves claude alias', () => {
138
- const result = resolve('claude');
139
- expect(result).toBe(ALIASES['claude']);
140
- });
141
- it('resolves gpt alias', () => {
142
- const result = resolve('gpt');
143
- expect(result).toBe(ALIASES['gpt']);
144
- });
145
- it('resolves llama alias', () => {
146
- const result = resolve('llama');
147
- expect(result).toBe(ALIASES['llama']);
148
- });
149
- it('is case-insensitive for aliases', () => {
150
- const lower = resolve('opus');
151
- const upper = resolve('OPUS');
152
- const mixed = resolve('Opus');
153
- expect(lower).toBe(upper);
154
- expect(lower).toBe(mixed);
155
- });
156
- it('handles whitespace in input', () => {
157
- const result = resolve(' opus ');
158
- expect(result).toBe(ALIASES['opus']);
159
- });
160
- it('resolves all documented aliases', () => {
161
- // Test key aliases from the README
162
- const aliasesToTest = [
163
- ['opus', 'anthropic/claude-opus-4.5'],
164
- ['sonnet', 'anthropic/claude-sonnet-4.5'],
165
- ['haiku', 'anthropic/claude-haiku-4.5'],
166
- ['gpt-4o', 'openai/gpt-4o'],
167
- ['gemini', 'google/gemini-2.5-flash'],
168
- ['llama-70b', 'meta-llama/llama-3.3-70b-instruct'],
169
- ['mistral', 'mistralai/mistral-large-2411'],
170
- ];
171
- for (const [alias, expected] of aliasesToTest) {
172
- const result = resolve(alias);
173
- expect(result).toBe(expected);
174
- }
175
- });
176
- });
177
- describe('full ID passthrough', () => {
178
- it('returns full ID as-is if it exists', () => {
179
- const models = list();
180
- if (models.length > 0) {
181
- const model = models[0];
182
- const result = resolve(model.id);
183
- expect(result).toBe(model.id);
184
- }
185
- });
186
- it('returns unknown full ID as-is', () => {
187
- const unknownId = 'unknown-provider/unknown-model';
188
- const result = resolve(unknownId);
189
- expect(result).toBe(unknownId);
190
- });
191
- it('detects full ID by slash character', () => {
192
- const result = resolve('custom/model-name');
193
- expect(result).toBe('custom/model-name');
194
- });
195
- });
196
- describe('partial name search', () => {
197
- it('finds model by partial name', () => {
198
- const models = list();
199
- if (models.length > 0) {
200
- const model = models[0];
201
- const provider = model.id.split('/')[0];
202
- const result = resolve(provider);
203
- // Should find a model from that provider
204
- expect(result).toContain('/');
205
- }
206
- });
207
- it('returns first match for partial search', () => {
208
- const result = resolve('claude');
209
- // Should return an alias if it exists, or search result
210
- expect(result).toBeTruthy();
211
- expect(typeof result).toBe('string');
212
- });
213
- it('returns input as-is if no matches found', () => {
214
- const input = 'unknown-model-xyz';
215
- const result = resolve(input);
216
- expect(result).toBe(input);
217
- });
218
- });
219
- describe('resolution priority', () => {
220
- it('prioritizes aliases over search', () => {
221
- // 'opus' is an alias, so it should resolve to the alias target
222
- // even if there are other models containing 'opus'
223
- const result = resolve('opus');
224
- expect(result).toBe(ALIASES['opus']);
225
- });
226
- it('checks full ID before partial search', () => {
227
- const models = list();
228
- if (models.length > 0) {
229
- const model = models[0];
230
- const result = resolve(model.id);
231
- expect(result).toBe(model.id);
232
- }
233
- });
234
- });
235
- });
236
- describe('resolveWithProvider', () => {
237
- it('extracts provider from model ID', () => {
238
- const result = resolveWithProvider('opus');
239
- expect(result.provider).toBe('anthropic');
240
- });
241
- it('includes resolved model ID', () => {
242
- const result = resolveWithProvider('opus');
243
- expect(result.id).toBe(ALIASES['opus']);
244
- });
245
- it('identifies direct routing support', () => {
246
- const anthropic = resolveWithProvider('opus');
247
- expect(anthropic.supportsDirectRouting).toBe(true);
248
- const openai = resolveWithProvider('gpt');
249
- expect(openai.supportsDirectRouting).toBe(true);
250
- const google = resolveWithProvider('gemini');
251
- expect(google.supportsDirectRouting).toBe(true);
252
- });
253
- it('identifies non-direct providers', () => {
254
- // Use a model from a provider not in DIRECT_PROVIDERS
255
- const models = list();
256
- const nonDirectModel = models.find(m => {
257
- const provider = m.id.split('/')[0];
258
- return !DIRECT_PROVIDERS.includes(provider);
259
- });
260
- if (nonDirectModel) {
261
- const result = resolveWithProvider(nonDirectModel.id);
262
- expect(result.supportsDirectRouting).toBe(false);
263
- }
264
- });
265
- it('includes full model info if available', () => {
266
- const result = resolveWithProvider('opus');
267
- if (result.model) {
268
- expect(result.model).toHaveProperty('id');
269
- expect(result.model).toHaveProperty('name');
270
- expect(result.model).toHaveProperty('pricing');
271
- }
272
- });
273
- it('includes provider model ID if available', () => {
274
- const result = resolveWithProvider('opus');
275
- if (result.model?.provider_model_id) {
276
- expect(result.providerModelId).toBeDefined();
277
- expect(typeof result.providerModelId).toBe('string');
278
- }
279
- });
280
- it('handles unknown models gracefully', () => {
281
- const result = resolveWithProvider('unknown/model');
282
- expect(result.id).toBe('unknown/model');
283
- expect(result.provider).toBe('unknown');
284
- expect(result.model).toBeUndefined();
285
- });
286
- it('handles models without provider prefix', () => {
287
- const result = resolveWithProvider('opus');
288
- expect(result.provider).toBeTruthy();
289
- expect(result.id).toContain('/');
290
- });
291
- });
292
- describe('DIRECT_PROVIDERS', () => {
293
- it('contains expected providers', () => {
294
- expect(DIRECT_PROVIDERS).toContain('anthropic');
295
- expect(DIRECT_PROVIDERS).toContain('openai');
296
- expect(DIRECT_PROVIDERS).toContain('google');
297
- });
298
- it('has exactly 3 providers', () => {
299
- expect(DIRECT_PROVIDERS.length).toBe(3);
300
- });
301
- it('is readonly', () => {
302
- // Type check - this should compile
303
- const providers = DIRECT_PROVIDERS;
304
- expect(providers).toBeDefined();
305
- });
306
- });
307
- describe('ModelInfo type', () => {
308
- it('models have correct structure', () => {
309
- const models = list();
310
- if (models.length > 0) {
311
- const model = models[0];
312
- expect(typeof model.id).toBe('string');
313
- expect(typeof model.name).toBe('string');
314
- expect(typeof model.context_length).toBe('number');
315
- expect(typeof model.pricing.prompt).toBe('string');
316
- expect(typeof model.pricing.completion).toBe('string');
317
- if (model.architecture) {
318
- expect(typeof model.architecture.modality).toBe('string');
319
- expect(Array.isArray(model.architecture.input_modalities)).toBe(true);
320
- expect(Array.isArray(model.architecture.output_modalities)).toBe(true);
321
- }
322
- }
323
- });
324
- });
325
- describe('ResolvedModel type', () => {
326
- it('returns complete resolution info', () => {
327
- const result = resolveWithProvider('opus');
328
- expect(result).toHaveProperty('id');
329
- expect(result).toHaveProperty('provider');
330
- expect(result).toHaveProperty('supportsDirectRouting');
331
- expect(typeof result.id).toBe('string');
332
- expect(typeof result.provider).toBe('string');
333
- expect(typeof result.supportsDirectRouting).toBe('boolean');
334
- });
335
- });
package/vitest.config.js DELETED
@@ -1,10 +0,0 @@
1
- import { defineConfig } from 'vitest/config';
2
- export default defineConfig({
3
- test: {
4
- globals: false,
5
- environment: 'node',
6
- include: ['src/**/*.test.ts'],
7
- testTimeout: 10000,
8
- hookTimeout: 10000,
9
- },
10
- });