@linxiraos/pi-catalog 1.1.5 → 1.1.6
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 +4 -0
- package/dist/types/identity/family.d.ts +15 -11
- package/dist/types/model-manager.d.ts +20 -4
- package/dist/types/provider-models/models-dev-policies.d.ts +8 -0
- package/dist/types/provider-models/openai-compat.d.ts +20 -4
- package/dist/types/wire/cloudflare-ai-gateway.d.ts +18 -0
- package/package.json +4 -4
- package/src/identity/family.ts +43 -24
- package/src/model-manager.ts +112 -71
- package/src/model-thinking.ts +8 -0
- package/src/models.json +53 -36
- package/src/provider-models/cache-provider-id.ts +4 -1
- package/src/provider-models/models-dev-policies.ts +38 -0
- package/src/provider-models/openai-compat.ts +186 -66
- package/src/variant-collapse.ts +1 -1
- package/src/wire/cloudflare-ai-gateway.ts +44 -0
package/CHANGELOG.md
CHANGED
|
@@ -145,24 +145,28 @@ export declare const supportsCodexReasoningSummary: (modelId: string) => boolean
|
|
|
145
145
|
export declare const isOpenAISamplingRestrictedModelId: (modelId: string) => boolean;
|
|
146
146
|
/**
|
|
147
147
|
* Reasoning-capable GLM coding SKUs: glm-4.5 and up on the base / `-air` /
|
|
148
|
-
* `-turbo` lines
|
|
149
|
-
* `-
|
|
150
|
-
* keeps newly-bumped integers (`glm-5.
|
|
151
|
-
* allowlist.
|
|
148
|
+
* `-turbo` lines, plus `-flash` from GLM-5.3-Flash on. Excludes the vision
|
|
149
|
+
* (`…v`) shape, the non-reasoning `-flashx`/`-preview` variants, and pre-4.5
|
|
150
|
+
* ids. Matching the family keeps newly-bumped integers (`glm-5.4`, `glm-6`, …)
|
|
151
|
+
* covered without a per-id allowlist.
|
|
152
152
|
*/
|
|
153
153
|
export declare const isReasoningGlmModelId: (modelId: string) => boolean;
|
|
154
154
|
/** GLM-5.2+ coding SKUs accept `reasoning_effort` in addition to binary thinking. */
|
|
155
155
|
export declare const isGlm52ReasoningEffortModelId: (modelId: string) => boolean;
|
|
156
156
|
/**
|
|
157
|
-
* GLM-5.3+
|
|
158
|
-
* host-specific),
|
|
159
|
-
* ladder on every host, and thinking can
|
|
160
|
-
* `thinking.type` must always be `enabled`. Matching
|
|
161
|
-
* bumps (`glm-5.4`, `glm-6`, …) covered while excluding
|
|
162
|
-
* shape and the non-reasoning `-
|
|
157
|
+
* GLM-5.3+ reasoning-effort SKUs. Unlike GLM-5.2 (whose dialect is
|
|
158
|
+
* host-specific), the base, `-air`, `-turbo`, and `-flash` variants expose a
|
|
159
|
+
* uniform wire-exact `low`/`high`/`max` ladder on every host, and thinking can
|
|
160
|
+
* no longer be disabled — `thinking.type` must always be `enabled`. Matching
|
|
161
|
+
* the family keeps future bumps (`glm-5.4`, `glm-6`, …) covered while excluding
|
|
162
|
+
* the vision (`…v`) shape and the non-reasoning `-flashx`/`-preview` variants.
|
|
163
163
|
*/
|
|
164
164
|
export declare const isGlm53ReasoningEffortModelId: (modelId: string) => boolean;
|
|
165
|
-
/**
|
|
165
|
+
/**
|
|
166
|
+
* GLM SKUs that accept image input: the `v` that attaches to the version
|
|
167
|
+
* (`glm-4v`, `glm-4.5v`) and the natively multimodal `-flash` line from
|
|
168
|
+
* GLM-5.3-Flash on, whose id carries no `v` marker.
|
|
169
|
+
*/
|
|
166
170
|
export declare const isGlmVisionModelId: (modelId: string) => boolean;
|
|
167
171
|
/**
|
|
168
172
|
* Coarse vendor-lineage token for "are two models the same family?" checks
|
|
@@ -11,6 +11,8 @@ export interface ModelsDevFallback<TApi extends Api = Api, TPayload = unknown> {
|
|
|
11
11
|
fetch(): Promise<TPayload>;
|
|
12
12
|
/** Maps payload into provider models. */
|
|
13
13
|
map(payload: TPayload, providerId: Provider): readonly ModelSpec<TApi>[];
|
|
14
|
+
/** When true, mapped rows can add model ids but cannot replace static metadata. */
|
|
15
|
+
additiveOnly?: boolean;
|
|
14
16
|
}
|
|
15
17
|
/**
|
|
16
18
|
* Configuration for provider model resolution.
|
|
@@ -45,18 +47,25 @@ export interface ModelManagerOptions<TApi extends Api = Api, TModelsDevPayload =
|
|
|
45
47
|
/** Clock override for deterministic tests. */
|
|
46
48
|
now?: () => number;
|
|
47
49
|
}
|
|
50
|
+
/**
|
|
51
|
+
* Catalog source that most recently refreshed the resolved provider snapshot.
|
|
52
|
+
*/
|
|
53
|
+
export type ModelResolutionSource = "bundled" | "cache" | "models.dev" | "provider";
|
|
48
54
|
/**
|
|
49
55
|
* Resolution result.
|
|
50
56
|
*
|
|
51
57
|
* `stale` is false when the resolved catalog is authoritative for the selected provider:
|
|
52
|
-
* - a
|
|
58
|
+
* - a provider endpoint fetch succeeded in this call (an empty catalog is still
|
|
53
59
|
* authoritative for the cycle, so downstream pruning of removed models runs),
|
|
60
|
+
* - a models.dev fetch succeeded for a provider without endpoint discovery,
|
|
54
61
|
* - a still-fresh authoritative cache was reused in `online-if-uncached` mode, or
|
|
55
|
-
* - the provider has no
|
|
62
|
+
* - the provider has no remote fetcher configured.
|
|
56
63
|
*/
|
|
57
64
|
export interface ModelResolutionResult<TApi extends Api = Api> {
|
|
58
65
|
models: Model<TApi>[];
|
|
59
66
|
stale: boolean;
|
|
67
|
+
source: ModelResolutionSource;
|
|
68
|
+
updatedAt?: number;
|
|
60
69
|
}
|
|
61
70
|
/**
|
|
62
71
|
* Stateful facade over provider model resolution.
|
|
@@ -70,8 +79,15 @@ export interface ModelManager<TApi extends Api = Api> {
|
|
|
70
79
|
export declare function createModelManager<TApi extends Api = Api, TModelsDevPayload = unknown>(options: ModelManagerOptions<TApi, TModelsDevPayload>): ModelManager<TApi>;
|
|
71
80
|
/**
|
|
72
81
|
* Resolves provider models with source precedence:
|
|
73
|
-
* static -> stencil.so ->
|
|
82
|
+
* static -> cached fallback -> stencil.so -> dynamic.
|
|
74
83
|
*
|
|
75
|
-
* Later sources override earlier ones by model id.
|
|
84
|
+
* Later sources override earlier ones by model id. Cached rows participate only
|
|
85
|
+
* when at least one configured remote source did not refresh successfully.
|
|
76
86
|
*/
|
|
77
87
|
export declare function resolveProviderModels<TApi extends Api = Api, TModelsDevPayload = unknown>(options: ModelManagerOptions<TApi, TModelsDevPayload>, strategy?: ModelRefreshStrategy): Promise<ModelResolutionResult<TApi>>;
|
|
88
|
+
/**
|
|
89
|
+
* Return the versioned, low-collision model-cache identity for a static provider
|
|
90
|
+
* slice. Results are cached by array reference so repeat cold-start paths skip
|
|
91
|
+
* the JSON serialization and hash.
|
|
92
|
+
*/
|
|
93
|
+
export declare function fingerprintStaticModels<TApi extends Api>(models: readonly (ModelSpec<TApi> | Model<TApi>)[], dynamicModelsAuthoritative?: boolean): string;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { Api, ModelSpec } from "../types.js";
|
|
2
|
+
/**
|
|
3
|
+
* Remove models.dev rows that OMP cannot route successfully.
|
|
4
|
+
*
|
|
5
|
+
* Generation and runtime refresh share this policy so a live catalog cannot
|
|
6
|
+
* reintroduce selectors deliberately excluded from the bundled catalog.
|
|
7
|
+
*/
|
|
8
|
+
export declare function filterModelsDevCatalogRows<TApi extends Api>(models: readonly ModelSpec<TApi>[]): ModelSpec<TApi>[];
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ModelManagerOptions } from "../model-manager.js";
|
|
1
|
+
import type { ModelManagerOptions, ModelsDevFallback } from "../model-manager.js";
|
|
2
2
|
import { getBundledModels } from "../models.js";
|
|
3
3
|
import type { Api, FetchImpl, ModelSpec, Provider } from "../types.js";
|
|
4
4
|
import { ALIBABA_TOKEN_PLAN_BASE_URL } from "../wire/alibaba-token-plan.js";
|
|
@@ -32,9 +32,12 @@ export interface ModelsDevModel {
|
|
|
32
32
|
* The frame magic is sniffed rather than trusting content-type so plain-JSON
|
|
33
33
|
* responses (test stubs, fallback mirrors) parse identically.
|
|
34
34
|
*
|
|
35
|
-
* Fetched fully once per
|
|
36
|
-
* request, repeat callers send a
|
|
37
|
-
*
|
|
35
|
+
* Fetched fully once per fetch context: concurrent callers sharing a fetch
|
|
36
|
+
* implementation reuse one transport request, while repeat callers send a
|
|
37
|
+
* conditional GET that the server answers with `304`. Each subscriber may stop
|
|
38
|
+
* waiting independently; the shared transport retains its own hard deadline.
|
|
39
|
+
* Transient failures reuse the last in-memory payload for callers that only
|
|
40
|
+
* need best-effort metadata.
|
|
38
41
|
*/
|
|
39
42
|
export declare function fetchWellKnownModels(fetchImpl?: FetchImpl, signal?: AbortSignal): Promise<unknown>;
|
|
40
43
|
/**
|
|
@@ -685,3 +688,16 @@ export interface ModelsDevProviderDescriptor {
|
|
|
685
688
|
export declare function mapModelsDevToModels(data: Record<string, unknown>, descriptors: readonly ModelsDevProviderDescriptor[]): ModelSpec<Api>[];
|
|
686
689
|
/** All provider descriptors for models.dev data mapping in generate-models.ts. */
|
|
687
690
|
export declare const MODELS_DEV_PROVIDER_DESCRIPTORS: readonly ModelsDevProviderDescriptor[];
|
|
691
|
+
/** Providers whose bundled catalog can receive additive models.dev updates at runtime. */
|
|
692
|
+
export declare const MODELS_DEV_CATALOG_PROVIDER_IDS: readonly string[];
|
|
693
|
+
/**
|
|
694
|
+
* Build the shared models.dev fallback for one known provider.
|
|
695
|
+
*
|
|
696
|
+
* Provider managers sharing one fetch implementation reuse its conditional
|
|
697
|
+
* catalog session. Each mapped provider slice is persisted independently so
|
|
698
|
+
* startup can restore it without parsing the full catalog.
|
|
699
|
+
*
|
|
700
|
+
* `timeoutMs` bounds the catalog request. It is configurable for callers with a
|
|
701
|
+
* stricter startup budget and for deterministic timeout tests.
|
|
702
|
+
*/
|
|
703
|
+
export declare function modelsDevCatalogFallback(providerId: string, fetchImpl?: FetchImpl, timeoutMs?: number): ModelsDevFallback<Api> | undefined;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/** Provider-native AI Gateway prefix before the upstream route segment. */
|
|
2
|
+
export declare const CLOUDFLARE_AI_GATEWAY_BASE_URL = "https://gateway.ai.cloudflare.com/v1/<account>/<gateway>";
|
|
3
|
+
/** Anthropic Messages passthrough endpoint. */
|
|
4
|
+
export declare const CLOUDFLARE_AI_GATEWAY_ANTHROPIC_BASE_URL = "https://gateway.ai.cloudflare.com/v1/<account>/<gateway>/anthropic";
|
|
5
|
+
/** OpenAI Responses passthrough endpoint. */
|
|
6
|
+
export declare const CLOUDFLARE_AI_GATEWAY_OPENAI_BASE_URL = "https://gateway.ai.cloudflare.com/v1/<account>/<gateway>/openai";
|
|
7
|
+
/** OpenAI-compatible Workers AI endpoint. */
|
|
8
|
+
export declare const CLOUDFLARE_AI_GATEWAY_COMPAT_BASE_URL = "https://gateway.ai.cloudflare.com/v1/<account>/<gateway>/compat";
|
|
9
|
+
/** Opaque API-key credential payload persisted by Cloudflare AI Gateway login. */
|
|
10
|
+
export interface CloudflareAiGatewayCredential {
|
|
11
|
+
token: string;
|
|
12
|
+
accountId?: string;
|
|
13
|
+
gatewayId?: string;
|
|
14
|
+
}
|
|
15
|
+
/** Parse both structured login credentials and legacy plain gateway tokens. */
|
|
16
|
+
export declare function parseCloudflareAiGatewayCredential(value: string): CloudflareAiGatewayCredential | null;
|
|
17
|
+
/** Serialize the gateway token with the routing identifiers collected during login. */
|
|
18
|
+
export declare function serializeCloudflareAiGatewayCredential(token: string, accountId: string, gatewayId: string): string;
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@linxiraos/pi-catalog",
|
|
4
|
-
"version": "1.1.
|
|
4
|
+
"version": "1.1.6",
|
|
5
5
|
"description": "Model catalog for omp: bundled model database, provider discovery descriptors, model identity, classification, and equivalence",
|
|
6
6
|
"homepage": "https://linxira-os.github.io/zeta/",
|
|
7
7
|
"author": "Can Boluk",
|
|
@@ -34,11 +34,11 @@
|
|
|
34
34
|
"gen:proto": "bun scripts/generate-protocols.ts"
|
|
35
35
|
},
|
|
36
36
|
"dependencies": {
|
|
37
|
-
"@linxiraos/pi-omptype": "1.1.
|
|
38
|
-
"@linxiraos/pi-utils": "1.1.
|
|
37
|
+
"@linxiraos/pi-omptype": "1.1.6",
|
|
38
|
+
"@linxiraos/pi-utils": "1.1.6"
|
|
39
39
|
},
|
|
40
40
|
"devDependencies": {
|
|
41
|
-
"@linxiraos/pi-ai": "1.1.
|
|
41
|
+
"@linxiraos/pi-ai": "1.1.6",
|
|
42
42
|
"@types/bun": "^1.3.14"
|
|
43
43
|
},
|
|
44
44
|
"engines": {
|
package/src/identity/family.ts
CHANGED
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
|
|
10
10
|
import {
|
|
11
11
|
bareModelId,
|
|
12
|
+
type GlmModel,
|
|
12
13
|
isAnthropicAdaptiveGenAtLeast,
|
|
13
14
|
isFableOrMythos,
|
|
14
15
|
parseAnthropicModel,
|
|
@@ -283,19 +284,35 @@ export const isOpenAISamplingRestrictedModelId = memo((modelId: string): boolean
|
|
|
283
284
|
return isOpenAIWireGen5Plus(modelId) || O_SERIES_REASONING_RE.test(bare);
|
|
284
285
|
});
|
|
285
286
|
|
|
287
|
+
/**
|
|
288
|
+
* GLM SKU shapes that carry the reasoning ladders. `base`/`air`/`turbo` have
|
|
289
|
+
* always been reasoning lines; `flash` joins at GLM-5.3-Flash, the first flash
|
|
290
|
+
* SKU whose thinking is mandatory (pre-5.3 `-flash` SKUs are non-reasoning).
|
|
291
|
+
* `flashx` and `preview` stay out.
|
|
292
|
+
*/
|
|
293
|
+
function isGlmReasoningVariant(glm: GlmModel): boolean {
|
|
294
|
+
switch (glm.variant) {
|
|
295
|
+
case "base":
|
|
296
|
+
case "air":
|
|
297
|
+
case "turbo":
|
|
298
|
+
return true;
|
|
299
|
+
case "flash":
|
|
300
|
+
return semverGte(glm.version, "5.3");
|
|
301
|
+
default:
|
|
302
|
+
return false;
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
286
306
|
/**
|
|
287
307
|
* Reasoning-capable GLM coding SKUs: glm-4.5 and up on the base / `-air` /
|
|
288
|
-
* `-turbo` lines
|
|
289
|
-
* `-
|
|
290
|
-
* keeps newly-bumped integers (`glm-5.
|
|
291
|
-
* allowlist.
|
|
308
|
+
* `-turbo` lines, plus `-flash` from GLM-5.3-Flash on. Excludes the vision
|
|
309
|
+
* (`…v`) shape, the non-reasoning `-flashx`/`-preview` variants, and pre-4.5
|
|
310
|
+
* ids. Matching the family keeps newly-bumped integers (`glm-5.4`, `glm-6`, …)
|
|
311
|
+
* covered without a per-id allowlist.
|
|
292
312
|
*/
|
|
293
313
|
export const isReasoningGlmModelId = memo((modelId: string): boolean => {
|
|
294
314
|
const glm = parseGlmModel(bareModelId(modelId));
|
|
295
|
-
if (!glm || glm.vision) {
|
|
296
|
-
return false;
|
|
297
|
-
}
|
|
298
|
-
if (glm.variant !== "base" && glm.variant !== "air" && glm.variant !== "turbo") {
|
|
315
|
+
if (!glm || glm.vision || !isGlmReasoningVariant(glm)) {
|
|
299
316
|
return false;
|
|
300
317
|
}
|
|
301
318
|
return semverGte(glm.version, "4.5");
|
|
@@ -304,37 +321,39 @@ export const isReasoningGlmModelId = memo((modelId: string): boolean => {
|
|
|
304
321
|
/** GLM-5.2+ coding SKUs accept `reasoning_effort` in addition to binary thinking. */
|
|
305
322
|
export const isGlm52ReasoningEffortModelId = memo((modelId: string): boolean => {
|
|
306
323
|
const glm = parseGlmModel(bareModelId(modelId));
|
|
307
|
-
if (!glm || glm.vision) {
|
|
308
|
-
return false;
|
|
309
|
-
}
|
|
310
|
-
if (glm.variant !== "base" && glm.variant !== "air" && glm.variant !== "turbo") {
|
|
324
|
+
if (!glm || glm.vision || !isGlmReasoningVariant(glm)) {
|
|
311
325
|
return false;
|
|
312
326
|
}
|
|
313
327
|
return semverGte(glm.version, "5.2");
|
|
314
328
|
});
|
|
315
329
|
|
|
316
330
|
/**
|
|
317
|
-
* GLM-5.3+
|
|
318
|
-
* host-specific),
|
|
319
|
-
* ladder on every host, and thinking can
|
|
320
|
-
* `thinking.type` must always be `enabled`. Matching
|
|
321
|
-
* bumps (`glm-5.4`, `glm-6`, …) covered while excluding
|
|
322
|
-
* shape and the non-reasoning `-
|
|
331
|
+
* GLM-5.3+ reasoning-effort SKUs. Unlike GLM-5.2 (whose dialect is
|
|
332
|
+
* host-specific), the base, `-air`, `-turbo`, and `-flash` variants expose a
|
|
333
|
+
* uniform wire-exact `low`/`high`/`max` ladder on every host, and thinking can
|
|
334
|
+
* no longer be disabled — `thinking.type` must always be `enabled`. Matching
|
|
335
|
+
* the family keeps future bumps (`glm-5.4`, `glm-6`, …) covered while excluding
|
|
336
|
+
* the vision (`…v`) shape and the non-reasoning `-flashx`/`-preview` variants.
|
|
323
337
|
*/
|
|
324
338
|
export const isGlm53ReasoningEffortModelId = memo((modelId: string): boolean => {
|
|
325
339
|
const glm = parseGlmModel(bareModelId(modelId));
|
|
326
|
-
if (!glm || glm.vision) {
|
|
327
|
-
return false;
|
|
328
|
-
}
|
|
329
|
-
if (glm.variant !== "base" && glm.variant !== "air" && glm.variant !== "turbo") {
|
|
340
|
+
if (!glm || glm.vision || !isGlmReasoningVariant(glm)) {
|
|
330
341
|
return false;
|
|
331
342
|
}
|
|
332
343
|
return semverGte(glm.version, "5.3");
|
|
333
344
|
});
|
|
334
345
|
|
|
335
|
-
/**
|
|
346
|
+
/**
|
|
347
|
+
* GLM SKUs that accept image input: the `v` that attaches to the version
|
|
348
|
+
* (`glm-4v`, `glm-4.5v`) and the natively multimodal `-flash` line from
|
|
349
|
+
* GLM-5.3-Flash on, whose id carries no `v` marker.
|
|
350
|
+
*/
|
|
336
351
|
export const isGlmVisionModelId = memo((modelId: string): boolean => {
|
|
337
|
-
|
|
352
|
+
const glm = parseGlmModel(bareModelId(modelId));
|
|
353
|
+
if (!glm) {
|
|
354
|
+
return false;
|
|
355
|
+
}
|
|
356
|
+
return glm.vision || (glm.variant === "flash" && semverGte(glm.version, "5.3"));
|
|
338
357
|
});
|
|
339
358
|
|
|
340
359
|
/**
|
package/src/model-manager.ts
CHANGED
|
@@ -21,6 +21,8 @@ export interface ModelsDevFallback<TApi extends Api = Api, TPayload = unknown> {
|
|
|
21
21
|
fetch(): Promise<TPayload>;
|
|
22
22
|
/** Maps payload into provider models. */
|
|
23
23
|
map(payload: TPayload, providerId: Provider): readonly ModelSpec<TApi>[];
|
|
24
|
+
/** When true, mapped rows can add model ids but cannot replace static metadata. */
|
|
25
|
+
additiveOnly?: boolean;
|
|
24
26
|
}
|
|
25
27
|
|
|
26
28
|
/**
|
|
@@ -57,18 +59,26 @@ export interface ModelManagerOptions<TApi extends Api = Api, TModelsDevPayload =
|
|
|
57
59
|
now?: () => number;
|
|
58
60
|
}
|
|
59
61
|
|
|
62
|
+
/**
|
|
63
|
+
* Catalog source that most recently refreshed the resolved provider snapshot.
|
|
64
|
+
*/
|
|
65
|
+
export type ModelResolutionSource = "bundled" | "cache" | "models.dev" | "provider";
|
|
66
|
+
|
|
60
67
|
/**
|
|
61
68
|
* Resolution result.
|
|
62
69
|
*
|
|
63
70
|
* `stale` is false when the resolved catalog is authoritative for the selected provider:
|
|
64
|
-
* - a
|
|
71
|
+
* - a provider endpoint fetch succeeded in this call (an empty catalog is still
|
|
65
72
|
* authoritative for the cycle, so downstream pruning of removed models runs),
|
|
73
|
+
* - a models.dev fetch succeeded for a provider without endpoint discovery,
|
|
66
74
|
* - a still-fresh authoritative cache was reused in `online-if-uncached` mode, or
|
|
67
|
-
* - the provider has no
|
|
75
|
+
* - the provider has no remote fetcher configured.
|
|
68
76
|
*/
|
|
69
77
|
export interface ModelResolutionResult<TApi extends Api = Api> {
|
|
70
78
|
models: Model<TApi>[];
|
|
71
79
|
stale: boolean;
|
|
80
|
+
source: ModelResolutionSource;
|
|
81
|
+
updatedAt?: number;
|
|
72
82
|
}
|
|
73
83
|
|
|
74
84
|
/**
|
|
@@ -171,9 +181,10 @@ function restoreCachedModelHeaders<TApi extends Api>(
|
|
|
171
181
|
|
|
172
182
|
/**
|
|
173
183
|
* Resolves provider models with source precedence:
|
|
174
|
-
* static -> stencil.so ->
|
|
184
|
+
* static -> cached fallback -> stencil.so -> dynamic.
|
|
175
185
|
*
|
|
176
|
-
* Later sources override earlier ones by model id.
|
|
186
|
+
* Later sources override earlier ones by model id. Cached rows participate only
|
|
187
|
+
* when at least one configured remote source did not refresh successfully.
|
|
177
188
|
*/
|
|
178
189
|
export async function resolveProviderModels<TApi extends Api = Api, TModelsDevPayload = unknown>(
|
|
179
190
|
options: ModelManagerOptions<TApi, TModelsDevPayload>,
|
|
@@ -187,6 +198,10 @@ export async function resolveProviderModels<TApi extends Api = Api, TModelsDevPa
|
|
|
187
198
|
const staticModels = options.staticModels
|
|
188
199
|
? passModelList<TApi>(options.staticModels)
|
|
189
200
|
: (getBundledModels(options.providerId as GeneratedProvider) as Model<TApi>[]);
|
|
201
|
+
const additiveStaticModelIds =
|
|
202
|
+
options.modelsDev?.additiveOnly && staticModels.length > 0
|
|
203
|
+
? new Set(staticModels.map(model => model.id))
|
|
204
|
+
: undefined;
|
|
190
205
|
const cache = readModelCache<TApi>(cacheProviderId, ttlMs, now, dbPath);
|
|
191
206
|
const restoredCache = restoreCachedModelHeaders(
|
|
192
207
|
cache?.models ?? [],
|
|
@@ -200,7 +215,7 @@ export async function resolveProviderModels<TApi extends Api = Api, TModelsDevPa
|
|
|
200
215
|
const cacheHasUnresolvedHeaders = restoredCache.unresolvedModelIds.size > 0;
|
|
201
216
|
const dynamicModelsAuthoritative = options.dynamicModelsAuthoritative ?? false;
|
|
202
217
|
const cacheDropIds = options.dropCachedModelIdsOnStaticMismatch;
|
|
203
|
-
const staticCatalogFingerprint =
|
|
218
|
+
const staticCatalogFingerprint = fingerprintStaticModels(staticModels, dynamicModelsAuthoritative);
|
|
204
219
|
// Endpoint-migration policy is cache identity: adding an id must invalidate
|
|
205
220
|
// matching-static-catalog caches written by the prior resolver.
|
|
206
221
|
const staticFingerprint =
|
|
@@ -219,20 +234,18 @@ export async function resolveProviderModels<TApi extends Api = Api, TModelsDevPa
|
|
|
219
234
|
(!dynamicModelsAuthoritative || cacheFingerprintMatches);
|
|
220
235
|
const dynamicFetcher = options.fetchDynamicModels;
|
|
221
236
|
const hasDynamicFetcher = typeof dynamicFetcher === "function";
|
|
222
|
-
const
|
|
237
|
+
const hasModelsDevFetcher = options.modelsDev !== undefined;
|
|
238
|
+
const hasRemoteFetcher = hasDynamicFetcher || hasModelsDevFetcher;
|
|
239
|
+
const hasAuthoritativeCache = ((cache?.authoritative ?? false) && hasUsableFreshCache) || !hasRemoteFetcher;
|
|
223
240
|
const cacheAgeMs = cache ? now() - cache.updatedAt : Number.POSITIVE_INFINITY;
|
|
224
|
-
const shouldFetchFromNetwork =
|
|
225
|
-
strategy,
|
|
226
|
-
hasUsableFreshCache,
|
|
227
|
-
hasAuthoritativeCache,
|
|
228
|
-
cacheAgeMs,
|
|
229
|
-
);
|
|
241
|
+
const shouldFetchFromNetwork =
|
|
242
|
+
hasRemoteFetcher && shouldFetchRemoteSources(strategy, hasUsableFreshCache, hasAuthoritativeCache, cacheAgeMs);
|
|
230
243
|
|
|
231
244
|
// Cold-start fast path: when a fresh, authoritative cache exists, the network
|
|
232
245
|
// fetch is skipped, AND the static catalog slice is byte-identical to what
|
|
233
246
|
// was merged in last time, the cache row IS the authoritative merge result.
|
|
234
|
-
//
|
|
235
|
-
//
|
|
247
|
+
// Additive caches still need same-id rows stripped because an older binary
|
|
248
|
+
// may have written the snapshot before additive semantics were enabled.
|
|
236
249
|
if (
|
|
237
250
|
!shouldFetchFromNetwork &&
|
|
238
251
|
cache?.fresh &&
|
|
@@ -240,17 +253,40 @@ export async function resolveProviderModels<TApi extends Api = Api, TModelsDevPa
|
|
|
240
253
|
cacheFingerprintMatches &&
|
|
241
254
|
!cacheHasUnresolvedHeaders
|
|
242
255
|
) {
|
|
243
|
-
|
|
256
|
+
const cacheContribution = additiveStaticModelIds
|
|
257
|
+
? restoredCache.models.filter(model => !additiveStaticModelIds.has(model.id))
|
|
258
|
+
: restoredCache.models;
|
|
259
|
+
const cachedModels = additiveStaticModelIds
|
|
260
|
+
? mergeDynamicModels(staticModels, cacheContribution)
|
|
261
|
+
: restoredCache.models;
|
|
262
|
+
const source: ModelResolutionSource = cacheContribution.length > 0 ? "cache" : "bundled";
|
|
263
|
+
return {
|
|
264
|
+
models: collapseBuiltModelVariants(cachedModels),
|
|
265
|
+
stale: false,
|
|
266
|
+
source,
|
|
267
|
+
...(source === "cache" ? { updatedAt: cache.updatedAt } : {}),
|
|
268
|
+
};
|
|
244
269
|
}
|
|
245
270
|
|
|
246
271
|
const [fetchedModelsDevModels, fetchedDynamicModels] = shouldFetchFromNetwork
|
|
247
272
|
? await Promise.all([fetchModelsDev(options), dynamicFetcher ? fetchDynamicModels(dynamicFetcher) : null])
|
|
248
273
|
: [null, null];
|
|
249
|
-
const
|
|
274
|
+
const modelsDevFetchSucceeded = fetchedModelsDevModels !== null;
|
|
275
|
+
const normalizedModelsDevModels = normalizeModelList<TApi>(fetchedModelsDevModels ?? []);
|
|
276
|
+
const modelsDevModels = additiveStaticModelIds
|
|
277
|
+
? normalizedModelsDevModels.filter(model => !additiveStaticModelIds.has(model.id))
|
|
278
|
+
: normalizedModelsDevModels;
|
|
250
279
|
const shouldUseFreshCacheAsAuthoritative =
|
|
251
280
|
strategy === "online-if-uncached" && hasUsableFreshCache && hasAuthoritativeCache;
|
|
252
281
|
const dynamicFetchSucceeded = fetchedDynamicModels !== null;
|
|
253
|
-
const
|
|
282
|
+
const anyRemoteFetchSucceeded = modelsDevFetchSucceeded || dynamicFetchSucceeded;
|
|
283
|
+
const allConfiguredRemoteFetchesSucceeded =
|
|
284
|
+
hasRemoteFetcher &&
|
|
285
|
+
(!hasModelsDevFetcher || modelsDevFetchSucceeded) &&
|
|
286
|
+
(!hasDynamicFetcher || dynamicFetchSucceeded);
|
|
287
|
+
const authoritativeDynamicFetchSucceeded = dynamicModelsAuthoritative && dynamicFetchSucceeded;
|
|
288
|
+
const remoteResolutionComplete = authoritativeDynamicFetchSucceeded || allConfiguredRemoteFetchesSucceeded;
|
|
289
|
+
const preparedCacheModels = remoteResolutionComplete
|
|
254
290
|
? []
|
|
255
291
|
: prepareCacheModelsForStaticMismatch(
|
|
256
292
|
usableCachedModels,
|
|
@@ -258,37 +294,44 @@ export async function resolveProviderModels<TApi extends Api = Api, TModelsDevPa
|
|
|
258
294
|
cacheFingerprintMatches,
|
|
259
295
|
options.dropCachedModelIdsOnStaticMismatch,
|
|
260
296
|
);
|
|
297
|
+
// Additive shared-catalog rows may only introduce IDs. Apply that boundary
|
|
298
|
+
// to cache fallback too, including snapshots written by an older binary.
|
|
299
|
+
const cacheModels = additiveStaticModelIds
|
|
300
|
+
? preparedCacheModels.filter(model => !additiveStaticModelIds.has(model.id))
|
|
301
|
+
: preparedCacheModels;
|
|
261
302
|
const dynamicModels = fetchedDynamicModels ?? [];
|
|
262
|
-
// A successful empty result stays authoritative for THIS cycle (so an
|
|
303
|
+
// A successful empty endpoint result stays authoritative for THIS cycle (so an
|
|
263
304
|
// intentional catalog emptying still prunes removed models downstream), but
|
|
264
305
|
// is NOT pinned into the cache as authoritative — that would suppress the
|
|
265
|
-
// short retry that recovers a transient empty response (#6620).
|
|
266
|
-
//
|
|
267
|
-
const
|
|
268
|
-
|
|
269
|
-
|
|
306
|
+
// short retry that recovers a transient empty response (#6620). Shared
|
|
307
|
+
// models.dev snapshots may be empty for one provider and remain authoritative.
|
|
308
|
+
const cacheAuthoritative = hasDynamicFetcher
|
|
309
|
+
? dynamicFetchSucceeded &&
|
|
310
|
+
dynamicModels.length > 0 &&
|
|
311
|
+
(dynamicModelsAuthoritative || !hasModelsDevFetcher || modelsDevFetchSucceeded)
|
|
312
|
+
: modelsDevFetchSucceeded;
|
|
313
|
+
const mergedWithCache = mergeDynamicModels(staticModels, cacheModels);
|
|
314
|
+
const mergedWithModelsDev = mergeDynamicModels(mergedWithCache, modelsDevModels);
|
|
315
|
+
const mergedModels = mergeDynamicModels(mergedWithModelsDev, dynamicModels);
|
|
270
316
|
const models = collapseBuiltModelVariants(
|
|
271
|
-
|
|
317
|
+
authoritativeDynamicFetchSucceeded ? retainModelIds(mergedModels, dynamicModels) : mergedModels,
|
|
272
318
|
);
|
|
273
|
-
const
|
|
319
|
+
const resolutionAuthoritative = !hasRemoteFetcher || remoteResolutionComplete || shouldUseFreshCacheAsAuthoritative;
|
|
320
|
+
const remoteUpdatedAt = anyRemoteFetchSucceeded ? now() : undefined;
|
|
274
321
|
if (shouldFetchFromNetwork) {
|
|
275
|
-
if (
|
|
276
|
-
const mergedSnapshot = mergeDynamicModels(mergeModelSources(staticModels, modelsDevModels), dynamicModels);
|
|
277
|
-
const snapshotModels = dynamicModelsAuthoritative
|
|
278
|
-
? retainModelIds(mergedSnapshot, dynamicModels)
|
|
279
|
-
: mergedSnapshot;
|
|
322
|
+
if (anyRemoteFetchSucceeded) {
|
|
280
323
|
writeModelCache(
|
|
281
324
|
cacheProviderId,
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
325
|
+
remoteUpdatedAt!,
|
|
326
|
+
models,
|
|
327
|
+
cacheAuthoritative,
|
|
285
328
|
staticFingerprint,
|
|
286
329
|
dbPath,
|
|
287
330
|
staticModels,
|
|
288
331
|
restorableHeaderFallback,
|
|
289
332
|
);
|
|
290
333
|
} else {
|
|
291
|
-
//
|
|
334
|
+
// Remote fetch failed — update cache with a non-authoritative snapshot so
|
|
292
335
|
// stale state remains visible while retry backoff still applies.
|
|
293
336
|
const latestCache = readModelCache<TApi>(cacheProviderId, ttlMs, now, dbPath);
|
|
294
337
|
const latestRestoredCache = restoreCachedModelHeaders(
|
|
@@ -302,16 +345,17 @@ export async function resolveProviderModels<TApi extends Api = Api, TModelsDevPa
|
|
|
302
345
|
const latestUsableCacheModels = latestRestoredCache.models.filter(
|
|
303
346
|
model => !latestRestoredCache.unresolvedModelIds.has(model.id),
|
|
304
347
|
);
|
|
348
|
+
const preparedLatestCacheModels = prepareCacheModelsForStaticMismatch(
|
|
349
|
+
latestUsableCacheModels,
|
|
350
|
+
staticModels,
|
|
351
|
+
cacheFingerprintMatches,
|
|
352
|
+
options.dropCachedModelIdsOnStaticMismatch,
|
|
353
|
+
);
|
|
354
|
+
const latestCacheModels = additiveStaticModelIds
|
|
355
|
+
? preparedLatestCacheModels.filter(model => !additiveStaticModelIds.has(model.id))
|
|
356
|
+
: preparedLatestCacheModels;
|
|
305
357
|
const fallbackSnapshotModels = collapseBuiltModelVariants(
|
|
306
|
-
mergeDynamicModels(
|
|
307
|
-
mergeModelSources(staticModels, modelsDevModels),
|
|
308
|
-
prepareCacheModelsForStaticMismatch(
|
|
309
|
-
latestUsableCacheModels,
|
|
310
|
-
staticModels,
|
|
311
|
-
cacheFingerprintMatches,
|
|
312
|
-
options.dropCachedModelIdsOnStaticMismatch,
|
|
313
|
-
),
|
|
314
|
-
),
|
|
358
|
+
mergeDynamicModels(mergeDynamicModels(staticModels, latestCacheModels), modelsDevModels),
|
|
315
359
|
);
|
|
316
360
|
writeModelCache(
|
|
317
361
|
cacheProviderId,
|
|
@@ -325,9 +369,23 @@ export async function resolveProviderModels<TApi extends Api = Api, TModelsDevPa
|
|
|
325
369
|
);
|
|
326
370
|
}
|
|
327
371
|
}
|
|
372
|
+
const cacheContributed = cacheModels.length > 0;
|
|
373
|
+
const source: ModelResolutionSource = dynamicFetchSucceeded
|
|
374
|
+
? "provider"
|
|
375
|
+
: modelsDevFetchSucceeded
|
|
376
|
+
? "models.dev"
|
|
377
|
+
: cacheContributed
|
|
378
|
+
? "cache"
|
|
379
|
+
: "bundled";
|
|
328
380
|
return {
|
|
329
381
|
models,
|
|
330
|
-
stale: !
|
|
382
|
+
stale: !resolutionAuthoritative,
|
|
383
|
+
source,
|
|
384
|
+
...(remoteUpdatedAt !== undefined
|
|
385
|
+
? { updatedAt: remoteUpdatedAt }
|
|
386
|
+
: cacheContributed && cache
|
|
387
|
+
? { updatedAt: cache.updatedAt }
|
|
388
|
+
: {}),
|
|
331
389
|
};
|
|
332
390
|
}
|
|
333
391
|
|
|
@@ -409,23 +467,6 @@ function prepareCacheModelsForStaticMismatch<TApi extends Api>(
|
|
|
409
467
|
return sanitizedModels;
|
|
410
468
|
}
|
|
411
469
|
|
|
412
|
-
function mergeModelSources<TApi extends Api>(...sources: readonly (readonly Model<TApi>[])[]): Model<TApi>[] {
|
|
413
|
-
// Strip out empty/missing sources up front. The hot path is `(static, [])`
|
|
414
|
-
// (modelsDev disabled / failed) — a single non-empty source means we can
|
|
415
|
-
// skip the Map churn entirely and just hand back the array.
|
|
416
|
-
const nonEmpty = sources.filter(source => source.length > 0);
|
|
417
|
-
if (nonEmpty.length === 0) return [];
|
|
418
|
-
if (nonEmpty.length === 1) return [...nonEmpty[0]];
|
|
419
|
-
const merged = new Map<string, Model<TApi>>();
|
|
420
|
-
for (const source of nonEmpty) {
|
|
421
|
-
for (const model of source) {
|
|
422
|
-
if (!model?.id) continue;
|
|
423
|
-
merged.set(model.id, model);
|
|
424
|
-
}
|
|
425
|
-
}
|
|
426
|
-
return Array.from(merged.values());
|
|
427
|
-
}
|
|
428
|
-
|
|
429
470
|
function mergeDynamicModels<TApi extends Api>(
|
|
430
471
|
baseModels: readonly Model<TApi>[],
|
|
431
472
|
dynamicModels: readonly Model<TApi>[],
|
|
@@ -459,22 +500,22 @@ function retainModelIds<TApi extends Api>(
|
|
|
459
500
|
return models.filter(model => retainedIds.has(model.id));
|
|
460
501
|
}
|
|
461
502
|
|
|
462
|
-
/**
|
|
463
|
-
* Stable, low-collision fingerprint of a static catalog slice. Cached by
|
|
464
|
-
* reference so repeat calls in the same process (e.g. multiple cold-start
|
|
465
|
-
* arms calling `resolveProviderModels` with the same `staticModels` array)
|
|
466
|
-
* skip the JSON+hash work after the first call.
|
|
467
|
-
*/
|
|
468
503
|
const MODEL_CACHE_FINGERPRINT_VERSION = "merge-v3";
|
|
469
504
|
const kStaticFingerprint = Symbol("model-manager.staticFingerprint");
|
|
470
|
-
type ModelArrayWithFingerprint = readonly Model<Api>[] & { [kStaticFingerprint]?: string };
|
|
471
|
-
|
|
472
|
-
|
|
505
|
+
type ModelArrayWithFingerprint = readonly (ModelSpec<Api> | Model<Api>)[] & { [kStaticFingerprint]?: string };
|
|
506
|
+
|
|
507
|
+
/**
|
|
508
|
+
* Return the versioned, low-collision model-cache identity for a static provider
|
|
509
|
+
* slice. Results are cached by array reference so repeat cold-start paths skip
|
|
510
|
+
* the JSON serialization and hash.
|
|
511
|
+
*/
|
|
512
|
+
export function fingerprintStaticModels<TApi extends Api>(
|
|
513
|
+
models: readonly (ModelSpec<TApi> | Model<TApi>)[],
|
|
473
514
|
dynamicModelsAuthoritative = false,
|
|
474
515
|
): string {
|
|
475
516
|
if (models.length === 0) return `${MODEL_CACHE_FINGERPRINT_VERSION}:empty`;
|
|
476
517
|
if (dynamicModelsAuthoritative)
|
|
477
|
-
return `${MODEL_CACHE_FINGERPRINT_VERSION}:authoritative:${
|
|
518
|
+
return `${MODEL_CACHE_FINGERPRINT_VERSION}:authoritative:${fingerprintStaticModels(models)}`;
|
|
478
519
|
const tagged = models as ModelArrayWithFingerprint;
|
|
479
520
|
const cached = tagged[kStaticFingerprint];
|
|
480
521
|
if (cached !== undefined) return cached;
|
package/src/model-thinking.ts
CHANGED
|
@@ -777,6 +777,14 @@ function inferThinkingControlMode<TApi extends Api>(
|
|
|
777
777
|
return "anthropic-budget-effort";
|
|
778
778
|
}
|
|
779
779
|
}
|
|
780
|
+
// Bedrock serves the GPT-5.x models through OpenAI's own request
|
|
781
|
+
// schema, which rejects Anthropic's budget block outright:
|
|
782
|
+
// `unknown_parameter: 'thinking'`. It takes `reasoning.effort`
|
|
783
|
+
// instead. gpt-oss parses as `unknown` (no `gpt-<digits>`), so it
|
|
784
|
+
// keeps the budget path it ships with today.
|
|
785
|
+
if (parsedModel.family === "openai") {
|
|
786
|
+
return "effort";
|
|
787
|
+
}
|
|
780
788
|
return "budget";
|
|
781
789
|
|
|
782
790
|
default:
|
package/src/models.json
CHANGED
|
@@ -28688,7 +28688,7 @@
|
|
|
28688
28688
|
"contextWindow": 1050000,
|
|
28689
28689
|
"maxTokens": 128000,
|
|
28690
28690
|
"thinking": {
|
|
28691
|
-
"mode": "
|
|
28691
|
+
"mode": "effort",
|
|
28692
28692
|
"efforts": [
|
|
28693
28693
|
"low",
|
|
28694
28694
|
"medium",
|
|
@@ -28727,7 +28727,7 @@
|
|
|
28727
28727
|
"contextWindow": 1050000,
|
|
28728
28728
|
"maxTokens": 128000,
|
|
28729
28729
|
"thinking": {
|
|
28730
|
-
"mode": "
|
|
28730
|
+
"mode": "effort",
|
|
28731
28731
|
"efforts": [
|
|
28732
28732
|
"low",
|
|
28733
28733
|
"medium",
|
|
@@ -28766,7 +28766,7 @@
|
|
|
28766
28766
|
"contextWindow": 1050000,
|
|
28767
28767
|
"maxTokens": 128000,
|
|
28768
28768
|
"thinking": {
|
|
28769
|
-
"mode": "
|
|
28769
|
+
"mode": "effort",
|
|
28770
28770
|
"efforts": [
|
|
28771
28771
|
"low",
|
|
28772
28772
|
"medium",
|
|
@@ -64887,38 +64887,6 @@
|
|
|
64887
64887
|
"requiresGlyphTokenization": false,
|
|
64888
64888
|
"supportsComputerUse": false
|
|
64889
64889
|
},
|
|
64890
|
-
"gemini-3.7-flash-tiered": {
|
|
64891
|
-
"id": "gemini-3.7-flash-tiered",
|
|
64892
|
-
"name": "gemini-3.7-flash-tiered",
|
|
64893
|
-
"api": "google-gemini-cli",
|
|
64894
|
-
"provider": "google-antigravity",
|
|
64895
|
-
"baseUrl": "https://daily-cloudcode-pa.googleapis.com",
|
|
64896
|
-
"reasoning": true,
|
|
64897
|
-
"input": [
|
|
64898
|
-
"text",
|
|
64899
|
-
"image"
|
|
64900
|
-
],
|
|
64901
|
-
"cost": {
|
|
64902
|
-
"input": 0,
|
|
64903
|
-
"output": 0,
|
|
64904
|
-
"cacheRead": 0,
|
|
64905
|
-
"cacheWrite": 0
|
|
64906
|
-
},
|
|
64907
|
-
"contextWindow": 1048576,
|
|
64908
|
-
"maxTokens": 65536,
|
|
64909
|
-
"thinking": {
|
|
64910
|
-
"mode": "google-level",
|
|
64911
|
-
"efforts": [
|
|
64912
|
-
"minimal",
|
|
64913
|
-
"low",
|
|
64914
|
-
"medium",
|
|
64915
|
-
"high"
|
|
64916
|
-
],
|
|
64917
|
-
"requiresEffort": true
|
|
64918
|
-
},
|
|
64919
|
-
"requiresGlyphTokenization": false,
|
|
64920
|
-
"supportsComputerUse": false
|
|
64921
|
-
},
|
|
64922
64890
|
"gpt-oss-120b": {
|
|
64923
64891
|
"id": "gpt-oss-120b",
|
|
64924
64892
|
"name": "GPT OSS 120B",
|
|
@@ -294589,6 +294557,55 @@
|
|
|
294589
294557
|
},
|
|
294590
294558
|
"supportsComputerUseConfig": false
|
|
294591
294559
|
},
|
|
294560
|
+
"glm-5.3-flash": {
|
|
294561
|
+
"id": "glm-5.3-flash",
|
|
294562
|
+
"name": "GLM-5.3-Flash",
|
|
294563
|
+
"api": "anthropic-messages",
|
|
294564
|
+
"provider": "zai",
|
|
294565
|
+
"baseUrl": "https://api.z.ai/api/anthropic",
|
|
294566
|
+
"reasoning": true,
|
|
294567
|
+
"input": [
|
|
294568
|
+
"text",
|
|
294569
|
+
"image"
|
|
294570
|
+
],
|
|
294571
|
+
"cost": {
|
|
294572
|
+
"input": 0.15,
|
|
294573
|
+
"output": 0.5,
|
|
294574
|
+
"cacheRead": 0.03,
|
|
294575
|
+
"cacheWrite": 0
|
|
294576
|
+
},
|
|
294577
|
+
"contextWindow": 1000000,
|
|
294578
|
+
"maxTokens": 131072,
|
|
294579
|
+
"thinking": {
|
|
294580
|
+
"mode": "anthropic-budget-effort",
|
|
294581
|
+
"efforts": [
|
|
294582
|
+
"low",
|
|
294583
|
+
"high",
|
|
294584
|
+
"max"
|
|
294585
|
+
],
|
|
294586
|
+
"defaultLevel": "max",
|
|
294587
|
+
"requiresEffort": true
|
|
294588
|
+
},
|
|
294589
|
+
"requiresGlyphTokenization": false,
|
|
294590
|
+
"tokenizer": "glm5",
|
|
294591
|
+
"supportsComputerUse": false,
|
|
294592
|
+
"compat": {
|
|
294593
|
+
"officialEndpoint": false,
|
|
294594
|
+
"signingEndpoint": false,
|
|
294595
|
+
"disableStrictTools": false,
|
|
294596
|
+
"disableAdaptiveThinking": false,
|
|
294597
|
+
"allowAnthropicHeaderOverrides": false,
|
|
294598
|
+
"supportsEagerToolInputStreaming": false,
|
|
294599
|
+
"supportsLongCacheRetention": false,
|
|
294600
|
+
"supportsMidConversationSystem": false,
|
|
294601
|
+
"supportsForcedToolChoice": true,
|
|
294602
|
+
"supportsSamplingParams": true,
|
|
294603
|
+
"requiresToolResultId": true,
|
|
294604
|
+
"requiresThinkingEnabled": false,
|
|
294605
|
+
"replayUnsignedThinking": true,
|
|
294606
|
+
"escapeBuiltinToolNames": false
|
|
294607
|
+
}
|
|
294608
|
+
},
|
|
294592
294609
|
"glm-5v-turbo": {
|
|
294593
294610
|
"id": "glm-5v-turbo",
|
|
294594
294611
|
"name": "GLM-5V-Turbo",
|
|
@@ -312261,4 +312278,4 @@
|
|
|
312261
312278
|
}
|
|
312262
312279
|
}
|
|
312263
312280
|
}
|
|
312264
|
-
}
|
|
312281
|
+
}
|
|
@@ -60,7 +60,10 @@ export function resolveModelCacheProviderId(providerId: string, options: ModelCa
|
|
|
60
60
|
return "cursor:default-effort-v4";
|
|
61
61
|
case "litellm": {
|
|
62
62
|
const baseUrl = options.baseUrl ?? getDefaultModelDiscoveryBaseUrl(providerId)!;
|
|
63
|
-
|
|
63
|
+
// rich-v8 invalidates rows whose `compatConfig` retained a colliding
|
|
64
|
+
// bundled model's provider-specific transport (e.g. Fireworks
|
|
65
|
+
// `wireModelIdMode`) before that leak was fixed (issue #9938).
|
|
66
|
+
return `litellm:rich-v8:${Bun.hash(baseUrl).toString(36)}`;
|
|
64
67
|
}
|
|
65
68
|
case "opencode-go":
|
|
66
69
|
case "opencode-zen": {
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { Api, ModelSpec } from "../types";
|
|
2
|
+
|
|
3
|
+
const BEDROCK_MANTLE_OPENAI_MODEL_IDS: Readonly<Record<string, true>> = {
|
|
4
|
+
"openai.gpt-5.4": true,
|
|
5
|
+
"openai.gpt-5.5": true,
|
|
6
|
+
"openai.gpt-5.6-luna": true,
|
|
7
|
+
"openai.gpt-5.6-sol": true,
|
|
8
|
+
"openai.gpt-5.6-terra": true,
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Remove models.dev rows that OMP cannot route successfully.
|
|
13
|
+
*
|
|
14
|
+
* Generation and runtime refresh share this policy so a live catalog cannot
|
|
15
|
+
* reintroduce selectors deliberately excluded from the bundled catalog.
|
|
16
|
+
*/
|
|
17
|
+
export function filterModelsDevCatalogRows<TApi extends Api>(models: readonly ModelSpec<TApi>[]): ModelSpec<TApi>[] {
|
|
18
|
+
return models.filter(model => {
|
|
19
|
+
if (model.provider === "amazon-bedrock") {
|
|
20
|
+
// AWS does not document the jp. Opus 5 profile, and the openai.gpt-5.x
|
|
21
|
+
// rows are Mantle-only ids that Bedrock rejects or misroutes.
|
|
22
|
+
return model.id !== "jp.anthropic.claude-opus-5" && !BEDROCK_MANTLE_OPENAI_MODEL_IDS[model.id];
|
|
23
|
+
}
|
|
24
|
+
if (model.provider === "zai") {
|
|
25
|
+
// [1m] is a Claude Code selector convention, not an inference id.
|
|
26
|
+
return !model.id.endsWith("[1m]");
|
|
27
|
+
}
|
|
28
|
+
if (model.provider === "fireworks" || model.provider === "firepass") {
|
|
29
|
+
// Control-plane resource ids are not accepted by the inference API.
|
|
30
|
+
return !model.id.startsWith("accounts/fireworks/");
|
|
31
|
+
}
|
|
32
|
+
if (model.provider === "xiaomi" || model.provider.startsWith("xiaomi-token-plan-")) {
|
|
33
|
+
// Text-chat transports cannot serve Xiaomi's audio-only models.
|
|
34
|
+
return !model.id.includes("-tts") && !model.id.includes("-asr");
|
|
35
|
+
}
|
|
36
|
+
return true;
|
|
37
|
+
});
|
|
38
|
+
}
|
|
@@ -21,7 +21,7 @@ import {
|
|
|
21
21
|
isReasoningGlmModelId,
|
|
22
22
|
} from "../identity/family";
|
|
23
23
|
import { resolveModelReference } from "../identity/reference";
|
|
24
|
-
import type { ModelManagerOptions } from "../model-manager";
|
|
24
|
+
import type { ModelManagerOptions, ModelsDevFallback } from "../model-manager";
|
|
25
25
|
import { type GeneratedProvider, getBundledModels } from "../models";
|
|
26
26
|
import { OPENAI_GPT_56_CYBER_STANDARD_COST, OPENAI_GPT_56_SOL_STANDARD_COST } from "../openai-pricing";
|
|
27
27
|
import type {
|
|
@@ -47,6 +47,7 @@ import {
|
|
|
47
47
|
import { createBundledReferenceMap, createReferenceResolver, toModelSpec } from "./bundled-references";
|
|
48
48
|
import { getDefaultModelDiscoveryBaseUrl, resolveModelCacheProviderId } from "./cache-provider-id";
|
|
49
49
|
import type { ModelManagerConfig } from "./descriptor-types";
|
|
50
|
+
import { filterModelsDevCatalogRows } from "./models-dev-policies";
|
|
50
51
|
|
|
51
52
|
const MODELS_DEV_URL = "https://catalog.stencil.so/models.json.zstd";
|
|
52
53
|
|
|
@@ -114,17 +115,40 @@ function toInputCapabilities(value: unknown): ("text" | "image")[] {
|
|
|
114
115
|
}
|
|
115
116
|
|
|
116
117
|
/**
|
|
117
|
-
*
|
|
118
|
-
*
|
|
119
|
-
*
|
|
120
|
-
*
|
|
118
|
+
* Catalog sessions are scoped to the fetch implementation that owns their
|
|
119
|
+
* network and authentication context. Callers sharing one fetch reuse the same
|
|
120
|
+
* conditional request state; isolated registries cannot observe each other's
|
|
121
|
+
* payloads, ETags, or in-flight requests.
|
|
121
122
|
*/
|
|
122
|
-
|
|
123
|
+
interface CatalogSession {
|
|
123
124
|
inflight: Promise<unknown> | null;
|
|
124
125
|
payload: unknown;
|
|
125
126
|
etag: string | null;
|
|
126
127
|
hasPayload: boolean;
|
|
127
|
-
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const defaultCatalogSession: CatalogSession = { inflight: null, payload: undefined, etag: null, hasPayload: false };
|
|
131
|
+
const catalogSessionsByFetch = new WeakMap<FetchImpl, CatalogSession>();
|
|
132
|
+
|
|
133
|
+
function getCatalogSession(fetchImpl: FetchImpl | undefined): CatalogSession {
|
|
134
|
+
if (!fetchImpl) return defaultCatalogSession;
|
|
135
|
+
const existing = catalogSessionsByFetch.get(fetchImpl);
|
|
136
|
+
if (existing) return existing;
|
|
137
|
+
const created: CatalogSession = { inflight: null, payload: undefined, etag: null, hasPayload: false };
|
|
138
|
+
catalogSessionsByFetch.set(fetchImpl, created);
|
|
139
|
+
return created;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function waitForCatalogRequest<T>(request: Promise<T>, signal?: AbortSignal): Promise<T> {
|
|
143
|
+
if (!signal) return request;
|
|
144
|
+
if (signal.aborted) return Promise.reject(signal.reason);
|
|
145
|
+
const aborted = Promise.withResolvers<never>();
|
|
146
|
+
const rejectAborted = () => aborted.reject(signal.reason);
|
|
147
|
+
signal.addEventListener("abort", rejectAborted, { once: true });
|
|
148
|
+
return Promise.race([request, aborted.promise]).finally(() => {
|
|
149
|
+
signal.removeEventListener("abort", rejectAborted);
|
|
150
|
+
});
|
|
151
|
+
}
|
|
128
152
|
|
|
129
153
|
const CATALOG_USER_AGENT = USER_AGENT;
|
|
130
154
|
|
|
@@ -134,52 +158,66 @@ const CATALOG_USER_AGENT = USER_AGENT;
|
|
|
134
158
|
* The frame magic is sniffed rather than trusting content-type so plain-JSON
|
|
135
159
|
* responses (test stubs, fallback mirrors) parse identically.
|
|
136
160
|
*
|
|
137
|
-
* Fetched fully once per
|
|
138
|
-
* request, repeat callers send a
|
|
139
|
-
*
|
|
161
|
+
* Fetched fully once per fetch context: concurrent callers sharing a fetch
|
|
162
|
+
* implementation reuse one transport request, while repeat callers send a
|
|
163
|
+
* conditional GET that the server answers with `304`. Each subscriber may stop
|
|
164
|
+
* waiting independently; the shared transport retains its own hard deadline.
|
|
165
|
+
* Transient failures reuse the last in-memory payload for callers that only
|
|
166
|
+
* need best-effort metadata.
|
|
140
167
|
*/
|
|
141
168
|
export function fetchWellKnownModels(fetchImpl?: FetchImpl, signal?: AbortSignal): Promise<unknown> {
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
169
|
+
const session = getCatalogSession(fetchImpl);
|
|
170
|
+
return fetchRevalidatedWellKnownModels(fetchImpl, signal).catch(error => {
|
|
171
|
+
if (session.hasPayload) return session.payload;
|
|
172
|
+
throw error;
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function fetchRevalidatedWellKnownModels(fetchImpl?: FetchImpl, signal?: AbortSignal): Promise<unknown> {
|
|
177
|
+
const session = getCatalogSession(fetchImpl);
|
|
178
|
+
if (!session.inflight) {
|
|
179
|
+
session.inflight = withCatalogDiscoveryTimeout(DEFAULT_OPENAI_COMPATIBLE_DISCOVERY_TIMEOUT_MS, transportSignal =>
|
|
180
|
+
fetchCatalogPayload(fetchImpl ?? discoveryFetch(), session, transportSignal),
|
|
181
|
+
).finally(() => {
|
|
182
|
+
session.inflight = null;
|
|
145
183
|
});
|
|
146
184
|
}
|
|
147
|
-
return
|
|
185
|
+
return waitForCatalogRequest(session.inflight, signal);
|
|
148
186
|
}
|
|
149
187
|
|
|
150
|
-
|
|
188
|
+
function fetchRevalidatedWellKnownModelsWithTimeout(
|
|
189
|
+
fetchImpl?: FetchImpl,
|
|
190
|
+
timeoutMs = DEFAULT_OPENAI_COMPATIBLE_DISCOVERY_TIMEOUT_MS,
|
|
191
|
+
): Promise<unknown> {
|
|
192
|
+
return withCatalogDiscoveryTimeout(timeoutMs, signal => fetchRevalidatedWellKnownModels(fetchImpl, signal));
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
async function fetchCatalogPayload(
|
|
196
|
+
fetchImpl: FetchImpl,
|
|
197
|
+
session: CatalogSession,
|
|
198
|
+
signal?: AbortSignal,
|
|
199
|
+
): Promise<unknown> {
|
|
151
200
|
const headers: Record<string, string> = {
|
|
152
201
|
Accept: "application/zstd, application/json",
|
|
153
202
|
"User-Agent": CATALOG_USER_AGENT,
|
|
154
203
|
};
|
|
155
|
-
if (
|
|
156
|
-
headers["If-None-Match"] =
|
|
157
|
-
}
|
|
158
|
-
let response: Response;
|
|
159
|
-
try {
|
|
160
|
-
response = await fetchImpl(MODELS_DEV_URL, { method: "GET", headers, signal });
|
|
161
|
-
} catch (error) {
|
|
162
|
-
if (catalogSession.hasPayload) {
|
|
163
|
-
return catalogSession.payload;
|
|
164
|
-
}
|
|
165
|
-
throw error;
|
|
204
|
+
if (session.hasPayload && session.etag) {
|
|
205
|
+
headers["If-None-Match"] = session.etag;
|
|
166
206
|
}
|
|
167
|
-
|
|
168
|
-
|
|
207
|
+
const response = await fetchImpl(MODELS_DEV_URL, { method: "GET", headers, signal });
|
|
208
|
+
if (response.status === 304 && session.hasPayload) {
|
|
209
|
+
return session.payload;
|
|
169
210
|
}
|
|
170
211
|
if (!response.ok) {
|
|
171
|
-
if (catalogSession.hasPayload) {
|
|
172
|
-
return catalogSession.payload;
|
|
173
|
-
}
|
|
174
212
|
throw new Error(`models catalog fetch failed: ${response.status}`);
|
|
175
213
|
}
|
|
176
214
|
const bytes = new Uint8Array(await response.arrayBuffer());
|
|
177
215
|
const isZstd = bytes.length >= 4 && new DataView(bytes.buffer, bytes.byteOffset).getUint32(0, true) === ZSTD_MAGIC;
|
|
178
216
|
const text = new TextDecoder().decode(isZstd ? await Bun.zstdDecompress(bytes) : bytes);
|
|
179
217
|
const payload: unknown = JSON.parse(text);
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
218
|
+
session.payload = payload;
|
|
219
|
+
session.etag = response.headers.get("etag");
|
|
220
|
+
session.hasPayload = true;
|
|
183
221
|
return payload;
|
|
184
222
|
}
|
|
185
223
|
|
|
@@ -2785,6 +2823,9 @@ const OPENCODE_GO_API_ID_OVERRIDES: Readonly<Record<string, Api>> = {
|
|
|
2785
2823
|
"qwen3.5-plus": "openai-completions",
|
|
2786
2824
|
"qwen3.6-plus": "openai-completions",
|
|
2787
2825
|
};
|
|
2826
|
+
// Runtime-discovered rows cached before model-identity corrections retain
|
|
2827
|
+
// stale capability metadata until the authoritative catalog TTL expires.
|
|
2828
|
+
const OPENCODE_CACHE_MIGRATION_MODEL_IDS = ["glm-5.3-flash"] as const;
|
|
2788
2829
|
|
|
2789
2830
|
// Billing-variant suffixes the OpenCode gateways append to a base model id
|
|
2790
2831
|
// without changing its transport (`deepseek-v4-flash-free`,
|
|
@@ -2845,14 +2886,12 @@ function openCodeModelManagerOptions(
|
|
|
2845
2886
|
providerId,
|
|
2846
2887
|
cacheProviderId: resolveModelCacheProviderId(providerId, { apiKey, baseUrl: discoveryBaseUrl }),
|
|
2847
2888
|
dynamicModelsAuthoritative: true,
|
|
2848
|
-
//
|
|
2849
|
-
// before a
|
|
2850
|
-
//
|
|
2851
|
-
|
|
2852
|
-
// by the 2h cache TTL instead.
|
|
2853
|
-
dropCachedModelIdsOnStaticMismatch: Object.keys(apiOverrides),
|
|
2889
|
+
// Per-id route pins and capability migrations are cache identity:
|
|
2890
|
+
// without this, rows cached before a correction keep the stale route or
|
|
2891
|
+
// thinking surface until TTL expiry (#8957, #9960).
|
|
2892
|
+
dropCachedModelIdsOnStaticMismatch: [...Object.keys(apiOverrides), ...OPENCODE_CACHE_MIGRATION_MODEL_IDS],
|
|
2854
2893
|
modelsDev: {
|
|
2855
|
-
fetch: () =>
|
|
2894
|
+
fetch: () => fetchRevalidatedWellKnownModelsWithTimeout(config?.fetch),
|
|
2856
2895
|
map: payload => {
|
|
2857
2896
|
if (!isRecord(payload)) return [];
|
|
2858
2897
|
return mapModelsDevToModels(payload, OPENCODE_MODELS_DEV_DESCRIPTORS)
|
|
@@ -5054,6 +5093,7 @@ type LiteLLMRichEndpointModel<TApi extends Api> = {
|
|
|
5054
5093
|
hasToolMetadata: boolean;
|
|
5055
5094
|
hasSupportedOpenAIParams: boolean;
|
|
5056
5095
|
hasCost: boolean;
|
|
5096
|
+
reportedCost: Partial<ModelSpec<Api>["cost"]>;
|
|
5057
5097
|
};
|
|
5058
5098
|
type LiteLLMRichEndpointFailure = {
|
|
5059
5099
|
endpoint: string;
|
|
@@ -5178,30 +5218,36 @@ function getLiteLLMParams(entry: LiteLLMRichModelEntry): LiteLLMRichModelEntry |
|
|
|
5178
5218
|
function getLiteLLMMetadataValue(entry: LiteLLMRichModelEntry, key: string): unknown {
|
|
5179
5219
|
return entry[key] ?? getLiteLLMModelInfo(entry)?.[key];
|
|
5180
5220
|
}
|
|
5181
|
-
|
|
5182
|
-
/** Per-million USD cost from a `*_per_token` LiteLLM field, or `undefined` when absent/non-positive. */
|
|
5221
|
+
/** Per-million USD cost from a positive `*_per_token` LiteLLM field. */
|
|
5183
5222
|
function getLiteLLMPerMillionCost(entry: LiteLLMRichModelEntry, key: string): number | undefined {
|
|
5184
5223
|
const perToken = toNumber(getLiteLLMMetadataValue(entry, key));
|
|
5185
5224
|
return perToken !== undefined && perToken > 0 ? perToken * 1_000_000 : undefined;
|
|
5186
5225
|
}
|
|
5187
5226
|
|
|
5188
|
-
/**
|
|
5189
|
-
|
|
5190
|
-
* cache costs) onto {@link ModelSpec.cost} in $/million tokens. Returns `undefined`
|
|
5191
|
-
* when LiteLLM reports neither an input nor an output price so callers keep the
|
|
5192
|
-
* bundled reference cost.
|
|
5193
|
-
*/
|
|
5194
|
-
function getLiteLLMCost(entry: LiteLLMRichModelEntry): ModelSpec<Api>["cost"] | undefined {
|
|
5227
|
+
/** Map positive LiteLLM per-token prices onto their per-million cost fields. */
|
|
5228
|
+
function getLiteLLMReportedCost(entry: LiteLLMRichModelEntry): Partial<ModelSpec<Api>["cost"]> {
|
|
5195
5229
|
const input = getLiteLLMPerMillionCost(entry, "input_cost_per_token");
|
|
5196
5230
|
const output = getLiteLLMPerMillionCost(entry, "output_cost_per_token");
|
|
5197
|
-
|
|
5231
|
+
const cacheRead = getLiteLLMPerMillionCost(entry, "cache_read_input_token_cost");
|
|
5232
|
+
const cacheWrite = getLiteLLMPerMillionCost(entry, "cache_creation_input_token_cost");
|
|
5233
|
+
return {
|
|
5234
|
+
...(input !== undefined ? { input } : {}),
|
|
5235
|
+
...(output !== undefined ? { output } : {}),
|
|
5236
|
+
...(cacheRead !== undefined ? { cacheRead } : {}),
|
|
5237
|
+
...(cacheWrite !== undefined ? { cacheWrite } : {}),
|
|
5238
|
+
};
|
|
5239
|
+
}
|
|
5240
|
+
|
|
5241
|
+
function getLiteLLMCost(entry: LiteLLMRichModelEntry): ModelSpec<Api>["cost"] | undefined {
|
|
5242
|
+
const cost = getLiteLLMReportedCost(entry);
|
|
5243
|
+
if (cost.input === undefined && cost.output === undefined) {
|
|
5198
5244
|
return undefined;
|
|
5199
5245
|
}
|
|
5200
5246
|
return {
|
|
5201
|
-
input: input ?? 0,
|
|
5202
|
-
output: output ?? 0,
|
|
5203
|
-
cacheRead:
|
|
5204
|
-
cacheWrite:
|
|
5247
|
+
input: cost.input ?? 0,
|
|
5248
|
+
output: cost.output ?? 0,
|
|
5249
|
+
cacheRead: cost.cacheRead ?? 0,
|
|
5250
|
+
cacheWrite: cost.cacheWrite ?? 0,
|
|
5205
5251
|
};
|
|
5206
5252
|
}
|
|
5207
5253
|
|
|
@@ -5352,12 +5398,29 @@ function mapLiteLLMRichEntry<TApi extends Api>(
|
|
|
5352
5398
|
["tools", "tool_choice", "functions", "function_call"].includes(param),
|
|
5353
5399
|
)
|
|
5354
5400
|
: reference?.supportsTools;
|
|
5401
|
+
// Enrich from the bundled reference with provider-INDEPENDENT reasoning
|
|
5402
|
+
// hints only. The reference is resolved against the global bundled catalog,
|
|
5403
|
+
// so a custom endpoint exposing an alias that collides with a bundled model
|
|
5404
|
+
// (a LiteLLM proxy serving `kimi-k3`, which matches Fireworks' bundled
|
|
5405
|
+
// `kimi-k3`) must not inherit that provider's transport compat. Spreading
|
|
5406
|
+
// the resolved `reference.compat` wholesale leaked `wireModelIdMode`,
|
|
5407
|
+
// `toolSchemaFlavor`, `thinkingFormat`, etc. across the provider boundary —
|
|
5408
|
+
// rewriting the wire id to `accounts/fireworks/models/kimi-k3` for a
|
|
5409
|
+
// non-Fireworks endpoint (issue #9938). `buildModel` re-derives every
|
|
5410
|
+
// transport field from the discovered provider and model id, so only the
|
|
5411
|
+
// effort vocabulary flows through here. Mirrors `discoverOpenAIModelsList`.
|
|
5412
|
+
const referenceCompat = reference?.compat as OpenAICompat | undefined;
|
|
5355
5413
|
const compat: OpenAICompat = {
|
|
5356
|
-
...(reference?.compat ?? {}),
|
|
5357
5414
|
supportsStore: false,
|
|
5358
5415
|
supportsDeveloperRole: false,
|
|
5359
5416
|
...(supportedOpenAIParams !== undefined
|
|
5360
5417
|
? { supportsReasoningEffort: supportedOpenAIParams.includes("reasoning_effort") }
|
|
5418
|
+
: referenceCompat?.supportsReasoningEffort !== undefined
|
|
5419
|
+
? { supportsReasoningEffort: referenceCompat.supportsReasoningEffort }
|
|
5420
|
+
: {}),
|
|
5421
|
+
...(referenceCompat?.reasoningEffortMap ? { reasoningEffortMap: referenceCompat.reasoningEffortMap } : {}),
|
|
5422
|
+
...(referenceCompat?.omitReasoningEffort !== undefined
|
|
5423
|
+
? { omitReasoningEffort: referenceCompat.omitReasoningEffort }
|
|
5361
5424
|
: {}),
|
|
5362
5425
|
};
|
|
5363
5426
|
return {
|
|
@@ -5401,13 +5464,23 @@ function mergeLiteLLMRichEndpointModels<TApi extends Api>(
|
|
|
5401
5464
|
maxTokens: next.hasMaxTokens ? next.model.maxTokens : existing.model.maxTokens,
|
|
5402
5465
|
input: next.supportsVision === true || next.supportsVision === false ? next.model.input : existing.model.input,
|
|
5403
5466
|
reasoning: typeof next.supportsReasoning === "boolean" ? next.model.reasoning : existing.model.reasoning,
|
|
5404
|
-
cost:
|
|
5467
|
+
cost: { ...existing.model.cost, ...existing.reportedCost, ...next.reportedCost },
|
|
5405
5468
|
compat: next.hasSupportedOpenAIParams ? next.model.compat : existing.model.compat,
|
|
5406
5469
|
};
|
|
5407
5470
|
if (next.hasToolMetadata) {
|
|
5408
5471
|
model.supportsTools = next.model.supportsTools;
|
|
5409
5472
|
}
|
|
5410
|
-
return {
|
|
5473
|
+
return {
|
|
5474
|
+
...next,
|
|
5475
|
+
apiRoute,
|
|
5476
|
+
model,
|
|
5477
|
+
reportedCost: { ...existing.reportedCost, ...next.reportedCost },
|
|
5478
|
+
hasContextWindow: existing.hasContextWindow || next.hasContextWindow,
|
|
5479
|
+
hasMaxTokens: existing.hasMaxTokens || next.hasMaxTokens,
|
|
5480
|
+
hasToolMetadata: existing.hasToolMetadata || next.hasToolMetadata,
|
|
5481
|
+
hasSupportedOpenAIParams: existing.hasSupportedOpenAIParams || next.hasSupportedOpenAIParams,
|
|
5482
|
+
hasCost: existing.hasCost || next.hasCost,
|
|
5483
|
+
};
|
|
5411
5484
|
}
|
|
5412
5485
|
|
|
5413
5486
|
async function fetchLiteLLMRichEndpoint<TApi extends Api>(
|
|
@@ -5469,6 +5542,7 @@ async function fetchLiteLLMRichEndpoint<TApi extends Api>(
|
|
|
5469
5542
|
supportedOpenAIParams !== undefined,
|
|
5470
5543
|
hasSupportedOpenAIParams: supportedOpenAIParams !== undefined,
|
|
5471
5544
|
hasCost: getLiteLLMCost(entry) !== undefined,
|
|
5545
|
+
reportedCost: getLiteLLMReportedCost(entry),
|
|
5472
5546
|
};
|
|
5473
5547
|
const existing = deduped.get(model.id);
|
|
5474
5548
|
deduped.set(model.id, existing ? mergeLiteLLMRichEndpointModels(existing, next) : next);
|
|
@@ -5530,7 +5604,12 @@ async function fetchLiteLLMRichModelsInternal<TApi extends Api>(
|
|
|
5530
5604
|
for (const entry of deduped.values()) {
|
|
5531
5605
|
if (
|
|
5532
5606
|
(entry.supportsVision !== true && entry.supportsVision !== false) ||
|
|
5533
|
-
(options.resolveApi !== undefined && entry.apiRoute === "unknown")
|
|
5607
|
+
(options.resolveApi !== undefined && entry.apiRoute === "unknown") ||
|
|
5608
|
+
(Object.keys(entry.reportedCost).length > 0 &&
|
|
5609
|
+
(entry.reportedCost.input === undefined ||
|
|
5610
|
+
entry.reportedCost.output === undefined ||
|
|
5611
|
+
entry.reportedCost.cacheRead === undefined ||
|
|
5612
|
+
entry.reportedCost.cacheWrite === undefined))
|
|
5534
5613
|
) {
|
|
5535
5614
|
needsMoreMetadata = true;
|
|
5536
5615
|
break;
|
|
@@ -5567,12 +5646,14 @@ export function litellmModelManagerOptions(config?: LiteLLMModelManagerConfig):
|
|
|
5567
5646
|
const baseUrl = config?.baseUrl ?? getDefaultModelDiscoveryBaseUrl("litellm")!;
|
|
5568
5647
|
return {
|
|
5569
5648
|
providerId: "litellm",
|
|
5570
|
-
// rich-
|
|
5571
|
-
//
|
|
5572
|
-
//
|
|
5573
|
-
//
|
|
5574
|
-
//
|
|
5575
|
-
//
|
|
5649
|
+
// rich-v8 invalidates rows whose `compatConfig` retained a colliding
|
|
5650
|
+
// bundled model's provider-specific transport (e.g. Fireworks
|
|
5651
|
+
// `wireModelIdMode`) before that leak was fixed. Earlier versions added
|
|
5652
|
+
// bundled reference fallback, moved OpenAI models to Responses, continued
|
|
5653
|
+
// past incomplete vision/API metadata and endpoints omitting cache
|
|
5654
|
+
// pricing, stripped reseller usage suffixes, filtered placeholder rows,
|
|
5655
|
+
// and mapped rich pricing. Bump the version whenever these mappers change,
|
|
5656
|
+
// or warm authoritative caches keep serving pre-change rows for the full TTL.
|
|
5576
5657
|
cacheProviderId: resolveModelCacheProviderId("litellm", { baseUrl }),
|
|
5577
5658
|
// litellm is a local-only proxy and is never bundled in models.json (that
|
|
5578
5659
|
// would leak the machine's localhost catalog). Prefer the proxy's richer
|
|
@@ -6095,7 +6176,7 @@ export function anthropicModelManagerOptions(
|
|
|
6095
6176
|
return {
|
|
6096
6177
|
providerId: "anthropic",
|
|
6097
6178
|
modelsDev: {
|
|
6098
|
-
fetch: () =>
|
|
6179
|
+
fetch: () => fetchRevalidatedWellKnownModelsWithTimeout(config?.fetch),
|
|
6099
6180
|
map: payload => mapAnthropicModelsDev(payload, baseUrl),
|
|
6100
6181
|
},
|
|
6101
6182
|
...(apiKey && {
|
|
@@ -6767,3 +6848,42 @@ export const MODELS_DEV_PROVIDER_DESCRIPTORS: readonly ModelsDevProviderDescript
|
|
|
6767
6848
|
...MODELS_DEV_PROVIDER_DESCRIPTORS_CODING_PLANS,
|
|
6768
6849
|
...MODELS_DEV_PROVIDER_DESCRIPTORS_SPECIALIZED,
|
|
6769
6850
|
];
|
|
6851
|
+
|
|
6852
|
+
const MODELS_DEV_DESCRIPTORS_BY_PROVIDER: Record<string, ModelsDevProviderDescriptor[]> = Object.create(null);
|
|
6853
|
+
for (const descriptor of MODELS_DEV_PROVIDER_DESCRIPTORS) {
|
|
6854
|
+
const providerDescriptors = MODELS_DEV_DESCRIPTORS_BY_PROVIDER[descriptor.providerId];
|
|
6855
|
+
if (providerDescriptors) {
|
|
6856
|
+
providerDescriptors.push(descriptor);
|
|
6857
|
+
} else {
|
|
6858
|
+
MODELS_DEV_DESCRIPTORS_BY_PROVIDER[descriptor.providerId] = [descriptor];
|
|
6859
|
+
}
|
|
6860
|
+
}
|
|
6861
|
+
|
|
6862
|
+
/** Providers whose bundled catalog can receive additive models.dev updates at runtime. */
|
|
6863
|
+
export const MODELS_DEV_CATALOG_PROVIDER_IDS: readonly string[] = Object.freeze(
|
|
6864
|
+
Object.keys(MODELS_DEV_DESCRIPTORS_BY_PROVIDER),
|
|
6865
|
+
);
|
|
6866
|
+
|
|
6867
|
+
/**
|
|
6868
|
+
* Build the shared models.dev fallback for one known provider.
|
|
6869
|
+
*
|
|
6870
|
+
* Provider managers sharing one fetch implementation reuse its conditional
|
|
6871
|
+
* catalog session. Each mapped provider slice is persisted independently so
|
|
6872
|
+
* startup can restore it without parsing the full catalog.
|
|
6873
|
+
*
|
|
6874
|
+
* `timeoutMs` bounds the catalog request. It is configurable for callers with a
|
|
6875
|
+
* stricter startup budget and for deterministic timeout tests.
|
|
6876
|
+
*/
|
|
6877
|
+
export function modelsDevCatalogFallback(
|
|
6878
|
+
providerId: string,
|
|
6879
|
+
fetchImpl?: FetchImpl,
|
|
6880
|
+
timeoutMs = DEFAULT_OPENAI_COMPATIBLE_DISCOVERY_TIMEOUT_MS,
|
|
6881
|
+
): ModelsDevFallback<Api> | undefined {
|
|
6882
|
+
const descriptors = MODELS_DEV_DESCRIPTORS_BY_PROVIDER[providerId];
|
|
6883
|
+
if (!descriptors) return undefined;
|
|
6884
|
+
return {
|
|
6885
|
+
additiveOnly: true,
|
|
6886
|
+
fetch: () => fetchRevalidatedWellKnownModelsWithTimeout(fetchImpl, timeoutMs),
|
|
6887
|
+
map: payload => (isRecord(payload) ? filterModelsDevCatalogRows(mapModelsDevToModels(payload, descriptors)) : []),
|
|
6888
|
+
};
|
|
6889
|
+
}
|
package/src/variant-collapse.ts
CHANGED
|
@@ -307,7 +307,7 @@ function geminiLevelFlashFamily(version: "3.6" | "3.7", ...additionalMembers: st
|
|
|
307
307
|
}
|
|
308
308
|
|
|
309
309
|
const GEMINI_36_FLASH_FAMILY = geminiLevelFlashFamily("3.6", "gemini-3.6-flash-tiered");
|
|
310
|
-
const GEMINI_37_FLASH_FAMILY = geminiLevelFlashFamily("3.7");
|
|
310
|
+
const GEMINI_37_FLASH_FAMILY = geminiLevelFlashFamily("3.7", "gemini-3.7-flash-tiered");
|
|
311
311
|
|
|
312
312
|
function geminiProFamily(mode: "budget" | "google-level"): EffortVariantFamily {
|
|
313
313
|
const budget = mode === "budget";
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { isRecord } from "../utils";
|
|
2
|
+
|
|
3
|
+
/** Provider-native AI Gateway prefix before the upstream route segment. */
|
|
4
|
+
export const CLOUDFLARE_AI_GATEWAY_BASE_URL = "https://gateway.ai.cloudflare.com/v1/<account>/<gateway>";
|
|
5
|
+
/** Anthropic Messages passthrough endpoint. */
|
|
6
|
+
export const CLOUDFLARE_AI_GATEWAY_ANTHROPIC_BASE_URL = `${CLOUDFLARE_AI_GATEWAY_BASE_URL}/anthropic`;
|
|
7
|
+
/** OpenAI Responses passthrough endpoint. */
|
|
8
|
+
export const CLOUDFLARE_AI_GATEWAY_OPENAI_BASE_URL = `${CLOUDFLARE_AI_GATEWAY_BASE_URL}/openai`;
|
|
9
|
+
/** OpenAI-compatible Workers AI endpoint. */
|
|
10
|
+
export const CLOUDFLARE_AI_GATEWAY_COMPAT_BASE_URL = `${CLOUDFLARE_AI_GATEWAY_BASE_URL}/compat`;
|
|
11
|
+
|
|
12
|
+
/** Opaque API-key credential payload persisted by Cloudflare AI Gateway login. */
|
|
13
|
+
export interface CloudflareAiGatewayCredential {
|
|
14
|
+
token: string;
|
|
15
|
+
accountId?: string;
|
|
16
|
+
gatewayId?: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** Parse both structured login credentials and legacy plain gateway tokens. */
|
|
20
|
+
export function parseCloudflareAiGatewayCredential(value: string): CloudflareAiGatewayCredential | null {
|
|
21
|
+
const trimmed = value.trim();
|
|
22
|
+
if (!trimmed) return null;
|
|
23
|
+
if (!trimmed.startsWith("{")) return { token: trimmed };
|
|
24
|
+
try {
|
|
25
|
+
const parsed: unknown = JSON.parse(trimmed);
|
|
26
|
+
if (!isRecord(parsed)) return null;
|
|
27
|
+
if (typeof parsed.token !== "string" || !parsed.token.trim()) return null;
|
|
28
|
+
if (parsed.accountId !== undefined && typeof parsed.accountId !== "string") return null;
|
|
29
|
+
if (parsed.gatewayId !== undefined && typeof parsed.gatewayId !== "string") return null;
|
|
30
|
+
const credential: CloudflareAiGatewayCredential = { token: parsed.token.trim() };
|
|
31
|
+
const accountId = parsed.accountId?.trim();
|
|
32
|
+
const gatewayId = parsed.gatewayId?.trim();
|
|
33
|
+
if (accountId) credential.accountId = accountId;
|
|
34
|
+
if (gatewayId) credential.gatewayId = gatewayId;
|
|
35
|
+
return credential;
|
|
36
|
+
} catch {
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Serialize the gateway token with the routing identifiers collected during login. */
|
|
42
|
+
export function serializeCloudflareAiGatewayCredential(token: string, accountId: string, gatewayId: string): string {
|
|
43
|
+
return JSON.stringify({ token: token.trim(), accountId: accountId.trim(), gatewayId: gatewayId.trim() });
|
|
44
|
+
}
|