@oh-my-pi/pi-catalog 18.2.6 → 18.2.7
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/CHANGELOG.md +14 -0
- package/THIRD-PARTY-NOTICES.txt +0 -37
- package/dist/types/build.d.ts +7 -1
- package/dist/types/compat/auth-ids.d.ts +1 -1
- package/dist/types/compat/axes.d.ts +1 -1
- package/dist/types/compat/cascade.d.ts +3 -2
- package/dist/types/compat/provider-ids.d.ts +1 -1
- package/dist/types/compat/resolve.d.ts +2 -0
- package/dist/types/compat/taxonomy.d.ts +2 -2
- package/dist/types/compat/types.d.ts +18 -7
- package/dist/types/discovery/index.d.ts +1 -0
- package/dist/types/discovery/openai-compatible.d.ts +2 -0
- package/dist/types/discovery/typesafe.d.ts +26 -0
- package/dist/types/model-manager.d.ts +1 -1
- package/dist/types/provider-models/openai-compat.d.ts +6 -4
- package/dist/types/provider-models/special.d.ts +10 -0
- package/dist/types/types.d.ts +20 -0
- package/package.json +4 -4
- package/src/build.ts +27 -5
- package/src/compat/auth-ids.ts +2 -0
- package/src/compat/axes.ts +9 -1
- package/src/compat/cascade.ts +45 -23
- package/src/compat/provider-ids.ts +3 -0
- package/src/compat/resolve.ts +24 -14
- package/src/compat/rules/README.md +5 -3
- package/src/compat/rules/auth/local.kdl +4 -0
- package/src/compat/rules/auth/typesafe.kdl +3 -6
- package/src/compat/rules/auth/web.kdl +4 -0
- package/src/compat/rules/classes/qwen.kdl +6 -5
- package/src/compat/rules/providers/anthropic.kdl +1 -0
- package/src/compat/rules/providers/deepinfra.kdl +28 -0
- package/src/compat/rules/providers/google-antigravity.kdl +17 -0
- package/src/compat/rules/providers/google.kdl +9 -0
- package/src/compat/rules/providers/llama.cpp.kdl +23 -0
- package/src/compat/rules/providers/local.kdl +110 -0
- package/src/compat/rules/providers/openai-codex.kdl +18 -0
- package/src/compat/rules/providers/openai.kdl +3 -0
- package/src/compat/rules/providers/openrouter.kdl +24 -0
- package/src/compat/rules/providers/typesafe.kdl +21 -0
- package/src/compat/rules/providers/web.kdl +153 -0
- package/src/compat/rules/providers/xai-oauth.kdl +26 -0
- package/src/compat/rules/providers/xai.kdl +22 -0
- package/src/compat/rules/runtime/behavior.kdl +2 -1
- package/src/compat/rules/taxonomy/qwen.kdl +2 -0
- package/src/compat/rules.json +1 -1
- package/src/compat/taxonomy.ts +92 -31
- package/src/compat/types.ts +22 -7
- package/src/discovery/index.ts +1 -0
- package/src/discovery/openai-compatible.ts +4 -1
- package/src/discovery/typesafe.ts +118 -0
- package/src/model-manager.ts +44 -15
- package/src/models.json +1 -1
- package/src/provider-models/descriptors.ts +6 -0
- package/src/provider-models/openai-compat.ts +246 -79
- package/src/provider-models/special.ts +53 -0
- package/src/types.ts +33 -0
package/src/compat/taxonomy.ts
CHANGED
|
@@ -14,13 +14,13 @@ import { formatRevision, parseRevisionPrefix } from "./revision";
|
|
|
14
14
|
import rules from "./rules.json";
|
|
15
15
|
import type { CompiledClass, CompiledIdentityOverride, CompiledMatcher, ModelIdentity } from "./types";
|
|
16
16
|
|
|
17
|
-
/** Two classes
|
|
17
|
+
/** Two classes, families, or reviewed override patterns tie for one model id. */
|
|
18
18
|
export class AmbiguousIdentityError extends Error {
|
|
19
19
|
constructor(
|
|
20
20
|
readonly model: string,
|
|
21
21
|
readonly first: string,
|
|
22
22
|
readonly second: string,
|
|
23
|
-
kind: "class" | "family",
|
|
23
|
+
kind: "class" | "family" | "override",
|
|
24
24
|
) {
|
|
25
25
|
super(`ambiguous ${kind} for \`${model}\`: \`${first}\` and \`${second}\` tie`);
|
|
26
26
|
this.name = "AmbiguousIdentityError";
|
|
@@ -201,27 +201,50 @@ function ranksInClass(classId: string, model: string, lenient: boolean): Omit<Cl
|
|
|
201
201
|
return out;
|
|
202
202
|
}
|
|
203
203
|
|
|
204
|
-
//
|
|
205
|
-
//
|
|
206
|
-
// in tree order wins per (model, provider) slot, matching the linear scan it
|
|
207
|
-
// replaced.
|
|
204
|
+
// Exact overrides retain a direct lowercase index. Pattern overrides are few
|
|
205
|
+
// and remain in one static list, ranked only after exact lookup misses.
|
|
208
206
|
interface OverrideBucket {
|
|
209
207
|
byProvider: Map<string, CompiledIdentityOverride>;
|
|
210
208
|
agnostic: CompiledIdentityOverride | undefined;
|
|
211
209
|
}
|
|
212
210
|
|
|
213
|
-
|
|
211
|
+
interface IndexedPatternOverride {
|
|
212
|
+
override: CompiledIdentityOverride;
|
|
213
|
+
glob: string;
|
|
214
|
+
provider?: string;
|
|
215
|
+
specificity: number;
|
|
216
|
+
}
|
|
214
217
|
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
218
|
+
interface OverrideIndexes {
|
|
219
|
+
exact: Map<string, OverrideBucket>;
|
|
220
|
+
patterns: IndexedPatternOverride[];
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
let overrideIndexes: OverrideIndexes | undefined;
|
|
224
|
+
|
|
225
|
+
function getOverrideIndexes(): OverrideIndexes {
|
|
226
|
+
if (overrideIndexes !== undefined) return overrideIndexes;
|
|
227
|
+
const exact = new Map<string, OverrideBucket>();
|
|
228
|
+
const patterns: IndexedPatternOverride[] = [];
|
|
229
|
+
const classes: readonly CompiledClass[] = rules.taxonomy.classes;
|
|
230
|
+
for (const cls of classes) {
|
|
219
231
|
for (const override of cls.overrides) {
|
|
232
|
+
if (override.model === undefined) {
|
|
233
|
+
if (override.glob !== undefined) {
|
|
234
|
+
patterns.push({
|
|
235
|
+
override,
|
|
236
|
+
glob: override.glob,
|
|
237
|
+
provider: override.provider?.toLowerCase(),
|
|
238
|
+
specificity: nonWildcardBytes(override.glob),
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
continue;
|
|
242
|
+
}
|
|
220
243
|
const key = override.model.toLowerCase();
|
|
221
|
-
let bucket =
|
|
244
|
+
let bucket = exact.get(key);
|
|
222
245
|
if (bucket === undefined) {
|
|
223
246
|
bucket = { byProvider: new Map(), agnostic: undefined };
|
|
224
|
-
|
|
247
|
+
exact.set(key, bucket);
|
|
225
248
|
}
|
|
226
249
|
if (override.provider !== undefined) {
|
|
227
250
|
const providerKey = override.provider.toLowerCase();
|
|
@@ -231,30 +254,68 @@ function getOverrideIndex(): Map<string, OverrideBucket> {
|
|
|
231
254
|
}
|
|
232
255
|
}
|
|
233
256
|
}
|
|
234
|
-
|
|
235
|
-
return
|
|
257
|
+
overrideIndexes = { exact, patterns };
|
|
258
|
+
return overrideIndexes;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function overrideIsActive(override: CompiledIdentityOverride, observedAtMs: number | undefined): boolean {
|
|
262
|
+
return override.expiresAtMs === undefined || observedAtMs === undefined || observedAtMs < override.expiresAtMs;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
interface PatternOverrideRanking {
|
|
266
|
+
winner?: { specificity: number; override: CompiledIdentityOverride };
|
|
267
|
+
tied?: readonly [CompiledIdentityOverride, CompiledIdentityOverride];
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
function rankPatternOverrides(
|
|
271
|
+
patterns: readonly IndexedPatternOverride[],
|
|
272
|
+
provider: string | undefined,
|
|
273
|
+
bareModel: string,
|
|
274
|
+
observedAtMs: number | undefined,
|
|
275
|
+
): PatternOverrideRanking {
|
|
276
|
+
let winner: { specificity: number; override: CompiledIdentityOverride } | undefined;
|
|
277
|
+
let tied: readonly [CompiledIdentityOverride, CompiledIdentityOverride] | undefined;
|
|
278
|
+
for (const pattern of patterns) {
|
|
279
|
+
const { override, specificity } = pattern;
|
|
280
|
+
if (pattern.provider !== provider || !overrideIsActive(override, observedAtMs)) continue;
|
|
281
|
+
if (!globMatch(pattern.glob, bareModel)) continue;
|
|
282
|
+
if (winner?.specificity === specificity) {
|
|
283
|
+
tied = [winner.override, override];
|
|
284
|
+
} else if (winner === undefined || winner.specificity < specificity) {
|
|
285
|
+
winner = { specificity, override };
|
|
286
|
+
tied = undefined;
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
return { winner, tied };
|
|
236
290
|
}
|
|
237
291
|
|
|
238
292
|
function findIdentityOverride(
|
|
239
293
|
provider: string,
|
|
240
294
|
bareModel: string,
|
|
241
295
|
observedAtMs: number | undefined,
|
|
296
|
+
lenient: boolean,
|
|
242
297
|
): CompiledIdentityOverride | undefined {
|
|
243
|
-
const
|
|
244
|
-
|
|
245
|
-
const
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
298
|
+
const lowerProvider = provider.toLowerCase();
|
|
299
|
+
const lowerBareModel = bareModel.toLowerCase();
|
|
300
|
+
const indexes = getOverrideIndexes();
|
|
301
|
+
const bucket = indexes.exact.get(lowerBareModel);
|
|
302
|
+
if (bucket !== undefined) {
|
|
303
|
+
const scoped = bucket.byProvider.get(lowerProvider);
|
|
304
|
+
if (scoped !== undefined && overrideIsActive(scoped, observedAtMs)) return scoped;
|
|
305
|
+
const agnostic = bucket.agnostic;
|
|
306
|
+
if (agnostic !== undefined && overrideIsActive(agnostic, observedAtMs)) return agnostic;
|
|
250
307
|
}
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
308
|
+
|
|
309
|
+
const scoped = rankPatternOverrides(indexes.patterns, lowerProvider, lowerBareModel, observedAtMs);
|
|
310
|
+
const ranked =
|
|
311
|
+
scoped.winner === undefined && scoped.tied === undefined
|
|
312
|
+
? rankPatternOverrides(indexes.patterns, undefined, lowerBareModel, observedAtMs)
|
|
313
|
+
: scoped;
|
|
314
|
+
if (ranked.tied !== undefined) {
|
|
315
|
+
if (lenient) return undefined;
|
|
316
|
+
throw new AmbiguousIdentityError(lowerBareModel, ranked.tied[0].id, ranked.tied[1].id, "override");
|
|
256
317
|
}
|
|
257
|
-
return
|
|
318
|
+
return ranked.winner?.override;
|
|
258
319
|
}
|
|
259
320
|
|
|
260
321
|
/** Result of collapsing a wire id through the suffix vocabulary. */
|
|
@@ -369,8 +430,8 @@ export function stripThinkingVariantSuffix(model: string): string | undefined {
|
|
|
369
430
|
* other model classified under the same key. Calls with an explicit
|
|
370
431
|
* observedAtMs skip the memo so override expiry stays exact.
|
|
371
432
|
*
|
|
372
|
-
* @throws AmbiguousIdentityError on equal-rank cross-class
|
|
373
|
-
* matches unless `opts.lenient`.
|
|
433
|
+
* @throws AmbiguousIdentityError on equal-rank cross-class, cross-family, or
|
|
434
|
+
* reviewed-pattern matches unless `opts.lenient`.
|
|
374
435
|
*/
|
|
375
436
|
const classifyMemo = new Map<string, ModelIdentity>();
|
|
376
437
|
const CLASSIFY_MEMO_MAX = 4096;
|
|
@@ -396,7 +457,7 @@ function classifyModelUncached(provider: string, modelId: string, opts?: Classif
|
|
|
396
457
|
const lenient = opts?.lenient === true;
|
|
397
458
|
const trimmed = modelId.trim();
|
|
398
459
|
const bare = bareOf(trimmed);
|
|
399
|
-
const override = findIdentityOverride(provider, bare, opts?.observedAtMs);
|
|
460
|
+
const override = findIdentityOverride(provider, bare, opts?.observedAtMs, lenient);
|
|
400
461
|
if (override) {
|
|
401
462
|
const logical = override.logical ?? trimmed;
|
|
402
463
|
const cls = override.class ?? classifyRanks(logical, lenient).class;
|
package/src/compat/types.ts
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* `behavior.ts`, `resolve.ts`) exposes to consumers.
|
|
6
6
|
*/
|
|
7
7
|
import type { Effort } from "../effort";
|
|
8
|
-
import type {
|
|
8
|
+
import type { Api, ThinkingControlMode, TokenCost } from "../types";
|
|
9
9
|
import type { RevisionOp } from "./revision";
|
|
10
10
|
|
|
11
11
|
/** Class-membership matcher kinds, most to least specific. */
|
|
@@ -32,11 +32,9 @@ export interface CompiledRevisionPrefix {
|
|
|
32
32
|
anywhere?: boolean;
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
-
|
|
36
|
-
export interface CompiledIdentityOverride {
|
|
35
|
+
interface CompiledIdentityOverrideFields {
|
|
37
36
|
id: string;
|
|
38
37
|
provider?: string;
|
|
39
|
-
model: string;
|
|
40
38
|
logical?: string;
|
|
41
39
|
class?: string;
|
|
42
40
|
family?: string;
|
|
@@ -49,6 +47,21 @@ export interface CompiledIdentityOverride {
|
|
|
49
47
|
expiresAtMs?: number;
|
|
50
48
|
}
|
|
51
49
|
|
|
50
|
+
/** One compiled reviewed identity correction with exactly one bare-model selector. */
|
|
51
|
+
export type CompiledIdentityOverride = CompiledIdentityOverrideFields &
|
|
52
|
+
(
|
|
53
|
+
| {
|
|
54
|
+
/** Exact bare-model selector. */
|
|
55
|
+
model: string;
|
|
56
|
+
glob?: never;
|
|
57
|
+
}
|
|
58
|
+
| {
|
|
59
|
+
model?: never;
|
|
60
|
+
/** Anchored, case-insensitive bare-model glob. */
|
|
61
|
+
glob: string;
|
|
62
|
+
}
|
|
63
|
+
);
|
|
64
|
+
|
|
52
65
|
/** One compiled model class: matchers, families, revision rules, overrides. */
|
|
53
66
|
export interface CompiledClass {
|
|
54
67
|
id: string;
|
|
@@ -588,7 +601,7 @@ export interface CompiledProviderDiscovery {
|
|
|
588
601
|
export interface CompiledSeedModel {
|
|
589
602
|
id: string;
|
|
590
603
|
name: string;
|
|
591
|
-
api:
|
|
604
|
+
api: Api;
|
|
592
605
|
provider: string;
|
|
593
606
|
baseUrl: string;
|
|
594
607
|
reasoning: boolean;
|
|
@@ -615,8 +628,8 @@ export interface CompiledSeed {
|
|
|
615
628
|
}
|
|
616
629
|
|
|
617
630
|
/**
|
|
618
|
-
* One
|
|
619
|
-
*
|
|
631
|
+
* One model provider's catalog entry: the non-code half of what the runtime
|
|
632
|
+
* and generator know about a provider. A `providers/<id>.kdl` file
|
|
620
633
|
* declares one by carrying `default-model`; files without it are wire-compat
|
|
621
634
|
* only (custom provider ids such as `llama.cpp`).
|
|
622
635
|
*/
|
|
@@ -634,6 +647,8 @@ export interface CompiledProvider {
|
|
|
634
647
|
skipCrossProviderReferenceFills?: boolean;
|
|
635
648
|
/** Present only for providers enrolled in `generate-models.ts` discovery. */
|
|
636
649
|
discovery?: CompiledProviderDiscovery;
|
|
650
|
+
/** Non-chat model kinds mapped to their runtime transport APIs. */
|
|
651
|
+
kindApis?: Partial<Record<"image" | "tts" | "stt", Api>>;
|
|
637
652
|
/** Authored bundled rows, when the provider cannot be discovered at generation time. */
|
|
638
653
|
seed?: CompiledSeed;
|
|
639
654
|
}
|
package/src/discovery/index.ts
CHANGED
|
@@ -106,6 +106,8 @@ export interface FetchOpenAICompatibleModelsOptions<TApi extends Api> {
|
|
|
106
106
|
apiKey?: string;
|
|
107
107
|
/** Additional request headers. */
|
|
108
108
|
headers?: Record<string, string>;
|
|
109
|
+
/** Query parameters appended to the `/models` probe (e.g. OpenRouter's `output_modalities` roster filter). */
|
|
110
|
+
query?: Record<string, string>;
|
|
109
111
|
/** Optional AbortSignal for request cancellation; caller owns its lifecycle. */
|
|
110
112
|
signal?: AbortSignal;
|
|
111
113
|
/**
|
|
@@ -155,10 +157,11 @@ export async function fetchOpenAICompatibleModels<TApi extends Api>(
|
|
|
155
157
|
}
|
|
156
158
|
|
|
157
159
|
const fetchImpl = discoveryFetch(options.fetch);
|
|
160
|
+
const query = options.query ? `?${new URLSearchParams(options.query)}` : "";
|
|
158
161
|
const fetchPayload = async (signal?: AbortSignal): Promise<unknown | null> => {
|
|
159
162
|
let response: Response;
|
|
160
163
|
try {
|
|
161
|
-
response = await fetchImpl(`${baseUrl}${MODELS_PATH}`, {
|
|
164
|
+
response = await fetchImpl(`${baseUrl}${MODELS_PATH}${query}`, {
|
|
162
165
|
method: "GET",
|
|
163
166
|
headers: requestHeaders,
|
|
164
167
|
signal,
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import type { FetchImpl, ModelSpec } from "../types";
|
|
2
|
+
import { discoveryFetch, isRecord } from "../utils";
|
|
3
|
+
|
|
4
|
+
/** Public TypeSafe API root shared by discovery and judgment requests. */
|
|
5
|
+
export const TYPESAFE_DEFAULT_BASE_URL = "https://api.typesafe.ai";
|
|
6
|
+
|
|
7
|
+
/** Wire card returned by TypeSafe's authenticated `GET /v1/models`. */
|
|
8
|
+
export interface TypeSafeModelCard {
|
|
9
|
+
name: string;
|
|
10
|
+
description: string;
|
|
11
|
+
release_date: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** Credentials and transport overrides for TypeSafe account model discovery. */
|
|
15
|
+
export interface FetchTypeSafeModelsOptions {
|
|
16
|
+
apiKey: string;
|
|
17
|
+
baseUrl?: string;
|
|
18
|
+
fetch?: FetchImpl;
|
|
19
|
+
signal?: AbortSignal;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function normalizeBaseUrl(value: string): string | null {
|
|
23
|
+
try {
|
|
24
|
+
const url = new URL(value);
|
|
25
|
+
return url.toString().replace(/\/+$/, "");
|
|
26
|
+
} catch {
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function parseModelCard(value: unknown): TypeSafeModelCard | null {
|
|
32
|
+
if (
|
|
33
|
+
!isRecord(value) ||
|
|
34
|
+
typeof value.name !== "string" ||
|
|
35
|
+
value.name.trim().length === 0 ||
|
|
36
|
+
typeof value.description !== "string" ||
|
|
37
|
+
typeof value.release_date !== "string"
|
|
38
|
+
) {
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
return {
|
|
42
|
+
name: value.name.trim(),
|
|
43
|
+
description: value.description.trim(),
|
|
44
|
+
release_date: value.release_date,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Validate and de-duplicate the official `{ models: [...] }` response. */
|
|
49
|
+
export function parseTypeSafeModelCards(payload: unknown): TypeSafeModelCard[] | null {
|
|
50
|
+
if (!isRecord(payload) || !Array.isArray(payload.models)) return null;
|
|
51
|
+
const cards: TypeSafeModelCard[] = [];
|
|
52
|
+
const seen = new Set<string>();
|
|
53
|
+
for (const raw of payload.models) {
|
|
54
|
+
const card = parseModelCard(raw);
|
|
55
|
+
if (card === null) return null;
|
|
56
|
+
if (seen.has(card.name)) continue;
|
|
57
|
+
seen.add(card.name);
|
|
58
|
+
cards.push(card);
|
|
59
|
+
}
|
|
60
|
+
return cards;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Fetch TypeSafe's account-visible judge models.
|
|
65
|
+
*
|
|
66
|
+
* Returns `null` for transport, HTTP, or envelope failures. A valid empty
|
|
67
|
+
* roster returns `[]`, allowing the model manager to distinguish it from an
|
|
68
|
+
* unavailable discovery endpoint.
|
|
69
|
+
*/
|
|
70
|
+
export async function fetchTypeSafeModels(
|
|
71
|
+
options: FetchTypeSafeModelsOptions,
|
|
72
|
+
): Promise<ModelSpec<"typesafe">[] | null> {
|
|
73
|
+
const baseUrl = normalizeBaseUrl(options.baseUrl ?? TYPESAFE_DEFAULT_BASE_URL);
|
|
74
|
+
if (baseUrl === null) return null;
|
|
75
|
+
|
|
76
|
+
let response: Response;
|
|
77
|
+
try {
|
|
78
|
+
response = await discoveryFetch(options.fetch)(`${baseUrl}/v1/models`, {
|
|
79
|
+
method: "GET",
|
|
80
|
+
headers: {
|
|
81
|
+
Accept: "application/json",
|
|
82
|
+
Authorization: `Bearer ${options.apiKey}`,
|
|
83
|
+
},
|
|
84
|
+
signal: options.signal,
|
|
85
|
+
});
|
|
86
|
+
} catch {
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
89
|
+
if (!response.ok) return null;
|
|
90
|
+
|
|
91
|
+
let payload: unknown;
|
|
92
|
+
try {
|
|
93
|
+
payload = await response.json();
|
|
94
|
+
} catch {
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
const cards = parseTypeSafeModelCards(payload);
|
|
98
|
+
if (cards === null) return null;
|
|
99
|
+
|
|
100
|
+
const models: ModelSpec<"typesafe">[] = [];
|
|
101
|
+
for (const card of cards) {
|
|
102
|
+
models.push({
|
|
103
|
+
id: card.name,
|
|
104
|
+
name: card.description || card.name,
|
|
105
|
+
api: "typesafe",
|
|
106
|
+
provider: "typesafe",
|
|
107
|
+
baseUrl,
|
|
108
|
+
kind: "judge",
|
|
109
|
+
reasoning: false,
|
|
110
|
+
input: ["text"],
|
|
111
|
+
supportsTools: false,
|
|
112
|
+
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
113
|
+
contextWindow: null,
|
|
114
|
+
maxTokens: null,
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
return models;
|
|
118
|
+
}
|
package/src/model-manager.ts
CHANGED
|
@@ -5,7 +5,15 @@ import { applyCatalogMetrics, CatalogMetricsIndex } from "./identity/metrics";
|
|
|
5
5
|
import { readModelCache, writeModelCache } from "./model-cache";
|
|
6
6
|
import { type GeneratedProvider, getBundledModels } from "./models";
|
|
7
7
|
import { isTimeBasedCost } from "./pricing";
|
|
8
|
-
import
|
|
8
|
+
import {
|
|
9
|
+
type Api,
|
|
10
|
+
type Model,
|
|
11
|
+
type ModelCost,
|
|
12
|
+
modelKind,
|
|
13
|
+
type ModelSpec,
|
|
14
|
+
type Provider,
|
|
15
|
+
type TokenCost,
|
|
16
|
+
} from "./types";
|
|
9
17
|
import { isRecord } from "./utils";
|
|
10
18
|
|
|
11
19
|
const DEFAULT_CACHE_TTL_MS = 2 * 60 * 60 * 1000;
|
|
@@ -292,7 +300,7 @@ export async function resolveProviderModels<TApi extends Api = Api, TModelsDevPa
|
|
|
292
300
|
? await Promise.all([fetchModelsDev(options), dynamicFetcher ? fetchDynamicModels(dynamicFetcher) : null])
|
|
293
301
|
: [null, null];
|
|
294
302
|
const modelsDevFetchSucceeded = fetchedModelsDevModels !== null;
|
|
295
|
-
const normalizedModelsDevModels = fetchedModelsDevModels ?? [];
|
|
303
|
+
const normalizedModelsDevModels = fetchedModelsDevModels?.models ?? [];
|
|
296
304
|
const modelsDevModels = additiveStaticModelIds
|
|
297
305
|
? normalizedModelsDevModels.filter(model => !additiveStaticModelIds.has(model.id))
|
|
298
306
|
: normalizedModelsDevModels;
|
|
@@ -319,7 +327,7 @@ export async function resolveProviderModels<TApi extends Api = Api, TModelsDevPa
|
|
|
319
327
|
const cacheModels = additiveStaticModelIds
|
|
320
328
|
? preparedCacheModels.filter(model => !additiveStaticModelIds.has(model.id))
|
|
321
329
|
: preparedCacheModels;
|
|
322
|
-
const dynamicModels = fetchedDynamicModels ?? [];
|
|
330
|
+
const dynamicModels = fetchedDynamicModels?.models ?? [];
|
|
323
331
|
// A successful empty endpoint result stays authoritative for THIS cycle (so an
|
|
324
332
|
// intentional catalog emptying still prunes removed models downstream), but
|
|
325
333
|
// is NOT pinned into the cache as authoritative — that would suppress the
|
|
@@ -331,12 +339,20 @@ export async function resolveProviderModels<TApi extends Api = Api, TModelsDevPa
|
|
|
331
339
|
(dynamicModelsAuthoritative || !hasModelsDevFetcher || modelsDevFetchSucceeded)
|
|
332
340
|
: modelsDevFetchSucceeded;
|
|
333
341
|
const mergedWithCache = mergeDynamicModels(staticModels, cacheModels);
|
|
334
|
-
const mergedWithModelsDev = mergeDynamicModels(
|
|
342
|
+
const mergedWithModelsDev = mergeDynamicModels(
|
|
343
|
+
mergedWithCache,
|
|
344
|
+
modelsDevModels,
|
|
345
|
+
fetchedModelsDevModels?.explicitKindModels,
|
|
346
|
+
);
|
|
335
347
|
const catalogMetricsSource = modelsDevFetchSucceeded ? normalizedModelsDevModels : preparedCacheModels;
|
|
336
348
|
const mergedWithCatalogMetrics = additiveStaticModelIds
|
|
337
349
|
? mergeCatalogMetrics(mergedWithModelsDev, catalogMetricsSource)
|
|
338
350
|
: mergedWithModelsDev;
|
|
339
|
-
const mergedModels = mergeDynamicModels(
|
|
351
|
+
const mergedModels = mergeDynamicModels(
|
|
352
|
+
mergedWithCatalogMetrics,
|
|
353
|
+
dynamicModels,
|
|
354
|
+
fetchedDynamicModels?.explicitKindModels,
|
|
355
|
+
);
|
|
340
356
|
const models = collapseBuiltVariants(
|
|
341
357
|
authoritativeDynamicFetchSucceeded ? retainModelIds(mergedModels, dynamicModels) : mergedModels,
|
|
342
358
|
);
|
|
@@ -423,9 +439,15 @@ export async function resolveProviderModels<TApi extends Api = Api, TModelsDevPa
|
|
|
423
439
|
};
|
|
424
440
|
}
|
|
425
441
|
|
|
442
|
+
/** Materialized discovery rows plus kind provenance captured before policy can supply a KDL kind. */
|
|
443
|
+
interface DiscoveredModelSet<TApi extends Api> {
|
|
444
|
+
models: Model<TApi>[];
|
|
445
|
+
explicitKindModels: ReadonlySet<Model<TApi>>;
|
|
446
|
+
}
|
|
447
|
+
|
|
426
448
|
async function fetchModelsDev<TApi extends Api, TModelsDevPayload>(
|
|
427
449
|
options: ModelManagerOptions<TApi, TModelsDevPayload>,
|
|
428
|
-
): Promise<
|
|
450
|
+
): Promise<DiscoveredModelSet<TApi> | null> {
|
|
429
451
|
if (!options.modelsDev) {
|
|
430
452
|
return null;
|
|
431
453
|
}
|
|
@@ -440,7 +462,7 @@ async function fetchModelsDev<TApi extends Api, TModelsDevPayload>(
|
|
|
440
462
|
|
|
441
463
|
async function fetchDynamicModels<TApi extends Api>(
|
|
442
464
|
fetcher: () => Promise<readonly ModelSpec<TApi>[] | null>,
|
|
443
|
-
): Promise<
|
|
465
|
+
): Promise<DiscoveredModelSet<TApi> | null> {
|
|
444
466
|
try {
|
|
445
467
|
const models = await fetcher();
|
|
446
468
|
if (models === null) {
|
|
@@ -512,6 +534,7 @@ function mergeCatalogMetrics<TApi extends Api>(
|
|
|
512
534
|
function mergeDynamicModels<TApi extends Api>(
|
|
513
535
|
baseModels: readonly Model<TApi>[],
|
|
514
536
|
dynamicModels: readonly Model<TApi>[],
|
|
537
|
+
explicitKindModels?: ReadonlySet<Model<TApi>>,
|
|
515
538
|
): Model<TApi>[] {
|
|
516
539
|
// Empty-side fast paths: `mergeDynamicModels(base, [])` is the common shape
|
|
517
540
|
// after we've already merged the first pair, and `(...)` with no base
|
|
@@ -528,6 +551,9 @@ function mergeDynamicModels<TApi extends Api>(
|
|
|
528
551
|
merged.set(dynamicModel.id, dynamicModel);
|
|
529
552
|
continue;
|
|
530
553
|
}
|
|
554
|
+
// A policy-derived kind on a chat row is not permission to replace an
|
|
555
|
+
// authored runner. Only a kind present before materialization can do so.
|
|
556
|
+
if (modelKind(existingModel) !== "chat" && !explicitKindModels?.has(dynamicModel)) continue;
|
|
531
557
|
merged.set(dynamicModel.id, mergeDynamicModel(existingModel, dynamicModel));
|
|
532
558
|
}
|
|
533
559
|
return Array.from(merged.values());
|
|
@@ -537,12 +563,12 @@ function retainModelIds<TApi extends Api>(
|
|
|
537
563
|
models: readonly Model<TApi>[],
|
|
538
564
|
retainedModels: readonly Model<TApi>[],
|
|
539
565
|
): Model<TApi>[] {
|
|
540
|
-
if (
|
|
566
|
+
if (models.length === 0) return [];
|
|
541
567
|
const retainedIds = new Set(retainedModels.map(model => model.id));
|
|
542
|
-
return models.filter(model => retainedIds.has(model.id));
|
|
568
|
+
return models.filter(model => modelKind(model) !== "chat" || retainedIds.has(model.id));
|
|
543
569
|
}
|
|
544
570
|
|
|
545
|
-
const MODEL_CACHE_FINGERPRINT_VERSION = `merge-
|
|
571
|
+
const MODEL_CACHE_FINGERPRINT_VERSION = `merge-v5:${VERSION}`;
|
|
546
572
|
|
|
547
573
|
/**
|
|
548
574
|
* Return the versioned content identity for a static provider slice. Callers
|
|
@@ -663,17 +689,20 @@ function preferDiscoveryLimit(discoveryLimit: number | null, fallbackLimit: numb
|
|
|
663
689
|
return discoveryLimit;
|
|
664
690
|
}
|
|
665
691
|
|
|
666
|
-
function buildDiscoveredModelSpecs<TApi extends Api>(value: unknown):
|
|
692
|
+
function buildDiscoveredModelSpecs<TApi extends Api>(value: unknown): DiscoveredModelSet<TApi> {
|
|
693
|
+
const models: Model<TApi>[] = [];
|
|
694
|
+
const explicitKindModels = new Set<Model<TApi>>();
|
|
667
695
|
if (!Array.isArray(value)) {
|
|
668
|
-
return
|
|
696
|
+
return { models, explicitKindModels };
|
|
669
697
|
}
|
|
670
|
-
const models: Model<TApi>[] = [];
|
|
671
698
|
for (const item of value) {
|
|
672
699
|
if (isModelLike(item)) {
|
|
673
|
-
|
|
700
|
+
const model = buildModel(item as ModelSpec<TApi>);
|
|
701
|
+
models.push(model);
|
|
702
|
+
if (item.kind !== undefined) explicitKindModels.add(model);
|
|
674
703
|
}
|
|
675
704
|
}
|
|
676
|
-
return models;
|
|
705
|
+
return { models, explicitKindModels };
|
|
677
706
|
}
|
|
678
707
|
|
|
679
708
|
function isModelLike(value: unknown): value is ModelSpec<Api> {
|