@oh-my-pi/pi-catalog 18.2.0 → 18.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/CHANGELOG.md +39 -0
  2. package/dist/types/compat/behavior.d.ts +11 -0
  3. package/dist/types/compat/cascade.d.ts +10 -0
  4. package/dist/types/compat/collapse.d.ts +8 -0
  5. package/dist/types/compat/resolve.d.ts +10 -0
  6. package/dist/types/compat/types.d.ts +21 -0
  7. package/dist/types/discovery/antigravity.d.ts +10 -1
  8. package/dist/types/model-thinking.d.ts +7 -0
  9. package/dist/types/provider-models/openai-compat.d.ts +2 -0
  10. package/dist/types/types.d.ts +36 -1
  11. package/package.json +4 -4
  12. package/src/build.ts +59 -0
  13. package/src/compat/axes.ts +15 -0
  14. package/src/compat/behavior.ts +22 -2
  15. package/src/compat/cascade.ts +27 -1
  16. package/src/compat/collapse.ts +100 -8
  17. package/src/compat/context-window.ts +11 -1
  18. package/src/compat/resolve.ts +56 -17
  19. package/src/compat/rules/README.md +3 -1
  20. package/src/compat/rules/classes/deepseek.kdl +9 -1
  21. package/src/compat/rules/classes/kimi.kdl +6 -0
  22. package/src/compat/rules/providers/alibaba-token-plan.kdl +16 -8
  23. package/src/compat/rules/providers/amazon-bedrock.kdl +30 -0
  24. package/src/compat/rules/providers/azure.kdl +6 -0
  25. package/src/compat/rules/providers/cerebras.kdl +10 -0
  26. package/src/compat/rules/providers/commandcode.kdl +20 -4
  27. package/src/compat/rules/providers/cursor.kdl +32 -0
  28. package/src/compat/rules/providers/deepseek.kdl +5 -5
  29. package/src/compat/rules/providers/devin.kdl +46 -0
  30. package/src/compat/rules/providers/google-vertex.kdl +15 -0
  31. package/src/compat/rules/providers/kimi-code.kdl +48 -0
  32. package/src/compat/rules/providers/meta.kdl +3 -0
  33. package/src/compat/rules/providers/muse-code.kdl +3 -0
  34. package/src/compat/rules/providers/openrouter.kdl +6 -0
  35. package/src/compat/rules/runtime/behavior.kdl +17 -0
  36. package/src/compat/rules/taxonomy/deepseek.kdl +5 -0
  37. package/src/compat/rules.json +1 -1
  38. package/src/compat/types.ts +23 -0
  39. package/src/discovery/antigravity.ts +80 -43
  40. package/src/discovery/devin.ts +22 -4
  41. package/src/identity/bundled.ts +4 -3
  42. package/src/model-cache.ts +154 -120
  43. package/src/model-thinking.ts +10 -7
  44. package/src/models.json +1 -1
  45. package/src/provider-models/bundled-references.ts +4 -3
  46. package/src/provider-models/cache-provider-id.ts +14 -8
  47. package/src/provider-models/ollama.ts +11 -31
  48. package/src/provider-models/openai-compat.ts +297 -37
  49. package/src/types.ts +38 -0
@@ -1,3 +1,4 @@
1
+ import { isBareIdReferenceProvider } from "../compat/behavior";
1
2
  import { isZeroCostXaiOAuthReference } from "../identity/reference";
2
3
  import { getBundledModels, getBundledProviders } from "../models";
3
4
  import type { Api, Model, ModelSpec } from "../types";
@@ -45,9 +46,9 @@ function getGlobalReferences(): Map<string, Model<Api>> {
45
46
  for (const provider of getBundledProviders()) {
46
47
  for (const model of getBundledModels(provider as Parameters<typeof getBundledModels>[0])) {
47
48
  const candidate = model as Model<Api>;
48
- // ClinePass limits, pricing, and reasoning controls are gateway-specific;
49
- // matching them by bare id would contaminate unrelated proxy models.
50
- if (candidate.provider === "cline-pass" || isZeroCostXaiOAuthReference(candidate)) {
49
+ // Gateway-specific metadata must remain provider-local when proxy
50
+ // discovery resolves references by bare model id.
51
+ if (!isBareIdReferenceProvider(candidate.provider) || isZeroCostXaiOAuthReference(candidate)) {
51
52
  continue;
52
53
  }
53
54
  const existing = references.get(candidate.id);
@@ -89,15 +89,21 @@ export function resolveModelCacheProviderId(providerId: string, options: ModelCa
89
89
  }
90
90
  case "litellm": {
91
91
  const baseUrl = options.baseUrl ?? getDefaultModelDiscoveryBaseUrl(providerId)!;
92
- // rich-v9 unions compat across the management endpoints and keys the
93
- // deployment's `supports_vision` declaration into it, so a warm
94
- // rich-v8 row would keep retracting axes an earlier endpoint reported
95
- // (issue #11982). rich-v8 invalidated rows whose `compatConfig`
96
- // retained a colliding bundled model's provider-specific transport
97
- // (e.g. Fireworks `wireModelIdMode`) before that leak was fixed
98
- // (issue #9938).
99
- return `litellm:rich-v9:${Bun.hash(baseUrl).toString(36)}`;
92
+ // rich-v11 invalidates rows that inherited ClinePass gateway metadata
93
+ // through generic models.dev bare-id enrichment (issue #10932). rich-v10
94
+ // filtered known non-conversational LiteLLM modes, unioned compat across
95
+ // the management endpoints, and keyed the deployment's `supports_vision`
96
+ // declaration into it; earlier versions invalidated rows whose
97
+ // `compatConfig` retained a colliding bundled model's provider-specific
98
+ // transport (e.g. Fireworks `wireModelIdMode`) (issue #9938).
99
+ return `litellm:rich-v11:${Bun.hash(baseUrl).toString(36)}`;
100
100
  }
101
+ case "gmi-cloud":
102
+ case "siliconflow":
103
+ case "siliconflow-cn":
104
+ // models-v1 moves rows enriched before cross-provider reference
105
+ // isolation out of the legacy bare-provider namespaces (#10932).
106
+ return `${providerId}:models-v1`;
101
107
  case "opencode-go":
102
108
  case "opencode-zen": {
103
109
  // v3: gateway-first rows cached before stencil enrichment carry null
@@ -1,9 +1,6 @@
1
1
  import { fetchWithRetry } from "@oh-my-pi/pi-utils";
2
- import { compareRevision, parseRevision } from "../compat/revision";
3
- import { classifyModel } from "../compat/taxonomy";
4
- import { Effort } from "../effort";
5
2
  import type { ModelManagerOptions } from "../model-manager";
6
- import type { FetchImpl, ModelSpec, ThinkingConfig } from "../types";
3
+ import type { FetchImpl, ModelSpec } from "../types";
7
4
  import { discoveryFetch } from "../utils";
8
5
  import { createBundledReferenceMap, createReferenceResolver } from "./bundled-references";
9
6
 
@@ -54,11 +51,6 @@ export function isOllamaCloudOutputCapped(id: string): boolean {
54
51
  return OLLAMA_CLOUD_OUTPUT_CAPPED_BASE_IDS[baseId] === true;
55
52
  }
56
53
 
57
- const OLLAMA_CLOUD_GLM_52_THINKING: ThinkingConfig = {
58
- mode: "effort",
59
- efforts: [Effort.High, Effort.Max],
60
- };
61
-
62
54
  function trimTrailingSlash(value: string): string {
63
55
  return value.endsWith("/") ? value.slice(0, -1) : value;
64
56
  }
@@ -93,27 +85,6 @@ function getContextWindow(modelInfo: Record<string, unknown> | undefined): numbe
93
85
  }
94
86
  }
95
87
 
96
- function getThinkingConfig(modelId: string, capabilities: string[] | undefined): ThinkingConfig | undefined {
97
- if (!capabilities?.includes("thinking")) {
98
- return undefined;
99
- }
100
- const identity = classifyModel("ollama-cloud", modelId, { lenient: true });
101
- const revision = identity.revision === undefined ? undefined : parseRevision(identity.revision);
102
- const floor = parseRevision(identity.family === "flash" ? "5.3" : "5.2");
103
- const isGlmEffortModel =
104
- identity.class === "glm" &&
105
- (identity.family === undefined ||
106
- identity.family === "air" ||
107
- identity.family === "turbo" ||
108
- identity.family === "flash") &&
109
- revision !== undefined &&
110
- floor !== undefined &&
111
- compareRevision(revision, floor) >= 0;
112
- if (isGlmEffortModel) {
113
- return OLLAMA_CLOUD_GLM_52_THINKING;
114
- }
115
- return { mode: "effort", efforts: [Effort.Minimal, Effort.Low, Effort.Medium, Effort.High] };
116
- }
117
88
  async function fetchShowMetadata(
118
89
  baseUrl: string,
119
90
  apiKey: string,
@@ -182,7 +153,16 @@ export function ollamaCloudModelManagerOptions(
182
153
  // reference limit, falling back to the historical safe cap otherwise.
183
154
  const contextWindow = discoveredContextWindow ?? 128000;
184
155
  const reasoning = capabilities ? capabilities.includes("thinking") : (reference?.reasoning ?? false);
185
- const thinking = capabilities ? getThinkingConfig(id, capabilities) : reference?.thinking;
156
+ // `/api/show` reports only a boolean `thinking` capability, never a
157
+ // tier vocabulary, so the effort ladder is left to the compat rules
158
+ // (which own every other host's ladder too). Synthesizing one here
159
+ // shadowed the rules: discovery rows carry explicit `thinking`, and
160
+ // `resolveThinkingPolicy` treats explicit metadata as authoritative
161
+ // over the KDL, so a fabricated `minimal..high` ladder overrode the
162
+ // DeepSeek V4 `low/high/max` contract and silently clamped `max`
163
+ // down to `high` (#8334 regression). Models whose ladder the rules
164
+ // do not know still fall back to the generic four-tier default.
165
+ const thinking = capabilities ? undefined : reference?.thinking;
186
166
  const input = capabilities
187
167
  ? capabilities.includes("vision")
188
168
  ? (["text", "image"] as Array<"text" | "image">)
@@ -4,13 +4,15 @@ import { toClinePassPublicModelId } from "../cline-pass-model-id";
4
4
  import {
5
5
  apiRouteExactModelIds,
6
6
  apiRouteFor,
7
+ isBareIdReferenceProvider,
8
+ isExcludedDiscoveryMode,
7
9
  isExcludedModel,
8
10
  isLikelyOpenAIResponsesId,
9
11
  modelLimitsFor,
10
12
  pricingPeerFor,
11
13
  } from "../compat/behavior";
12
14
  import { xaiResponsesReasoningEffortMap } from "../compat/openai";
13
- import { resolveModelPolicy } from "../compat/resolve";
15
+ import { hasModelScopedEffortLadder, resolveModelPolicy } from "../compat/resolve";
14
16
  import { compareRevision, parseRevision } from "../compat/revision";
15
17
  import { seedModels } from "../compat/providers";
16
18
  import { billingVariantPlain, classifyModel, discoveryVocabulary } from "../compat/taxonomy";
@@ -239,6 +241,192 @@ async function fetchCatalogPayload(
239
241
  return payload;
240
242
  }
241
243
 
244
+ /**
245
+ * The wire effort tiers the catalog publishes for a model, in canonical order.
246
+ * Undefined when the row has no effort-addressed thinking, or names no tier
247
+ * omp knows.
248
+ */
249
+ function publishedEffortLadder(model: ModelsDevModel): Effort[] | undefined {
250
+ const values = model.reasoning_options?.find(option => option?.type === "effort")?.values;
251
+ if (!Array.isArray(values)) return undefined;
252
+ const ladder = THINKING_EFFORTS.filter(effort => values.includes(effort));
253
+ return ladder.length > 0 ? ladder : undefined;
254
+ }
255
+
256
+ /**
257
+ * Published ladders, addressable both by the host that published them and by
258
+ * bare id.
259
+ *
260
+ * `byHost` (keyed `provider\0id`) is authoritative: a ladder is only valid for
261
+ * the deployment that published it, so the endpoint's own catalog identity is
262
+ * consulted first. `byId` answers when the host is unknown — a gateway id with
263
+ * no catalog provider of its own — and only while every host publishing that
264
+ * id agrees that it takes an effort dial and on which tiers; an id whose hosts
265
+ * disagree, or that any host publishes with no dial at all, is dropped from
266
+ * it, so the ladder stays unknown instead of borrowing an arbitrary host's.
267
+ *
268
+ * `withoutLadder` carries the same `provider\0id` key for a row the catalog
269
+ * does publish but with no effort dial on it. The host serving an id outranks
270
+ * every other host on the question of what that deployment accepts, so its
271
+ * silence blocks the bare-id fallback for that id rather than letting a
272
+ * foreign ladder answer in its place. When the serving host is unknown that
273
+ * per-host veto cannot fire, which is why a dialless row also disqualifies the
274
+ * bare id outright.
275
+ */
276
+ interface PublishedEffortLadders {
277
+ byHost: ReadonlyMap<string, readonly Effort[]>;
278
+ byId: ReadonlyMap<string, readonly Effort[]>;
279
+ withoutLadder: ReadonlySet<string>;
280
+ }
281
+
282
+ const EMPTY_PUBLISHED_EFFORT_LADDERS: PublishedEffortLadders = {
283
+ byHost: new Map(),
284
+ byId: new Map(),
285
+ withoutLadder: new Set(),
286
+ };
287
+
288
+ function indexPublishedEffortLadders(payload: unknown): PublishedEffortLadders {
289
+ const byHost = new Map<string, readonly Effort[]>();
290
+ const byId = new Map<string, readonly Effort[]>();
291
+ const withoutLadder = new Set<string>();
292
+ const index: PublishedEffortLadders = { byHost, byId, withoutLadder };
293
+ const unshareable = new Set<string>();
294
+ if (!isRecord(payload)) return index;
295
+ for (const [providerKey, provider] of Object.entries(payload)) {
296
+ if (!isRecord(provider) || !isRecord(provider.models)) continue;
297
+ for (const [modelId, rawModel] of Object.entries(provider.models)) {
298
+ if (!isRecord(rawModel)) continue;
299
+ const key = `${providerKey}\u0000${modelId}`;
300
+ const ladder = publishedEffortLadder(rawModel as ModelsDevModel);
301
+ if (!ladder) {
302
+ withoutLadder.add(key);
303
+ // Some deployment of this id rejects an effort dial; without
304
+ // knowing which host serves a bare id, none may claim one.
305
+ byId.delete(modelId);
306
+ unshareable.add(modelId);
307
+ continue;
308
+ }
309
+ byHost.set(key, ladder);
310
+ if (unshareable.has(modelId)) continue;
311
+ const shared = byId.get(modelId);
312
+ if (shared === undefined) {
313
+ byId.set(modelId, ladder);
314
+ } else if (shared.length !== ladder.length || shared.some((effort, index) => effort !== ladder[index])) {
315
+ byId.delete(modelId);
316
+ unshareable.add(modelId);
317
+ }
318
+ }
319
+ }
320
+ return index;
321
+ }
322
+
323
+ /**
324
+ * The index is tagged onto the catalog payload it was built from, so each
325
+ * catalog version is indexed once. {@link fetchWellKnownModels} already scopes
326
+ * payloads by fetch context, coalesces concurrent requests, answers a `304`
327
+ * with the same object, and hands back the last good payload when a refresh
328
+ * fails, so the tag inherits all of that lifetime behaviour for free.
329
+ */
330
+ const kPublishedEffortLadders = Symbol("catalog.publishedEffortLadders");
331
+
332
+ interface IndexedCatalogPayload {
333
+ [kPublishedEffortLadders]?: PublishedEffortLadders;
334
+ }
335
+
336
+ async function loadPublishedEffortLadders(fetchImpl?: FetchImpl): Promise<PublishedEffortLadders> {
337
+ const payload = await fetchWellKnownModels(fetchImpl);
338
+ if (!isRecord(payload)) return EMPTY_PUBLISHED_EFFORT_LADDERS;
339
+ const tagged = payload as IndexedCatalogPayload;
340
+ return (tagged[kPublishedEffortLadders] ??= indexPublishedEffortLadders(payload));
341
+ }
342
+
343
+ /**
344
+ * The catalog provider keys this endpoint publishes under. A provider whose
345
+ * catalog identity differs from its omp id (`moonshot` → `moonshotai`) is
346
+ * resolved through its descriptors; the omp id stays as a candidate for
347
+ * providers the descriptors do not cover.
348
+ */
349
+ function catalogProviderKeys(providerId: string): readonly string[] {
350
+ const keys = new Set<string>();
351
+ for (const descriptor of MODELS_DEV_DESCRIPTORS_BY_PROVIDER[providerId] ?? []) keys.add(descriptor.modelsDevKey);
352
+ keys.add(providerId);
353
+ return [...keys];
354
+ }
355
+
356
+ /**
357
+ * The ladder published for a discovered id, preferring the host that serves it.
358
+ *
359
+ * Gateway prefixes are peeled (`deepseek/deepseek-v4` → `deepseek-v4`), and
360
+ * each peeled segment joins the host candidates ahead of the endpoint's own
361
+ * keys: on an aggregator the prefix names the real upstream. All host-scoped
362
+ * candidates are checked before accepting any shared-id fallback.
363
+ * A bare id is only accepted from {@link PublishedEffortLadders.byId}, which
364
+ * holds it only while every publishing host agrees that it takes an effort
365
+ * dial and on which tiers. A host serving the id that published it without a
366
+ * dial is this deployment's own answer and outranks any other host's ladder,
367
+ * so it vetoes the candidate here; a dialless host omp cannot recognize as the
368
+ * server already kept the id out of `byId` when the index was built.
369
+ */
370
+ function lookupPublishedEffortLadder(
371
+ ladders: PublishedEffortLadders,
372
+ providerKeys: readonly string[],
373
+ modelId: string,
374
+ ): readonly Effort[] | undefined {
375
+ const hosts = [...providerKeys];
376
+ let shared: readonly Effort[] | undefined;
377
+ for (let candidate = modelId; ;) {
378
+ let dialless = false;
379
+ for (const host of hosts) {
380
+ const key = `${host}\u0000${candidate}`;
381
+ const scoped = ladders.byHost.get(key);
382
+ if (scoped) return scoped;
383
+ dialless ||= ladders.withoutLadder.has(key);
384
+ }
385
+ if (dialless) return undefined;
386
+ shared ??= ladders.byId.get(candidate);
387
+ const slash = candidate.indexOf("/");
388
+ if (slash < 0) return shared;
389
+ hosts.unshift(candidate.slice(0, slash));
390
+ candidate = candidate.slice(slash + 1);
391
+ }
392
+ }
393
+
394
+ /**
395
+ * Fill the effort ladder of discovered reasoning models whose tiers omp would
396
+ * otherwise guess from the neutral wire or provider-wide unknown-class default.
397
+ *
398
+ * Source precedence is unchanged: a provider that reports its own thinking
399
+ * surface, and any model whose ladder reviewed rules declare, are left exactly
400
+ * as they are. Only the guess is corrected, and only for ids the catalog
401
+ * actually publishes for this endpoint (or publishes unambiguously), so no
402
+ * request is made when every discovered model is already covered.
403
+ */
404
+ async function applyPublishedEffortLadders<TApi extends Api>(
405
+ models: readonly ModelSpec<TApi>[] | null,
406
+ providerId: string,
407
+ fetchImpl?: FetchImpl,
408
+ ): Promise<readonly ModelSpec<TApi>[] | null> {
409
+ if (models === null) return null;
410
+ const guessed = new Set(
411
+ models
412
+ .filter(
413
+ model => model.reasoning === true && model.thinking === undefined && !hasModelScopedEffortLadder(model),
414
+ )
415
+ .map(model => model.id),
416
+ );
417
+ if (guessed.size === 0) return models;
418
+ // An unreachable catalog with no prior payload is not a discovery failure:
419
+ // the endpoint's own listing stands and the guess stays in place.
420
+ const ladders = await loadPublishedEffortLadders(fetchImpl).catch(() => EMPTY_PUBLISHED_EFFORT_LADDERS);
421
+ if (ladders.byHost.size === 0) return models;
422
+ const providerKeys = catalogProviderKeys(providerId);
423
+ return models.map(model => {
424
+ if (!guessed.has(model.id)) return model;
425
+ const efforts = lookupPublishedEffortLadder(ladders, providerKeys, model.id);
426
+ return efforts ? { ...model, thinking: { mode: "effort" as const, efforts } } : model;
427
+ });
428
+ }
429
+
242
430
  function mapAnthropicModelsDev(payload: unknown, baseUrl: string): ModelSpec<"anthropic-messages">[] {
243
431
  if (!isRecord(payload)) {
244
432
  return [];
@@ -579,6 +767,7 @@ type OpenAICompatibleModelManagerBuilderOptions<TApi extends Api> = {
579
767
  dynamicModelsAuthoritative?: true;
580
768
  requireApiKey?: true;
581
769
  dropCachedModelIdsOnStaticMismatch?: readonly string[];
770
+ cacheProviderId?: string;
582
771
  filterModel?: (
583
772
  entry: OpenAICompatibleModelRecord,
584
773
  model: ModelSpec<TApi>,
@@ -600,24 +789,29 @@ function createOpenAICompatibleModelManagerOptions<TApi extends Api>(
600
789
  const filterModel = options.filterModel;
601
790
  return {
602
791
  providerId: options.providerId,
792
+ ...(options.cacheProviderId && { cacheProviderId: options.cacheProviderId }),
603
793
  ...(options.dynamicModelsAuthoritative && { dynamicModelsAuthoritative: true }),
604
794
  ...(options.dropCachedModelIdsOnStaticMismatch && {
605
795
  dropCachedModelIdsOnStaticMismatch: options.dropCachedModelIdsOnStaticMismatch,
606
796
  }),
607
797
  ...((!options.requireApiKey || apiKey) && {
608
- fetchDynamicModels: () =>
609
- fetchOpenAICompatibleModels({
610
- api: options.api,
611
- provider: options.providerId,
612
- baseUrl,
613
- apiKey,
614
- ...(options.headers && { headers: resolveSimpleProviderHeaders(options.headers) }),
615
- ...(filterModel && {
616
- filterModel: (entry, model) => filterModel(entry, model, references),
798
+ fetchDynamicModels: async () =>
799
+ applyPublishedEffortLadders(
800
+ await fetchOpenAICompatibleModels({
801
+ api: options.api,
802
+ provider: options.providerId,
803
+ baseUrl,
804
+ apiKey,
805
+ ...(options.headers && { headers: resolveSimpleProviderHeaders(options.headers) }),
806
+ ...(filterModel && {
807
+ filterModel: (entry, model) => filterModel(entry, model, references),
808
+ }),
809
+ mapModel: (entry, defaults) => options.mapModel(entry, defaults, references.get(defaults.id)),
810
+ fetch: options.config?.fetch,
617
811
  }),
618
- mapModel: (entry, defaults) => options.mapModel(entry, defaults, references.get(defaults.id)),
619
- fetch: options.config?.fetch,
620
- }),
812
+ options.providerId,
813
+ options.config?.fetch,
814
+ ),
621
815
  }),
622
816
  };
623
817
  }
@@ -982,6 +1176,7 @@ export function gmiCloudModelManagerOptions(
982
1176
  api: "openai-completions",
983
1177
  providerId: "gmi-cloud",
984
1178
  defaultBaseUrl: GMI_CLOUD_BASE_URL,
1179
+ cacheProviderId: resolveModelCacheProviderId("gmi-cloud"),
985
1180
  config,
986
1181
  requireApiKey: true,
987
1182
  mapModel: mapGmiCloudModel,
@@ -1201,6 +1396,13 @@ function mapDeepinfraModel(
1201
1396
  return null;
1202
1397
  }
1203
1398
  const pricing = isRecord(metadata.pricing) ? metadata.pricing : {};
1399
+ // `metadata.discount` is a promotional fraction in [0, 1): DeepInfra bills
1400
+ // `pricing * (1 - discount)` (verified against the site — GLM-5.2 lists
1401
+ // input 0.75 with discount 0.35 and charges 0.4875), while `pricing.*`
1402
+ // stays at list price. Fold it into the rate card so cost reporting matches
1403
+ // what the user is actually billed. Values outside (0, 1) are ignored.
1404
+ const discount = toNumber(metadata.discount);
1405
+ const discountMultiplier = discount !== undefined && discount > 0 && discount < 1 ? 1 - discount : 1;
1204
1406
  // `reasoning_effort` marks models whose effort dial is advertised. The
1205
1407
  // parameter itself is validated and accepted platform-wide on DeepInfra
1206
1408
  // (verified: 200 on effort-tagged, reasoning-only, and plain-chat models;
@@ -1243,9 +1445,9 @@ function mapDeepinfraModel(
1243
1445
  ...(thinking ? { thinking } : {}),
1244
1446
  input: tags.includes("vision") || tags.includes("vlm") ? ["text", "image"] : ["text"],
1245
1447
  cost: {
1246
- input: toPositiveNumber(pricing.input_tokens, 0),
1247
- output: toPositiveNumber(pricing.output_tokens, 0),
1248
- cacheRead: toPositiveNumber(pricing.cache_read_tokens, 0),
1448
+ input: toPositiveNumber(pricing.input_tokens, 0) * discountMultiplier,
1449
+ output: toPositiveNumber(pricing.output_tokens, 0) * discountMultiplier,
1450
+ cacheRead: toPositiveNumber(pricing.cache_read_tokens, 0) * discountMultiplier,
1249
1451
  cacheWrite: 0,
1250
1452
  },
1251
1453
  contextWindow,
@@ -1711,6 +1913,7 @@ function createSiliconFlowModelManagerOptions(
1711
1913
  const baseUrl = config?.baseUrl ?? defaultBaseUrl;
1712
1914
  return {
1713
1915
  providerId,
1916
+ cacheProviderId: resolveModelCacheProviderId(providerId),
1714
1917
  dynamicModelsAuthoritative: true,
1715
1918
  ...(apiKey && {
1716
1919
  fetchDynamicModels: async () => {
@@ -2134,6 +2337,9 @@ function createModelsDevReferenceMap<TApi extends Api>(
2134
2337
  const references = new Map<string, ModelSpec<TApi>>();
2135
2338
  for (const model of models) {
2136
2339
  const candidate = model as ModelSpec<TApi>;
2340
+ if (!isBareIdReferenceProvider(candidate.provider)) {
2341
+ continue;
2342
+ }
2137
2343
  const existing = references.get(candidate.id);
2138
2344
  if (!existing) {
2139
2345
  references.set(candidate.id, candidate);
@@ -3866,10 +4072,17 @@ export interface BasetenModelManagerConfig {
3866
4072
  fetch?: FetchImpl;
3867
4073
  }
3868
4074
 
3869
- // A previous version of OMP shipped this model without reasoning levels. We've
3870
- // since fixed that. This const lets us bust the cache so that users on that
3871
- // version of OMP pick up the reasoning levels immediately.
3872
- const BASETEN_CACHE_MIGRATION_MODEL_IDS = ["zai-org/GLM-5.3", "zai-org/GLM-5.3-Flash"] as const;
4075
+ // A previous version of OMP shipped these models without reasoning levels.
4076
+ // We've since fixed that (V4-generation whitelist). This const lets us bust
4077
+ // the cache so that users on that version of OMP pick up the reasoning levels
4078
+ // immediately.
4079
+ const BASETEN_CACHE_MIGRATION_MODEL_IDS = [
4080
+ "zai-org/GLM-5.3",
4081
+ "zai-org/GLM-5.3-Flash",
4082
+ "deepseek-ai/DeepSeek-V4-Flash-0731",
4083
+ "deepseek-ai/DeepSeek-V4.1-Flash",
4084
+ "deepseek-ai/DeepSeek-V4-Pro-0813",
4085
+ ] as const;
3873
4086
 
3874
4087
  export function basetenModelManagerOptions(
3875
4088
  config?: BasetenModelManagerConfig,
@@ -3900,7 +4113,7 @@ export function basetenModelManagerOptions(
3900
4113
  (identity.class === "kimi" && identity.family === "k3") ||
3901
4114
  isGlmReasoningIdentity("baseten", defaults.id, "5.2") ||
3902
4115
  defaults.id === "openai/gpt-oss-120b" ||
3903
- defaults.id === "deepseek-ai/DeepSeek-V4-Pro";
4116
+ isDeepseekV4Generation("baseten", defaults.id);
3904
4117
  const reasoning =
3905
4118
  isSupportedBasetenReasoningModel &&
3906
4119
  (features.includes("reasoning") || features.includes("reasoning_effort"));
@@ -4678,7 +4891,11 @@ type LiteLLMRichEndpointFailure = {
4678
4891
  error?: unknown;
4679
4892
  };
4680
4893
  type LiteLLMRichEndpointResult<TApi extends Api> =
4681
- | { models: LiteLLMRichEndpointModel<TApi>[]; incompleteVisionMetadata: boolean }
4894
+ | {
4895
+ models: LiteLLMRichEndpointModel<TApi>[];
4896
+ excludedModelIds: ReadonlySet<string>;
4897
+ incompleteVisionMetadata: boolean;
4898
+ }
4682
4899
  | { failure: LiteLLMRichEndpointFailure };
4683
4900
 
4684
4901
  const LITELLM_RICH_ENDPOINTS = ["/model_group/info", "/v2/model/info", "/model/info", "/v1/model/info"] as const;
@@ -4707,6 +4924,11 @@ function warnLiteLLMMetadataFallback(managementBaseUrl: string, failure: LiteLLM
4707
4924
  });
4708
4925
  }
4709
4926
 
4927
+ /** Exclude only known non-conversational modes; unknown and non-string modes remain selectable for aliases. */
4928
+ export function isSelectableLiteLLMModelMode(mode: unknown): boolean {
4929
+ return typeof mode !== "string" || !isExcludedDiscoveryMode("litellm", mode);
4930
+ }
4931
+
4710
4932
  export function normalizeLiteLLMManagementBaseUrl(baseUrl: string): string {
4711
4933
  const trimmed = baseUrl.trim().replace(/\/+$/g, "");
4712
4934
  if (!trimmed) {
@@ -4747,7 +4969,10 @@ function mapLiteLLMOpenAICompatibleModel(
4747
4969
  entry: OpenAICompatibleModelRecord,
4748
4970
  defaults: ModelSpec<Api>,
4749
4971
  reference: ModelSpec<Api> | undefined,
4750
- ): ModelSpec<Api> {
4972
+ ): ModelSpec<Api> | null {
4973
+ if (!isSelectableLiteLLMModelMode(entry.mode)) {
4974
+ return null;
4975
+ }
4751
4976
  const model = mapWithBundledReference(entry, defaults, reference);
4752
4977
  return {
4753
4978
  ...model,
@@ -4943,7 +5168,10 @@ function mapLiteLLMRichEntry<TApi extends Api>(
4943
5168
  options: FetchLiteLLMRichModelsOptions<TApi>,
4944
5169
  runtimeBaseUrl: string,
4945
5170
  ): ModelSpec<TApi> | null {
4946
- if (isLiteLLMUnusableSentinelPlaceholder(entry)) {
5171
+ if (
5172
+ !isSelectableLiteLLMModelMode(getLiteLLMMetadataValue(entry, "mode")) ||
5173
+ isLiteLLMUnusableSentinelPlaceholder(entry)
5174
+ ) {
4947
5175
  return null;
4948
5176
  }
4949
5177
  const id = getLiteLLMRichModelId(entry);
@@ -5151,7 +5379,22 @@ async function fetchLiteLLMRichEndpoint<TApi extends Api>(
5151
5379
  return null;
5152
5380
  }
5153
5381
  const deduped = new Map<string, LiteLLMRichEndpointModel<TApi>>();
5382
+ const excludedModelIds = new Set<string>();
5154
5383
  for (const entry of entries) {
5384
+ if (isLiteLLMUnusableSentinelPlaceholder(entry)) {
5385
+ continue;
5386
+ }
5387
+ const modelId = getLiteLLMRichModelId(entry);
5388
+ if (!isSelectableLiteLLMModelMode(getLiteLLMMetadataValue(entry, "mode"))) {
5389
+ if (modelId) {
5390
+ excludedModelIds.add(modelId);
5391
+ deduped.delete(modelId);
5392
+ }
5393
+ continue;
5394
+ }
5395
+ if (modelId && excludedModelIds.has(modelId)) {
5396
+ continue;
5397
+ }
5155
5398
  const model = mapLiteLLMRichEntry(entry, options, runtimeBaseUrl);
5156
5399
  if (model) {
5157
5400
  const supportsVision = getLiteLLMMetadataValue(entry, "supports_vision");
@@ -5177,12 +5420,13 @@ async function fetchLiteLLMRichEndpoint<TApi extends Api>(
5177
5420
  deduped.set(model.id, existing ? mergeLiteLLMRichEndpointModels(existing, next) : next);
5178
5421
  }
5179
5422
  }
5180
- if (deduped.size === 0) {
5423
+ if (deduped.size === 0 && excludedModelIds.size === 0) {
5181
5424
  return null;
5182
5425
  }
5183
5426
  const models = Array.from(deduped.values()).sort((left, right) => left.model.id.localeCompare(right.model.id));
5184
5427
  return {
5185
5428
  models,
5429
+ excludedModelIds,
5186
5430
  incompleteVisionMetadata: models.some(entry => entry.supportsVision !== true && entry.supportsVision !== false),
5187
5431
  };
5188
5432
  }
@@ -5197,6 +5441,7 @@ async function fetchLiteLLMRichModelsInternal<TApi extends Api>(
5197
5441
  }
5198
5442
  const fetchModels = async (signal?: AbortSignal): Promise<ModelSpec<TApi>[] | null> => {
5199
5443
  const deduped = new Map<string, LiteLLMRichEndpointModel<TApi>>();
5444
+ const excludedModelIds = new Set<string>();
5200
5445
  let metadataFailure: LiteLLMRichEndpointFailure | undefined;
5201
5446
  for (const endpoint of LITELLM_RICH_ENDPOINTS) {
5202
5447
  const result = await fetchLiteLLMRichEndpoint(endpoint, options, managementBaseUrl, runtimeBaseUrl, signal);
@@ -5218,8 +5463,15 @@ async function fetchLiteLLMRichModelsInternal<TApi extends Api>(
5218
5463
  }
5219
5464
  continue;
5220
5465
  }
5466
+ for (const modelId of result.excludedModelIds) {
5467
+ excludedModelIds.add(modelId);
5468
+ deduped.delete(modelId);
5469
+ }
5221
5470
  const hadPriorModels = deduped.size > 0;
5222
5471
  for (const next of result.models) {
5472
+ if (excludedModelIds.has(next.model.id)) {
5473
+ continue;
5474
+ }
5223
5475
  const existing = deduped.get(next.model.id);
5224
5476
  if (!existing) {
5225
5477
  if (!hadPriorModels) {
@@ -5229,6 +5481,9 @@ async function fetchLiteLLMRichModelsInternal<TApi extends Api>(
5229
5481
  }
5230
5482
  deduped.set(next.model.id, mergeLiteLLMRichEndpointModels(existing, next));
5231
5483
  }
5484
+ if (deduped.size === 0) {
5485
+ continue;
5486
+ }
5232
5487
  let needsMoreMetadata = false;
5233
5488
  for (const entry of deduped.values()) {
5234
5489
  if (
@@ -5249,6 +5504,9 @@ async function fetchLiteLLMRichModelsInternal<TApi extends Api>(
5249
5504
  }
5250
5505
  }
5251
5506
  if (deduped.size === 0) {
5507
+ if (excludedModelIds.size > 0) {
5508
+ return [];
5509
+ }
5252
5510
  if (metadataFailure) {
5253
5511
  warnLiteLLMMetadataFallback(managementBaseUrl, metadataFailure);
5254
5512
  }
@@ -5275,17 +5533,18 @@ export function litellmModelManagerOptions(config?: LiteLLMModelManagerConfig):
5275
5533
  const baseUrl = config?.baseUrl ?? getDefaultModelDiscoveryBaseUrl("litellm")!;
5276
5534
  return {
5277
5535
  providerId: "litellm",
5278
- // rich-v9 keys the deployment's `supports_vision` declaration into the
5279
- // cached compat and unions compat across management endpoints instead of
5280
- // letting a later one retract what an earlier one reported (issue
5281
- // #11982). rich-v8 invalidated rows whose `compatConfig` retained a
5282
- // colliding bundled model's provider-specific transport (e.g. Fireworks
5283
- // `wireModelIdMode`) before that leak was fixed. Earlier versions added
5284
- // bundled reference fallback, moved OpenAI models to Responses, continued
5285
- // past incomplete vision/API metadata and endpoints omitting cache
5286
- // pricing, stripped reseller usage suffixes, filtered placeholder rows,
5287
- // and mapped rich pricing. Bump the version whenever these mappers change,
5288
- // or warm authoritative caches keep serving pre-change rows for the full TTL.
5536
+ // rich-v11 invalidates rows that inherited ClinePass gateway metadata
5537
+ // through generic models.dev bare-id enrichment (issue #10932). rich-v10
5538
+ // filtered known non-conversational LiteLLM modes, keyed the deployment's
5539
+ // `supports_vision` declaration into cached compat, and unioned compat
5540
+ // across management endpoints instead of letting a later endpoint retract
5541
+ // what an earlier one reported (issue #11982). Earlier versions fixed
5542
+ // provider-specific transport leakage, added bundled reference fallback,
5543
+ // moved OpenAI models to Responses, continued past incomplete vision/API
5544
+ // metadata and endpoints omitting cache pricing, stripped reseller usage
5545
+ // suffixes, filtered placeholder rows, and mapped rich pricing. Bump the
5546
+ // version whenever these mappers change, or warm authoritative caches keep
5547
+ // serving pre-change rows for the full TTL.
5289
5548
  cacheProviderId: resolveModelCacheProviderId("litellm", { baseUrl }),
5290
5549
  // litellm is a local-only proxy and is never bundled in models.json (that
5291
5550
  // would leak the machine's localhost catalog). Prefer the proxy's richer
@@ -5304,7 +5563,7 @@ export function litellmModelManagerOptions(config?: LiteLLMModelManagerConfig):
5304
5563
  resolveApi: resolveLiteLLMApi,
5305
5564
  timeoutMs: 10_000,
5306
5565
  });
5307
- if (richModels && richModels.length > 0) {
5566
+ if (richModels !== null) {
5308
5567
  return richModels;
5309
5568
  }
5310
5569
  return fetchOpenAICompatibleModels<Api>({
@@ -6308,6 +6567,7 @@ const MODELS_DEV_PROVIDER_DESCRIPTORS_CORE: readonly ModelsDevProviderDescriptor
6308
6567
  return {
6309
6568
  ...model,
6310
6569
  id,
6570
+ name: id,
6311
6571
  thinking: model.reasoning ? buildClinePassThinking(raw, model) : undefined,
6312
6572
  };
6313
6573
  },