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.
- package/README.md +25 -10
- package/package.json +1 -1
- package/src/backend-identity.ts +14 -4
- package/src/discover.ts +42 -17
- package/src/index.ts +211 -50
- package/src/mcp-tools.ts +35 -22
- package/src/model-groups.ts +78 -34
- package/src/public-catalog.ts +24 -15
- package/src/skills.ts +23 -76
- package/src/thinking-levels.ts +7 -2
- package/src/types.ts +3 -0
package/src/model-groups.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
|
204
|
-
//
|
|
205
|
-
//
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
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
|
|
852
|
-
|
|
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,
|
package/src/public-catalog.ts
CHANGED
|
@@ -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)
|
|
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
|
-
|
|
256
|
-
|
|
257
|
-
|
|
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-
|
|
269
|
+
if (model) return toPiRecord("pi-adapter", piProvider, model);
|
|
260
270
|
}
|
|
261
271
|
}
|
|
262
|
-
|
|
263
|
-
|
|
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-
|
|
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
|
|
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
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
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.
|
|
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
|
|
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 =
|
|
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
|
);
|
package/src/thinking-levels.ts
CHANGED
|
@@ -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
|
|
30
|
-
if (
|
|
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;
|