Skip to content

Commit 4bb1a89

Browse files
committed
add api endpoint for specific model info (including benchmarks)
1 parent 641d31b commit 4bb1a89

2 files changed

Lines changed: 355 additions & 0 deletions

File tree

api-docs.md

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1561,6 +1561,79 @@ curl -X GET "http://localhost:3432/api/models" \
15611561

15621562
---
15631563

1564+
#### GET `/api/models/[modelId]/info`
1565+
Get detailed model info (including benchmarks) for a single model. Intended to be called on-demand when the user requests more details.
1566+
1567+
**Authentication**: Session or API Key
1568+
1569+
**Path Parameters**:
1570+
- `modelId`: (Required) Model ID.
1571+
1572+
**Response**:
1573+
```json
1574+
{
1575+
"model": {
1576+
"id": "string",
1577+
"name": "string",
1578+
"description": "string",
1579+
"icon_url": "string",
1580+
"owned_by": "string",
1581+
"context_length": "number",
1582+
"max_output_tokens": "number",
1583+
"created": "number",
1584+
"pricing": {
1585+
"prompt": "string",
1586+
"completion": "string",
1587+
"image": "string",
1588+
"request": "string"
1589+
},
1590+
"cost_estimate": "number",
1591+
"subscription": {
1592+
"included": "boolean",
1593+
"note": "string"
1594+
},
1595+
"capabilities": {
1596+
"vision": "boolean",
1597+
"reasoning": "boolean",
1598+
"images": "boolean",
1599+
"video": "boolean"
1600+
}
1601+
},
1602+
"benchmarks": {
1603+
"available": "boolean",
1604+
"stale": "boolean",
1605+
"source": "string",
1606+
"source_url": "string",
1607+
"llm": {
1608+
"name": "string",
1609+
"slug": "string",
1610+
"intelligence": "number",
1611+
"coding": "number",
1612+
"math": "number",
1613+
"speed_tokens_per_second": "number"
1614+
},
1615+
"image": {
1616+
"name": "string",
1617+
"slug": "string",
1618+
"elo": "number",
1619+
"rank": "number"
1620+
}
1621+
}
1622+
}
1623+
```
1624+
1625+
**Notes**:
1626+
- Uses cached Artificial Analysis data (1 hour TTL). `available` will be `false` if `ARTIFICIAL_ANALYSIS_API_KEY` is not configured.
1627+
- `llm`/`image` benchmark blocks are omitted when no match is found.
1628+
1629+
**CURL Example**:
1630+
```bash
1631+
curl -X GET "http://localhost:3432/api/models/gpt-4/info" \
1632+
-b "session_cookie=your_session"
1633+
```
1634+
1635+
---
1636+
15641637
### Model Providers
15651638

15661639
#### GET `/api/model-providers`
Lines changed: 282 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,282 @@
1+
import { json, type RequestHandler } from '@sveltejs/kit';
2+
import { getNanoGPTModels, type NanoGPTModel } from '$lib/backend/models/nano-gpt';
3+
import type { AALLMModel, AAImageModel, AABenchmarkData } from '$lib/types/artificial-analysis';
4+
import { getAuthenticatedUserId } from '$lib/backend/auth-utils';
5+
6+
let cachedBenchmarks: AABenchmarkData | null = null;
7+
let benchmarkCacheTimestamp = 0;
8+
const BENCHMARK_CACHE_TTL_MS = 60 * 60 * 1000;
9+
10+
async function fetchBenchmarkData(): Promise<{
11+
available: boolean;
12+
stale?: boolean;
13+
data: AABenchmarkData;
14+
}> {
15+
const apiKey = process.env.ARTIFICIAL_ANALYSIS_API_KEY;
16+
if (!apiKey) {
17+
return { available: false, data: { llms: [], imageModels: [] } };
18+
}
19+
20+
const now = Date.now();
21+
if (cachedBenchmarks && now - benchmarkCacheTimestamp < BENCHMARK_CACHE_TTL_MS) {
22+
return { available: true, data: cachedBenchmarks };
23+
}
24+
25+
try {
26+
const [llmResponse, imageResponse] = await Promise.all([
27+
fetch('https://artificialanalysis.ai/api/v2/data/llms/models', {
28+
headers: { 'x-api-key': apiKey },
29+
}),
30+
fetch('https://artificialanalysis.ai/api/v2/data/media/text-to-image', {
31+
headers: { 'x-api-key': apiKey },
32+
}),
33+
]);
34+
35+
let llms: AALLMModel[] = [];
36+
let imageModels: AAImageModel[] = [];
37+
38+
if (llmResponse.ok) {
39+
const llmData = await llmResponse.json();
40+
llms = llmData.data || [];
41+
}
42+
43+
if (imageResponse.ok) {
44+
const imageData = await imageResponse.json();
45+
imageModels = imageData.data || [];
46+
}
47+
48+
cachedBenchmarks = { llms, imageModels };
49+
benchmarkCacheTimestamp = now;
50+
51+
return { available: true, data: cachedBenchmarks };
52+
} catch (error) {
53+
console.error('[model-info] Failed to fetch Artificial Analysis benchmarks:', error);
54+
55+
if (cachedBenchmarks) {
56+
return { available: true, stale: true, data: cachedBenchmarks };
57+
}
58+
59+
return { available: false, data: { llms: [], imageModels: [] } };
60+
}
61+
}
62+
63+
function normalizeForMatch(str: string): string {
64+
return str
65+
.toLowerCase()
66+
.replace(/[^a-z0-9]/g, '')
67+
.trim();
68+
}
69+
70+
function stripSuffixes(str: string): string {
71+
return str
72+
.replace(/[-_]?original$/i, '')
73+
.replace(/[-_]?\d{8}$/i, '')
74+
.trim();
75+
}
76+
77+
function extractModelName(id: string): string {
78+
const parts = id.split('/');
79+
const name = (parts.length > 1 ? parts[parts.length - 1] : id) ?? id;
80+
return stripSuffixes(name);
81+
}
82+
83+
function extractKeyTokens(name: string): Set<string> {
84+
const normalized = name
85+
.toLowerCase()
86+
.replace(/(\d+)\.(\d+)/g, '$1$2')
87+
.replace(/[^a-z0-9]+/g, ' ');
88+
const tokens = normalized.split(' ').filter((token) => token.length > 0);
89+
return new Set(tokens.filter((token) => !/^\d{8,}$/.test(token)));
90+
}
91+
92+
function tokensMatch(set1: Set<string>, set2: Set<string>): boolean {
93+
if (set1.size === 0 || set2.size === 0) return false;
94+
let matches = 0;
95+
for (const token of set1) {
96+
if (set2.has(token)) matches++;
97+
}
98+
return matches >= 2;
99+
}
100+
101+
function isImageOnlyModel(model: NanoGPTModel): boolean {
102+
return (
103+
(model.architecture?.output_modalities?.includes('image') &&
104+
model.architecture?.output_modalities?.length === 1) ??
105+
false
106+
);
107+
}
108+
109+
function findBestLlmBenchmark(model: NanoGPTModel, llms: AALLMModel[]): AALLMModel | null {
110+
if (!llms.length) return null;
111+
112+
const modelName = stripSuffixes(model.name).toLowerCase();
113+
const modelIdFull = model.id.toLowerCase();
114+
const modelIdShort = extractModelName(model.id).toLowerCase();
115+
const normalizedName = normalizeForMatch(stripSuffixes(model.name));
116+
const normalizedId = normalizeForMatch(modelIdShort);
117+
const modelNameTokens = extractKeyTokens(stripSuffixes(model.name));
118+
const modelIdTokens = extractKeyTokens(modelIdShort);
119+
120+
let bestMatch: AALLMModel | null = null;
121+
let bestScore = 0;
122+
123+
for (const llm of llms) {
124+
const aaName = llm.name.toLowerCase();
125+
const aaSlug = llm.slug.toLowerCase();
126+
const normalizedAaName = normalizeForMatch(llm.name);
127+
const normalizedAaSlug = normalizeForMatch(llm.slug);
128+
129+
let score = 0;
130+
131+
if (modelName === aaName) {
132+
score = 100;
133+
} else if (modelIdShort === aaSlug || modelIdFull === aaSlug) {
134+
score = 100;
135+
} else if (normalizedName === normalizedAaName) {
136+
score = 90;
137+
} else if (normalizedId === normalizedAaSlug) {
138+
score = 90;
139+
} else {
140+
const aaNameTokens = extractKeyTokens(llm.name);
141+
const aaSlugTokens = extractKeyTokens(llm.slug);
142+
143+
let nameMatches = 0;
144+
for (const token of modelNameTokens) {
145+
if (aaNameTokens.has(token)) nameMatches++;
146+
}
147+
let slugMatches = 0;
148+
for (const token of modelIdTokens) {
149+
if (aaSlugTokens.has(token)) slugMatches++;
150+
}
151+
152+
const maxTokenMatches = Math.max(nameMatches, slugMatches);
153+
const minTokensNeeded = Math.min(
154+
modelNameTokens.size,
155+
modelIdTokens.size,
156+
aaNameTokens.size,
157+
aaSlugTokens.size
158+
);
159+
160+
if (maxTokenMatches >= 2 && maxTokenMatches >= minTokensNeeded) {
161+
score = 50 + maxTokenMatches * 10;
162+
}
163+
}
164+
165+
if (score > bestScore) {
166+
bestScore = score;
167+
bestMatch = llm;
168+
}
169+
}
170+
171+
return bestMatch && bestScore >= 50 ? bestMatch : null;
172+
}
173+
174+
function findImageBenchmark(model: NanoGPTModel, imageModels: AAImageModel[]): AAImageModel | null {
175+
if (!imageModels.length) return null;
176+
177+
const modelName = model.name.toLowerCase();
178+
const modelIdShort = extractModelName(model.id).toLowerCase();
179+
const normalizedName = normalizeForMatch(model.name);
180+
const normalizedId = normalizeForMatch(modelIdShort);
181+
const modelNameTokens = extractKeyTokens(model.name);
182+
const modelIdTokens = extractKeyTokens(modelIdShort);
183+
184+
return (
185+
imageModels.find((img) => {
186+
const aaName = img.name.toLowerCase();
187+
const aaSlug = img.slug.toLowerCase();
188+
const normalizedAaName = normalizeForMatch(img.name);
189+
const normalizedAaSlug = normalizeForMatch(img.slug);
190+
191+
if (modelName === aaName || modelIdShort === aaSlug) return true;
192+
if (normalizedName === normalizedAaName || normalizedId === normalizedAaSlug) return true;
193+
if (modelName.includes(aaName) || aaName.includes(modelName)) return true;
194+
if (modelIdShort.includes(aaSlug) || aaSlug.includes(modelIdShort)) return true;
195+
if (normalizedName.includes(normalizedAaName) || normalizedAaName.includes(normalizedName)) return true;
196+
if (normalizedId.includes(normalizedAaSlug) || normalizedAaSlug.includes(normalizedId)) return true;
197+
198+
const aaNameTokens = extractKeyTokens(img.name);
199+
const aaSlugTokens = extractKeyTokens(img.slug);
200+
if (tokensMatch(modelNameTokens, aaNameTokens)) return true;
201+
if (tokensMatch(modelIdTokens, aaSlugTokens)) return true;
202+
if (tokensMatch(modelNameTokens, aaSlugTokens)) return true;
203+
if (tokensMatch(modelIdTokens, aaNameTokens)) return true;
204+
205+
return false;
206+
}) ?? null
207+
);
208+
}
209+
210+
export const GET: RequestHandler = async ({ params, request }) => {
211+
await getAuthenticatedUserId(request);
212+
213+
const modelId = params.modelId;
214+
if (!modelId) {
215+
return json({ error: 'modelId is required' }, { status: 400 });
216+
}
217+
218+
const modelsResult = await getNanoGPTModels();
219+
const models = modelsResult.unwrapOr([] as NanoGPTModel[]);
220+
const model = models.find((item) => item.id === modelId);
221+
222+
if (!model) {
223+
return json({ error: 'Model not found' }, { status: 404 });
224+
}
225+
226+
const outputModalities = model.architecture?.output_modalities || [];
227+
const capabilities = {
228+
vision: model.capabilities?.vision ?? false,
229+
reasoning: model.capabilities?.reasoning ?? false,
230+
images: outputModalities.includes('image') && outputModalities.length === 1,
231+
video: outputModalities.includes('video'),
232+
};
233+
234+
const benchmarkData = await fetchBenchmarkData();
235+
const llmBenchmark = isImageOnlyModel(model)
236+
? null
237+
: findBestLlmBenchmark(model, benchmarkData.data.llms);
238+
const imageBenchmark = isImageOnlyModel(model)
239+
? findImageBenchmark(model, benchmarkData.data.imageModels)
240+
: null;
241+
242+
return json({
243+
model: {
244+
id: model.id,
245+
name: model.name,
246+
description: model.description,
247+
icon_url: model.icon_url,
248+
owned_by: model.owned_by,
249+
context_length: model.context_length,
250+
max_output_tokens: model.max_output_tokens,
251+
created: model.created,
252+
pricing: model.pricing,
253+
cost_estimate: model.cost_estimate,
254+
subscription: model.subscription || { included: false, note: '' },
255+
capabilities,
256+
},
257+
benchmarks: {
258+
available: benchmarkData.available,
259+
stale: benchmarkData.stale ?? false,
260+
source: 'artificialanalysis',
261+
source_url: 'https://artificialanalysis.ai',
262+
llm: llmBenchmark
263+
? {
264+
name: llmBenchmark.name,
265+
slug: llmBenchmark.slug,
266+
intelligence: llmBenchmark.evaluations?.artificial_analysis_intelligence_index,
267+
coding: llmBenchmark.evaluations?.artificial_analysis_coding_index,
268+
math: llmBenchmark.evaluations?.artificial_analysis_math_index,
269+
speed_tokens_per_second: llmBenchmark.median_output_tokens_per_second,
270+
}
271+
: null,
272+
image: imageBenchmark
273+
? {
274+
name: imageBenchmark.name,
275+
slug: imageBenchmark.slug,
276+
elo: imageBenchmark.elo,
277+
rank: imageBenchmark.rank,
278+
}
279+
: null,
280+
},
281+
});
282+
};

0 commit comments

Comments
 (0)