@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.
- package/CHANGELOG.md +39 -0
- package/dist/types/compat/behavior.d.ts +11 -0
- package/dist/types/compat/cascade.d.ts +10 -0
- package/dist/types/compat/collapse.d.ts +8 -0
- package/dist/types/compat/resolve.d.ts +10 -0
- package/dist/types/compat/types.d.ts +21 -0
- package/dist/types/discovery/antigravity.d.ts +10 -1
- package/dist/types/model-thinking.d.ts +7 -0
- package/dist/types/provider-models/openai-compat.d.ts +2 -0
- package/dist/types/types.d.ts +36 -1
- package/package.json +4 -4
- package/src/build.ts +59 -0
- package/src/compat/axes.ts +15 -0
- package/src/compat/behavior.ts +22 -2
- package/src/compat/cascade.ts +27 -1
- package/src/compat/collapse.ts +100 -8
- package/src/compat/context-window.ts +11 -1
- package/src/compat/resolve.ts +56 -17
- package/src/compat/rules/README.md +3 -1
- package/src/compat/rules/classes/deepseek.kdl +9 -1
- package/src/compat/rules/classes/kimi.kdl +6 -0
- package/src/compat/rules/providers/alibaba-token-plan.kdl +16 -8
- package/src/compat/rules/providers/amazon-bedrock.kdl +30 -0
- package/src/compat/rules/providers/azure.kdl +6 -0
- package/src/compat/rules/providers/cerebras.kdl +10 -0
- package/src/compat/rules/providers/commandcode.kdl +20 -4
- package/src/compat/rules/providers/cursor.kdl +32 -0
- package/src/compat/rules/providers/deepseek.kdl +5 -5
- package/src/compat/rules/providers/devin.kdl +46 -0
- package/src/compat/rules/providers/google-vertex.kdl +15 -0
- package/src/compat/rules/providers/kimi-code.kdl +48 -0
- package/src/compat/rules/providers/meta.kdl +3 -0
- package/src/compat/rules/providers/muse-code.kdl +3 -0
- package/src/compat/rules/providers/openrouter.kdl +6 -0
- package/src/compat/rules/runtime/behavior.kdl +17 -0
- package/src/compat/rules/taxonomy/deepseek.kdl +5 -0
- package/src/compat/rules.json +1 -1
- package/src/compat/types.ts +23 -0
- package/src/discovery/antigravity.ts +80 -43
- package/src/discovery/devin.ts +22 -4
- package/src/identity/bundled.ts +4 -3
- package/src/model-cache.ts +154 -120
- package/src/model-thinking.ts +10 -7
- package/src/models.json +1 -1
- package/src/provider-models/bundled-references.ts +4 -3
- package/src/provider-models/cache-provider-id.ts +14 -8
- package/src/provider-models/ollama.ts +11 -31
- package/src/provider-models/openai-compat.ts +297 -37
- 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
|
-
//
|
|
49
|
-
//
|
|
50
|
-
if (candidate.provider
|
|
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-
|
|
93
|
-
//
|
|
94
|
-
//
|
|
95
|
-
//
|
|
96
|
-
//
|
|
97
|
-
//
|
|
98
|
-
// (issue #9938).
|
|
99
|
-
return `litellm:rich-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
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
|
-
|
|
619
|
-
|
|
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
|
|
3870
|
-
// since fixed that. This const lets us bust
|
|
3871
|
-
// version of OMP pick up the reasoning levels
|
|
3872
|
-
|
|
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
|
|
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
|
-
| {
|
|
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 (
|
|
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-
|
|
5279
|
-
//
|
|
5280
|
-
//
|
|
5281
|
-
//
|
|
5282
|
-
//
|
|
5283
|
-
//
|
|
5284
|
-
//
|
|
5285
|
-
// past incomplete vision/API
|
|
5286
|
-
// pricing, stripped reseller usage
|
|
5287
|
-
// and mapped rich pricing. Bump the
|
|
5288
|
-
//
|
|
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
|
|
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
|
},
|