pi-provider-litellm 3.0.1 → 3.2.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.
@@ -2,7 +2,32 @@ import type { Api, Model } from "@earendil-works/pi-ai";
2
2
  import { intersectThinkingLevelMaps, THINKING_LEVEL_DEFINITIONS } from "./thinking-levels.js";
3
3
  import type { DiscoveredModel, ModelInfoEntry } from "./types.js";
4
4
 
5
- export const DEFAULT_CONTEXT_WINDOW = 128_000;
5
+ const BUILTIN_CONTEXT_WINDOW = 128_000;
6
+
7
+ /**
8
+ * Context window used when neither /model/info nor the catalog reports one.
9
+ * LITELLM_DEFAULT_CONTEXT_WINDOW raises it for proxies whose model map cannot
10
+ * populate `max_input_tokens`; an unset or unusable value keeps the 128K default.
11
+ */
12
+ export function defaultContextWindow(): number {
13
+ // Number, not parseInt: parseInt takes a numeric prefix, so `1.5` would become a
14
+ // 1-token window and `922000junk` would pass as a limit instead of being rejected.
15
+ const parsed = Number(process.env.LITELLM_DEFAULT_CONTEXT_WINDOW ?? "");
16
+ return Number.isSafeInteger(parsed) && parsed > 0 ? parsed : BUILTIN_CONTEXT_WINDOW;
17
+ }
18
+
19
+ /**
20
+ * Whether a stored window is one this extension assumed rather than read. A model cached
21
+ * under the built-in default must still read as evidence-free once an operator configures
22
+ * LITELLM_DEFAULT_CONTEXT_WINDOW, or the setting would never reach cached entries.
23
+ * ponytail: a window stored under a setting that was later lowered is indistinguishable
24
+ * from measured evidence and stays until the next online discovery; persist a fallback
25
+ * marker on the model if that ever matters.
26
+ */
27
+ export function isFallbackContextWindow(contextWindow: number): boolean {
28
+ return contextWindow === BUILTIN_CONTEXT_WINDOW || contextWindow === defaultContextWindow();
29
+ }
30
+
6
31
  export const DEFAULT_MAX_TOKENS = 16_384;
7
32
 
8
33
  export type SemanticFamily = "claude" | "deepseek" | "gemini" | "kimi" | "openai";
@@ -52,6 +77,7 @@ export interface ReducedModelGroup {
52
77
  thinkingLevelMap?: DiscoveredModel["thinkingLevelMap"];
53
78
  vision: boolean;
54
79
  contextWindow: number;
80
+ contextWindowDefaulted?: boolean;
55
81
  maxTokens: number;
56
82
  cost: DiscoveredModel["cost"];
57
83
  hasCompleteCost: boolean;
@@ -181,6 +207,17 @@ function normalizeEffort(level: string): (typeof EXTENDED_LEVELS)[number] | unde
181
207
  return EXTENDED_LEVELS.find((candidate) => candidate === normalized);
182
208
  }
183
209
 
210
+ // LiteLLM reads a declared `reasoning_effort_levels` list whole, ahead of the
211
+ // per-level flags, because flags cannot state a set such as low/high/max: medium
212
+ // has no opt-out. A declared list therefore answers every level for its deployment.
213
+ function reportedLevel(entry: ModelInfoEntry, level: (typeof EXTENDED_LEVELS)[number]): boolean | undefined {
214
+ const declared: unknown = entry.model_info?.reasoning_effort_levels;
215
+ if (Array.isArray(declared)) {
216
+ return declared.some((effort) => typeof effort === "string" && normalizeEffort(effort) === level);
217
+ }
218
+ return wireBoolean(entry.model_info?.[LITELLM_LEVEL_FLAGS[level]]);
219
+ }
220
+
184
221
  // A public effort list is complete: listed standard levels are enabled and omitted
185
222
  // ones are denied. Without a public list, standard levels stay absent so Pi keeps
186
223
  // its defaults. LiteLLM flags may add or remove levels, while xhigh/max always
@@ -200,23 +237,25 @@ function reasoningLevelMap(
200
237
  map[level] = publicSets.every((set) => set.has(level)) ? (level === "off" ? "none" : level) : null;
201
238
  }
202
239
  }
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]));
240
+ // A catalog level map is not a complete list: a null denies, a value supplies the
241
+ // wire spelling, and an omitted standard level keeps Pi's default. Flattening it
242
+ // into a list denied every standard level a map left implicit, such as Codex
243
+ // `gpt-5.6-sol`, which only states `xhigh`, `max`, and `minimal`.
244
+ for (const catalogMap of catalogMaps) {
245
+ for (const [level, value] of Object.entries(catalogMap ?? {}) as [keyof typeof map, string | null][]) {
246
+ if (value === null || (value !== undefined && map[level] !== null)) map[level] = value;
247
+ }
248
+ }
249
+
250
+ for (const level of EXTENDED_LEVELS) {
251
+ const reported = entries.map((entry) => reportedLevel(entry, level));
212
252
  if (reported.some((value) => value === false)) map[level] = null;
213
253
  else if (reported.length > 0 && reported.every((value) => value === true)) {
214
254
  map[level] = level === "off" ? "none" : level;
215
255
  }
216
256
  }
217
257
  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;
258
+ if (!entries.every((entry) => reportedLevel(entry, level) === true)) map[level] = null;
220
259
  }
221
260
  return map;
222
261
  }
@@ -241,6 +280,15 @@ function acceptedParams(entry: ModelInfoEntry): Set<string> {
241
280
  return params;
242
281
  }
243
282
 
283
+ // LiteLLM omits supported_openai_params (or returns null) for a deployment its
284
+ // model map does not describe, so that absence says nothing about the carrier. An
285
+ // explicit `supports_reasoning: true` is the operator's opt-in there, as in
286
+ // LiteLLM's own effort resolution. Present but malformed data is not an absence.
287
+ function optsIntoEffortCarrier(entry: ModelInfoEntry): boolean {
288
+ const supported: unknown = entry.model_info?.supported_openai_params;
289
+ return (supported === undefined || supported === null) && wireBoolean(entry.model_info?.supports_reasoning) === true;
290
+ }
291
+
244
292
  function intersectParams(entries: readonly ModelInfoEntry[]): string[] {
245
293
  const [first, ...rest] = entries.map(acceptedParams);
246
294
  if (!first) return [];
@@ -779,7 +827,8 @@ export function reduceModelGroup(
779
827
  (entry) => wireBoolean(entry.model_info?.supports_reasoning) === false,
780
828
  );
781
829
  const vision = visionEvidence.every((value) => value ?? false);
782
- const contextWindow = min(contextWindowEvidence.map((value) => value ?? DEFAULT_CONTEXT_WINDOW));
830
+ const fallbackContextWindow = defaultContextWindow();
831
+ const contextWindow = min(contextWindowEvidence.map((value) => value ?? fallbackContextWindow));
783
832
  const maxTokens = min(maxTokensEvidence.map((value) => value ?? DEFAULT_MAX_TOKENS));
784
833
 
785
834
  const costValues = COST_FIELDS.map((field) =>
@@ -830,16 +879,17 @@ export function reduceModelGroup(
830
879
  if (tiers) cost.tiers = tiers;
831
880
  }
832
881
  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),
882
+ // Kimi and DeepSeek generations name their own carriers, so an operator opt-in
883
+ // cannot substitute for the parameter evidence their contracts require.
884
+ const namedCarrierFamily = catalogs.some((catalog) =>
885
+ ["kimi", "deepseek", "conflicting"].includes(catalog?.semanticFamily ?? ""),
842
886
  );
887
+ const acceptsResponsesReasoningControl =
888
+ acceptedOpenAIParams.includes("reasoning_effort") ||
889
+ (!namedCarrierFamily &&
890
+ deployments.length > 0 &&
891
+ deployments.every((entry) => acceptedParams(entry).has("reasoning_effort") || optsIntoEffortCarrier(entry)));
892
+ const publicEfforts = catalogAuthority.map((catalog) => catalog?.effortLevels);
843
893
  const evidenceLevelMap = reasoningLevelMap(deployments, publicEfforts, [
844
894
  intersectThinkingLevelMaps(catalogAuthority.map((catalog) => catalog?.thinkingLevelMap)),
845
895
  ]);
@@ -848,10 +898,8 @@ export function reduceModelGroup(
848
898
  thinkingLevelMap = intersectThinkingLevelMaps(catalogs.map((catalog) => catalog?.messagesThinkingLevelMap));
849
899
  // Native serializer restrictions apply even when catalog pricing is withheld.
850
900
  // 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]));
901
+ for (const level of EXTENDED_LEVELS) {
902
+ const reported = deployments.map((entry) => reportedLevel(entry, level));
855
903
  if (
856
904
  reported.some((value) => value === false) ||
857
905
  ((level === "xhigh" || level === "max") &&
@@ -886,9 +934,7 @@ export function reduceModelGroup(
886
934
  const semanticMap = semanticReasoningPolicy.thinkingLevelMap;
887
935
  const hasEvidenceLevels = Object.values(evidenceLevelMap ?? {}).some((level) => level !== null);
888
936
  const semanticBinary = semanticReasoningPolicy.compat?.supportsReasoningEffort === false && !hasEvidenceLevels;
889
- const explicitOffDenial = deployments.some(
890
- (entry) => wireBoolean(entry.model_info?.supports_none_reasoning_effort) === false,
891
- );
937
+ const explicitOffDenial = deployments.some((entry) => reportedLevel(entry, "off") === false);
892
938
  const reasoningPolicy = semanticMap
893
939
  ? {
894
940
  ...semanticReasoningPolicy,
@@ -911,6 +957,9 @@ export function reduceModelGroup(
911
957
  ...(thinkingLevelMap ? { thinkingLevelMap } : {}),
912
958
  vision,
913
959
  contextWindow,
960
+ ...(contextWindowEvidence.includes(undefined) && contextWindow === fallbackContextWindow
961
+ ? { contextWindowDefaulted: true }
962
+ : {}),
914
963
  maxTokens,
915
964
  cost,
916
965
  hasCompleteCost,
@@ -933,11 +982,6 @@ export function catalogResolution(provider: string, model: Model<Api>): CatalogR
933
982
  provider,
934
983
  catalogModelId: model.id,
935
984
  reasoning: model.reasoning,
936
- effortLevels: model.thinkingLevelMap
937
- ? Object.entries(model.thinkingLevelMap)
938
- .filter(([, value]) => value !== null)
939
- .map(([level]) => level)
940
- : undefined,
941
985
  thinkingLevelMap: model.thinkingLevelMap,
942
986
  vision: model.input.includes("image"),
943
987
  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"],
@@ -68,6 +73,7 @@ const PROVIDER_ALIASES: Readonly<Record<string, readonly string[]>> = {
68
73
  const PI_PROVIDER_ALIASES: Readonly<Record<string, readonly string[]>> = {
69
74
  "amazon-bedrock": ["amazon-bedrock"],
70
75
  azure: ["azure-openai-responses"],
76
+ azure_ai: ["azure-openai-responses"],
71
77
  chatgpt: ["openai-codex"],
72
78
  "fireworks-ai": ["fireworks"],
73
79
  openai: ["openai"],
@@ -199,11 +205,6 @@ function mapModelsDev(provider: string, modelId: string, model: ModelsDevModel):
199
205
 
200
206
  function toPiRecord(source: "pi-vendor" | "pi-adapter", provider: string, model: Model<Api>): PublicCatalogRecord {
201
207
  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
208
  return {
208
209
  source,
209
210
  provider,
@@ -216,7 +217,6 @@ function toPiRecord(source: "pi-vendor" | "pi-adapter", provider: string, model:
216
217
  cacheWrite: model.cost.cacheWrite,
217
218
  },
218
219
  modalities: model.input.includes("image") ? ["text", "image"] : ["text"],
219
- ...(effortLevels.length > 0 ? { effortLevels } : {}),
220
220
  ...(thinkingLevelMap ? { thinkingLevelMap } : {}),
221
221
  };
222
222
  }
@@ -245,25 +245,34 @@ export async function loadPublicCatalog(options: LoadPublicCatalogOptions = {}):
245
245
  lookup(provider, id) {
246
246
  const providers = providerCandidates(provider);
247
247
  const ids = lookupIds(id);
248
+ const vendor = providers.find((candidate) => candidate !== "azure") ?? providers[0];
249
+ const adapter = provider?.trim().toLowerCase();
248
250
  for (const candidate of providers) {
249
251
  const models = catalog?.[candidate]?.models;
250
252
  for (const modelId of ids) {
251
253
  const model = models?.[modelId];
252
- if (model) return mapModelsDev(candidate, modelId, model);
254
+ if (!model) continue;
255
+ const record = mapModelsDev(candidate, modelId, model);
256
+ if (candidate === providers[0] || !vendor) return record;
257
+ const fallback =
258
+ adapter && (PI_PROVIDER_ALIASES[adapter] ?? [adapter]).some((piProvider) => findPiModel(piProvider, ids))
259
+ ? adapter
260
+ : vendor;
261
+ return { ...record, piProvider: (PI_PROVIDER_ALIASES[fallback] ?? [fallback])[0] };
253
262
  }
254
263
  }
255
- const vendor = providers.find((candidate) => candidate !== "azure") ?? providers[0];
256
- if (vendor) {
257
- for (const piProvider of PI_PROVIDER_ALIASES[vendor] ?? [vendor]) {
264
+ // Provider-specific limits and pricing must not be replaced by the generic
265
+ // vendor's entry just because both catalogs recognize the model id.
266
+ if (adapter && adapter !== vendor) {
267
+ for (const piProvider of PI_PROVIDER_ALIASES[adapter] ?? [adapter]) {
258
268
  const model = findPiModel(piProvider, ids);
259
- if (model) return toPiRecord("pi-vendor", piProvider, model);
269
+ if (model) return toPiRecord("pi-adapter", piProvider, model);
260
270
  }
261
271
  }
262
- const adapter = provider?.trim().toLowerCase();
263
- if (adapter) {
264
- for (const piProvider of PI_PROVIDER_ALIASES[adapter] ?? [adapter]) {
272
+ if (vendor) {
273
+ for (const piProvider of PI_PROVIDER_ALIASES[vendor] ?? [vendor]) {
265
274
  const model = findPiModel(piProvider, ids);
266
- if (model) return toPiRecord("pi-adapter", piProvider, model);
275
+ if (model) return toPiRecord("pi-vendor", piProvider, model);
267
276
  }
268
277
  }
269
278
  return undefined;
package/src/skills.ts CHANGED
@@ -42,97 +42,55 @@ export async function listSkills(
42
42
  return skillsCache.skills;
43
43
  }
44
44
 
45
- const fetchSkills = async (path: string): Promise<LiteLLMSkill[]> => {
45
+ const fetchSkills = async (path: string): Promise<LiteLLMSkill[] | undefined> => {
46
46
  try {
47
47
  const response = await fetch(`${normalizedBaseUrl}${path}`, {
48
48
  headers: { ...headers, Authorization: `Bearer ${apiKey}`, Accept: "application/json" },
49
49
  signal: AbortSignal.timeout(10_000),
50
50
  });
51
- return response.ok ? getSkillsFromBody(await response.json()) : [];
51
+ return response.ok ? getSkillsFromBody(await response.json()) : undefined;
52
52
  } catch {
53
- return [];
53
+ return undefined;
54
54
  }
55
55
  };
56
56
 
57
- const [skillHub, legacy] = await Promise.all([
58
- fetchSkills("/claude-code/marketplace.json"),
59
- fetchSkills("/v1/skills"),
60
- ]);
61
- const skillHubNames = new Set(skillHub.map((skill) => skill.name));
62
- const skills = [...skillHub, ...legacy.filter((skill) => !skillHubNames.has(skill.name))];
57
+ const skills = (await fetchSkills("/claude-code/marketplace.json")) ?? (await fetchSkills("/v1/skills")) ?? [];
63
58
  skillsCache = { baseUrl: normalizedBaseUrl, apiKey, fetchedAt: Date.now(), skills };
64
59
  return skills;
65
60
  }
66
61
 
62
+ // Only the Skill Hub creates skills from JSON. LiteLLM's POST /v1/skills is Anthropic's multipart
63
+ // Skills API, so a JSON code skill sent there never becomes a listable skill.
67
64
  export async function createSkill(
68
65
  baseUrl: string,
69
66
  apiKey: string,
70
- input: {
71
- name: string;
72
- description?: string;
73
- source?: Record<string, unknown>;
74
- code?: string;
75
- inputSchema?: Record<string, unknown>;
76
- },
67
+ input: { name: string; description?: string; source: Record<string, unknown> },
77
68
  headers?: Record<string, string>,
78
69
  allowInsecureHttp = false,
79
70
  ): Promise<unknown> {
80
- if (!input.source && !input.code) throw new Error("code is required when source is omitted");
71
+ if (!input.source) throw new Error("source is required: skills are created through the LiteLLM Skill Hub");
81
72
  const normalizedBaseUrl = normalizeBaseUrl(baseUrl, allowInsecureHttp);
82
- let usedSkillHub = input.source !== undefined;
83
- const skillHubPayload = input.source
84
- ? {
85
- name: input.name,
86
- description: input.description,
87
- source: input.source,
88
- }
89
- : {
90
- name: input.name,
91
- description: input.description,
92
- code: input.code,
93
- input_schema: input.inputSchema ?? { type: "object", properties: {} },
94
- };
95
- let response = await fetch(`${normalizedBaseUrl}${input.source ? "/claude-code/plugins" : "/v1/skills"}`, {
73
+ const response = await fetch(`${normalizedBaseUrl}/claude-code/plugins`, {
96
74
  method: "POST",
97
75
  headers: {
98
76
  ...headers,
99
77
  Authorization: `Bearer ${apiKey}`,
100
78
  "Content-Type": "application/json",
101
79
  },
102
- body: JSON.stringify(skillHubPayload),
80
+ body: JSON.stringify({ name: input.name, description: input.description, source: input.source }),
103
81
  signal: AbortSignal.timeout(10_000),
104
82
  });
105
- if (input.source && response.status === 404) {
106
- usedSkillHub = false;
107
- response = await fetch(`${normalizedBaseUrl}/v1/skills`, {
108
- method: "POST",
109
- headers: {
110
- ...headers,
111
- Authorization: `Bearer ${apiKey}`,
112
- "Content-Type": "application/json",
113
- },
114
- body: JSON.stringify({
115
- name: input.name,
116
- description: input.description,
117
- code: input.code,
118
- input_schema: input.inputSchema ?? { type: "object", properties: {} },
119
- }),
120
- signal: AbortSignal.timeout(10_000),
121
- });
122
- }
123
83
  if (!response.ok) throw new Error(`LiteLLM skill create failed: HTTP ${response.status}`);
124
84
  skillsCache = undefined;
125
- if (usedSkillHub) {
126
- const enableResponse = await fetch(
127
- `${normalizedBaseUrl}/claude-code/plugins/${encodeURIComponent(input.name)}/enable`,
128
- {
129
- method: "POST",
130
- headers: { ...headers, Authorization: `Bearer ${apiKey}` },
131
- signal: AbortSignal.timeout(10_000),
132
- },
133
- );
134
- if (!enableResponse.ok) throw new Error(`LiteLLM skill enable failed: HTTP ${enableResponse.status}`);
135
- }
85
+ const enableResponse = await fetch(
86
+ `${normalizedBaseUrl}/claude-code/plugins/${encodeURIComponent(input.name)}/enable`,
87
+ {
88
+ method: "POST",
89
+ headers: { ...headers, Authorization: `Bearer ${apiKey}` },
90
+ signal: AbortSignal.timeout(10_000),
91
+ },
92
+ );
93
+ if (!enableResponse.ok) throw new Error(`LiteLLM skill enable failed: HTTP ${enableResponse.status}`);
136
94
  return response.json().catch(() => ({}));
137
95
  }
138
96
 
@@ -196,9 +154,7 @@ function formatSkills(skills: LiteLLMSkill[]): string {
196
154
  const CreateSkillParams = Type.Object({
197
155
  name: Type.String({ description: "Skill name" }),
198
156
  description: Type.Optional(Type.String({ description: "Skill description" })),
199
- sourceJson: Type.Optional(Type.String({ description: "Optional Skill Hub source metadata JSON object" })),
200
- code: Type.Optional(Type.String({ description: "Legacy Skills Gateway implementation or prompt code" })),
201
- inputSchemaJson: Type.Optional(Type.String({ description: "Optional JSON Schema string for skill inputs" })),
157
+ sourceJson: Type.String({ description: "Skill Hub source metadata JSON object" }),
202
158
  });
203
159
 
204
160
  const DeleteSkillParams = Type.Object({
@@ -232,24 +188,15 @@ export function createSkillToolDefinitions(
232
188
  defineTool({
233
189
  name: "litellm_skill_create",
234
190
  label: "Create LiteLLM Skill",
235
- description: "Create a skill on the LiteLLM proxy Skills Gateway.",
191
+ description: "Create and enable a skill on the LiteLLM proxy Skill Hub from source metadata.",
236
192
  parameters: CreateSkillParams,
237
193
  async execute(_toolCallId, params: Static<typeof CreateSkillParams>, _signal, _onUpdate, ctx) {
238
194
  const auth = await getAuth(ctx);
239
- const source = params.sourceJson ? parseJsonObject(params.sourceJson, "sourceJson") : undefined;
240
- const inputSchema = params.inputSchemaJson
241
- ? parseJsonObject(params.inputSchemaJson, "inputSchemaJson")
242
- : undefined;
195
+ const source = parseJsonObject(params.sourceJson, "sourceJson");
243
196
  const result = await createSkill(
244
197
  auth.baseUrl,
245
198
  auth.apiKey,
246
- {
247
- name: params.name,
248
- description: params.description,
249
- source,
250
- code: params.code,
251
- inputSchema,
252
- },
199
+ { name: params.name, description: params.description, source },
253
200
  auth.headers,
254
201
  auth.allowInsecureHttp,
255
202
  );
@@ -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;