@tonyclaw/llm-inspector 1.14.1 → 1.14.2
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/.output/nitro.json +1 -1
- package/.output/public/assets/index-DDrUlr6L.js +105 -0
- package/.output/public/assets/index-DOG5AdQ9.css +1 -0
- package/.output/public/assets/{main-BzK2SzIB.js → main-BElVT2p3.js} +3 -3
- package/.output/server/_ssr/{index-Cso39vJc.mjs → index-DmLit8Ad.mjs} +145 -112
- package/.output/server/_ssr/index.mjs +2 -2
- package/.output/server/_ssr/{router-B8X3GXM2.mjs → router-BoeSXWHG.mjs} +539 -286
- package/.output/server/{_tanstack-start-manifest_v-vO4aM6jK.mjs → _tanstack-start-manifest_v-B6idtbmL.mjs} +1 -1
- package/.output/server/index.mjs +21 -21
- package/package.json +1 -1
- package/src/components/providers/ProviderCard.tsx +111 -6
- package/src/components/providers/ProviderForm.tsx +70 -34
- package/src/components/providers/ProvidersPanel.tsx +3 -3
- package/src/lib/providerContract.ts +1 -0
- package/src/lib/providerTestContract.ts +8 -0
- package/src/proxy/providers.ts +119 -21
- package/src/routes/api/providers.$providerId.test.log.ts +293 -0
- package/src/routes/api/providers.$providerId.ts +2 -1
- package/src/routes/api/providers.ts +3 -2
- package/.output/public/assets/index-DEUddp_2.css +0 -1
- package/.output/public/assets/index-ax85pt2A.js +0 -105
package/src/proxy/providers.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import Conf from "conf";
|
|
3
3
|
import { randomUUID } from "crypto";
|
|
4
|
-
import { mkdirSync } from "node:fs";
|
|
4
|
+
import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
5
5
|
import { join } from "node:path";
|
|
6
6
|
import { logger } from "./logger";
|
|
7
7
|
import { getDataDir, hasExplicitDataDir } from "./dataDir";
|
|
@@ -119,6 +119,79 @@ function migrateProviders(): void {
|
|
|
119
119
|
// Run migration on module load
|
|
120
120
|
migrateProviders();
|
|
121
121
|
|
|
122
|
+
/**
|
|
123
|
+
* Migrate: merge providers with identical (apiKey, anthropicBaseUrl, openaiBaseUrl)
|
|
124
|
+
* into multi-model providers. Existing single-model providers get their `model`
|
|
125
|
+
* value promoted to `models: [model]`.
|
|
126
|
+
*/
|
|
127
|
+
function migrateMultiModel(): void {
|
|
128
|
+
const providers = store.get("providers", []);
|
|
129
|
+
if (providers.length === 0) return;
|
|
130
|
+
let changed = false;
|
|
131
|
+
|
|
132
|
+
// Step 1: Promote single model to models array for any provider missing models
|
|
133
|
+
const promoted = providers.map((p) => {
|
|
134
|
+
if (p.models !== undefined && p.models.length > 0) return p;
|
|
135
|
+
const models = p.model !== undefined && p.model !== "" ? [p.model] : [];
|
|
136
|
+
changed = true;
|
|
137
|
+
return { ...p, models };
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
// Step 2: Merge providers with same (apiKey, anthropicBaseUrl, openaiBaseUrl)
|
|
141
|
+
const groups = new Map<string, ProviderConfig[]>();
|
|
142
|
+
for (const p of promoted) {
|
|
143
|
+
const key = `${p.apiKey}::${p.anthropicBaseUrl ?? ""}::${p.openaiBaseUrl ?? ""}`;
|
|
144
|
+
const group = groups.get(key);
|
|
145
|
+
if (group !== undefined) {
|
|
146
|
+
group.push(p);
|
|
147
|
+
} else {
|
|
148
|
+
groups.set(key, [p]);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const merged: ProviderConfig[] = [];
|
|
153
|
+
for (const group of groups.values()) {
|
|
154
|
+
const first = group[0];
|
|
155
|
+
if (group.length === 1 && first !== undefined) {
|
|
156
|
+
merged.push(first);
|
|
157
|
+
continue;
|
|
158
|
+
}
|
|
159
|
+
changed = true;
|
|
160
|
+
// Merge: keep the earliest provider's metadata, combine all models (deduplicated)
|
|
161
|
+
const sorted = group.toSorted(
|
|
162
|
+
(a, b) => new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime(),
|
|
163
|
+
);
|
|
164
|
+
const earliest = sorted[0];
|
|
165
|
+
if (earliest === undefined) continue;
|
|
166
|
+
const allModels = new Set<string>();
|
|
167
|
+
for (const p of sorted) {
|
|
168
|
+
for (const m of p.models ?? []) {
|
|
169
|
+
if (m !== "") allModels.add(m);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
// Take first non-empty source
|
|
173
|
+
const mergedSource = sorted.find((p) => p.source !== undefined)?.source;
|
|
174
|
+
merged.push({
|
|
175
|
+
...earliest,
|
|
176
|
+
models: [...allModels],
|
|
177
|
+
source: mergedSource,
|
|
178
|
+
updatedAt: new Date().toISOString(),
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
if (changed && merged.length > 0) {
|
|
183
|
+
// Safety backup before writing
|
|
184
|
+
try {
|
|
185
|
+
writeFileSync(`${store.path}.bak`, readFileSync(store.path, "utf-8"));
|
|
186
|
+
} catch {
|
|
187
|
+
// best-effort, ignore
|
|
188
|
+
}
|
|
189
|
+
store.set("providers", merged);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
migrateMultiModel();
|
|
194
|
+
|
|
122
195
|
// Override with JSON env var if provided (for testing)
|
|
123
196
|
const providersJson = process.env["LLM_INSPECTOR_PROVIDERS_JSON"];
|
|
124
197
|
if (providersJson !== undefined) {
|
|
@@ -155,7 +228,7 @@ export function addProvider(
|
|
|
155
228
|
apiKey: string,
|
|
156
229
|
format?: "anthropic" | "openai",
|
|
157
230
|
baseUrl?: string,
|
|
158
|
-
|
|
231
|
+
models?: string[],
|
|
159
232
|
authHeader?: "bearer" | "x-api-key",
|
|
160
233
|
apiDocsUrl?: string,
|
|
161
234
|
anthropicBaseUrl?: string,
|
|
@@ -163,11 +236,37 @@ export function addProvider(
|
|
|
163
236
|
source?: "company" | "personal",
|
|
164
237
|
): ProviderConfig {
|
|
165
238
|
const providers = getProviders();
|
|
239
|
+
const normalizedKey = normalizeApiKey(apiKey);
|
|
240
|
+
const resolvedAnthropicUrl =
|
|
241
|
+
anthropicBaseUrl ?? (format === "anthropic" && baseUrl !== undefined ? baseUrl : "");
|
|
242
|
+
const resolvedOpenaiUrl =
|
|
243
|
+
openaiBaseUrl ?? (format === "openai" && baseUrl !== undefined ? baseUrl : "");
|
|
166
244
|
const now = new Date().toISOString();
|
|
245
|
+
|
|
246
|
+
// If a provider with the same (apiKey, anthropicBaseUrl, openaiBaseUrl) already exists,
|
|
247
|
+
// merge the new models into it instead of creating a duplicate.
|
|
248
|
+
const existing = providers.find(
|
|
249
|
+
(p) =>
|
|
250
|
+
p.apiKey === normalizedKey &&
|
|
251
|
+
(p.anthropicBaseUrl ?? "") === (resolvedAnthropicUrl ?? "") &&
|
|
252
|
+
(p.openaiBaseUrl ?? "") === (resolvedOpenaiUrl ?? ""),
|
|
253
|
+
);
|
|
254
|
+
if (existing) {
|
|
255
|
+
const newModels = (models ?? []).filter((m) => m !== "");
|
|
256
|
+
if (newModels.length > 0) {
|
|
257
|
+
const mergedModels = new Set(existing.models ?? []);
|
|
258
|
+
for (const m of newModels) mergedModels.add(m);
|
|
259
|
+
existing.models = [...mergedModels];
|
|
260
|
+
existing.updatedAt = now;
|
|
261
|
+
store.set("providers", providers);
|
|
262
|
+
}
|
|
263
|
+
return existing;
|
|
264
|
+
}
|
|
265
|
+
|
|
167
266
|
const newProvider: ProviderConfig = {
|
|
168
267
|
id: randomUUID(),
|
|
169
268
|
name,
|
|
170
|
-
apiKey:
|
|
269
|
+
apiKey: normalizedKey,
|
|
171
270
|
format:
|
|
172
271
|
format ??
|
|
173
272
|
(anthropicBaseUrl !== undefined
|
|
@@ -176,14 +275,13 @@ export function addProvider(
|
|
|
176
275
|
? "openai"
|
|
177
276
|
: undefined),
|
|
178
277
|
baseUrl,
|
|
179
|
-
|
|
278
|
+
models: models ?? [],
|
|
180
279
|
authHeader: authHeader ?? "bearer",
|
|
181
280
|
apiDocsUrl,
|
|
182
281
|
createdAt: now,
|
|
183
282
|
updatedAt: now,
|
|
184
|
-
anthropicBaseUrl:
|
|
185
|
-
|
|
186
|
-
openaiBaseUrl: openaiBaseUrl ?? (format === "openai" && baseUrl !== undefined ? baseUrl : ""),
|
|
283
|
+
anthropicBaseUrl: resolvedAnthropicUrl,
|
|
284
|
+
openaiBaseUrl: resolvedOpenaiUrl,
|
|
187
285
|
source,
|
|
188
286
|
};
|
|
189
287
|
providers.push(newProvider);
|
|
@@ -203,7 +301,7 @@ export function updateProvider(
|
|
|
203
301
|
id: existing.id,
|
|
204
302
|
name: updates.name ?? existing.name,
|
|
205
303
|
apiKey: updates.apiKey !== undefined ? normalizeApiKey(updates.apiKey) : existing.apiKey,
|
|
206
|
-
|
|
304
|
+
models: updates.models !== undefined ? updates.models : existing.models,
|
|
207
305
|
format: updates.format ?? existing.format,
|
|
208
306
|
baseUrl: updates.baseUrl !== undefined ? updates.baseUrl : existing.baseUrl,
|
|
209
307
|
authHeader: updates.authHeader ?? existing.authHeader,
|
|
@@ -371,21 +469,21 @@ export function findProviderByModel(model: string): ProviderConfig | null {
|
|
|
371
469
|
if (modelLower.startsWith(providerPrefix)) {
|
|
372
470
|
return provider;
|
|
373
471
|
}
|
|
374
|
-
// Strategy 2: match against provider.
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
return provider;
|
|
472
|
+
// Strategy 2: match against provider.models array with normalization
|
|
473
|
+
const modelList = provider.models ?? (provider.model !== undefined ? [provider.model] : []);
|
|
474
|
+
for (const m of modelList) {
|
|
475
|
+
if (m !== "" && modelNormalized === normalizeModelName(m)) {
|
|
476
|
+
return provider;
|
|
477
|
+
}
|
|
381
478
|
}
|
|
382
479
|
// Strategy 3: fallback - concatenate provider name with model part and compare
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
480
|
+
for (const m of modelList) {
|
|
481
|
+
if (m !== "") {
|
|
482
|
+
const modelPart = modelNormalized.replace(normalizeModelName(provider.name), "");
|
|
483
|
+
const concatenated = normalizeModelName(provider.name) + modelPart;
|
|
484
|
+
if (modelNormalized === concatenated) {
|
|
485
|
+
return provider;
|
|
486
|
+
}
|
|
389
487
|
}
|
|
390
488
|
}
|
|
391
489
|
}
|
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
import { createFileRoute } from "@tanstack/react-router";
|
|
2
|
+
import { getProvider, getModelUsageName } from "../../proxy/providers";
|
|
3
|
+
import { appendLogEntry } from "../../proxy/logger";
|
|
4
|
+
import { addTestLogEntry } from "../../proxy/store";
|
|
5
|
+
import {
|
|
6
|
+
ProviderTestResultsSchema,
|
|
7
|
+
type ProviderTestResult as TestResult,
|
|
8
|
+
type ProviderTestState,
|
|
9
|
+
} from "../../lib/providerTestContract";
|
|
10
|
+
|
|
11
|
+
function hasSuccessField(result: ProviderTestState): result is TestResult {
|
|
12
|
+
return Object.prototype.hasOwnProperty.call(result, "success");
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function createTestLogEntry(
|
|
16
|
+
providerName: string,
|
|
17
|
+
path: string,
|
|
18
|
+
body: string,
|
|
19
|
+
upstreamUrl: string,
|
|
20
|
+
result: TestResult,
|
|
21
|
+
isTest: boolean,
|
|
22
|
+
): Record<string, unknown> {
|
|
23
|
+
return {
|
|
24
|
+
timestamp: new Date().toISOString(),
|
|
25
|
+
id: `test-${Date.now()}`,
|
|
26
|
+
method: "POST",
|
|
27
|
+
path,
|
|
28
|
+
model: isTest ? result.model : undefined,
|
|
29
|
+
sessionId: null,
|
|
30
|
+
rawRequestBody: body,
|
|
31
|
+
responseStatus: result.success ? 200 : 500,
|
|
32
|
+
responseText: result.rawResponse ?? JSON.stringify(result),
|
|
33
|
+
inputTokens: result.inputTokens ?? null,
|
|
34
|
+
outputTokens: result.outputTokens ?? null,
|
|
35
|
+
elapsedMs: result.latencyMs ?? 0,
|
|
36
|
+
streaming: result.streaming ?? false,
|
|
37
|
+
userAgent: "provider-test",
|
|
38
|
+
origin: null,
|
|
39
|
+
upstreamUrl,
|
|
40
|
+
error: result.success ? null : (result.error?.message ?? String(result.error)),
|
|
41
|
+
isTest: true,
|
|
42
|
+
providerName,
|
|
43
|
+
headers: result.requestHeaders ?? {},
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async function logModelResults(
|
|
48
|
+
displayName: string,
|
|
49
|
+
providerName: string,
|
|
50
|
+
anthropicUrl: string | undefined,
|
|
51
|
+
openaiUrl: string | undefined,
|
|
52
|
+
modelResults: {
|
|
53
|
+
anthropic: { nonStreaming: ProviderTestState; streaming: ProviderTestState };
|
|
54
|
+
openai: { nonStreaming: ProviderTestState; streaming: ProviderTestState };
|
|
55
|
+
},
|
|
56
|
+
): Promise<void> {
|
|
57
|
+
const usageModel = getModelUsageName(displayName, providerName);
|
|
58
|
+
|
|
59
|
+
if (anthropicUrl !== undefined) {
|
|
60
|
+
const nsResult = modelResults.anthropic.nonStreaming;
|
|
61
|
+
const sResult = modelResults.anthropic.streaming;
|
|
62
|
+
if (hasSuccessField(nsResult) && hasSuccessField(sResult)) {
|
|
63
|
+
const nonStreamingResult = nsResult;
|
|
64
|
+
const streamingResult = sResult;
|
|
65
|
+
|
|
66
|
+
const requestBody = JSON.stringify({
|
|
67
|
+
model: usageModel,
|
|
68
|
+
messages: [{ role: "user", content: "say hello and briefly introduce yourself" }],
|
|
69
|
+
max_tokens: 1024,
|
|
70
|
+
});
|
|
71
|
+
const upstreamUrl = `${anthropicUrl}/v1/messages`;
|
|
72
|
+
|
|
73
|
+
await addTestLogEntry({
|
|
74
|
+
timestamp: new Date().toISOString(),
|
|
75
|
+
method: "POST",
|
|
76
|
+
path: "/v1/messages",
|
|
77
|
+
model: nonStreamingResult.model ?? displayName,
|
|
78
|
+
sessionId: null,
|
|
79
|
+
rawRequestBody: requestBody,
|
|
80
|
+
responseStatus: nonStreamingResult.success ? 200 : 500,
|
|
81
|
+
responseText: nonStreamingResult.rawResponse ?? JSON.stringify(nonStreamingResult),
|
|
82
|
+
inputTokens: nonStreamingResult.inputTokens ?? null,
|
|
83
|
+
outputTokens: nonStreamingResult.outputTokens ?? null,
|
|
84
|
+
cacheCreationInputTokens: nonStreamingResult.cacheCreationInputTokens ?? null,
|
|
85
|
+
cacheReadInputTokens: nonStreamingResult.cacheReadInputTokens ?? null,
|
|
86
|
+
elapsedMs: nonStreamingResult.latencyMs ?? 0,
|
|
87
|
+
streaming: false,
|
|
88
|
+
userAgent: "provider-test",
|
|
89
|
+
origin: null,
|
|
90
|
+
apiFormat: "anthropic",
|
|
91
|
+
isTest: true,
|
|
92
|
+
providerName,
|
|
93
|
+
headers: nonStreamingResult.requestHeaders ?? {},
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
appendLogEntry(
|
|
97
|
+
createTestLogEntry(
|
|
98
|
+
providerName,
|
|
99
|
+
"/v1/messages",
|
|
100
|
+
requestBody,
|
|
101
|
+
upstreamUrl,
|
|
102
|
+
nonStreamingResult,
|
|
103
|
+
true,
|
|
104
|
+
),
|
|
105
|
+
);
|
|
106
|
+
|
|
107
|
+
const streamingRequestBody = JSON.stringify({
|
|
108
|
+
model: usageModel,
|
|
109
|
+
messages: [{ role: "user", content: "say hello" }],
|
|
110
|
+
max_tokens: 256,
|
|
111
|
+
stream: true,
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
await addTestLogEntry({
|
|
115
|
+
timestamp: new Date().toISOString(),
|
|
116
|
+
method: "POST",
|
|
117
|
+
path: "/v1/messages",
|
|
118
|
+
model: streamingResult.model ?? displayName,
|
|
119
|
+
sessionId: null,
|
|
120
|
+
rawRequestBody: streamingRequestBody,
|
|
121
|
+
responseStatus: streamingResult.success ? 200 : 500,
|
|
122
|
+
responseText: streamingResult.rawResponse ?? JSON.stringify(streamingResult),
|
|
123
|
+
inputTokens: streamingResult.inputTokens ?? null,
|
|
124
|
+
outputTokens: streamingResult.outputTokens ?? null,
|
|
125
|
+
cacheCreationInputTokens: streamingResult.cacheCreationInputTokens ?? null,
|
|
126
|
+
cacheReadInputTokens: streamingResult.cacheReadInputTokens ?? null,
|
|
127
|
+
elapsedMs: streamingResult.latencyMs ?? 0,
|
|
128
|
+
streaming: true,
|
|
129
|
+
streamingChunks: streamingResult.streamingChunks,
|
|
130
|
+
userAgent: "provider-test",
|
|
131
|
+
origin: null,
|
|
132
|
+
apiFormat: "anthropic",
|
|
133
|
+
isTest: true,
|
|
134
|
+
providerName,
|
|
135
|
+
headers: streamingResult.requestHeaders ?? {},
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
appendLogEntry(
|
|
139
|
+
createTestLogEntry(
|
|
140
|
+
providerName,
|
|
141
|
+
"/v1/messages",
|
|
142
|
+
streamingRequestBody,
|
|
143
|
+
upstreamUrl,
|
|
144
|
+
streamingResult,
|
|
145
|
+
true,
|
|
146
|
+
),
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (openaiUrl !== undefined) {
|
|
152
|
+
const nsResult = modelResults.openai.nonStreaming;
|
|
153
|
+
const sResult = modelResults.openai.streaming;
|
|
154
|
+
if (hasSuccessField(nsResult) && hasSuccessField(sResult)) {
|
|
155
|
+
const nonStreamingResult = nsResult;
|
|
156
|
+
const streamingResult = sResult;
|
|
157
|
+
|
|
158
|
+
const requestBody = JSON.stringify({
|
|
159
|
+
model: usageModel,
|
|
160
|
+
messages: [{ role: "user", content: "say hello and briefly introduce yourself" }],
|
|
161
|
+
max_tokens: 1024,
|
|
162
|
+
});
|
|
163
|
+
const upstreamUrl = `${openaiUrl}/v1/chat/completions`;
|
|
164
|
+
|
|
165
|
+
await addTestLogEntry({
|
|
166
|
+
timestamp: new Date().toISOString(),
|
|
167
|
+
method: "POST",
|
|
168
|
+
path: "/v1/chat/completions",
|
|
169
|
+
model: nonStreamingResult.model ?? displayName,
|
|
170
|
+
sessionId: null,
|
|
171
|
+
rawRequestBody: requestBody,
|
|
172
|
+
responseStatus: nonStreamingResult.success ? 200 : 500,
|
|
173
|
+
responseText: nonStreamingResult.rawResponse ?? JSON.stringify(nonStreamingResult),
|
|
174
|
+
inputTokens: nonStreamingResult.inputTokens ?? null,
|
|
175
|
+
outputTokens: nonStreamingResult.outputTokens ?? null,
|
|
176
|
+
cacheCreationInputTokens: null,
|
|
177
|
+
cacheReadInputTokens: null,
|
|
178
|
+
elapsedMs: nonStreamingResult.latencyMs ?? 0,
|
|
179
|
+
streaming: false,
|
|
180
|
+
userAgent: "provider-test",
|
|
181
|
+
origin: null,
|
|
182
|
+
apiFormat: "openai",
|
|
183
|
+
isTest: true,
|
|
184
|
+
providerName,
|
|
185
|
+
headers: nonStreamingResult.requestHeaders ?? {},
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
appendLogEntry(
|
|
189
|
+
createTestLogEntry(
|
|
190
|
+
providerName,
|
|
191
|
+
"/v1/chat/completions",
|
|
192
|
+
requestBody,
|
|
193
|
+
upstreamUrl,
|
|
194
|
+
nonStreamingResult,
|
|
195
|
+
true,
|
|
196
|
+
),
|
|
197
|
+
);
|
|
198
|
+
|
|
199
|
+
const streamingRequestBody = JSON.stringify({
|
|
200
|
+
model: usageModel,
|
|
201
|
+
messages: [{ role: "user", content: "say hello" }],
|
|
202
|
+
max_tokens: 256,
|
|
203
|
+
stream: true,
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
await addTestLogEntry({
|
|
207
|
+
timestamp: new Date().toISOString(),
|
|
208
|
+
method: "POST",
|
|
209
|
+
path: "/v1/chat/completions",
|
|
210
|
+
model: streamingResult.model ?? displayName,
|
|
211
|
+
sessionId: null,
|
|
212
|
+
rawRequestBody: streamingRequestBody,
|
|
213
|
+
responseStatus: streamingResult.success ? 200 : 500,
|
|
214
|
+
responseText: streamingResult.rawResponse ?? JSON.stringify(streamingResult),
|
|
215
|
+
inputTokens: streamingResult.inputTokens ?? null,
|
|
216
|
+
outputTokens: streamingResult.outputTokens ?? null,
|
|
217
|
+
cacheCreationInputTokens: null,
|
|
218
|
+
cacheReadInputTokens: null,
|
|
219
|
+
elapsedMs: streamingResult.latencyMs ?? 0,
|
|
220
|
+
streaming: true,
|
|
221
|
+
streamingChunks: streamingResult.streamingChunks,
|
|
222
|
+
userAgent: "provider-test",
|
|
223
|
+
origin: null,
|
|
224
|
+
apiFormat: "openai",
|
|
225
|
+
isTest: true,
|
|
226
|
+
providerName,
|
|
227
|
+
headers: streamingResult.requestHeaders ?? {},
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
appendLogEntry(
|
|
231
|
+
createTestLogEntry(
|
|
232
|
+
providerName,
|
|
233
|
+
"/v1/chat/completions",
|
|
234
|
+
streamingRequestBody,
|
|
235
|
+
upstreamUrl,
|
|
236
|
+
streamingResult,
|
|
237
|
+
true,
|
|
238
|
+
),
|
|
239
|
+
);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
export const Route = createFileRoute("/api/providers/$providerId/test/log")({
|
|
245
|
+
server: {
|
|
246
|
+
handlers: {
|
|
247
|
+
POST: async ({ params, request }: { params: { providerId: string }; request: Request }) => {
|
|
248
|
+
const provider = getProvider(params.providerId);
|
|
249
|
+
if (!provider) {
|
|
250
|
+
return Response.json({ error: "Provider not found" }, { status: 404 });
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
let body: unknown;
|
|
254
|
+
try {
|
|
255
|
+
body = await request.json();
|
|
256
|
+
} catch {
|
|
257
|
+
return Response.json({ error: "Invalid JSON body" }, { status: 400 });
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
const parsed = ProviderTestResultsSchema.safeParse(body);
|
|
261
|
+
if (!parsed.success) {
|
|
262
|
+
return Response.json(
|
|
263
|
+
{ error: `Invalid test results: ${parsed.error.message}` },
|
|
264
|
+
{ status: 400 },
|
|
265
|
+
);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
const results = parsed.data;
|
|
269
|
+
const anthropicUrl =
|
|
270
|
+
provider.anthropicBaseUrl !== undefined && provider.anthropicBaseUrl.length > 0
|
|
271
|
+
? provider.anthropicBaseUrl
|
|
272
|
+
: undefined;
|
|
273
|
+
const openaiUrl =
|
|
274
|
+
provider.openaiBaseUrl !== undefined && provider.openaiBaseUrl.length > 0
|
|
275
|
+
? provider.openaiBaseUrl
|
|
276
|
+
: undefined;
|
|
277
|
+
|
|
278
|
+
const entries: Promise<void>[] = [];
|
|
279
|
+
if (results.models !== undefined) {
|
|
280
|
+
for (const [modelName, modelResult] of Object.entries(results.models)) {
|
|
281
|
+
entries.push(
|
|
282
|
+
logModelResults(modelName, provider.name, anthropicUrl, openaiUrl, modelResult),
|
|
283
|
+
);
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
await Promise.all(entries);
|
|
288
|
+
|
|
289
|
+
return Response.json({ logged: entries.length });
|
|
290
|
+
},
|
|
291
|
+
},
|
|
292
|
+
},
|
|
293
|
+
});
|
|
@@ -7,7 +7,8 @@ const ProviderUpdateSchema = z.object({
|
|
|
7
7
|
apiKey: z.string().min(1, "API key is required").optional(),
|
|
8
8
|
format: z.enum(["anthropic", "openai"]).optional(),
|
|
9
9
|
baseUrl: z.string().min(1, "Base URL is required").optional(),
|
|
10
|
-
model: z.string().
|
|
10
|
+
model: z.string().optional(),
|
|
11
|
+
models: z.array(z.string()).optional(),
|
|
11
12
|
authHeader: z.enum(["bearer", "x-api-key"]).optional(),
|
|
12
13
|
anthropicBaseUrl: z.string().optional(),
|
|
13
14
|
openaiBaseUrl: z.string().optional(),
|
|
@@ -8,7 +8,8 @@ const ProviderInputSchema = z.object({
|
|
|
8
8
|
format: z.enum(["anthropic", "openai"]).optional(),
|
|
9
9
|
anthropicBaseUrl: z.string().optional(),
|
|
10
10
|
openaiBaseUrl: z.string().optional(),
|
|
11
|
-
|
|
11
|
+
models: z.array(z.string()).min(1, "At least one model is required"),
|
|
12
|
+
model: z.string().optional(),
|
|
12
13
|
authHeader: z.enum(["bearer", "x-api-key"]).optional().default("bearer"),
|
|
13
14
|
apiDocsUrl: z.string().optional(),
|
|
14
15
|
source: z.enum(["company", "personal"]).optional(),
|
|
@@ -30,7 +31,7 @@ export const Route = createFileRoute("/api/providers")({
|
|
|
30
31
|
parsed.data.apiKey,
|
|
31
32
|
parsed.data.format,
|
|
32
33
|
undefined, // baseUrl (legacy) — use format-specific URLs instead
|
|
33
|
-
parsed.data.
|
|
34
|
+
parsed.data.models,
|
|
34
35
|
parsed.data.authHeader,
|
|
35
36
|
parsed.data.apiDocsUrl,
|
|
36
37
|
parsed.data.anthropicBaseUrl,
|