@axiom-lattice/protocols 4.5.0 → 4.6.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.
- package/.turbo/turbo-build.log +10 -10
- package/CHANGELOG.md +53 -0
- package/dist/index.d.mts +662 -1
- package/dist/index.d.ts +662 -1
- package/dist/index.js +19 -0
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +17 -0
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
- package/src/ModelLatticeProtocol.ts +10 -0
- package/src/ModelProviderProtocol.ts +821 -0
- package/src/index.ts +1 -0
|
@@ -0,0 +1,821 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ModelProviderProtocol
|
|
3
|
+
*
|
|
4
|
+
* Tenant-scoped model provider catalog for the Axiom Lattice framework.
|
|
5
|
+
* A tenant owner/admin configures an upstream `baseURL` + `apiKey`, and the
|
|
6
|
+
* gateway pulls the available model list from that upstream and stores it as a
|
|
7
|
+
* snapshot.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type { LLMConfig } from "./ModelLatticeProtocol";
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Wire protocol used to talk to the upstream provider.
|
|
14
|
+
* Phase 1 implements only `openai-compatible`.
|
|
15
|
+
*/
|
|
16
|
+
export type ModelProviderWireProtocol = "openai-compatible";
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Which request style the upstream speaks.
|
|
20
|
+
*
|
|
21
|
+
* This is not the same question as `wireProtocol`: two upstreams can both be
|
|
22
|
+
* "OpenAI-compatible" yet expose different routes and request fields.
|
|
23
|
+
* Chat Completions posts `messages` to `/v1/chat/completions`; the Responses
|
|
24
|
+
* API posts `input` to `/v1/responses`. Sending one where the other is expected
|
|
25
|
+
* surfaces as unrecognised parameters, a missing route, or endless retries.
|
|
26
|
+
*/
|
|
27
|
+
export type ModelProviderApiStyle = "chat-completions" | "responses";
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Value handed to `LLMConfig.provider` when a discovered model is turned into a
|
|
31
|
+
* runtime client.
|
|
32
|
+
*
|
|
33
|
+
* Derived from `LLMConfig` rather than restated, so the two cannot drift.
|
|
34
|
+
*/
|
|
35
|
+
export type ModelProviderLlmProvider = LLMConfig["provider"];
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Build the tenant-scoped model key.
|
|
39
|
+
*
|
|
40
|
+
* The provider id scopes the upstream model id, so the same upstream id offered
|
|
41
|
+
* by two providers yields two independent, independently-credentialed models.
|
|
42
|
+
* The provider id is immutable, so renaming a provider never invalidates a key
|
|
43
|
+
* already stored in agent or eval configuration.
|
|
44
|
+
*
|
|
45
|
+
* @param providerId - Provider identifier
|
|
46
|
+
* @param modelId - Upstream model id
|
|
47
|
+
*/
|
|
48
|
+
export function toModelKey(providerId: string, modelId: string): string {
|
|
49
|
+
return `${providerId}/${modelId}`;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Split a model key back into its provider and upstream model id.
|
|
54
|
+
*
|
|
55
|
+
* Splits on the first separator only: upstream model ids routinely contain
|
|
56
|
+
* `/` themselves (`Qwen/Qwen2.5-7B-Instruct`).
|
|
57
|
+
*
|
|
58
|
+
* @param modelKey - Key produced by {@link toModelKey}
|
|
59
|
+
* @returns The two parts, or null when the key carries no separator
|
|
60
|
+
*/
|
|
61
|
+
export function parseModelKey(
|
|
62
|
+
modelKey: string
|
|
63
|
+
): { providerId: string; modelId: string } | null {
|
|
64
|
+
const separator = modelKey.indexOf("/");
|
|
65
|
+
if (separator <= 0 || separator === modelKey.length - 1) {
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return {
|
|
70
|
+
providerId: modelKey.slice(0, separator),
|
|
71
|
+
modelId: modelKey.slice(separator + 1),
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Outcome of the most recent discovery attempt for a provider.
|
|
77
|
+
*/
|
|
78
|
+
export type ModelProviderStatus = "unknown" | "ok" | "error";
|
|
79
|
+
|
|
80
|
+
/** The codes a probe can fail with — all of them upstream-side. */
|
|
81
|
+
export type ModelProviderUpstreamErrorCode =
|
|
82
|
+
| "UPSTREAM_UNREACHABLE"
|
|
83
|
+
| "UPSTREAM_AUTH_FAILED"
|
|
84
|
+
| "UPSTREAM_REDIRECT_BLOCKED"
|
|
85
|
+
| "UPSTREAM_INVALID_RESPONSE"
|
|
86
|
+
| "UPSTREAM_RATE_LIMITED"
|
|
87
|
+
| "UPSTREAM_TIMEOUT";
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Stable error codes returned by the model provider endpoints.
|
|
91
|
+
*
|
|
92
|
+
* Declared here rather than in the gateway so clients can type the code they
|
|
93
|
+
* receive and switch over it exhaustively. A console that re-types the list as
|
|
94
|
+
* bare strings cannot be told when the server adds or renames one.
|
|
95
|
+
*/
|
|
96
|
+
export type ModelProviderErrorCode =
|
|
97
|
+
| "UNAUTHENTICATED"
|
|
98
|
+
| "TENANT_ACCESS_DENIED"
|
|
99
|
+
| "FORBIDDEN_ROLE"
|
|
100
|
+
| "INVALID_INPUT"
|
|
101
|
+
| "INVALID_BASE_URL"
|
|
102
|
+
| "UNSUPPORTED_PROTOCOL"
|
|
103
|
+
| "PROVIDER_NOT_FOUND"
|
|
104
|
+
| "PROVIDER_DISABLED"
|
|
105
|
+
| "PROVIDER_NAME_CONFLICT"
|
|
106
|
+
| "MODEL_KEY_CONFLICT"
|
|
107
|
+
| "UPSTREAM_UNREACHABLE"
|
|
108
|
+
| "UPSTREAM_AUTH_FAILED"
|
|
109
|
+
| "UPSTREAM_REDIRECT_BLOCKED"
|
|
110
|
+
| "UPSTREAM_INVALID_RESPONSE"
|
|
111
|
+
| "UPSTREAM_RATE_LIMITED"
|
|
112
|
+
| "UPSTREAM_TIMEOUT"
|
|
113
|
+
| "INTERNAL_ERROR";
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* A tenant-scoped model provider configuration.
|
|
117
|
+
*
|
|
118
|
+
* `apiKey` holds the decrypted secret and is for **server-side use only**.
|
|
119
|
+
* It must never be serialized into an API response — route responses go through
|
|
120
|
+
* the service's public projection, which exposes `hasApiKey` / `apiKeyHint`.
|
|
121
|
+
*/
|
|
122
|
+
export interface ModelProviderEntry {
|
|
123
|
+
/**
|
|
124
|
+
* Unique identifier for the provider configuration
|
|
125
|
+
*/
|
|
126
|
+
id: string;
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Tenant identifier for multi-tenant isolation
|
|
130
|
+
*/
|
|
131
|
+
tenantId: string;
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Business name for the provider (unique within tenant)
|
|
135
|
+
*/
|
|
136
|
+
name: string;
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Optional friendly name for display
|
|
140
|
+
*/
|
|
141
|
+
displayName?: string;
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Wire protocol used for discovery
|
|
145
|
+
*/
|
|
146
|
+
protocol: ModelProviderWireProtocol;
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Request style the upstream speaks. Defaults to Chat Completions.
|
|
150
|
+
*/
|
|
151
|
+
apiStyle: ModelProviderApiStyle;
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* LLMConfig provider value used when constructing a runtime client
|
|
155
|
+
*/
|
|
156
|
+
llmProvider: ModelProviderLlmProvider;
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Upstream base URL
|
|
160
|
+
*/
|
|
161
|
+
baseURL: string;
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Decrypted API key. Server-side only — never serialize into a response.
|
|
165
|
+
*/
|
|
166
|
+
apiKey?: string;
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Last 4 characters of the API key. Safe to expose.
|
|
170
|
+
*/
|
|
171
|
+
apiKeyHint?: string;
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Whether this provider participates in discovery and registration
|
|
175
|
+
*/
|
|
176
|
+
enabled: boolean;
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Outcome of the most recent discovery attempt
|
|
180
|
+
*/
|
|
181
|
+
status: ModelProviderStatus;
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Stable error code from the most recent failed discovery.
|
|
185
|
+
* Never the raw upstream response body.
|
|
186
|
+
*/
|
|
187
|
+
lastError?: string;
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Timestamp of the most recent successful discovery
|
|
191
|
+
*/
|
|
192
|
+
lastDiscoveredAt?: Date;
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* User who created the configuration
|
|
196
|
+
*/
|
|
197
|
+
createdBy?: string;
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Creation timestamp
|
|
201
|
+
*/
|
|
202
|
+
createdAt: Date;
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Last update timestamp
|
|
206
|
+
*/
|
|
207
|
+
updatedAt: Date;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Request to create a new model provider configuration
|
|
212
|
+
*/
|
|
213
|
+
export interface CreateModelProviderRequest {
|
|
214
|
+
/**
|
|
215
|
+
* Business name for the provider (unique within tenant)
|
|
216
|
+
*/
|
|
217
|
+
name: string;
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Optional friendly name for display
|
|
221
|
+
*/
|
|
222
|
+
displayName?: string;
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Wire protocol used for discovery
|
|
226
|
+
*/
|
|
227
|
+
protocol: ModelProviderWireProtocol;
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Request style the upstream speaks. Defaults to Chat Completions.
|
|
231
|
+
*/
|
|
232
|
+
apiStyle?: ModelProviderApiStyle;
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Upstream base URL
|
|
236
|
+
*/
|
|
237
|
+
baseURL: string;
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Upstream API key. Required: a provider must always carry a credential.
|
|
241
|
+
*
|
|
242
|
+
* A keyless provider is not merely useless — it is unsafe. `initChatModel`
|
|
243
|
+
* falls back to `process.env[apiKeyEnvName ?? "OPENAI_API_KEY"]` when the
|
|
244
|
+
* config carries no key, so a keyless provider pointed at a tenant-chosen
|
|
245
|
+
* host would send the gateway's own deployment credential to it.
|
|
246
|
+
*/
|
|
247
|
+
apiKey: string;
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Whether this provider participates in discovery and registration
|
|
251
|
+
*/
|
|
252
|
+
enabled?: boolean;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* Request to update an existing model provider configuration.
|
|
257
|
+
*
|
|
258
|
+
* An omitted or empty `apiKey` means "keep the existing key" — the store must
|
|
259
|
+
* not overwrite stored credentials in that case. To remove a stored key
|
|
260
|
+
* outright, set `clearApiKey`.
|
|
261
|
+
*/
|
|
262
|
+
export interface UpdateModelProviderRequest {
|
|
263
|
+
/**
|
|
264
|
+
* Business name for the provider
|
|
265
|
+
*/
|
|
266
|
+
name?: string;
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* Optional friendly name for display
|
|
270
|
+
*/
|
|
271
|
+
displayName?: string;
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* Wire protocol used for discovery
|
|
275
|
+
*/
|
|
276
|
+
protocol?: ModelProviderWireProtocol;
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* Request style the upstream speaks
|
|
280
|
+
*/
|
|
281
|
+
apiStyle?: ModelProviderApiStyle;
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* Upstream base URL
|
|
285
|
+
*/
|
|
286
|
+
baseURL?: string;
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* Upstream API key. Omitted or empty keeps the existing key.
|
|
290
|
+
*/
|
|
291
|
+
apiKey?: string;
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* Remove the stored API key.
|
|
295
|
+
*
|
|
296
|
+
* Needed because an empty `apiKey` means "keep": without this there is no way
|
|
297
|
+
* to revoke a leaked credential short of deleting the provider.
|
|
298
|
+
* Takes precedence over `apiKey`.
|
|
299
|
+
*/
|
|
300
|
+
clearApiKey?: boolean;
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* Whether this provider participates in discovery and registration
|
|
304
|
+
*/
|
|
305
|
+
enabled?: boolean;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/**
|
|
309
|
+
* A single model returned by an upstream discovery call
|
|
310
|
+
*/
|
|
311
|
+
export interface DiscoveredModel {
|
|
312
|
+
/**
|
|
313
|
+
* Upstream model id — this becomes the tenant-scoped `modelKey`
|
|
314
|
+
*/
|
|
315
|
+
id: string;
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* Optional human-readable name from the upstream
|
|
319
|
+
*/
|
|
320
|
+
displayName?: string;
|
|
321
|
+
|
|
322
|
+
/**
|
|
323
|
+
* Upstream `owned_by` field, when present
|
|
324
|
+
*/
|
|
325
|
+
ownedBy?: string;
|
|
326
|
+
|
|
327
|
+
/**
|
|
328
|
+
* Upstream `created` field, when present
|
|
329
|
+
*/
|
|
330
|
+
createdAt?: string;
|
|
331
|
+
|
|
332
|
+
/**
|
|
333
|
+
* Raw upstream payload for this model
|
|
334
|
+
*/
|
|
335
|
+
raw?: unknown;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/**
|
|
339
|
+
* A model to store for a provider.
|
|
340
|
+
*
|
|
341
|
+
* Extends the discovered shape with an optional `enabled` override. Discovery
|
|
342
|
+
* leaves it out, which preserves whatever the tenant chose for a model that was
|
|
343
|
+
* already in the snapshot; an explicit selection states it.
|
|
344
|
+
*/
|
|
345
|
+
export interface ProviderModelInput extends DiscoveredModel {
|
|
346
|
+
enabled?: boolean;
|
|
347
|
+
/**
|
|
348
|
+
* The upstream no longer offers this model, but the row is kept.
|
|
349
|
+
*
|
|
350
|
+
* Distinct from `enabled: false`, which means the tenant switched it off.
|
|
351
|
+
*/
|
|
352
|
+
stale?: boolean;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
/**
|
|
356
|
+
* One model in an explicit provider model selection.
|
|
357
|
+
*
|
|
358
|
+
* Both states are stored: a deselected model is written with `enabled: false`
|
|
359
|
+
* rather than dropped. Only then can a later sync tell "the upstream no longer
|
|
360
|
+
* offers this" apart from "the tenant switched it off" — the two are otherwise
|
|
361
|
+
* indistinguishable in the data.
|
|
362
|
+
*/
|
|
363
|
+
export interface ModelSelection {
|
|
364
|
+
modelId: string;
|
|
365
|
+
enabled: boolean;
|
|
366
|
+
/** The upstream no longer offers this model; the row is kept, flagged. */
|
|
367
|
+
stale?: boolean;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
/**
|
|
371
|
+
* A model recorded in a provider's snapshot
|
|
372
|
+
*/
|
|
373
|
+
export interface ModelProviderModelEntry {
|
|
374
|
+
/**
|
|
375
|
+
* Unique identifier for the snapshot row
|
|
376
|
+
*/
|
|
377
|
+
id: string;
|
|
378
|
+
|
|
379
|
+
/**
|
|
380
|
+
* Tenant identifier for multi-tenant isolation
|
|
381
|
+
*/
|
|
382
|
+
tenantId: string;
|
|
383
|
+
|
|
384
|
+
/**
|
|
385
|
+
* Identifier of the provider this model was discovered from
|
|
386
|
+
*/
|
|
387
|
+
providerId: string;
|
|
388
|
+
|
|
389
|
+
/**
|
|
390
|
+
* Upstream model id — the tenant-scoped model key
|
|
391
|
+
*/
|
|
392
|
+
modelId: string;
|
|
393
|
+
|
|
394
|
+
/**
|
|
395
|
+
* Optional human-readable name from the upstream
|
|
396
|
+
*/
|
|
397
|
+
displayName?: string;
|
|
398
|
+
|
|
399
|
+
/**
|
|
400
|
+
* Upstream `owned_by` field, when present
|
|
401
|
+
*/
|
|
402
|
+
ownedBy?: string;
|
|
403
|
+
|
|
404
|
+
/**
|
|
405
|
+
* Upstream `created` field, when present
|
|
406
|
+
*/
|
|
407
|
+
upstreamCreatedAt?: Date;
|
|
408
|
+
|
|
409
|
+
/**
|
|
410
|
+
* Whether this model may be used
|
|
411
|
+
*/
|
|
412
|
+
enabled: boolean;
|
|
413
|
+
|
|
414
|
+
/**
|
|
415
|
+
* The upstream no longer offers this model. The row is kept so configs that
|
|
416
|
+
* reference its key keep resolving, and so "dropped upstream" stays
|
|
417
|
+
* distinguishable from "switched off here".
|
|
418
|
+
*/
|
|
419
|
+
stale?: boolean;
|
|
420
|
+
|
|
421
|
+
/**
|
|
422
|
+
* Timestamp of the discovery that produced this row
|
|
423
|
+
*/
|
|
424
|
+
discoveredAt: Date;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
/**
|
|
428
|
+
* Public projection of a provider configuration.
|
|
429
|
+
*
|
|
430
|
+
* This is the only shape that leaves the server: it never carries the API key
|
|
431
|
+
* itself, only whether one is configured and its last four characters.
|
|
432
|
+
*/
|
|
433
|
+
export interface ModelProviderPublicEntry {
|
|
434
|
+
/**
|
|
435
|
+
* Unique identifier for the provider configuration
|
|
436
|
+
*/
|
|
437
|
+
id: string;
|
|
438
|
+
|
|
439
|
+
/**
|
|
440
|
+
* Tenant identifier
|
|
441
|
+
*/
|
|
442
|
+
tenantId: string;
|
|
443
|
+
|
|
444
|
+
/**
|
|
445
|
+
* Business name for the provider
|
|
446
|
+
*/
|
|
447
|
+
name: string;
|
|
448
|
+
|
|
449
|
+
/**
|
|
450
|
+
* Optional friendly name for display
|
|
451
|
+
*/
|
|
452
|
+
displayName?: string;
|
|
453
|
+
|
|
454
|
+
/**
|
|
455
|
+
* Wire protocol used for discovery
|
|
456
|
+
*/
|
|
457
|
+
protocol: ModelProviderWireProtocol;
|
|
458
|
+
|
|
459
|
+
/**
|
|
460
|
+
* Request style the upstream speaks
|
|
461
|
+
*/
|
|
462
|
+
apiStyle: ModelProviderApiStyle;
|
|
463
|
+
|
|
464
|
+
/**
|
|
465
|
+
* LLMConfig provider value used when constructing a runtime client
|
|
466
|
+
*/
|
|
467
|
+
llmProvider: ModelProviderLlmProvider;
|
|
468
|
+
|
|
469
|
+
/**
|
|
470
|
+
* Upstream base URL
|
|
471
|
+
*/
|
|
472
|
+
baseURL: string;
|
|
473
|
+
|
|
474
|
+
/**
|
|
475
|
+
* Whether an API key is configured. Replaces the key itself in responses.
|
|
476
|
+
*/
|
|
477
|
+
hasApiKey: boolean;
|
|
478
|
+
|
|
479
|
+
/**
|
|
480
|
+
* Last 4 characters of the API key, for display
|
|
481
|
+
*/
|
|
482
|
+
apiKeyHint?: string;
|
|
483
|
+
|
|
484
|
+
/**
|
|
485
|
+
* Whether this provider participates in discovery and registration
|
|
486
|
+
*/
|
|
487
|
+
enabled: boolean;
|
|
488
|
+
|
|
489
|
+
/**
|
|
490
|
+
* Outcome of the most recent discovery attempt
|
|
491
|
+
*/
|
|
492
|
+
status: ModelProviderStatus;
|
|
493
|
+
|
|
494
|
+
/**
|
|
495
|
+
* Stable error code from the most recent failed discovery
|
|
496
|
+
*/
|
|
497
|
+
lastError?: string;
|
|
498
|
+
|
|
499
|
+
/**
|
|
500
|
+
* Timestamp of the most recent successful discovery
|
|
501
|
+
*/
|
|
502
|
+
lastDiscoveredAt?: string;
|
|
503
|
+
|
|
504
|
+
/**
|
|
505
|
+
* Number of models in this provider's snapshot
|
|
506
|
+
*/
|
|
507
|
+
modelCount: number;
|
|
508
|
+
|
|
509
|
+
/**
|
|
510
|
+
* Creation timestamp
|
|
511
|
+
*/
|
|
512
|
+
createdAt: string;
|
|
513
|
+
|
|
514
|
+
/**
|
|
515
|
+
* Last update timestamp
|
|
516
|
+
*/
|
|
517
|
+
updatedAt: string;
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
/**
|
|
521
|
+
* A model visible to a tenant, from either the discovery snapshot or the
|
|
522
|
+
* in-memory model registry.
|
|
523
|
+
*/
|
|
524
|
+
export interface TenantModelPublicEntry {
|
|
525
|
+
/**
|
|
526
|
+
* Tenant-scoped model key — the upstream model id
|
|
527
|
+
*/
|
|
528
|
+
modelKey: string;
|
|
529
|
+
|
|
530
|
+
/**
|
|
531
|
+
* Optional human-readable name
|
|
532
|
+
*/
|
|
533
|
+
displayName?: string;
|
|
534
|
+
|
|
535
|
+
/**
|
|
536
|
+
* Identifier of the provider offering this model
|
|
537
|
+
*/
|
|
538
|
+
providerId: string;
|
|
539
|
+
|
|
540
|
+
/**
|
|
541
|
+
* Name of the provider offering this model
|
|
542
|
+
*/
|
|
543
|
+
providerName: string;
|
|
544
|
+
|
|
545
|
+
/**
|
|
546
|
+
* Upstream `owned_by` field, when present
|
|
547
|
+
*/
|
|
548
|
+
ownedBy?: string;
|
|
549
|
+
|
|
550
|
+
/**
|
|
551
|
+
* Where this entry came from
|
|
552
|
+
*/
|
|
553
|
+
source: "snapshot" | "registered";
|
|
554
|
+
|
|
555
|
+
/**
|
|
556
|
+
* Whether the tenant has this model switched on.
|
|
557
|
+
* Entries sourced from the registry are always enabled.
|
|
558
|
+
*/
|
|
559
|
+
enabled: boolean;
|
|
560
|
+
|
|
561
|
+
/**
|
|
562
|
+
* The upstream no longer offers this model (see `ModelProviderModelEntry.stale`)
|
|
563
|
+
*/
|
|
564
|
+
stale: boolean;
|
|
565
|
+
|
|
566
|
+
/**
|
|
567
|
+
* Whether this model can actually be executed by an agent.
|
|
568
|
+
* Phase 1: true only for the `default` tenant.
|
|
569
|
+
*/
|
|
570
|
+
executable: boolean;
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
/**
|
|
574
|
+
* The full model catalog visible to one tenant
|
|
575
|
+
*/
|
|
576
|
+
export interface TenantModelCatalog {
|
|
577
|
+
/**
|
|
578
|
+
* Provider configurations owned by the tenant
|
|
579
|
+
*/
|
|
580
|
+
providers: ModelProviderPublicEntry[];
|
|
581
|
+
|
|
582
|
+
/**
|
|
583
|
+
* Models visible to the tenant
|
|
584
|
+
*/
|
|
585
|
+
models: TenantModelPublicEntry[];
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
/**
|
|
589
|
+
* Result of running discovery against one provider
|
|
590
|
+
*/
|
|
591
|
+
export interface ModelDiscoveryResult {
|
|
592
|
+
/**
|
|
593
|
+
* Models returned by the upstream
|
|
594
|
+
*/
|
|
595
|
+
discovered: DiscoveredModel[];
|
|
596
|
+
|
|
597
|
+
/**
|
|
598
|
+
* Model keys registered for execution, of the form `${providerId}/${modelId}`
|
|
599
|
+
*/
|
|
600
|
+
registered: string[];
|
|
601
|
+
|
|
602
|
+
/**
|
|
603
|
+
* Provider status after this attempt
|
|
604
|
+
*/
|
|
605
|
+
status: ModelProviderStatus;
|
|
606
|
+
|
|
607
|
+
/**
|
|
608
|
+
* Stable error code if the attempt failed
|
|
609
|
+
*/
|
|
610
|
+
lastError?: string;
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
/**
|
|
614
|
+
* Ad-hoc probe of an upstream, used by the provider form to validate a base URL
|
|
615
|
+
* and API key before they are saved.
|
|
616
|
+
*
|
|
617
|
+
* Unlike `discover`, this accepts the URL from the request rather than loading
|
|
618
|
+
* it from the store — that is the whole point, since the values may not exist
|
|
619
|
+
* in the store yet. The URL is validated exactly as on create/update and is
|
|
620
|
+
* never persisted by a probe.
|
|
621
|
+
*/
|
|
622
|
+
export interface ModelProviderProbeRequest {
|
|
623
|
+
/**
|
|
624
|
+
* Upstream base URL to probe. Validated before any outbound request.
|
|
625
|
+
*/
|
|
626
|
+
baseURL: string;
|
|
627
|
+
|
|
628
|
+
/**
|
|
629
|
+
* API key to authenticate with.
|
|
630
|
+
* Omitted on an existing provider means "use the stored key", which is
|
|
631
|
+
* resolved through `providerId` — the form never receives the stored key.
|
|
632
|
+
*/
|
|
633
|
+
apiKey?: string;
|
|
634
|
+
|
|
635
|
+
/**
|
|
636
|
+
* Existing provider whose stored key should be used when `apiKey` is absent.
|
|
637
|
+
*/
|
|
638
|
+
providerId?: string;
|
|
639
|
+
|
|
640
|
+
/**
|
|
641
|
+
* Wire protocol to speak. Defaults to `openai-compatible`.
|
|
642
|
+
*/
|
|
643
|
+
protocol?: ModelProviderWireProtocol;
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
/**
|
|
647
|
+
* Outcome of a probe: what the upstream answered, and how long it took.
|
|
648
|
+
*
|
|
649
|
+
* A failed probe is still a successful request — the caller wants to render
|
|
650
|
+
* "unreachable: UPSTREAM_AUTH_FAILED" in the form, not handle an HTTP error.
|
|
651
|
+
* Only invalid input (bad URL, unknown protocol) is reported as an HTTP error.
|
|
652
|
+
*/
|
|
653
|
+
export interface ModelProviderProbeResult {
|
|
654
|
+
/**
|
|
655
|
+
* Whether the upstream answered with a model list
|
|
656
|
+
*/
|
|
657
|
+
reachable: boolean;
|
|
658
|
+
|
|
659
|
+
/**
|
|
660
|
+
* Round-trip time in milliseconds, measured over the model list request
|
|
661
|
+
*/
|
|
662
|
+
latencyMs: number;
|
|
663
|
+
|
|
664
|
+
/**
|
|
665
|
+
* Models the upstream returned. Empty when unreachable.
|
|
666
|
+
*/
|
|
667
|
+
models: DiscoveredModel[];
|
|
668
|
+
|
|
669
|
+
/**
|
|
670
|
+
* Stable error code when `reachable` is false.
|
|
671
|
+
*
|
|
672
|
+
* Narrowed to the upstream subset: an invalid URL or an unsupported protocol
|
|
673
|
+
* is rejected before the request is attempted, so those surface as HTTP
|
|
674
|
+
* errors rather than as an unreachable probe.
|
|
675
|
+
*/
|
|
676
|
+
error?: ModelProviderUpstreamErrorCode;
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
/**
|
|
680
|
+
* ModelProviderStore interface
|
|
681
|
+
* Provides tenant-scoped CRUD for provider configurations and their model snapshots
|
|
682
|
+
*/
|
|
683
|
+
export interface ModelProviderStore {
|
|
684
|
+
/**
|
|
685
|
+
* Get all provider configurations for a tenant
|
|
686
|
+
* @param tenantId - Tenant identifier
|
|
687
|
+
* @returns Array of provider configurations
|
|
688
|
+
*/
|
|
689
|
+
listProviders(tenantId: string): Promise<ModelProviderEntry[]>;
|
|
690
|
+
|
|
691
|
+
/**
|
|
692
|
+
* Get a provider configuration by ID
|
|
693
|
+
* @param tenantId - Tenant identifier
|
|
694
|
+
* @param id - Provider identifier
|
|
695
|
+
* @returns Provider configuration if found, null otherwise
|
|
696
|
+
*/
|
|
697
|
+
getProviderById(tenantId: string, id: string): Promise<ModelProviderEntry | null>;
|
|
698
|
+
|
|
699
|
+
/**
|
|
700
|
+
* Get a provider configuration by business name
|
|
701
|
+
* @param tenantId - Tenant identifier
|
|
702
|
+
* @param name - Provider business name
|
|
703
|
+
* @returns Provider configuration if found, null otherwise
|
|
704
|
+
*/
|
|
705
|
+
getProviderByName(tenantId: string, name: string): Promise<ModelProviderEntry | null>;
|
|
706
|
+
|
|
707
|
+
/**
|
|
708
|
+
* Create a new provider configuration
|
|
709
|
+
* @param tenantId - Tenant identifier
|
|
710
|
+
* @param id - Provider identifier
|
|
711
|
+
* @param data - Provider creation data
|
|
712
|
+
* @returns Created provider configuration
|
|
713
|
+
*/
|
|
714
|
+
createProvider(
|
|
715
|
+
tenantId: string,
|
|
716
|
+
id: string,
|
|
717
|
+
data: CreateModelProviderRequest
|
|
718
|
+
): Promise<ModelProviderEntry>;
|
|
719
|
+
|
|
720
|
+
/**
|
|
721
|
+
* Update an existing provider configuration.
|
|
722
|
+
* An omitted or empty `apiKey` leaves the stored key untouched.
|
|
723
|
+
* @param tenantId - Tenant identifier
|
|
724
|
+
* @param id - Provider identifier
|
|
725
|
+
* @param updates - Partial provider data to update
|
|
726
|
+
* @returns Updated provider configuration if found, null otherwise
|
|
727
|
+
*/
|
|
728
|
+
updateProvider(
|
|
729
|
+
tenantId: string,
|
|
730
|
+
id: string,
|
|
731
|
+
updates: Partial<UpdateModelProviderRequest>
|
|
732
|
+
): Promise<ModelProviderEntry | null>;
|
|
733
|
+
|
|
734
|
+
/**
|
|
735
|
+
* Delete a provider configuration and its model snapshot
|
|
736
|
+
* @param tenantId - Tenant identifier
|
|
737
|
+
* @param id - Provider identifier
|
|
738
|
+
* @returns true if deleted, false otherwise
|
|
739
|
+
*/
|
|
740
|
+
deleteProvider(tenantId: string, id: string): Promise<boolean>;
|
|
741
|
+
|
|
742
|
+
/**
|
|
743
|
+
* Record the outcome of a discovery attempt
|
|
744
|
+
* @param tenantId - Tenant identifier
|
|
745
|
+
* @param id - Provider identifier
|
|
746
|
+
* @param status - Discovery outcome
|
|
747
|
+
* @param lastError - Stable error code, if the attempt failed
|
|
748
|
+
* @param discoveredAt - Timestamp of a successful discovery
|
|
749
|
+
*/
|
|
750
|
+
markProviderStatus(
|
|
751
|
+
tenantId: string,
|
|
752
|
+
id: string,
|
|
753
|
+
status: ModelProviderStatus,
|
|
754
|
+
lastError?: string,
|
|
755
|
+
discoveredAt?: Date
|
|
756
|
+
): Promise<void>;
|
|
757
|
+
|
|
758
|
+
/**
|
|
759
|
+
* List snapshot models for a tenant
|
|
760
|
+
* @param tenantId - Tenant identifier
|
|
761
|
+
* @param providerId - Optional provider filter
|
|
762
|
+
* @returns Array of snapshot models
|
|
763
|
+
*/
|
|
764
|
+
listModels(tenantId: string, providerId?: string): Promise<ModelProviderModelEntry[]>;
|
|
765
|
+
|
|
766
|
+
/**
|
|
767
|
+
* Get a single snapshot model.
|
|
768
|
+
* Keyed by provider as well as model id, since two providers in the same
|
|
769
|
+
* tenant may expose the same upstream model id.
|
|
770
|
+
* @param tenantId - Tenant identifier
|
|
771
|
+
* @param providerId - Provider identifier
|
|
772
|
+
* @param modelId - Upstream model id
|
|
773
|
+
* @returns Snapshot model if found, null otherwise
|
|
774
|
+
*/
|
|
775
|
+
getModel(
|
|
776
|
+
tenantId: string,
|
|
777
|
+
providerId: string,
|
|
778
|
+
modelId: string
|
|
779
|
+
): Promise<ModelProviderModelEntry | null>;
|
|
780
|
+
|
|
781
|
+
/**
|
|
782
|
+
* Replace a provider's model snapshot with the result of one discovery.
|
|
783
|
+
*
|
|
784
|
+
* Model ids are scoped per provider, so the same upstream id may appear
|
|
785
|
+
* under several providers in one tenant — each is an independent model
|
|
786
|
+
* addressed by `${providerId}/${modelId}`.
|
|
787
|
+
*
|
|
788
|
+
* @param tenantId - Tenant identifier
|
|
789
|
+
* @param providerId - Provider identifier
|
|
790
|
+
* @param models - Models returned by the most recent discovery
|
|
791
|
+
* @returns Model ids now present in this provider's snapshot
|
|
792
|
+
*/
|
|
793
|
+
replaceProviderModels(
|
|
794
|
+
tenantId: string,
|
|
795
|
+
providerId: string,
|
|
796
|
+
models: ProviderModelInput[]
|
|
797
|
+
): Promise<string[]>;
|
|
798
|
+
|
|
799
|
+
/**
|
|
800
|
+
* Enable or disable a single snapshot model
|
|
801
|
+
* @param tenantId - Tenant identifier
|
|
802
|
+
* @param providerId - Provider identifier
|
|
803
|
+
* @param modelId - Model id
|
|
804
|
+
* @param enabled - Desired state
|
|
805
|
+
* @returns Updated snapshot model if found, null otherwise
|
|
806
|
+
*/
|
|
807
|
+
setModelEnabled(
|
|
808
|
+
tenantId: string,
|
|
809
|
+
providerId: string,
|
|
810
|
+
modelId: string,
|
|
811
|
+
enabled: boolean
|
|
812
|
+
): Promise<ModelProviderModelEntry | null>;
|
|
813
|
+
|
|
814
|
+
/**
|
|
815
|
+
* Delete every snapshot model belonging to a provider
|
|
816
|
+
* @param tenantId - Tenant identifier
|
|
817
|
+
* @param providerId - Provider identifier
|
|
818
|
+
* @returns Number of rows deleted
|
|
819
|
+
*/
|
|
820
|
+
deleteModelsByProvider(tenantId: string, providerId: string): Promise<number>;
|
|
821
|
+
}
|