pi-provider-litellm 3.0.1 → 3.1.0

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/README.md CHANGED
@@ -176,6 +176,7 @@ Native Messages authenticates with `x-api-key`; every transport carries the `x-l
176
176
  | `LITELLM_DISCOVERY_TIMEOUT_MS` | `5000` | Background and explicit discovery fetch timeout in ms; `0` disables automatic discovery |
177
177
  | `LITELLM_CLI_JWT_EXPIRATION_HOURS` | `24` | CLI SSO token lifetime fallback for older proxies whose poll response omits `expires_in`; mirror a non-default proxy setting locally |
178
178
  | `LITELLM_VERBOSE_DISCOVERY` | unset | If `1`, enable progress messages during model and MCP discovery (login, refresh, startup), including MCP prepared/registered/dropped counts. Progress messages are off by default; MCP safety diagnostics (see below) are always reported regardless of this setting |
179
+ | `LITELLM_MODELS_DEV` | unset | If `1`, enrich discovered metadata (limits, prices, effort lists) from models.dev, cached for 28 days in `litellm-models-dev.json`. Off by default because LiteLLM's `/model/info` is authoritative; use it when LiteLLM's model map lacks a model's metadata |
179
180
 
180
181
  Only use a trusted `LITELLM_API_KEY_HELPER` or `!command`. Prefer an absolute executable path, keep secrets out of command arguments, and print only the token to stdout without logging it to stderr.
181
182
 
@@ -293,7 +294,7 @@ Before tagging a release, keep `package.json` and `package-lock.json` versions i
293
294
 
294
295
  Dynamic catalogs are persisted by Pi in `~/.pi/agent/models-store.json`. Credentials remain in `~/.pi/agent/auth.json`. Legacy `litellm-models.json` model caches are ignored and never deleted. `litellm-models-dev.json` is the models.dev cache and is refreshed in place.
295
296
 
296
- For every genuine `/model/info` row, including deployment details fetched through `/health`, the extension requests `https://models.dev/api.json` for enrichment and caches the result for 28 days in `~/.pi/agent/litellm-models-dev.json`. Health-only entries without deployment details use the bounded Pi catalog lookup without models.dev enrichment. `PI_OFFLINE` suppresses activation-time discovery and the models.dev request. `LITELLM_OFFLINE=1` also disables LiteLLM discovery; direct discovery callers use only an existing models.dev cache and do not refresh it.
297
+ LiteLLM's `/model/info` is the metadata authority, so models.dev enrichment is an opt-in escape hatch. With `LITELLM_MODELS_DEV=1`, for every genuine `/model/info` row, including deployment details fetched through `/health`, the extension requests `https://models.dev/api.json` for enrichment and caches the result for 28 days in `~/.pi/agent/litellm-models-dev.json`. Without it, discovery neither requests models.dev nor reads that cache; Pi's own catalog still applies. Health-only entries without deployment details use the bounded Pi catalog lookup without models.dev enrichment. `PI_OFFLINE` suppresses activation-time discovery and the models.dev request. `LITELLM_OFFLINE=1` also disables LiteLLM discovery; direct discovery callers use only an existing models.dev cache and do not refresh it.
297
298
 
298
299
  Opening `/model` refreshes configured provider catalogs in the background using Pi's native model lifecycle.
299
300
 
@@ -329,7 +330,7 @@ Discovery policy is versioned in Pi's persisted model catalog. After upgrading,
329
330
  LiteLLM may load-balance one public `model_name` across deployments with different backends or model versions. The extension reduces `/model/info` rows conservatively before publishing one Pi model. A route group that mixes a chat-style deployment with an explicitly incompatible mode such as embedding is withheld entirely and reported with a bounded diagnostic.
330
331
 
331
332
  - Responses is selected only when every deployment supports it under the protocol rules above; a deployment that requires Chat keeps the group on Chat.
332
- - Vision and reasoning are advertised only when every routable deployment resolves them as supported. An explicit `supports_reasoning: false` suppresses all thinking controls, even when catalog or router effort metadata exists. Catalog thinking controls are intersected per level: a level is exposed only when every deployment maps it to the same value, while disagreement or absence becomes an explicit denial. Reasoning selectors additionally require an accepted wire carrier on every deployment. Public effort lists are intersected, explicit LiteLLM denials win, and `xhigh`/`max` need unanimous explicit support. Null or absent standard-effort flags have no opinion.
333
+ - Vision and reasoning are advertised only when every routable deployment resolves them as supported. An explicit `supports_reasoning: false` suppresses all thinking controls, even when catalog or router effort metadata exists. Catalog thinking controls are intersected per level: a level is exposed only when every deployment maps it to the same value, while disagreement or absence becomes an explicit denial. Catalog maps are not complete effort lists, so absence has one exception: an omitted `minimal`, `low`, `medium` or `high` counts as Pi's default spelling of that level. An omitted `off`, `xhigh` or `max` still differs from an explicit mapping. Reasoning selectors additionally require an accepted wire carrier on every deployment. Public effort lists are intersected, explicit LiteLLM denials win, and `xhigh`/`max` need unanimous explicit support. Null or absent standard-effort flags have no opinion. A deployment that declares `model_info.reasoning_effort_levels` answers every level from that list, ahead of its per-level flags, as LiteLLM itself reads it.
333
334
  - Context and output limits use the minimum resolved value across deployments.
334
335
  - Each displayed base-price field uses the maximum only when every deployment resolves that field; unresolved fields remain zero and the model name is suffixed with ` (incomplete metadata)`.
335
336
  - When complete base pricing comes from the catalog under unanimous catalog authority, tiered pricing is the conservative worst-case envelope. Its usable finite non-negative thresholds are the sorted union of every deployment's thresholds, and each price field at each interval is the maximum applicable rate across all deployments, floored by each deployment's base rate. Differing tier breakpoints are therefore preserved rather than treated as incompatible. An explicit `/model/info` base-price field replaces catalog pricing for that field at every threshold; catalog tiers remain for unaffected fields and are omitted only when every base-price field is explicit.
@@ -345,7 +346,9 @@ The ` (incomplete metadata)` suffix marks reduced `/model/info` groups, incomple
345
346
 
346
347
  ### Reasoning controls
347
348
 
348
- Reasoning levels require deployment evidence for the parameter Pi sends: `reasoning_effort` for effort levels or `thinking` for a native thinking switch. Public route names never authorize these controls. Evidence-free fallback models retain catalog presentation metadata but expose no speculative selector.
349
+ Reasoning levels require deployment evidence for the parameter Pi sends: `reasoning_effort` for effort levels or `thinking` for a native thinking switch. Public route names never authorize these controls.
350
+
351
+ LiteLLM omits `supported_openai_params` for a deployment its model map does not describe. Such a deployment keeps the `reasoning_effort` carrier when its `model_info` explicitly sets `supports_reasoning: true`, which is the operator's opt-in. A `supported_openai_params` list that omits `reasoning_effort` still denies effort levels unless `litellm_params.allowed_openai_params` adds it. Kimi and DeepSeek generations are excluded because their contracts name their own carriers. Evidence-free fallback models retain catalog presentation metadata but expose no speculative selector.
349
352
 
350
353
  Kimi K2.5/K2.6 use an on/off thinking switch; K2.7 Code/Highspeed is always thinking. Kimi K3 and DeepSeek V4 use public effort evidence with LiteLLM overrides. Responses levels are translated to valid `reasoning.effort` values; Chat-only compatibility fields never reach Responses models. Legacy cached level maps remain usable while discovery refreshes, and stored Moonshot compatibility can restore safe display normalization.
351
354
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-provider-litellm",
3
- "version": "3.0.1",
3
+ "version": "3.1.0",
4
4
  "description": "LiteLLM proxy provider extension for Pi",
5
5
  "type": "module",
6
6
  "license": "MIT",
package/src/discover.ts CHANGED
@@ -399,7 +399,7 @@ function adaptPublicCatalogRecord(
399
399
  catalogModelId: string,
400
400
  record: PublicCatalogRecord | undefined,
401
401
  ): CatalogResolution {
402
- const piProvider = adapterCatalogProvider(record?.provider ?? provider);
402
+ const piProvider = adapterCatalogProvider(record?.piProvider ?? record?.provider ?? provider);
403
403
  const resolved = piProvider
404
404
  ? (resolveCatalogModel(record?.modelId ?? catalogModelId, piProvider) ??
405
405
  resolveCatalogModel(catalogModelId, piProvider))
package/src/index.ts CHANGED
@@ -31,7 +31,14 @@ import {
31
31
  } from "./mcp-tools.js";
32
32
  import { createLiteLLMProvider, DEFAULT_LITELLM_BASE_URL, isPlaceholderHost, toNativeModels } from "./provider.js";
33
33
  import { createSkillsPromptSection, createSkillToolDefinitions, listSkills } from "./skills.js";
34
- import type { LiteLLMApi, LiteLLMModel, LiteLLMModelPolicy, LiteLLMRuntimeAuth, ResolvedCredentials } from "./types.js";
34
+ import type {
35
+ DiscoveryOptions,
36
+ LiteLLMApi,
37
+ LiteLLMModel,
38
+ LiteLLMModelPolicy,
39
+ LiteLLMRuntimeAuth,
40
+ ResolvedCredentials,
41
+ } from "./types.js";
35
42
 
36
43
  const PROVIDER_NAME = "litellm";
37
44
  const SETTINGS_KEY = "litellm";
@@ -44,6 +51,7 @@ const ENV_TIMEOUT = "LITELLM_DISCOVERY_TIMEOUT_MS";
44
51
  const ENV_CLI_JWT_EXPIRATION_HOURS = "LITELLM_CLI_JWT_EXPIRATION_HOURS";
45
52
  const ENV_OFFLINE = "LITELLM_OFFLINE";
46
53
  const ENV_VERBOSE_DISCOVERY = "LITELLM_VERBOSE_DISCOVERY";
54
+ const ENV_MODELS_DEV = "LITELLM_MODELS_DEV";
47
55
  const MODELS_DEV_CACHE_FILENAME = "litellm-models-dev.json";
48
56
  const DEFAULT_TIMEOUT_MS = 5000;
49
57
  const SEED_TIMEOUT_MS = 3000;
@@ -444,6 +452,16 @@ function isHostOffline(): boolean {
444
452
  return process.env.PI_OFFLINE !== undefined;
445
453
  }
446
454
 
455
+ /**
456
+ * LiteLLM's /model/info is the metadata authority, so models.dev is an opt-in escape hatch.
457
+ * Without a cache path and with refresh suppressed, discovery reads neither the models.dev
458
+ * network nor a cache left by an earlier release; Pi's own catalog still applies.
459
+ */
460
+ function modelsDevDiscoveryOptions(): Pick<DiscoveryOptions, "modelsDev" | "modelsDevCachePath"> {
461
+ if (process.env[ENV_MODELS_DEV] !== "1") return { modelsDev: false };
462
+ return { modelsDev: !isHostOffline(), modelsDevCachePath: join(getAgentDir(), MODELS_DEV_CACHE_FILENAME) };
463
+ }
464
+
447
465
  /**
448
466
  * Absolute budget for activation-time discovery. Pi cannot paint its UI until every extension has
449
467
  * activated, so this is deliberately capped rather than following LITELLM_DISCOVERY_TIMEOUT_MS,
@@ -1866,8 +1884,7 @@ export default async function (pi: ExtensionAPI): Promise<void> {
1866
1884
  signal,
1867
1885
  headers: auth.headers,
1868
1886
  allowInsecureHttp: auth.allowInsecureHttp,
1869
- modelsDev: !isHostOffline(),
1870
- modelsDevCachePath: join(getAgentDir(), MODELS_DEV_CACHE_FILENAME),
1887
+ ...modelsDevDiscoveryOptions(),
1871
1888
  silent: !isVerboseDiscovery(),
1872
1889
  onProgress: isVerboseDiscovery() ? (message) => process.stderr.write(`LiteLLM: ${message}\n`) : undefined,
1873
1890
  });
@@ -181,6 +181,17 @@ function normalizeEffort(level: string): (typeof EXTENDED_LEVELS)[number] | unde
181
181
  return EXTENDED_LEVELS.find((candidate) => candidate === normalized);
182
182
  }
183
183
 
184
+ // LiteLLM reads a declared `reasoning_effort_levels` list whole, ahead of the
185
+ // per-level flags, because flags cannot state a set such as low/high/max: medium
186
+ // has no opt-out. A declared list therefore answers every level for its deployment.
187
+ function reportedLevel(entry: ModelInfoEntry, level: (typeof EXTENDED_LEVELS)[number]): boolean | undefined {
188
+ const declared: unknown = entry.model_info?.reasoning_effort_levels;
189
+ if (Array.isArray(declared)) {
190
+ return declared.some((effort) => typeof effort === "string" && normalizeEffort(effort) === level);
191
+ }
192
+ return wireBoolean(entry.model_info?.[LITELLM_LEVEL_FLAGS[level]]);
193
+ }
194
+
184
195
  // A public effort list is complete: listed standard levels are enabled and omitted
185
196
  // ones are denied. Without a public list, standard levels stay absent so Pi keeps
186
197
  // its defaults. LiteLLM flags may add or remove levels, while xhigh/max always
@@ -200,23 +211,25 @@ function reasoningLevelMap(
200
211
  map[level] = publicSets.every((set) => set.has(level)) ? (level === "off" ? "none" : level) : null;
201
212
  }
202
213
  }
203
- // A catalog level map is direct evidence even when its selectable projection
204
- // is empty. Preserve its denials independently from public effort lists, whose
205
- // unrecognized values intentionally have no opinion.
206
- for (const catalogMap of catalogMaps) Object.assign(map, deniedLevels(catalogMap));
207
-
208
- for (const [level, flag] of Object.entries(LITELLM_LEVEL_FLAGS) as Array<
209
- [keyof typeof LITELLM_LEVEL_FLAGS, (typeof LITELLM_LEVEL_FLAGS)[keyof typeof LITELLM_LEVEL_FLAGS]]
210
- >) {
211
- const reported = entries.map((entry) => wireBoolean(entry.model_info?.[flag]));
214
+ // A catalog level map is not a complete list: a null denies, a value supplies the
215
+ // wire spelling, and an omitted standard level keeps Pi's default. Flattening it
216
+ // into a list denied every standard level a map left implicit, such as Codex
217
+ // `gpt-5.6-sol`, which only states `xhigh`, `max`, and `minimal`.
218
+ for (const catalogMap of catalogMaps) {
219
+ for (const [level, value] of Object.entries(catalogMap ?? {}) as [keyof typeof map, string | null][]) {
220
+ if (value === null || (value !== undefined && map[level] !== null)) map[level] = value;
221
+ }
222
+ }
223
+
224
+ for (const level of EXTENDED_LEVELS) {
225
+ const reported = entries.map((entry) => reportedLevel(entry, level));
212
226
  if (reported.some((value) => value === false)) map[level] = null;
213
227
  else if (reported.length > 0 && reported.every((value) => value === true)) {
214
228
  map[level] = level === "off" ? "none" : level;
215
229
  }
216
230
  }
217
231
  for (const level of ["xhigh", "max"] as const) {
218
- const flag = LITELLM_LEVEL_FLAGS[level];
219
- if (!entries.every((entry) => wireBoolean(entry.model_info?.[flag]) === true)) map[level] = null;
232
+ if (!entries.every((entry) => reportedLevel(entry, level) === true)) map[level] = null;
220
233
  }
221
234
  return map;
222
235
  }
@@ -241,6 +254,15 @@ function acceptedParams(entry: ModelInfoEntry): Set<string> {
241
254
  return params;
242
255
  }
243
256
 
257
+ // LiteLLM omits supported_openai_params (or returns null) for a deployment its
258
+ // model map does not describe, so that absence says nothing about the carrier. An
259
+ // explicit `supports_reasoning: true` is the operator's opt-in there, as in
260
+ // LiteLLM's own effort resolution. Present but malformed data is not an absence.
261
+ function optsIntoEffortCarrier(entry: ModelInfoEntry): boolean {
262
+ const supported: unknown = entry.model_info?.supported_openai_params;
263
+ return (supported === undefined || supported === null) && wireBoolean(entry.model_info?.supports_reasoning) === true;
264
+ }
265
+
244
266
  function intersectParams(entries: readonly ModelInfoEntry[]): string[] {
245
267
  const [first, ...rest] = entries.map(acceptedParams);
246
268
  if (!first) return [];
@@ -830,16 +852,17 @@ export function reduceModelGroup(
830
852
  if (tiers) cost.tiers = tiers;
831
853
  }
832
854
  const acceptedOpenAIParams = intersectParams(deployments);
833
- const acceptsResponsesReasoningControl = acceptedOpenAIParams.includes("reasoning_effort");
834
- const publicEfforts = catalogAuthority.map(
835
- (catalog) =>
836
- catalog?.effortLevels ??
837
- (catalog?.thinkingLevelMap
838
- ? Object.entries(catalog.thinkingLevelMap)
839
- .filter(([, value]) => value !== null)
840
- .map(([level]) => level)
841
- : undefined),
855
+ // Kimi and DeepSeek generations name their own carriers, so an operator opt-in
856
+ // cannot substitute for the parameter evidence their contracts require.
857
+ const namedCarrierFamily = catalogs.some((catalog) =>
858
+ ["kimi", "deepseek", "conflicting"].includes(catalog?.semanticFamily ?? ""),
842
859
  );
860
+ const acceptsResponsesReasoningControl =
861
+ acceptedOpenAIParams.includes("reasoning_effort") ||
862
+ (!namedCarrierFamily &&
863
+ deployments.length > 0 &&
864
+ deployments.every((entry) => acceptedParams(entry).has("reasoning_effort") || optsIntoEffortCarrier(entry)));
865
+ const publicEfforts = catalogAuthority.map((catalog) => catalog?.effortLevels);
843
866
  const evidenceLevelMap = reasoningLevelMap(deployments, publicEfforts, [
844
867
  intersectThinkingLevelMaps(catalogAuthority.map((catalog) => catalog?.thinkingLevelMap)),
845
868
  ]);
@@ -848,10 +871,8 @@ export function reduceModelGroup(
848
871
  thinkingLevelMap = intersectThinkingLevelMaps(catalogs.map((catalog) => catalog?.messagesThinkingLevelMap));
849
872
  // Native serializer restrictions apply even when catalog pricing is withheld.
850
873
  // Router flags can also deny Pi's implicit default levels, never add a level.
851
- for (const [level, flag] of Object.entries(LITELLM_LEVEL_FLAGS) as Array<
852
- [keyof typeof LITELLM_LEVEL_FLAGS, (typeof LITELLM_LEVEL_FLAGS)[keyof typeof LITELLM_LEVEL_FLAGS]]
853
- >) {
854
- const reported = deployments.map((entry) => wireBoolean(entry.model_info?.[flag]));
874
+ for (const level of EXTENDED_LEVELS) {
875
+ const reported = deployments.map((entry) => reportedLevel(entry, level));
855
876
  if (
856
877
  reported.some((value) => value === false) ||
857
878
  ((level === "xhigh" || level === "max") &&
@@ -886,9 +907,7 @@ export function reduceModelGroup(
886
907
  const semanticMap = semanticReasoningPolicy.thinkingLevelMap;
887
908
  const hasEvidenceLevels = Object.values(evidenceLevelMap ?? {}).some((level) => level !== null);
888
909
  const semanticBinary = semanticReasoningPolicy.compat?.supportsReasoningEffort === false && !hasEvidenceLevels;
889
- const explicitOffDenial = deployments.some(
890
- (entry) => wireBoolean(entry.model_info?.supports_none_reasoning_effort) === false,
891
- );
910
+ const explicitOffDenial = deployments.some((entry) => reportedLevel(entry, "off") === false);
892
911
  const reasoningPolicy = semanticMap
893
912
  ? {
894
913
  ...semanticReasoningPolicy,
@@ -933,11 +952,6 @@ export function catalogResolution(provider: string, model: Model<Api>): CatalogR
933
952
  provider,
934
953
  catalogModelId: model.id,
935
954
  reasoning: model.reasoning,
936
- effortLevels: model.thinkingLevelMap
937
- ? Object.entries(model.thinkingLevelMap)
938
- .filter(([, value]) => value !== null)
939
- .map(([level]) => level)
940
- : undefined,
941
955
  thinkingLevelMap: model.thinkingLevelMap,
942
956
  vision: model.input.includes("image"),
943
957
  contextWindow: model.contextWindow,
@@ -12,6 +12,9 @@ const KNOWN_PROVIDERS = new Set<string>(getProviders());
12
12
  export interface PublicCatalogRecord {
13
13
  source: "models.dev" | "pi-vendor" | "pi-adapter";
14
14
  provider: string;
15
+ // The Pi catalog for fields a models.dev record omits, when the record was found
16
+ // under another vendor's key (ChatGPT routes read OpenAI's but bill as Codex).
17
+ piProvider?: string;
15
18
  modelId: string;
16
19
  limits?: { context?: number; output?: number };
17
20
  cost?: { input?: number; output?: number; cacheRead?: number; cacheWrite?: number };
@@ -54,6 +57,8 @@ const PROVIDER_ALIASES: Readonly<Record<string, readonly string[]>> = {
54
57
  azure_ai: ["azure", "openai"],
55
58
  bedrock: ["amazon-bedrock"],
56
59
  bedrock_converse: ["amazon-bedrock"],
60
+ // models.dev has no ChatGPT subscription provider; its routes serve OpenAI models.
61
+ chatgpt: ["chatgpt", "openai"],
57
62
  deepseek: ["deepseek"],
58
63
  fireworks: ["fireworks-ai"],
59
64
  fireworks_ai: ["fireworks-ai"],
@@ -199,11 +204,6 @@ function mapModelsDev(provider: string, modelId: string, model: ModelsDevModel):
199
204
 
200
205
  function toPiRecord(source: "pi-vendor" | "pi-adapter", provider: string, model: Model<Api>): PublicCatalogRecord {
201
206
  const thinkingLevelMap = model.thinkingLevelMap as Record<string, unknown> | undefined;
202
- const effortLevels = thinkingLevelMap
203
- ? Object.entries(thinkingLevelMap)
204
- .filter(([, value]) => value !== null)
205
- .map(([level]) => level)
206
- : [];
207
207
  return {
208
208
  source,
209
209
  provider,
@@ -216,7 +216,6 @@ function toPiRecord(source: "pi-vendor" | "pi-adapter", provider: string, model:
216
216
  cacheWrite: model.cost.cacheWrite,
217
217
  },
218
218
  modalities: model.input.includes("image") ? ["text", "image"] : ["text"],
219
- ...(effortLevels.length > 0 ? { effortLevels } : {}),
220
219
  ...(thinkingLevelMap ? { thinkingLevelMap } : {}),
221
220
  };
222
221
  }
@@ -245,14 +244,17 @@ export async function loadPublicCatalog(options: LoadPublicCatalogOptions = {}):
245
244
  lookup(provider, id) {
246
245
  const providers = providerCandidates(provider);
247
246
  const ids = lookupIds(id);
247
+ const vendor = providers.find((candidate) => candidate !== "azure") ?? providers[0];
248
248
  for (const candidate of providers) {
249
249
  const models = catalog?.[candidate]?.models;
250
250
  for (const modelId of ids) {
251
251
  const model = models?.[modelId];
252
- if (model) return mapModelsDev(candidate, modelId, model);
252
+ if (!model) continue;
253
+ const record = mapModelsDev(candidate, modelId, model);
254
+ if (candidate === providers[0] || !vendor) return record;
255
+ return { ...record, piProvider: (PI_PROVIDER_ALIASES[vendor] ?? [vendor])[0] };
253
256
  }
254
257
  }
255
- const vendor = providers.find((candidate) => candidate !== "azure") ?? providers[0];
256
258
  if (vendor) {
257
259
  for (const piProvider of PI_PROVIDER_ALIASES[vendor] ?? [vendor]) {
258
260
  const model = findPiModel(piProvider, ids);
@@ -19,6 +19,10 @@ type ThinkingLevelDefinitionsAreExhaustive = AssertNever<
19
19
 
20
20
  type ThinkingLevelMap = DiscoveredModel["thinkingLevelMap"];
21
21
 
22
+ // Pi sends an omitted standard level as its own name. An omitted `off` sends no
23
+ // disable value and omitted `xhigh`/`max` are unsupported, so those stay distinct.
24
+ const IMPLICIT_LEVELS = new Set<string>(["minimal", "low", "medium", "high"]);
25
+
22
26
  // Intersects independently authoritative maps. A level mentioned by any source
23
27
  // is denied unless every source supplies the same mapping for that level.
24
28
  export function intersectThinkingLevelMaps(maps: readonly ThinkingLevelMap[]): ThinkingLevelMap {
@@ -26,8 +30,9 @@ export function intersectThinkingLevelMaps(maps: readonly ThinkingLevelMap[]): T
26
30
 
27
31
  const intersection: NonNullable<ThinkingLevelMap> = {};
28
32
  for (const [level] of THINKING_LEVEL_DEFINITIONS) {
29
- const values = maps.map((map) => map?.[level]);
30
- if (values.every((value) => value === undefined)) continue;
33
+ const stated = maps.map((map) => map?.[level]);
34
+ if (stated.every((value) => value === undefined)) continue;
35
+ const values = stated.map((value) => (value === undefined && IMPLICIT_LEVELS.has(level) ? level : value));
31
36
  const first = values[0];
32
37
  intersection[level] = first !== undefined && values.every((value) => value === first) ? first : null;
33
38
  }
package/src/types.ts CHANGED
@@ -63,6 +63,8 @@ export interface DiscoveryOptions {
63
63
  signal?: AbortSignal;
64
64
  headers?: Record<string, string>;
65
65
  allowInsecureHttp?: boolean;
66
+ // `false` reads only an existing cache at `modelsDevCachePath`; with no cache
67
+ // path, models.dev is not consulted at all.
66
68
  modelsDev?: boolean;
67
69
  modelsDevCachePath?: string;
68
70
  }
@@ -89,6 +91,7 @@ export interface ModelInfoEntry {
89
91
  max_input_tokens?: number;
90
92
  max_output_tokens?: number;
91
93
  supports_reasoning?: boolean;
94
+ reasoning_effort_levels?: string[];
92
95
  supports_none_reasoning_effort?: boolean;
93
96
  supports_minimal_reasoning_effort?: boolean;
94
97
  supports_low_reasoning_effort?: boolean;