@linxiraos/pi-catalog 1.1.4 → 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 +5 -3
- package/dist/types/identity/family.d.ts +15 -11
- package/dist/types/model-manager.d.ts +20 -4
- package/dist/types/provider-models/descriptors.d.ts +20 -1
- package/dist/types/provider-models/models-dev-policies.d.ts +8 -0
- package/dist/types/provider-models/openai-compat.d.ts +56 -4
- package/dist/types/types.d.ts +2 -2
- package/dist/types/wire/cloudflare-ai-gateway.d.ts +18 -0
- package/package.json +5 -5
- package/src/compat/openai.ts +2 -0
- package/src/identity/family.ts +43 -24
- package/src/identity/priority.ts +1 -0
- package/src/model-manager.ts +121 -72
- package/src/model-thinking.ts +19 -0
- package/src/models.json +8777 -1081
- package/src/provider-models/cache-provider-id.ts +4 -1
- package/src/provider-models/descriptors.ts +19 -1
- package/src/provider-models/models-dev-policies.ts +38 -0
- package/src/provider-models/openai-compat.ts +488 -66
- package/src/types.ts +10 -2
- package/src/variant-collapse.ts +1 -1
- package/src/wire/cloudflare-ai-gateway.ts +44 -0
|
@@ -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);
|
|
186
|
+
}
|
|
187
|
+
|
|
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));
|
|
148
193
|
}
|
|
149
194
|
|
|
150
|
-
async function fetchCatalogPayload(
|
|
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
|
|
|
@@ -1305,6 +1343,181 @@ export function novitaModelManagerOptions(
|
|
|
1305
1343
|
});
|
|
1306
1344
|
}
|
|
1307
1345
|
|
|
1346
|
+
// ---------------------------------------------------------------------------
|
|
1347
|
+
// 5.6 DeepInfra
|
|
1348
|
+
// ---------------------------------------------------------------------------
|
|
1349
|
+
|
|
1350
|
+
export const DEEPINFRA_BASE_URL = "https://api.deepinfra.com/v1/openai";
|
|
1351
|
+
/**
|
|
1352
|
+
* `filter=with_meta` attaches per-model `metadata` (limits, pricing, tags);
|
|
1353
|
+
* `sort_by=omp` asks DeepInfra to return models in omp-priority order
|
|
1354
|
+
* (earlier = better). The mapper does not stamp `priority` yet — see
|
|
1355
|
+
* `mapDeepinfraModel` — but the params are sent so discovery picks the
|
|
1356
|
+
* ordering up as soon as the server honors it.
|
|
1357
|
+
*/
|
|
1358
|
+
const DEEPINFRA_MODELS_QUERY = "?filter=with_meta&sort_by=omp";
|
|
1359
|
+
const DEEPINFRA_EFFORTS = [Effort.Low, Effort.Medium, Effort.High] as const;
|
|
1360
|
+
|
|
1361
|
+
/** DeepInfra OpenAI-compatible discovery configuration. */
|
|
1362
|
+
export interface DeepinfraModelManagerConfig {
|
|
1363
|
+
apiKey?: string;
|
|
1364
|
+
baseUrl?: string;
|
|
1365
|
+
fetch?: FetchImpl;
|
|
1366
|
+
}
|
|
1367
|
+
|
|
1368
|
+
interface DeepinfraModelEntry {
|
|
1369
|
+
id?: unknown;
|
|
1370
|
+
metadata?: unknown;
|
|
1371
|
+
}
|
|
1372
|
+
|
|
1373
|
+
function deepinfraTags(metadata: Record<string, unknown>): readonly string[] {
|
|
1374
|
+
const tags = metadata.tags;
|
|
1375
|
+
return Array.isArray(tags) ? tags.filter((tag): tag is string => typeof tag === "string") : [];
|
|
1376
|
+
}
|
|
1377
|
+
|
|
1378
|
+
/**
|
|
1379
|
+
* Map one DeepInfra catalog entry to a chat model spec. Non-`chat` entries
|
|
1380
|
+
* (`tts`, `stt`, `embed`, `image-gen`, `video-gen`) are dropped — those
|
|
1381
|
+
* surfaces are served by dedicated tool backends, not the chat catalog.
|
|
1382
|
+
* DeepInfra reports token prices in USD per 1M tokens — omp's `ModelCost`
|
|
1383
|
+
* unit, used verbatim. A bundled reference (when the generated catalog has
|
|
1384
|
+
* one) is spread first so compat/tooling metadata can contribute, but the
|
|
1385
|
+
* live metadata always wins for limits, pricing, and modalities.
|
|
1386
|
+
*/
|
|
1387
|
+
function mapDeepinfraModel(
|
|
1388
|
+
entry: DeepinfraModelEntry,
|
|
1389
|
+
baseUrl: string,
|
|
1390
|
+
reference: ModelSpec<"openai-completions"> | undefined,
|
|
1391
|
+
): ModelSpec<"openai-completions"> | null {
|
|
1392
|
+
const id = typeof entry.id === "string" ? entry.id.trim() : "";
|
|
1393
|
+
if (!id) {
|
|
1394
|
+
return null;
|
|
1395
|
+
}
|
|
1396
|
+
const metadata = isRecord(entry.metadata) ? entry.metadata : {};
|
|
1397
|
+
const tags = deepinfraTags(metadata);
|
|
1398
|
+
if (!tags.includes("chat")) {
|
|
1399
|
+
return null;
|
|
1400
|
+
}
|
|
1401
|
+
const pricing = isRecord(metadata.pricing) ? metadata.pricing : {};
|
|
1402
|
+
// `reasoning_effort` marks models whose effort dial is advertised. The
|
|
1403
|
+
// parameter itself is validated and accepted platform-wide on DeepInfra
|
|
1404
|
+
// (verified: 200 on effort-tagged, reasoning-only, and plain-chat models;
|
|
1405
|
+
// 422 only for out-of-enum values), so a bundled reference's effort ladder
|
|
1406
|
+
// surviving the `...reference` spread after a tag disappears is harmless —
|
|
1407
|
+
// the host ignores the dial rather than rejecting the request.
|
|
1408
|
+
const thinking: ThinkingConfig | undefined = tags.includes("reasoning_effort")
|
|
1409
|
+
? { mode: "effort", efforts: [...DEEPINFRA_EFFORTS] }
|
|
1410
|
+
: undefined;
|
|
1411
|
+
const contextWindow = toPositiveNumber(metadata.context_length, reference?.contextWindow ?? null);
|
|
1412
|
+
// `metadata.max_tokens` mirrors `context_length` on every live row today —
|
|
1413
|
+
// it is the total token ceiling, not an output cap (the catalog exposes no
|
|
1414
|
+
// output-specific field). Stamping it into `maxTokens` would advertise
|
|
1415
|
+
// `output === context` for every model and default oversized `max_tokens`
|
|
1416
|
+
// onto models with smaller real output limits. Trust it only when it is
|
|
1417
|
+
// strictly below the context window (i.e. the API starts publishing a real
|
|
1418
|
+
// output cap); otherwise keep the bundled reference's cap (stencil.so fill
|
|
1419
|
+
// during generation) or leave the limit unknown so requests defer to the
|
|
1420
|
+
// server-side cap.
|
|
1421
|
+
const liveMaxTokens = toPositiveNumber(metadata.max_tokens, 0);
|
|
1422
|
+
const hasLiveOutputCap = contextWindow !== null && liveMaxTokens > 0 && liveMaxTokens < contextWindow;
|
|
1423
|
+
// A same-id reference carries the canonical deployment's output cap, which
|
|
1424
|
+
// can exceed the (smaller) window DeepInfra serves — e.g. a 500K cap against
|
|
1425
|
+
// a 256K context. An output cap above the context ceiling is meaningless, so
|
|
1426
|
+
// keep the fallback inside the window.
|
|
1427
|
+
const referenceMaxTokens = reference?.maxTokens ?? null;
|
|
1428
|
+
const maxTokens = hasLiveOutputCap
|
|
1429
|
+
? liveMaxTokens
|
|
1430
|
+
: referenceMaxTokens !== null && contextWindow !== null
|
|
1431
|
+
? Math.min(referenceMaxTokens, contextWindow)
|
|
1432
|
+
: referenceMaxTokens;
|
|
1433
|
+
return {
|
|
1434
|
+
...reference,
|
|
1435
|
+
id,
|
|
1436
|
+
name: reference?.name ?? id,
|
|
1437
|
+
api: "openai-completions",
|
|
1438
|
+
provider: "deepinfra",
|
|
1439
|
+
baseUrl,
|
|
1440
|
+
reasoning: tags.includes("reasoning") || tags.includes("reasoning_effort"),
|
|
1441
|
+
...(thinking ? { thinking } : {}),
|
|
1442
|
+
input: tags.includes("vision") || tags.includes("vlm") ? ["text", "image"] : ["text"],
|
|
1443
|
+
cost: {
|
|
1444
|
+
input: toPositiveNumber(pricing.input_tokens, 0),
|
|
1445
|
+
output: toPositiveNumber(pricing.output_tokens, 0),
|
|
1446
|
+
cacheRead: toPositiveNumber(pricing.cache_read_tokens, 0),
|
|
1447
|
+
cacheWrite: 0,
|
|
1448
|
+
},
|
|
1449
|
+
contextWindow,
|
|
1450
|
+
maxTokens,
|
|
1451
|
+
};
|
|
1452
|
+
}
|
|
1453
|
+
|
|
1454
|
+
/**
|
|
1455
|
+
* Bespoke fetch instead of `fetchOpenAICompatibleModels`: the shared helper
|
|
1456
|
+
* cannot carry the `filter`/`sort_by` query params and re-sorts results by id,
|
|
1457
|
+
* which would destroy DeepInfra's priority ordering once the server honors
|
|
1458
|
+
* `sort_by=omp`. Response order is preserved (dedupe keeps the first, i.e.
|
|
1459
|
+
* highest-priority, occurrence).
|
|
1460
|
+
*/
|
|
1461
|
+
async function fetchDeepinfraModels(options: {
|
|
1462
|
+
baseUrl: string;
|
|
1463
|
+
apiKey?: string;
|
|
1464
|
+
fetch?: FetchImpl;
|
|
1465
|
+
references: Map<string, ModelSpec<"openai-completions">>;
|
|
1466
|
+
}): Promise<ModelSpec<"openai-completions">[] | null> {
|
|
1467
|
+
const headers: Record<string, string> = { Accept: "application/json" };
|
|
1468
|
+
if (options.apiKey) {
|
|
1469
|
+
headers.Authorization = `Bearer ${options.apiKey}`;
|
|
1470
|
+
}
|
|
1471
|
+
const fetchImpl = discoveryFetch(options.fetch);
|
|
1472
|
+
let payload: unknown;
|
|
1473
|
+
try {
|
|
1474
|
+
const response = await withCatalogDiscoveryTimeout(DEFAULT_OPENAI_COMPATIBLE_DISCOVERY_TIMEOUT_MS, signal =>
|
|
1475
|
+
fetchImpl(`${options.baseUrl}/models${DEEPINFRA_MODELS_QUERY}`, { method: "GET", headers, signal }),
|
|
1476
|
+
);
|
|
1477
|
+
if (!response.ok) {
|
|
1478
|
+
return null;
|
|
1479
|
+
}
|
|
1480
|
+
payload = await response.json();
|
|
1481
|
+
} catch {
|
|
1482
|
+
return null;
|
|
1483
|
+
}
|
|
1484
|
+
if (!isRecord(payload) || !Array.isArray(payload.data)) {
|
|
1485
|
+
return null;
|
|
1486
|
+
}
|
|
1487
|
+
const models: ModelSpec<"openai-completions">[] = [];
|
|
1488
|
+
const seen = new Set<string>();
|
|
1489
|
+
for (const entry of payload.data) {
|
|
1490
|
+
if (!isRecord(entry)) {
|
|
1491
|
+
continue;
|
|
1492
|
+
}
|
|
1493
|
+
const reference = typeof entry.id === "string" ? options.references.get(entry.id) : undefined;
|
|
1494
|
+
const mapped = mapDeepinfraModel(entry as DeepinfraModelEntry, options.baseUrl, reference);
|
|
1495
|
+
if (mapped && !seen.has(mapped.id)) {
|
|
1496
|
+
seen.add(mapped.id);
|
|
1497
|
+
models.push(mapped);
|
|
1498
|
+
}
|
|
1499
|
+
}
|
|
1500
|
+
return models;
|
|
1501
|
+
}
|
|
1502
|
+
|
|
1503
|
+
/**
|
|
1504
|
+
* Builds DeepInfra's model-discovery manager. The catalog endpoint is public,
|
|
1505
|
+
* so discovery (and keyless `gen:models` generation) works without an API key;
|
|
1506
|
+
* model availability at runtime is still gated on credentials by the registry.
|
|
1507
|
+
*/
|
|
1508
|
+
export function deepinfraModelManagerOptions(
|
|
1509
|
+
config?: DeepinfraModelManagerConfig,
|
|
1510
|
+
): ModelManagerOptions<"openai-completions"> {
|
|
1511
|
+
const apiKey = config?.apiKey;
|
|
1512
|
+
const baseUrl = (config?.baseUrl ?? DEEPINFRA_BASE_URL).replace(/\/$/, "");
|
|
1513
|
+
const references = createBundledReferenceMap<"openai-completions">("deepinfra");
|
|
1514
|
+
return {
|
|
1515
|
+
providerId: "deepinfra",
|
|
1516
|
+
dynamicModelsAuthoritative: true,
|
|
1517
|
+
fetchDynamicModels: () => fetchDeepinfraModels({ baseUrl, apiKey, fetch: config?.fetch, references }),
|
|
1518
|
+
};
|
|
1519
|
+
}
|
|
1520
|
+
|
|
1308
1521
|
// ---------------------------------------------------------------------------
|
|
1309
1522
|
// 6. xAI
|
|
1310
1523
|
// ---------------------------------------------------------------------------
|
|
@@ -2610,6 +2823,9 @@ const OPENCODE_GO_API_ID_OVERRIDES: Readonly<Record<string, Api>> = {
|
|
|
2610
2823
|
"qwen3.5-plus": "openai-completions",
|
|
2611
2824
|
"qwen3.6-plus": "openai-completions",
|
|
2612
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;
|
|
2613
2829
|
|
|
2614
2830
|
// Billing-variant suffixes the OpenCode gateways append to a base model id
|
|
2615
2831
|
// without changing its transport (`deepseek-v4-flash-free`,
|
|
@@ -2670,14 +2886,12 @@ function openCodeModelManagerOptions(
|
|
|
2670
2886
|
providerId,
|
|
2671
2887
|
cacheProviderId: resolveModelCacheProviderId(providerId, { apiKey, baseUrl: discoveryBaseUrl }),
|
|
2672
2888
|
dynamicModelsAuthoritative: true,
|
|
2673
|
-
//
|
|
2674
|
-
// before a
|
|
2675
|
-
//
|
|
2676
|
-
|
|
2677
|
-
// by the 2h cache TTL instead.
|
|
2678
|
-
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],
|
|
2679
2893
|
modelsDev: {
|
|
2680
|
-
fetch: () =>
|
|
2894
|
+
fetch: () => fetchRevalidatedWellKnownModelsWithTimeout(config?.fetch),
|
|
2681
2895
|
map: payload => {
|
|
2682
2896
|
if (!isRecord(payload)) return [];
|
|
2683
2897
|
return mapModelsDevToModels(payload, OPENCODE_MODELS_DEV_DESCRIPTORS)
|
|
@@ -4585,6 +4799,133 @@ export function aiandModelManagerOptions(config?: AiandModelManagerConfig): Mode
|
|
|
4585
4799
|
};
|
|
4586
4800
|
}
|
|
4587
4801
|
|
|
4802
|
+
// ---------------------------------------------------------------------------
|
|
4803
|
+
// 16.7 Yolo-Auto (yolo-auto.com)
|
|
4804
|
+
// ---------------------------------------------------------------------------
|
|
4805
|
+
|
|
4806
|
+
const YOLO_AUTO_BASE_URL = "https://yolo-auto.com/v1";
|
|
4807
|
+
|
|
4808
|
+
/**
|
|
4809
|
+
* Documented Yolo-Auto catalog (yolo-auto.com/docs, 2026-08) bundled so the
|
|
4810
|
+
* provider is usable when generation and first boot have no live key. The
|
|
4811
|
+
* flat-rate `/v1/models` response is authoritative once discovery runs.
|
|
4812
|
+
* The compat block mirrors the provider's documented wire surface: the API
|
|
4813
|
+
* speaks the generic chat template with `reasoning_effort` support and rejects
|
|
4814
|
+
* the `developer` role and `store` param.
|
|
4815
|
+
*/
|
|
4816
|
+
export const YOLO_AUTO_STATIC_MODELS: readonly ModelSpec<"openai-completions">[] = [
|
|
4817
|
+
{
|
|
4818
|
+
id: "deepseek-flash-v4",
|
|
4819
|
+
name: "DeepSeek Flash V4",
|
|
4820
|
+
api: "openai-completions",
|
|
4821
|
+
provider: "yolo-auto",
|
|
4822
|
+
baseUrl: YOLO_AUTO_BASE_URL,
|
|
4823
|
+
reasoning: true,
|
|
4824
|
+
input: ["text", "image"],
|
|
4825
|
+
thinking: {
|
|
4826
|
+
mode: "effort",
|
|
4827
|
+
efforts: [Effort.Minimal, Effort.Low, Effort.Medium, Effort.High, Effort.XHigh, Effort.Max],
|
|
4828
|
+
effortMap: {
|
|
4829
|
+
[Effort.Minimal]: "low",
|
|
4830
|
+
[Effort.Low]: "low",
|
|
4831
|
+
[Effort.Medium]: "high",
|
|
4832
|
+
[Effort.High]: "high",
|
|
4833
|
+
[Effort.XHigh]: "max",
|
|
4834
|
+
[Effort.Max]: "max",
|
|
4835
|
+
},
|
|
4836
|
+
},
|
|
4837
|
+
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
4838
|
+
contextWindow: 131_072,
|
|
4839
|
+
maxTokens: null,
|
|
4840
|
+
compat: {
|
|
4841
|
+
supportsDeveloperRole: false,
|
|
4842
|
+
supportsStore: false,
|
|
4843
|
+
supportsReasoningEffort: true,
|
|
4844
|
+
thinkingFormat: "chat-template",
|
|
4845
|
+
},
|
|
4846
|
+
},
|
|
4847
|
+
];
|
|
4848
|
+
|
|
4849
|
+
export interface YoloAutoModelManagerConfig {
|
|
4850
|
+
apiKey?: string;
|
|
4851
|
+
baseUrl?: string;
|
|
4852
|
+
fetch?: FetchImpl;
|
|
4853
|
+
}
|
|
4854
|
+
|
|
4855
|
+
/**
|
|
4856
|
+
* Map a discovered entry onto its reference, then re-apply Yolo-Auto's
|
|
4857
|
+
* provider-wide wire constraints. A model that only exists in another
|
|
4858
|
+
* provider's bundle (e.g. `gpt-4o` served via the global reference index)
|
|
4859
|
+
* would otherwise inherit that provider's token pricing and `store`-capable
|
|
4860
|
+
* surface, which the flat-rate Yolo endpoint neither charges nor accepts.
|
|
4861
|
+
*/
|
|
4862
|
+
function mapYoloAutoModel(
|
|
4863
|
+
entry: OpenAICompatibleModelRecord,
|
|
4864
|
+
defaults: ModelSpec<"openai-completions">,
|
|
4865
|
+
reference: ModelSpec<"openai-completions"> | undefined,
|
|
4866
|
+
): ModelSpec<"openai-completions"> {
|
|
4867
|
+
const model = mapWithBundledReference(entry, defaults, reference);
|
|
4868
|
+
return {
|
|
4869
|
+
...model,
|
|
4870
|
+
// Flat-rate and no-store are provider-wide: they must win whether the
|
|
4871
|
+
// reference came from the yolo bundle, the global index, or nowhere.
|
|
4872
|
+
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
4873
|
+
compat: {
|
|
4874
|
+
...model.compat,
|
|
4875
|
+
supportsStore: false,
|
|
4876
|
+
supportsDeveloperRole: false,
|
|
4877
|
+
},
|
|
4878
|
+
};
|
|
4879
|
+
}
|
|
4880
|
+
/**
|
|
4881
|
+
* Yolo-Auto model manager: OpenAI-compatible chat completions at the
|
|
4882
|
+
* flat-rate `deepseek-flash-v4` endpoint. Live `/v1/models` discovery replaces the
|
|
4883
|
+
* bundled seed once a key is stored or present in `YOLO_AUTO_API_KEY`. Models
|
|
4884
|
+
* the provider adds later inherit metadata (reasoning, thinking, context)
|
|
4885
|
+
* from the global bundled reference index, so new ids work without a per-id
|
|
4886
|
+
* code change.
|
|
4887
|
+
*/
|
|
4888
|
+
export function yoloAutoModelManagerOptions(
|
|
4889
|
+
config?: YoloAutoModelManagerConfig,
|
|
4890
|
+
): ModelManagerOptions<"openai-completions"> {
|
|
4891
|
+
// Curated static models take precedence over the previously generated
|
|
4892
|
+
// bundle so later corrections to the curated metadata are honored; bundle
|
|
4893
|
+
// rows keep ids that earlier credentialed runs discovered, and the global
|
|
4894
|
+
// index covers everything else.
|
|
4895
|
+
const references = new Map<string, ModelSpec<"openai-completions">>();
|
|
4896
|
+
const bundled = createBundledReferenceMap<"openai-completions">("yolo-auto");
|
|
4897
|
+
for (const model of bundled.values()) {
|
|
4898
|
+
references.set(model.id, model);
|
|
4899
|
+
}
|
|
4900
|
+
for (const model of YOLO_AUTO_STATIC_MODELS) {
|
|
4901
|
+
const previous = references.get(model.id);
|
|
4902
|
+
references.set(model.id, {
|
|
4903
|
+
...previous,
|
|
4904
|
+
...model,
|
|
4905
|
+
// Generation-filled fields the curated seed leaves null: keep the
|
|
4906
|
+
// canonical values baked by the previous regen (max-output cap,
|
|
4907
|
+
// derived thinking) instead of dropping them.
|
|
4908
|
+
maxTokens: model.maxTokens ?? previous?.maxTokens ?? null,
|
|
4909
|
+
thinking: model.thinking ?? previous?.thinking,
|
|
4910
|
+
});
|
|
4911
|
+
}
|
|
4912
|
+
const resolveReference = createReferenceResolver(() => references);
|
|
4913
|
+
return {
|
|
4914
|
+
...createOpenAICompatibleModelManagerOptions({
|
|
4915
|
+
api: "openai-completions",
|
|
4916
|
+
providerId: "yolo-auto",
|
|
4917
|
+
defaultBaseUrl: YOLO_AUTO_BASE_URL,
|
|
4918
|
+
config,
|
|
4919
|
+
requireApiKey: true,
|
|
4920
|
+
mapModel: (entry, defaults, reference) =>
|
|
4921
|
+
mapYoloAutoModel(entry, defaults, resolveReference(defaults.id) ?? reference),
|
|
4922
|
+
dynamicModelsAuthoritative: true,
|
|
4923
|
+
}),
|
|
4924
|
+
};
|
|
4925
|
+
}
|
|
4926
|
+
|
|
4927
|
+
// ---------------------------------------------------------------------------
|
|
4928
|
+
|
|
4588
4929
|
// ---------------------------------------------------------------------------
|
|
4589
4930
|
// 17. Qwen Portal
|
|
4590
4931
|
// ---------------------------------------------------------------------------
|
|
@@ -4752,6 +5093,7 @@ type LiteLLMRichEndpointModel<TApi extends Api> = {
|
|
|
4752
5093
|
hasToolMetadata: boolean;
|
|
4753
5094
|
hasSupportedOpenAIParams: boolean;
|
|
4754
5095
|
hasCost: boolean;
|
|
5096
|
+
reportedCost: Partial<ModelSpec<Api>["cost"]>;
|
|
4755
5097
|
};
|
|
4756
5098
|
type LiteLLMRichEndpointFailure = {
|
|
4757
5099
|
endpoint: string;
|
|
@@ -4876,30 +5218,36 @@ function getLiteLLMParams(entry: LiteLLMRichModelEntry): LiteLLMRichModelEntry |
|
|
|
4876
5218
|
function getLiteLLMMetadataValue(entry: LiteLLMRichModelEntry, key: string): unknown {
|
|
4877
5219
|
return entry[key] ?? getLiteLLMModelInfo(entry)?.[key];
|
|
4878
5220
|
}
|
|
4879
|
-
|
|
4880
|
-
/** 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. */
|
|
4881
5222
|
function getLiteLLMPerMillionCost(entry: LiteLLMRichModelEntry, key: string): number | undefined {
|
|
4882
5223
|
const perToken = toNumber(getLiteLLMMetadataValue(entry, key));
|
|
4883
5224
|
return perToken !== undefined && perToken > 0 ? perToken * 1_000_000 : undefined;
|
|
4884
5225
|
}
|
|
4885
5226
|
|
|
4886
|
-
/**
|
|
4887
|
-
|
|
4888
|
-
* cache costs) onto {@link ModelSpec.cost} in $/million tokens. Returns `undefined`
|
|
4889
|
-
* when LiteLLM reports neither an input nor an output price so callers keep the
|
|
4890
|
-
* bundled reference cost.
|
|
4891
|
-
*/
|
|
4892
|
-
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"]> {
|
|
4893
5229
|
const input = getLiteLLMPerMillionCost(entry, "input_cost_per_token");
|
|
4894
5230
|
const output = getLiteLLMPerMillionCost(entry, "output_cost_per_token");
|
|
4895
|
-
|
|
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) {
|
|
4896
5244
|
return undefined;
|
|
4897
5245
|
}
|
|
4898
5246
|
return {
|
|
4899
|
-
input: input ?? 0,
|
|
4900
|
-
output: output ?? 0,
|
|
4901
|
-
cacheRead:
|
|
4902
|
-
cacheWrite:
|
|
5247
|
+
input: cost.input ?? 0,
|
|
5248
|
+
output: cost.output ?? 0,
|
|
5249
|
+
cacheRead: cost.cacheRead ?? 0,
|
|
5250
|
+
cacheWrite: cost.cacheWrite ?? 0,
|
|
4903
5251
|
};
|
|
4904
5252
|
}
|
|
4905
5253
|
|
|
@@ -5050,12 +5398,29 @@ function mapLiteLLMRichEntry<TApi extends Api>(
|
|
|
5050
5398
|
["tools", "tool_choice", "functions", "function_call"].includes(param),
|
|
5051
5399
|
)
|
|
5052
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;
|
|
5053
5413
|
const compat: OpenAICompat = {
|
|
5054
|
-
...(reference?.compat ?? {}),
|
|
5055
5414
|
supportsStore: false,
|
|
5056
5415
|
supportsDeveloperRole: false,
|
|
5057
5416
|
...(supportedOpenAIParams !== undefined
|
|
5058
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 }
|
|
5059
5424
|
: {}),
|
|
5060
5425
|
};
|
|
5061
5426
|
return {
|
|
@@ -5099,13 +5464,23 @@ function mergeLiteLLMRichEndpointModels<TApi extends Api>(
|
|
|
5099
5464
|
maxTokens: next.hasMaxTokens ? next.model.maxTokens : existing.model.maxTokens,
|
|
5100
5465
|
input: next.supportsVision === true || next.supportsVision === false ? next.model.input : existing.model.input,
|
|
5101
5466
|
reasoning: typeof next.supportsReasoning === "boolean" ? next.model.reasoning : existing.model.reasoning,
|
|
5102
|
-
cost:
|
|
5467
|
+
cost: { ...existing.model.cost, ...existing.reportedCost, ...next.reportedCost },
|
|
5103
5468
|
compat: next.hasSupportedOpenAIParams ? next.model.compat : existing.model.compat,
|
|
5104
5469
|
};
|
|
5105
5470
|
if (next.hasToolMetadata) {
|
|
5106
5471
|
model.supportsTools = next.model.supportsTools;
|
|
5107
5472
|
}
|
|
5108
|
-
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
|
+
};
|
|
5109
5484
|
}
|
|
5110
5485
|
|
|
5111
5486
|
async function fetchLiteLLMRichEndpoint<TApi extends Api>(
|
|
@@ -5167,6 +5542,7 @@ async function fetchLiteLLMRichEndpoint<TApi extends Api>(
|
|
|
5167
5542
|
supportedOpenAIParams !== undefined,
|
|
5168
5543
|
hasSupportedOpenAIParams: supportedOpenAIParams !== undefined,
|
|
5169
5544
|
hasCost: getLiteLLMCost(entry) !== undefined,
|
|
5545
|
+
reportedCost: getLiteLLMReportedCost(entry),
|
|
5170
5546
|
};
|
|
5171
5547
|
const existing = deduped.get(model.id);
|
|
5172
5548
|
deduped.set(model.id, existing ? mergeLiteLLMRichEndpointModels(existing, next) : next);
|
|
@@ -5228,7 +5604,12 @@ async function fetchLiteLLMRichModelsInternal<TApi extends Api>(
|
|
|
5228
5604
|
for (const entry of deduped.values()) {
|
|
5229
5605
|
if (
|
|
5230
5606
|
(entry.supportsVision !== true && entry.supportsVision !== false) ||
|
|
5231
|
-
(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))
|
|
5232
5613
|
) {
|
|
5233
5614
|
needsMoreMetadata = true;
|
|
5234
5615
|
break;
|
|
@@ -5265,12 +5646,14 @@ export function litellmModelManagerOptions(config?: LiteLLMModelManagerConfig):
|
|
|
5265
5646
|
const baseUrl = config?.baseUrl ?? getDefaultModelDiscoveryBaseUrl("litellm")!;
|
|
5266
5647
|
return {
|
|
5267
5648
|
providerId: "litellm",
|
|
5268
|
-
// rich-
|
|
5269
|
-
//
|
|
5270
|
-
//
|
|
5271
|
-
//
|
|
5272
|
-
//
|
|
5273
|
-
//
|
|
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.
|
|
5274
5657
|
cacheProviderId: resolveModelCacheProviderId("litellm", { baseUrl }),
|
|
5275
5658
|
// litellm is a local-only proxy and is never bundled in models.json (that
|
|
5276
5659
|
// would leak the machine's localhost catalog). Prefer the proxy's richer
|
|
@@ -5793,7 +6176,7 @@ export function anthropicModelManagerOptions(
|
|
|
5793
6176
|
return {
|
|
5794
6177
|
providerId: "anthropic",
|
|
5795
6178
|
modelsDev: {
|
|
5796
|
-
fetch: () =>
|
|
6179
|
+
fetch: () => fetchRevalidatedWellKnownModelsWithTimeout(config?.fetch),
|
|
5797
6180
|
map: payload => mapAnthropicModelsDev(payload, baseUrl),
|
|
5798
6181
|
},
|
|
5799
6182
|
...(apiKey && {
|
|
@@ -6465,3 +6848,42 @@ export const MODELS_DEV_PROVIDER_DESCRIPTORS: readonly ModelsDevProviderDescript
|
|
|
6465
6848
|
...MODELS_DEV_PROVIDER_DESCRIPTORS_CODING_PLANS,
|
|
6466
6849
|
...MODELS_DEV_PROVIDER_DESCRIPTORS_SPECIALIZED,
|
|
6467
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
|
+
}
|