offrouter-core 0.2.0 → 0.2.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/dist/audit.d.ts +137 -0
- package/dist/audit.d.ts.map +1 -0
- package/dist/audit.js +320 -0
- package/dist/audit.js.map +1 -0
- package/dist/auth/credential-chain.d.ts +84 -0
- package/dist/auth/credential-chain.d.ts.map +1 -0
- package/dist/auth/credential-chain.js +150 -0
- package/dist/auth/credential-chain.js.map +1 -0
- package/dist/auth/index.d.ts +10 -0
- package/dist/auth/index.d.ts.map +1 -0
- package/dist/auth/index.js +7 -0
- package/dist/auth/index.js.map +1 -0
- package/dist/auth/keychain.d.ts +60 -0
- package/dist/auth/keychain.d.ts.map +1 -0
- package/dist/auth/keychain.js +104 -0
- package/dist/auth/keychain.js.map +1 -0
- package/dist/auth/oauth-pkce.d.ts +138 -0
- package/dist/auth/oauth-pkce.d.ts.map +1 -0
- package/dist/auth/oauth-pkce.js +375 -0
- package/dist/auth/oauth-pkce.js.map +1 -0
- package/dist/auth/oauth-server.d.ts +36 -0
- package/dist/auth/oauth-server.d.ts.map +1 -0
- package/dist/auth/oauth-server.js +210 -0
- package/dist/auth/oauth-server.js.map +1 -0
- package/dist/config.d.ts +76 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +355 -0
- package/dist/config.js.map +1 -0
- package/dist/delegation.d.ts +123 -0
- package/dist/delegation.d.ts.map +1 -0
- package/dist/delegation.js +455 -0
- package/dist/delegation.js.map +1 -0
- package/dist/index.d.ts +41 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +26 -0
- package/dist/index.js.map +1 -0
- package/dist/policy.d.ts +45 -0
- package/dist/policy.d.ts.map +1 -0
- package/dist/policy.js +318 -0
- package/dist/policy.js.map +1 -0
- package/{src/protocol.ts → dist/protocol.d.ts} +2 -2
- package/dist/protocol.d.ts.map +1 -0
- package/dist/protocol.js +6 -0
- package/dist/protocol.js.map +1 -0
- package/dist/providers/adapter.d.ts +137 -0
- package/dist/providers/adapter.d.ts.map +1 -0
- package/dist/providers/adapter.js +163 -0
- package/dist/providers/adapter.js.map +1 -0
- package/dist/providers/catalog-data.d.ts +128 -0
- package/dist/providers/catalog-data.d.ts.map +1 -0
- package/dist/providers/catalog-data.js +397 -0
- package/dist/providers/catalog-data.js.map +1 -0
- package/dist/providers/catalog.d.ts +63 -0
- package/dist/providers/catalog.d.ts.map +1 -0
- package/dist/providers/catalog.js +394 -0
- package/dist/providers/catalog.js.map +1 -0
- package/dist/providers/fake.d.ts +46 -0
- package/dist/providers/fake.d.ts.map +1 -0
- package/dist/providers/fake.js +234 -0
- package/dist/providers/fake.js.map +1 -0
- package/dist/providers/openai-compatible.d.ts +65 -0
- package/dist/providers/openai-compatible.d.ts.map +1 -0
- package/dist/providers/openai-compatible.js +434 -0
- package/dist/providers/openai-compatible.js.map +1 -0
- package/dist/proxy/responses-mapper.d.ts +366 -0
- package/dist/proxy/responses-mapper.d.ts.map +1 -0
- package/dist/proxy/responses-mapper.js +517 -0
- package/dist/proxy/responses-mapper.js.map +1 -0
- package/dist/proxy/responses-server.d.ts +29 -0
- package/dist/proxy/responses-server.d.ts.map +1 -0
- package/dist/proxy/responses-server.js +360 -0
- package/dist/proxy/responses-server.js.map +1 -0
- package/dist/router.d.ts +18 -0
- package/dist/router.d.ts.map +1 -0
- package/dist/router.js +296 -0
- package/dist/router.js.map +1 -0
- package/dist/schemas.d.ts +560 -0
- package/dist/schemas.d.ts.map +1 -0
- package/{src/schemas.ts → dist/schemas.js} +83 -103
- package/dist/schemas.js.map +1 -0
- package/dist/secrets.d.ts +112 -0
- package/dist/secrets.d.ts.map +1 -0
- package/dist/secrets.js +326 -0
- package/dist/secrets.js.map +1 -0
- package/dist/types.d.ts +117 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +6 -0
- package/dist/types.js.map +1 -0
- package/dist/usage-file.d.ts +59 -0
- package/dist/usage-file.d.ts.map +1 -0
- package/dist/usage-file.js +316 -0
- package/dist/usage-file.js.map +1 -0
- package/dist/usage.d.ts +235 -0
- package/dist/usage.d.ts.map +1 -0
- package/dist/usage.js +517 -0
- package/dist/usage.js.map +1 -0
- package/package.json +9 -4
- package/src/audit.test.ts +0 -302
- package/src/audit.ts +0 -553
- package/src/auth/credential-chain.test.ts +0 -326
- package/src/auth/credential-chain.ts +0 -235
- package/src/auth/index.ts +0 -45
- package/src/auth/keychain.test.ts +0 -265
- package/src/auth/keychain.ts +0 -168
- package/src/auth/oauth-pkce.test.ts +0 -329
- package/src/auth/oauth-pkce.ts +0 -571
- package/src/auth/oauth-server.test.ts +0 -83
- package/src/auth/oauth-server.ts +0 -296
- package/src/config.test.ts +0 -479
- package/src/config.ts +0 -505
- package/src/delegation.test.ts +0 -368
- package/src/delegation.ts +0 -605
- package/src/index.ts +0 -280
- package/src/policy.test.ts +0 -634
- package/src/policy.ts +0 -420
- package/src/providers/adapter.test.ts +0 -293
- package/src/providers/adapter.ts +0 -328
- package/src/providers/catalog-data.test.ts +0 -258
- package/src/providers/catalog-data.ts +0 -498
- package/src/providers/catalog.test.ts +0 -84
- package/src/providers/catalog.ts +0 -483
- package/src/providers/fake.ts +0 -312
- package/src/providers/openai-compatible.test.ts +0 -366
- package/src/providers/openai-compatible.ts +0 -554
- package/src/proxy/responses-mapper.test.ts +0 -290
- package/src/proxy/responses-mapper.ts +0 -736
- package/src/proxy/responses-server.test.ts +0 -322
- package/src/proxy/responses-server.ts +0 -469
- package/src/router.test.ts +0 -699
- package/src/router.ts +0 -352
- package/src/schemas.test.ts +0 -291
- package/src/secrets.test.ts +0 -271
- package/src/secrets.ts +0 -461
- package/src/types.ts +0 -173
- package/src/usage-file.test.ts +0 -243
- package/src/usage-file.ts +0 -435
- package/src/usage.test.ts +0 -335
- package/src/usage.ts +0 -859
- package/tsconfig.json +0 -9
package/src/providers/adapter.ts
DELETED
|
@@ -1,328 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Provider adapter contract shared by fake and OpenAI-compatible backends.
|
|
3
|
-
* Keeps harness-specific shapes out of the core router.
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import type {
|
|
7
|
-
AuthScope,
|
|
8
|
-
AuthTier,
|
|
9
|
-
ProviderCandidate,
|
|
10
|
-
ProviderHealth,
|
|
11
|
-
RouteRequest,
|
|
12
|
-
SubscriptionStatus,
|
|
13
|
-
} from "../types.js";
|
|
14
|
-
import { redact } from "../secrets.js";
|
|
15
|
-
|
|
16
|
-
// ---------------------------------------------------------------------------
|
|
17
|
-
// Canonical provider errors
|
|
18
|
-
// ---------------------------------------------------------------------------
|
|
19
|
-
|
|
20
|
-
export type ProviderErrorCode =
|
|
21
|
-
| "auth_failed"
|
|
22
|
-
| "rate_limited"
|
|
23
|
-
| "quota_exhausted"
|
|
24
|
-
| "billing"
|
|
25
|
-
| "overload"
|
|
26
|
-
| "model_not_found"
|
|
27
|
-
| "context_window"
|
|
28
|
-
| "safety_refusal"
|
|
29
|
-
| "stream_interrupted"
|
|
30
|
-
| "cancelled"
|
|
31
|
-
| "timeout"
|
|
32
|
-
| "network"
|
|
33
|
-
| "invalid_request"
|
|
34
|
-
| "unknown";
|
|
35
|
-
|
|
36
|
-
export interface ProviderErrorOptions {
|
|
37
|
-
code: ProviderErrorCode;
|
|
38
|
-
message: string;
|
|
39
|
-
retryable?: boolean;
|
|
40
|
-
httpStatus?: number;
|
|
41
|
-
providerCode?: string;
|
|
42
|
-
cause?: unknown;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
export class ProviderError extends Error {
|
|
46
|
-
readonly code: ProviderErrorCode;
|
|
47
|
-
readonly retryable: boolean;
|
|
48
|
-
readonly httpStatus?: number;
|
|
49
|
-
readonly providerCode?: string;
|
|
50
|
-
|
|
51
|
-
constructor(options: ProviderErrorOptions) {
|
|
52
|
-
super(
|
|
53
|
-
options.message,
|
|
54
|
-
options.cause !== undefined ? { cause: options.cause } : undefined,
|
|
55
|
-
);
|
|
56
|
-
this.name = "ProviderError";
|
|
57
|
-
this.code = options.code;
|
|
58
|
-
this.retryable = options.retryable ?? false;
|
|
59
|
-
this.httpStatus = options.httpStatus;
|
|
60
|
-
this.providerCode = options.providerCode;
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
export function isProviderError(value: unknown): value is ProviderError {
|
|
65
|
-
return value instanceof ProviderError;
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
/**
|
|
69
|
-
* Map HTTP status + optional provider body into a ProviderError.
|
|
70
|
-
* Presence-only; never logs credential material.
|
|
71
|
-
*/
|
|
72
|
-
export function mapHttpToProviderError(
|
|
73
|
-
status: number,
|
|
74
|
-
bodyText?: string,
|
|
75
|
-
providerCode?: string,
|
|
76
|
-
): ProviderError {
|
|
77
|
-
const snippet =
|
|
78
|
-
typeof bodyText === "string" && bodyText.length > 0
|
|
79
|
-
? redact(bodyText.slice(0, 240))
|
|
80
|
-
: undefined;
|
|
81
|
-
const base = snippet ?? `HTTP ${status}`;
|
|
82
|
-
|
|
83
|
-
if (status === 401 || status === 403) {
|
|
84
|
-
return new ProviderError({
|
|
85
|
-
code: "auth_failed",
|
|
86
|
-
message: base,
|
|
87
|
-
retryable: false,
|
|
88
|
-
httpStatus: status,
|
|
89
|
-
providerCode,
|
|
90
|
-
});
|
|
91
|
-
}
|
|
92
|
-
if (status === 404) {
|
|
93
|
-
return new ProviderError({
|
|
94
|
-
code: "model_not_found",
|
|
95
|
-
message: base,
|
|
96
|
-
retryable: false,
|
|
97
|
-
httpStatus: status,
|
|
98
|
-
providerCode,
|
|
99
|
-
});
|
|
100
|
-
}
|
|
101
|
-
if (status === 408 || status === 504) {
|
|
102
|
-
return new ProviderError({
|
|
103
|
-
code: "timeout",
|
|
104
|
-
message: base,
|
|
105
|
-
retryable: true,
|
|
106
|
-
httpStatus: status,
|
|
107
|
-
providerCode,
|
|
108
|
-
});
|
|
109
|
-
}
|
|
110
|
-
if (status === 429) {
|
|
111
|
-
const lower = (bodyText ?? "").toLowerCase();
|
|
112
|
-
if (
|
|
113
|
-
lower.includes("quota") ||
|
|
114
|
-
lower.includes("billing") ||
|
|
115
|
-
lower.includes("insufficient")
|
|
116
|
-
) {
|
|
117
|
-
return new ProviderError({
|
|
118
|
-
code: "quota_exhausted",
|
|
119
|
-
message: base,
|
|
120
|
-
retryable: false,
|
|
121
|
-
httpStatus: status,
|
|
122
|
-
providerCode,
|
|
123
|
-
});
|
|
124
|
-
}
|
|
125
|
-
return new ProviderError({
|
|
126
|
-
code: "rate_limited",
|
|
127
|
-
message: base,
|
|
128
|
-
retryable: true,
|
|
129
|
-
httpStatus: status,
|
|
130
|
-
providerCode,
|
|
131
|
-
});
|
|
132
|
-
}
|
|
133
|
-
if (status === 402) {
|
|
134
|
-
return new ProviderError({
|
|
135
|
-
code: "billing",
|
|
136
|
-
message: base,
|
|
137
|
-
retryable: false,
|
|
138
|
-
httpStatus: status,
|
|
139
|
-
providerCode,
|
|
140
|
-
});
|
|
141
|
-
}
|
|
142
|
-
if (status === 413) {
|
|
143
|
-
return new ProviderError({
|
|
144
|
-
code: "context_window",
|
|
145
|
-
message: base,
|
|
146
|
-
retryable: false,
|
|
147
|
-
httpStatus: status,
|
|
148
|
-
providerCode,
|
|
149
|
-
});
|
|
150
|
-
}
|
|
151
|
-
if (status === 503 || status === 529) {
|
|
152
|
-
return new ProviderError({
|
|
153
|
-
code: "overload",
|
|
154
|
-
message: base,
|
|
155
|
-
retryable: true,
|
|
156
|
-
httpStatus: status,
|
|
157
|
-
providerCode,
|
|
158
|
-
});
|
|
159
|
-
}
|
|
160
|
-
if (status >= 500) {
|
|
161
|
-
return new ProviderError({
|
|
162
|
-
code: "network",
|
|
163
|
-
message: base,
|
|
164
|
-
retryable: true,
|
|
165
|
-
httpStatus: status,
|
|
166
|
-
providerCode,
|
|
167
|
-
});
|
|
168
|
-
}
|
|
169
|
-
if (status >= 400) {
|
|
170
|
-
return new ProviderError({
|
|
171
|
-
code: "invalid_request",
|
|
172
|
-
message: base,
|
|
173
|
-
retryable: false,
|
|
174
|
-
httpStatus: status,
|
|
175
|
-
providerCode,
|
|
176
|
-
});
|
|
177
|
-
}
|
|
178
|
-
return new ProviderError({
|
|
179
|
-
code: "unknown",
|
|
180
|
-
message: base,
|
|
181
|
-
retryable: false,
|
|
182
|
-
httpStatus: status,
|
|
183
|
-
providerCode,
|
|
184
|
-
});
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
// ---------------------------------------------------------------------------
|
|
188
|
-
// Supporting types
|
|
189
|
-
// ---------------------------------------------------------------------------
|
|
190
|
-
|
|
191
|
-
export interface ModelCapability {
|
|
192
|
-
modelId: string;
|
|
193
|
-
displayName?: string;
|
|
194
|
-
supportsTools: boolean;
|
|
195
|
-
supportsStreaming: boolean;
|
|
196
|
-
supportsJson: boolean;
|
|
197
|
-
supportsImages: boolean;
|
|
198
|
-
contextWindowTokens?: number;
|
|
199
|
-
maxOutputTokens?: number;
|
|
200
|
-
estimatedCostUsd?: number;
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
export interface AuthStatus {
|
|
204
|
-
authTier: AuthTier;
|
|
205
|
-
authScope: AuthScope;
|
|
206
|
-
configured: boolean;
|
|
207
|
-
/** Presence-only; never returns key material. */
|
|
208
|
-
hasApiKey: boolean;
|
|
209
|
-
subscriptionStatus?: SubscriptionStatus;
|
|
210
|
-
limitsDiscoverable: boolean;
|
|
211
|
-
health: ProviderHealth;
|
|
212
|
-
accountId?: string;
|
|
213
|
-
detail?: string;
|
|
214
|
-
/**
|
|
215
|
-
* Which credential source satisfied auth (if any).
|
|
216
|
-
* "none" when no credential was found.
|
|
217
|
-
*/
|
|
218
|
-
credentialSource?: "env" | "config" | "keychain" | "oauth" | "none";
|
|
219
|
-
/** Whether this auth path uses a subscription-backed credential. */
|
|
220
|
-
subscriptionEligible: boolean;
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
/**
|
|
224
|
-
* Limit snapshot. Opaque providers report discoverable=false and omit remaining.
|
|
225
|
-
*/
|
|
226
|
-
export interface LimitSnapshot {
|
|
227
|
-
authTier: AuthTier;
|
|
228
|
-
/** When false, remaining/reset fields are unknown; rely on errors for fallback. */
|
|
229
|
-
discoverable: boolean;
|
|
230
|
-
opaque: boolean;
|
|
231
|
-
remaining?: number;
|
|
232
|
-
limit?: number;
|
|
233
|
-
resetAt?: string;
|
|
234
|
-
unit?: string;
|
|
235
|
-
accountId?: string;
|
|
236
|
-
metadata?: Record<string, string | number | boolean | null>;
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
export interface CostEstimate {
|
|
240
|
-
model: string;
|
|
241
|
-
authTier: AuthTier;
|
|
242
|
-
estimatedCostUsd: number;
|
|
243
|
-
currency: "USD";
|
|
244
|
-
inputTokensEstimate?: number;
|
|
245
|
-
outputTokensEstimate?: number;
|
|
246
|
-
note?: string;
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
export interface ProviderInvokeMessage {
|
|
250
|
-
role: "system" | "user" | "assistant" | "tool";
|
|
251
|
-
content: string;
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
export interface ProviderInvokeRequest {
|
|
255
|
-
callId: string;
|
|
256
|
-
model: string;
|
|
257
|
-
messages: ProviderInvokeMessage[];
|
|
258
|
-
stream?: boolean;
|
|
259
|
-
maxOutputTokens?: number;
|
|
260
|
-
temperature?: number;
|
|
261
|
-
/** Optional abort signal for cancellation races. */
|
|
262
|
-
signal?: AbortSignal;
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
export type ProviderEvent =
|
|
266
|
-
| { type: "start"; callId: string; model: string }
|
|
267
|
-
| { type: "text-delta"; callId: string; text: string }
|
|
268
|
-
| { type: "tool-call"; callId: string; name: string; arguments: string }
|
|
269
|
-
| {
|
|
270
|
-
type: "usage";
|
|
271
|
-
callId: string;
|
|
272
|
-
inputTokens?: number;
|
|
273
|
-
outputTokens?: number;
|
|
274
|
-
totalTokens?: number;
|
|
275
|
-
}
|
|
276
|
-
| { type: "done"; callId: string; finishReason?: string }
|
|
277
|
-
| { type: "error"; callId: string; error: ProviderError };
|
|
278
|
-
|
|
279
|
-
export interface ProviderAdapter {
|
|
280
|
-
readonly id: string;
|
|
281
|
-
listModels(): Promise<ModelCapability[]>;
|
|
282
|
-
checkAuth(): Promise<AuthStatus>;
|
|
283
|
-
queryLimits(authTier: AuthTier): Promise<LimitSnapshot>;
|
|
284
|
-
estimate(
|
|
285
|
-
request: RouteRequest,
|
|
286
|
-
model: string,
|
|
287
|
-
authTier: AuthTier,
|
|
288
|
-
): Promise<CostEstimate>;
|
|
289
|
-
invoke(request: ProviderInvokeRequest): AsyncIterable<ProviderEvent>;
|
|
290
|
-
cancel(callId: string): Promise<void>;
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
/** Required method names on every adapter. */
|
|
294
|
-
export const PROVIDER_ADAPTER_METHODS = [
|
|
295
|
-
"listModels",
|
|
296
|
-
"checkAuth",
|
|
297
|
-
"queryLimits",
|
|
298
|
-
"estimate",
|
|
299
|
-
"invoke",
|
|
300
|
-
"cancel",
|
|
301
|
-
] as const satisfies readonly (keyof ProviderAdapter)[];
|
|
302
|
-
|
|
303
|
-
/** Convert catalog models to ProviderCandidate rows for routing tests. */
|
|
304
|
-
export function capabilityToCandidate(
|
|
305
|
-
providerId: string,
|
|
306
|
-
capability: ModelCapability,
|
|
307
|
-
extras: Pick<ProviderCandidate, "authTier" | "authScope"> &
|
|
308
|
-
Partial<ProviderCandidate>,
|
|
309
|
-
): ProviderCandidate {
|
|
310
|
-
return {
|
|
311
|
-
providerId,
|
|
312
|
-
modelId: capability.modelId,
|
|
313
|
-
displayName: capability.displayName,
|
|
314
|
-
authTier: extras.authTier,
|
|
315
|
-
authScope: extras.authScope,
|
|
316
|
-
subscriptionStatus: extras.subscriptionStatus,
|
|
317
|
-
health: extras.health ?? "healthy",
|
|
318
|
-
supportsTools: capability.supportsTools,
|
|
319
|
-
supportsStreaming: capability.supportsStreaming,
|
|
320
|
-
supportsJson: capability.supportsJson,
|
|
321
|
-
supportsImages: capability.supportsImages,
|
|
322
|
-
contextWindowTokens: capability.contextWindowTokens,
|
|
323
|
-
maxOutputTokens: capability.maxOutputTokens,
|
|
324
|
-
estimatedCostUsd: extras.estimatedCostUsd ?? capability.estimatedCostUsd,
|
|
325
|
-
accountId: extras.accountId,
|
|
326
|
-
subscriptionQuotaPercentRemaining: extras.subscriptionQuotaPercentRemaining,
|
|
327
|
-
};
|
|
328
|
-
}
|
|
@@ -1,258 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from "vitest";
|
|
2
|
-
import {
|
|
3
|
-
BUILTIN_PROVIDERS,
|
|
4
|
-
BUILTIN_PROVIDER_IDS,
|
|
5
|
-
BuiltinCatalogConfigSchema,
|
|
6
|
-
ProviderCatalogConfigSchema,
|
|
7
|
-
getBuiltinProvider,
|
|
8
|
-
parseBuiltinCatalogConfig,
|
|
9
|
-
selectBuiltinProviders,
|
|
10
|
-
type BuiltinCatalogConfig,
|
|
11
|
-
type BuiltinProviderEntry,
|
|
12
|
-
type ProviderCatalogConfig,
|
|
13
|
-
} from "./catalog-data.js";
|
|
14
|
-
import {
|
|
15
|
-
STATIC_CATALOG,
|
|
16
|
-
loadBuiltinCatalog,
|
|
17
|
-
} from "./catalog.js";
|
|
18
|
-
|
|
19
|
-
// Substrings that should never appear in metadata-only catalog values.
|
|
20
|
-
const SECRET_VALUE_PATTERNS = ["sk-", "Bearer ", "x-api-key:", "password="];
|
|
21
|
-
// Field names that must not exist in a metadata-only catalog.
|
|
22
|
-
const SECRET_KEY_RE = /(?:^|_)(key|secret|token|password|credential)(?:_|$)/i;
|
|
23
|
-
|
|
24
|
-
function collectKeyNames(value: unknown, into: string[]): void {
|
|
25
|
-
if (Array.isArray(value)) {
|
|
26
|
-
for (const v of value) collectKeyNames(v, into);
|
|
27
|
-
return;
|
|
28
|
-
}
|
|
29
|
-
if (value && typeof value === "object") {
|
|
30
|
-
for (const [k, v] of Object.entries(value as Record<string, unknown>)) {
|
|
31
|
-
into.push(k);
|
|
32
|
-
collectKeyNames(v, into);
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
function hasNoSecrets(value: unknown): boolean {
|
|
38
|
-
const text = JSON.stringify(value ?? "");
|
|
39
|
-
if (SECRET_VALUE_PATTERNS.some((p) => text.includes(p))) return false;
|
|
40
|
-
const names: string[] = [];
|
|
41
|
-
collectKeyNames(value, names);
|
|
42
|
-
return !names.some((n) => SECRET_KEY_RE.test(n));
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
describe("builtin provider catalog data", () => {
|
|
46
|
-
it("loads a non-empty builtin catalog offline", () => {
|
|
47
|
-
expect(BUILTIN_PROVIDERS.length).toBeGreaterThan(0);
|
|
48
|
-
for (const provider of BUILTIN_PROVIDERS) {
|
|
49
|
-
expect(provider.providerId).toBeTruthy();
|
|
50
|
-
expect(provider.displayName).toBeTruthy();
|
|
51
|
-
expect(provider.flavor).toBeTruthy();
|
|
52
|
-
expect(Array.isArray(provider.knownModels)).toBe(true);
|
|
53
|
-
}
|
|
54
|
-
});
|
|
55
|
-
|
|
56
|
-
it("covers the required provider families", () => {
|
|
57
|
-
const ids = new Set(BUILTIN_PROVIDER_IDS);
|
|
58
|
-
for (const id of [
|
|
59
|
-
"openai",
|
|
60
|
-
"anthropic",
|
|
61
|
-
"google",
|
|
62
|
-
"xai",
|
|
63
|
-
"openrouter",
|
|
64
|
-
"zai",
|
|
65
|
-
"moonshot",
|
|
66
|
-
"ollama",
|
|
67
|
-
"lm-studio",
|
|
68
|
-
"custom",
|
|
69
|
-
]) {
|
|
70
|
-
expect(ids.has(id), `missing provider ${id}`).toBe(true);
|
|
71
|
-
}
|
|
72
|
-
});
|
|
73
|
-
|
|
74
|
-
it("assigns correct default authTier and authScope per family", () => {
|
|
75
|
-
const byId = new Map(
|
|
76
|
-
BUILTIN_PROVIDERS.map((p) => [p.providerId, p] as const),
|
|
77
|
-
);
|
|
78
|
-
// Pure API providers default to api-key.
|
|
79
|
-
expect(byId.get("openai")?.defaultAuthTier).toBe("api-key");
|
|
80
|
-
expect(byId.get("anthropic")?.defaultAuthTier).toBe("api-key");
|
|
81
|
-
expect(byId.get("google")?.defaultAuthTier).toBe("api-key");
|
|
82
|
-
expect(byId.get("xai")?.defaultAuthTier).toBe("api-key");
|
|
83
|
-
expect(byId.get("zai")?.defaultAuthTier).toBe("api-key");
|
|
84
|
-
expect(byId.get("moonshot")?.defaultAuthTier).toBe("api-key");
|
|
85
|
-
// Aggregator is third-party api-key.
|
|
86
|
-
expect(byId.get("openrouter")?.defaultAuthTier).toBe("api-key");
|
|
87
|
-
expect(byId.get("openrouter")?.defaultAuthScope).toBe("third-party");
|
|
88
|
-
// Local runtimes.
|
|
89
|
-
expect(byId.get("ollama")?.defaultAuthTier).toBe("local");
|
|
90
|
-
expect(byId.get("lm-studio")?.defaultAuthTier).toBe("local");
|
|
91
|
-
// Custom endpoint is a local OpenAI-compatible gateway by default; scope
|
|
92
|
-
// is unknown until configured.
|
|
93
|
-
expect(byId.get("custom")?.defaultAuthTier).toBe("local");
|
|
94
|
-
expect(byId.get("custom")?.defaultAuthScope).toBe("unknown");
|
|
95
|
-
// First-party API vendors carry first-party scope (direct vendor apis;
|
|
96
|
-
// aggregators like openrouter stay third-party).
|
|
97
|
-
expect(byId.get("openai")?.defaultAuthScope).toBe("first-party");
|
|
98
|
-
expect(byId.get("anthropic")?.defaultAuthScope).toBe("first-party");
|
|
99
|
-
expect(byId.get("google")?.defaultAuthScope).toBe("first-party");
|
|
100
|
-
expect(byId.get("zai")?.defaultAuthScope).toBe("first-party");
|
|
101
|
-
expect(byId.get("moonshot")?.defaultAuthScope).toBe("first-party");
|
|
102
|
-
});
|
|
103
|
-
|
|
104
|
-
it("documents the official subscription surface without auto-promoting api-key", () => {
|
|
105
|
-
for (const provider of BUILTIN_PROVIDERS) {
|
|
106
|
-
expect(typeof provider.supportsSubscriptionRouting).toBe("boolean");
|
|
107
|
-
// API-key and local providers never claim a routing surface they lack.
|
|
108
|
-
if (provider.defaultAuthTier !== "subscription") {
|
|
109
|
-
// The flag is informational; subscription routing itself is the job of
|
|
110
|
-
// the harness adapters, not this metadata.
|
|
111
|
-
expect(provider.supportsSubscriptionRouting).toBe(false);
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
});
|
|
115
|
-
|
|
116
|
-
it("looks up a builtin provider by id", () => {
|
|
117
|
-
expect(getBuiltinProvider("ollama")?.flavor).toBe("ollama");
|
|
118
|
-
expect(getBuiltinProvider("missing")).toBeUndefined();
|
|
119
|
-
});
|
|
120
|
-
|
|
121
|
-
it("excludes disabled providers from selectBuiltinProviders", () => {
|
|
122
|
-
const config: BuiltinCatalogConfig = { openai: { enabled: false } };
|
|
123
|
-
const selected = selectBuiltinProviders(config);
|
|
124
|
-
expect(selected.find((p) => p.providerId === "openai")).toBeUndefined();
|
|
125
|
-
expect(selected.find((p) => p.providerId === "anthropic")).toBeDefined();
|
|
126
|
-
});
|
|
127
|
-
|
|
128
|
-
it("applies base URL overrides from provider config", () => {
|
|
129
|
-
const config: BuiltinCatalogConfig = {
|
|
130
|
-
custom: { enabled: true, baseUrl: "https://gateway.example.test/v1" },
|
|
131
|
-
};
|
|
132
|
-
const selected = selectBuiltinProviders(config);
|
|
133
|
-
const custom = selected.find((p) => p.providerId === "custom");
|
|
134
|
-
expect(custom?.defaultBaseUrl).toBe("https://gateway.example.test/v1");
|
|
135
|
-
});
|
|
136
|
-
|
|
137
|
-
it("returns copies so callers cannot mutate the static source", () => {
|
|
138
|
-
const selected = selectBuiltinProviders();
|
|
139
|
-
const before = getBuiltinProvider("openai")?.defaultBaseUrl;
|
|
140
|
-
selected[0]!.defaultBaseUrl = "mutated";
|
|
141
|
-
expect(getBuiltinProvider("openai")?.defaultBaseUrl).toBe(before);
|
|
142
|
-
});
|
|
143
|
-
|
|
144
|
-
it("custom OpenAI-compatible endpoint entry is usable when configured", () => {
|
|
145
|
-
const custom = getBuiltinProvider("custom") as BuiltinProviderEntry;
|
|
146
|
-
expect(custom.flavor).toBe("openai");
|
|
147
|
-
const entries = loadBuiltinCatalog({
|
|
148
|
-
providers: {
|
|
149
|
-
custom: {
|
|
150
|
-
enabled: true,
|
|
151
|
-
baseUrl: "https://gateway.example.test/v1",
|
|
152
|
-
} satisfies ProviderCatalogConfig,
|
|
153
|
-
},
|
|
154
|
-
});
|
|
155
|
-
const customRow = entries.find((e) => e.providerId === "custom");
|
|
156
|
-
expect(customRow).toBeDefined();
|
|
157
|
-
});
|
|
158
|
-
|
|
159
|
-
it("does not auto-promote api-key over subscription for the same family", () => {
|
|
160
|
-
const entries = loadBuiltinCatalog({ families: ["frontier-coder"] });
|
|
161
|
-
const tiers = new Set(entries.map((e) => e.authTier));
|
|
162
|
-
expect(tiers.has("subscription")).toBe(true);
|
|
163
|
-
expect(tiers.has("api-key")).toBe(true);
|
|
164
|
-
// Subscription entries must survive the merge (never replaced by api-key).
|
|
165
|
-
expect(entries.some((e) => e.authTier === "subscription")).toBe(true);
|
|
166
|
-
});
|
|
167
|
-
|
|
168
|
-
it("subscription-backed and api-key candidates coexist for shared families", () => {
|
|
169
|
-
for (const family of ["frontier-coder", "fast-coder"] as const) {
|
|
170
|
-
const entries = loadBuiltinCatalog({ families: [family] });
|
|
171
|
-
const tiers = new Set(entries.map((e) => e.authTier));
|
|
172
|
-
expect(tiers.has("subscription"), `${family} subscription`).toBe(true);
|
|
173
|
-
expect(tiers.has("api-key"), `${family} api-key`).toBe(true);
|
|
174
|
-
}
|
|
175
|
-
});
|
|
176
|
-
|
|
177
|
-
it("merges builtin entries with the existing static catalog", () => {
|
|
178
|
-
const merged = loadBuiltinCatalog();
|
|
179
|
-
// Static subscription rows must still be present.
|
|
180
|
-
const staticIds = new Set(STATIC_CATALOG.map((e) => e.providerId));
|
|
181
|
-
for (const id of staticIds) {
|
|
182
|
-
expect(
|
|
183
|
-
merged.some((e) => e.providerId === id),
|
|
184
|
-
`static provider ${id} dropped`,
|
|
185
|
-
).toBe(true);
|
|
186
|
-
}
|
|
187
|
-
// Builtin-only providers must appear too.
|
|
188
|
-
expect(merged.some((e) => e.providerId === "openai")).toBe(true);
|
|
189
|
-
expect(merged.some((e) => e.providerId === "anthropic")).toBe(true);
|
|
190
|
-
});
|
|
191
|
-
|
|
192
|
-
it("excludes disabled providers across both catalog sources", () => {
|
|
193
|
-
const entries = loadBuiltinCatalog({
|
|
194
|
-
providers: { openrouter: { enabled: false } },
|
|
195
|
-
});
|
|
196
|
-
expect(entries.every((e) => e.providerId !== "openrouter")).toBe(true);
|
|
197
|
-
});
|
|
198
|
-
|
|
199
|
-
it("dedups identical rows across static and builtin sources", () => {
|
|
200
|
-
// ollama/llava:latest appears in BOTH STATIC_CATALOG (vision-helper, local)
|
|
201
|
-
// and the builtin ollama entry with identical providerId/modelId/tier/
|
|
202
|
-
// family, so the structured dedup key must collapse them to a single row.
|
|
203
|
-
const merged = loadBuiltinCatalog();
|
|
204
|
-
const llava = merged.filter(
|
|
205
|
-
(e) =>
|
|
206
|
-
e.providerId === "ollama" &&
|
|
207
|
-
e.modelId === "llava:latest" &&
|
|
208
|
-
e.authTier === "local" &&
|
|
209
|
-
e.family === "vision-helper",
|
|
210
|
-
);
|
|
211
|
-
expect(llava.length).toBe(1);
|
|
212
|
-
});
|
|
213
|
-
|
|
214
|
-
it("contains no secrets anywhere in the catalog", () => {
|
|
215
|
-
expect(hasNoSecrets(BUILTIN_PROVIDERS)).toBe(true);
|
|
216
|
-
expect(hasNoSecrets(selectBuiltinProviders())).toBe(true);
|
|
217
|
-
expect(hasNoSecrets(loadBuiltinCatalog())).toBe(true);
|
|
218
|
-
});
|
|
219
|
-
|
|
220
|
-
it("every builtin catalog row carries authTier and authScope", () => {
|
|
221
|
-
const entries = loadBuiltinCatalog();
|
|
222
|
-
expect(entries.length).toBeGreaterThan(0);
|
|
223
|
-
for (const e of entries) {
|
|
224
|
-
expect(["subscription", "local", "api-key"]).toContain(e.authTier);
|
|
225
|
-
expect(["first-party", "third-party", "unknown"]).toContain(e.authScope);
|
|
226
|
-
}
|
|
227
|
-
});
|
|
228
|
-
|
|
229
|
-
it("Zod schemas reject invalid catalog config", () => {
|
|
230
|
-
// Wrong type for enabled.
|
|
231
|
-
expect(() =>
|
|
232
|
-
ProviderCatalogConfigSchema.parse({ enabled: "yes" }),
|
|
233
|
-
).toThrow();
|
|
234
|
-
// Unknown key rejected by strict schema.
|
|
235
|
-
expect(() =>
|
|
236
|
-
ProviderCatalogConfigSchema.parse({ nope: true }),
|
|
237
|
-
).toThrow();
|
|
238
|
-
// baseUrl must be a string.
|
|
239
|
-
expect(() =>
|
|
240
|
-
ProviderCatalogConfigSchema.parse({ baseUrl: 42 }),
|
|
241
|
-
).toThrow();
|
|
242
|
-
// Record schema rejects non-object provider entries.
|
|
243
|
-
expect(() =>
|
|
244
|
-
BuiltinCatalogConfigSchema.parse({ openai: "on" }),
|
|
245
|
-
).toThrow();
|
|
246
|
-
});
|
|
247
|
-
|
|
248
|
-
it("parseBuiltinCatalogConfig accepts valid config and rejects invalid", () => {
|
|
249
|
-
expect(() =>
|
|
250
|
-
parseBuiltinCatalogConfig({
|
|
251
|
-
openai: { enabled: true, baseUrl: "https://api.openai.com/v1" },
|
|
252
|
-
}),
|
|
253
|
-
).not.toThrow();
|
|
254
|
-
expect(() =>
|
|
255
|
-
parseBuiltinCatalogConfig({ openai: { enabled: "true" } }),
|
|
256
|
-
).toThrow();
|
|
257
|
-
});
|
|
258
|
-
});
|