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