pi-provider-litellm 2.3.0 → 3.0.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 +148 -24
- package/package.json +13 -18
- package/src/backend-identity.ts +100 -0
- package/src/cost.ts +52 -0
- package/src/discover.ts +1318 -0
- package/src/gcloud-token.ts +160 -0
- package/src/index.ts +2005 -0
- package/src/mcp-tools.ts +1341 -0
- package/src/model-groups.ts +947 -0
- package/src/protocols.ts +42 -0
- package/src/provider.ts +248 -0
- package/src/public-catalog.ts +272 -0
- package/src/skills.ts +275 -0
- package/src/thinking-levels.ts +35 -0
- package/src/types.ts +162 -0
- package/dist/cost.d.ts +0 -2
- package/dist/cost.js +0 -49
- package/dist/discover.d.ts +0 -12
- package/dist/discover.js +0 -380
- package/dist/gcloud-token-cli.d.ts +0 -1
- package/dist/gcloud-token-cli.js +0 -8
- package/dist/gcloud-token.d.ts +0 -7
- package/dist/gcloud-token.js +0 -133
- package/dist/index.d.ts +0 -2
- package/dist/index.js +0 -1226
- package/dist/litellm.d.ts +0 -1
- package/dist/litellm.js +0 -10
- package/dist/mcp-tools.d.ts +0 -5
- package/dist/mcp-tools.js +0 -181
- package/dist/provider.d.ts +0 -16
- package/dist/provider.js +0 -40
- package/dist/skills.d.ts +0 -14
- package/dist/skills.js +0 -225
- package/dist/types.d.ts +0 -108
- package/dist/types.js +0 -1
package/src/skills.ts
ADDED
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
import type { Static } from "@earendil-works/pi-ai";
|
|
2
|
+
import { Type } from "@earendil-works/pi-ai";
|
|
3
|
+
import { defineTool, type ExtensionContext, type ToolDefinition } from "@earendil-works/pi-coding-agent";
|
|
4
|
+
import { normalizeBaseUrl } from "./discover.js";
|
|
5
|
+
import type { LiteLLMRuntimeAuth, LiteLLMSkill } from "./types.js";
|
|
6
|
+
|
|
7
|
+
const SKILLS_CACHE_TTL_MS = 60_000;
|
|
8
|
+
|
|
9
|
+
let skillsCache:
|
|
10
|
+
| {
|
|
11
|
+
baseUrl: string;
|
|
12
|
+
apiKey: string;
|
|
13
|
+
fetchedAt: number;
|
|
14
|
+
skills: LiteLLMSkill[];
|
|
15
|
+
}
|
|
16
|
+
| undefined;
|
|
17
|
+
|
|
18
|
+
function getSkillsFromBody(body: unknown): LiteLLMSkill[] {
|
|
19
|
+
if (Array.isArray(body)) return body as LiteLLMSkill[];
|
|
20
|
+
if (body && typeof body === "object") {
|
|
21
|
+
const record = body as { plugins?: unknown; data?: unknown; skills?: unknown };
|
|
22
|
+
if (Array.isArray(record.plugins)) return record.plugins as LiteLLMSkill[];
|
|
23
|
+
if (Array.isArray(record.data)) return record.data as LiteLLMSkill[];
|
|
24
|
+
if (Array.isArray(record.skills)) return record.skills as LiteLLMSkill[];
|
|
25
|
+
}
|
|
26
|
+
return [];
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export async function listSkills(
|
|
30
|
+
baseUrl: string,
|
|
31
|
+
apiKey: string,
|
|
32
|
+
headers?: Record<string, string>,
|
|
33
|
+
allowInsecureHttp = false,
|
|
34
|
+
): Promise<LiteLLMSkill[]> {
|
|
35
|
+
const normalizedBaseUrl = normalizeBaseUrl(baseUrl, allowInsecureHttp);
|
|
36
|
+
if (
|
|
37
|
+
skillsCache &&
|
|
38
|
+
skillsCache.baseUrl === normalizedBaseUrl &&
|
|
39
|
+
skillsCache.apiKey === apiKey &&
|
|
40
|
+
Date.now() - skillsCache.fetchedAt < SKILLS_CACHE_TTL_MS
|
|
41
|
+
) {
|
|
42
|
+
return skillsCache.skills;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const fetchSkills = async (path: string): Promise<LiteLLMSkill[]> => {
|
|
46
|
+
try {
|
|
47
|
+
const response = await fetch(`${normalizedBaseUrl}${path}`, {
|
|
48
|
+
headers: { ...headers, Authorization: `Bearer ${apiKey}`, Accept: "application/json" },
|
|
49
|
+
signal: AbortSignal.timeout(10_000),
|
|
50
|
+
});
|
|
51
|
+
return response.ok ? getSkillsFromBody(await response.json()) : [];
|
|
52
|
+
} catch {
|
|
53
|
+
return [];
|
|
54
|
+
}
|
|
55
|
+
};
|
|
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))];
|
|
63
|
+
skillsCache = { baseUrl: normalizedBaseUrl, apiKey, fetchedAt: Date.now(), skills };
|
|
64
|
+
return skills;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export async function createSkill(
|
|
68
|
+
baseUrl: string,
|
|
69
|
+
apiKey: string,
|
|
70
|
+
input: {
|
|
71
|
+
name: string;
|
|
72
|
+
description?: string;
|
|
73
|
+
source?: Record<string, unknown>;
|
|
74
|
+
code?: string;
|
|
75
|
+
inputSchema?: Record<string, unknown>;
|
|
76
|
+
},
|
|
77
|
+
headers?: Record<string, string>,
|
|
78
|
+
allowInsecureHttp = false,
|
|
79
|
+
): Promise<unknown> {
|
|
80
|
+
if (!input.source && !input.code) throw new Error("code is required when source is omitted");
|
|
81
|
+
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"}`, {
|
|
96
|
+
method: "POST",
|
|
97
|
+
headers: {
|
|
98
|
+
...headers,
|
|
99
|
+
Authorization: `Bearer ${apiKey}`,
|
|
100
|
+
"Content-Type": "application/json",
|
|
101
|
+
},
|
|
102
|
+
body: JSON.stringify(skillHubPayload),
|
|
103
|
+
signal: AbortSignal.timeout(10_000),
|
|
104
|
+
});
|
|
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
|
+
if (!response.ok) throw new Error(`LiteLLM skill create failed: HTTP ${response.status}`);
|
|
124
|
+
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
|
+
}
|
|
136
|
+
return response.json().catch(() => ({}));
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export async function deleteSkill(
|
|
140
|
+
baseUrl: string,
|
|
141
|
+
apiKey: string,
|
|
142
|
+
skillId: string,
|
|
143
|
+
headers?: Record<string, string>,
|
|
144
|
+
allowInsecureHttp = false,
|
|
145
|
+
): Promise<void> {
|
|
146
|
+
const normalizedBaseUrl = normalizeBaseUrl(baseUrl, allowInsecureHttp);
|
|
147
|
+
let skillHubError: unknown;
|
|
148
|
+
const skillHubResponse = await fetch(`${normalizedBaseUrl}/claude-code/plugins/${encodeURIComponent(skillId)}`, {
|
|
149
|
+
method: "DELETE",
|
|
150
|
+
headers: { ...headers, Authorization: `Bearer ${apiKey}`, Accept: "application/json" },
|
|
151
|
+
signal: AbortSignal.timeout(10_000),
|
|
152
|
+
}).catch((error: unknown) => {
|
|
153
|
+
skillHubError = error;
|
|
154
|
+
return undefined;
|
|
155
|
+
});
|
|
156
|
+
let response = skillHubResponse;
|
|
157
|
+
if (!response || response.status === 404 || response.status >= 500) {
|
|
158
|
+
response = await fetch(`${normalizedBaseUrl}/v1/skills/${encodeURIComponent(skillId)}`, {
|
|
159
|
+
method: "DELETE",
|
|
160
|
+
headers: { ...headers, Authorization: `Bearer ${apiKey}` },
|
|
161
|
+
signal: AbortSignal.timeout(10_000),
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
if (response.status === 404 && skillHubResponse?.status !== 404) {
|
|
165
|
+
if (skillHubError instanceof Error) throw skillHubError;
|
|
166
|
+
throw new Error(`LiteLLM skill delete failed: HTTP ${skillHubResponse?.status}`);
|
|
167
|
+
}
|
|
168
|
+
if (!response.ok && response.status !== 404) throw new Error(`LiteLLM skill delete failed: HTTP ${response.status}`);
|
|
169
|
+
skillsCache = undefined;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function escapeXml(value: string): string {
|
|
173
|
+
return value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
export function createSkillsPromptSection(skills: LiteLLMSkill[]): string | undefined {
|
|
177
|
+
const enabled = skills.filter((skill) => skill.enabled !== false && skill.name);
|
|
178
|
+
if (enabled.length === 0) return undefined;
|
|
179
|
+
|
|
180
|
+
const body = enabled
|
|
181
|
+
.map((skill) => {
|
|
182
|
+
const description = skill.description?.trim() || "No description provided.";
|
|
183
|
+
return `<skill name="${escapeXml(skill.name)}">\n${escapeXml(description)}\n</skill>`;
|
|
184
|
+
})
|
|
185
|
+
.join("\n");
|
|
186
|
+
return `LiteLLM Skills Gateway provided these active skills. Use them as additional guidance when relevant.\n<litellm_skills>\n${body}\n</litellm_skills>`;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function formatSkills(skills: LiteLLMSkill[]): string {
|
|
190
|
+
if (skills.length === 0) return "No LiteLLM skills are registered.";
|
|
191
|
+
return skills
|
|
192
|
+
.map((skill) => `- ${skill.name}${skill.enabled === false ? " (disabled)" : ""}: ${skill.description ?? ""}`)
|
|
193
|
+
.join("\n");
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const CreateSkillParams = Type.Object({
|
|
197
|
+
name: Type.String({ description: "Skill name" }),
|
|
198
|
+
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" })),
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
const DeleteSkillParams = Type.Object({
|
|
205
|
+
skillId: Type.String({ description: "LiteLLM skill id" }),
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
function parseJsonObject(value: string, fieldName: string): Record<string, unknown> {
|
|
209
|
+
const parsed = JSON.parse(value) as unknown;
|
|
210
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
211
|
+
throw new Error(`${fieldName} must be a JSON object`);
|
|
212
|
+
}
|
|
213
|
+
return parsed as Record<string, unknown>;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
export function createSkillToolDefinitions(
|
|
217
|
+
getAuth: (ctx?: ExtensionContext) => Promise<LiteLLMRuntimeAuth>,
|
|
218
|
+
): ToolDefinition[] {
|
|
219
|
+
return [
|
|
220
|
+
defineTool({
|
|
221
|
+
name: "litellm_skill_list",
|
|
222
|
+
label: "LiteLLM Skills",
|
|
223
|
+
description: "List skills registered on the LiteLLM proxy Skills Gateway.",
|
|
224
|
+
promptSnippet: "List LiteLLM Skills Gateway skills",
|
|
225
|
+
parameters: Type.Object({}),
|
|
226
|
+
async execute(_toolCallId, _params, _signal, _onUpdate, ctx) {
|
|
227
|
+
const auth = await getAuth(ctx);
|
|
228
|
+
const skills = await listSkills(auth.baseUrl, auth.apiKey, auth.headers, auth.allowInsecureHttp);
|
|
229
|
+
return { content: [{ type: "text", text: formatSkills(skills) }], details: { count: skills.length } };
|
|
230
|
+
},
|
|
231
|
+
}),
|
|
232
|
+
defineTool({
|
|
233
|
+
name: "litellm_skill_create",
|
|
234
|
+
label: "Create LiteLLM Skill",
|
|
235
|
+
description: "Create a skill on the LiteLLM proxy Skills Gateway.",
|
|
236
|
+
parameters: CreateSkillParams,
|
|
237
|
+
async execute(_toolCallId, params: Static<typeof CreateSkillParams>, _signal, _onUpdate, ctx) {
|
|
238
|
+
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;
|
|
243
|
+
const result = await createSkill(
|
|
244
|
+
auth.baseUrl,
|
|
245
|
+
auth.apiKey,
|
|
246
|
+
{
|
|
247
|
+
name: params.name,
|
|
248
|
+
description: params.description,
|
|
249
|
+
source,
|
|
250
|
+
code: params.code,
|
|
251
|
+
inputSchema,
|
|
252
|
+
},
|
|
253
|
+
auth.headers,
|
|
254
|
+
auth.allowInsecureHttp,
|
|
255
|
+
);
|
|
256
|
+
return { content: [{ type: "text", text: "LiteLLM skill created." }], details: { result } };
|
|
257
|
+
},
|
|
258
|
+
}),
|
|
259
|
+
defineTool({
|
|
260
|
+
name: "litellm_skill_delete",
|
|
261
|
+
label: "Delete LiteLLM Skill",
|
|
262
|
+
description: "Delete a skill from the LiteLLM proxy Skills Gateway.",
|
|
263
|
+
parameters: DeleteSkillParams,
|
|
264
|
+
async execute(_toolCallId, params: Static<typeof DeleteSkillParams>, _signal, _onUpdate, ctx) {
|
|
265
|
+
const auth = await getAuth(ctx);
|
|
266
|
+
await deleteSkill(auth.baseUrl, auth.apiKey, params.skillId, auth.headers, auth.allowInsecureHttp);
|
|
267
|
+
return { content: [{ type: "text", text: `LiteLLM skill deleted: ${params.skillId}` }], details: {} };
|
|
268
|
+
},
|
|
269
|
+
}),
|
|
270
|
+
];
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
export function resetSkillsCache(): void {
|
|
274
|
+
skillsCache = undefined;
|
|
275
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { ModelThinkingLevel } from "@earendil-works/pi-ai";
|
|
2
|
+
import type { DiscoveredModel } from "./types.js";
|
|
3
|
+
|
|
4
|
+
export const THINKING_LEVEL_DEFINITIONS = [
|
|
5
|
+
["off", "none", "supports_none_reasoning_effort"],
|
|
6
|
+
["minimal", "minimal", "supports_minimal_reasoning_effort"],
|
|
7
|
+
["low", "low", "supports_low_reasoning_effort"],
|
|
8
|
+
["medium", "medium", undefined],
|
|
9
|
+
["high", "high", undefined],
|
|
10
|
+
["xhigh", "xhigh", "supports_xhigh_reasoning_effort"],
|
|
11
|
+
["max", "max", "supports_max_reasoning_effort"],
|
|
12
|
+
] as const;
|
|
13
|
+
|
|
14
|
+
type AssertNever<T extends never> = T;
|
|
15
|
+
// biome-ignore lint/correctness/noUnusedVariables: This assertion must fail if Pi adds an uncovered thinking level.
|
|
16
|
+
type ThinkingLevelDefinitionsAreExhaustive = AssertNever<
|
|
17
|
+
Exclude<ModelThinkingLevel, (typeof THINKING_LEVEL_DEFINITIONS)[number][0]>
|
|
18
|
+
>;
|
|
19
|
+
|
|
20
|
+
type ThinkingLevelMap = DiscoveredModel["thinkingLevelMap"];
|
|
21
|
+
|
|
22
|
+
// Intersects independently authoritative maps. A level mentioned by any source
|
|
23
|
+
// is denied unless every source supplies the same mapping for that level.
|
|
24
|
+
export function intersectThinkingLevelMaps(maps: readonly ThinkingLevelMap[]): ThinkingLevelMap {
|
|
25
|
+
if (maps.every((map) => map === undefined)) return undefined;
|
|
26
|
+
|
|
27
|
+
const intersection: NonNullable<ThinkingLevelMap> = {};
|
|
28
|
+
for (const [level] of THINKING_LEVEL_DEFINITIONS) {
|
|
29
|
+
const values = maps.map((map) => map?.[level]);
|
|
30
|
+
if (values.every((value) => value === undefined)) continue;
|
|
31
|
+
const first = values[0];
|
|
32
|
+
intersection[level] = first !== undefined && values.every((value) => value === first) ? first : null;
|
|
33
|
+
}
|
|
34
|
+
return Object.keys(intersection).length > 0 ? intersection : undefined;
|
|
35
|
+
}
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import type { Model } from "@earendil-works/pi-ai";
|
|
2
|
+
import type { BackendFamily, LITELLM_DISCOVERY_VERSION } from "./backend-identity.js";
|
|
3
|
+
|
|
4
|
+
export type DiscoverySource = "model_info" | "models_list" | "health";
|
|
5
|
+
|
|
6
|
+
export type LiteLLMApi = "anthropic-messages" | "openai-completions" | "openai-responses";
|
|
7
|
+
|
|
8
|
+
export type LiteLLMRuntimeAuth = {
|
|
9
|
+
baseUrl: string;
|
|
10
|
+
apiKey: string;
|
|
11
|
+
headers?: Record<string, string>;
|
|
12
|
+
allowInsecureHttp?: boolean;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export interface LiteLLMModelPolicy {
|
|
16
|
+
// Moonshot/Kimi's strict schema requires assistant tool calls to carry string
|
|
17
|
+
// content. Discovery enables this outbound rewrite only from deployment evidence.
|
|
18
|
+
normalizeStrictToolMessages: boolean;
|
|
19
|
+
// Moonshot routes can inline reasoning as `<think>` text in the visible
|
|
20
|
+
// answer. Whether to unwrap it is a per-model conclusion discovery reaches
|
|
21
|
+
// from deployment evidence, carried here so the `message_end` hook does not
|
|
22
|
+
// re-derive it from the route name.
|
|
23
|
+
normalizeThinkTags: boolean;
|
|
24
|
+
// Hide duplicate visible reasoning only for deployment-evidenced Kimi routes
|
|
25
|
+
// that do not use an always-thinking generation.
|
|
26
|
+
suppressReasoningVisibility: boolean;
|
|
27
|
+
// Gemini-backed deployments require lowercase effort values. Discovery carries
|
|
28
|
+
// this evidence so the request hook does not infer a backend from the route name.
|
|
29
|
+
normalizeGeminiReasoningEffort?: boolean;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export type DiscoveredModelFor<TApi extends LiteLLMApi> = Omit<Model<TApi>, "provider" | "baseUrl"> & {
|
|
33
|
+
suppressReasoningContent?: boolean;
|
|
34
|
+
litellmPolicy?: LiteLLMModelPolicy;
|
|
35
|
+
litellmResponsesReasoningControl?: true;
|
|
36
|
+
litellmBackendFamily?: BackendFamily;
|
|
37
|
+
litellmDiscoveryVersion?: typeof LITELLM_DISCOVERY_VERSION;
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
export type DiscoveredModel = {
|
|
41
|
+
[TApi in LiteLLMApi]: DiscoveredModelFor<TApi>;
|
|
42
|
+
}[LiteLLMApi];
|
|
43
|
+
|
|
44
|
+
export type ModelProtocol = {
|
|
45
|
+
[TApi in LiteLLMApi]: Pick<DiscoveredModelFor<TApi>, "api" | "compat">;
|
|
46
|
+
}[LiteLLMApi];
|
|
47
|
+
|
|
48
|
+
export type LiteLLMModel = Model<LiteLLMApi> & {
|
|
49
|
+
suppressReasoningContent?: boolean;
|
|
50
|
+
litellmPolicy?: LiteLLMModelPolicy;
|
|
51
|
+
litellmResponsesReasoningControl?: true;
|
|
52
|
+
litellmBackendFamily?: BackendFamily;
|
|
53
|
+
litellmDiscoveryVersion?: typeof LITELLM_DISCOVERY_VERSION;
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
export interface DiscoveryResult {
|
|
57
|
+
models: DiscoveredModel[];
|
|
58
|
+
source: DiscoverySource;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export interface DiscoveryOptions {
|
|
62
|
+
timeoutMs?: number;
|
|
63
|
+
signal?: AbortSignal;
|
|
64
|
+
headers?: Record<string, string>;
|
|
65
|
+
allowInsecureHttp?: boolean;
|
|
66
|
+
modelsDev?: boolean;
|
|
67
|
+
modelsDevCachePath?: string;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export interface ModelInfoEntry {
|
|
71
|
+
model_name?: string;
|
|
72
|
+
litellm_params?: {
|
|
73
|
+
model?: string;
|
|
74
|
+
custom_llm_provider?: string;
|
|
75
|
+
api_version?: string;
|
|
76
|
+
allowed_openai_params?: string[];
|
|
77
|
+
};
|
|
78
|
+
model_info?: {
|
|
79
|
+
id?: string;
|
|
80
|
+
mode?: string | null;
|
|
81
|
+
base_model?: string;
|
|
82
|
+
litellm_provider?: string;
|
|
83
|
+
supported_endpoints?: string[];
|
|
84
|
+
supported_openai_params?: string[];
|
|
85
|
+
input_cost_per_token?: number;
|
|
86
|
+
output_cost_per_token?: number;
|
|
87
|
+
cache_read_input_token_cost?: number;
|
|
88
|
+
cache_creation_input_token_cost?: number;
|
|
89
|
+
max_input_tokens?: number;
|
|
90
|
+
max_output_tokens?: number;
|
|
91
|
+
supports_reasoning?: boolean;
|
|
92
|
+
supports_none_reasoning_effort?: boolean;
|
|
93
|
+
supports_minimal_reasoning_effort?: boolean;
|
|
94
|
+
supports_low_reasoning_effort?: boolean;
|
|
95
|
+
supports_xhigh_reasoning_effort?: boolean;
|
|
96
|
+
supports_max_reasoning_effort?: boolean;
|
|
97
|
+
supports_vision?: boolean;
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export interface ModelInfoResponse {
|
|
102
|
+
data?: ModelInfoEntry[];
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export interface HealthModelEntry {
|
|
106
|
+
model?: string;
|
|
107
|
+
model_id?: string;
|
|
108
|
+
api_base?: string;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export interface HealthResponse {
|
|
112
|
+
healthy_endpoints?: HealthModelEntry[];
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export interface ModelsListEntry {
|
|
116
|
+
id?: string;
|
|
117
|
+
owned_by?: string;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export interface ModelsListResponse {
|
|
121
|
+
data?: ModelsListEntry[];
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export type AuthFileEntry =
|
|
125
|
+
| { type: "oauth"; access: string; refresh: string; expires: number; baseUrl?: string }
|
|
126
|
+
| { type: "api_key"; key: string };
|
|
127
|
+
|
|
128
|
+
export interface ResolvedCredentials {
|
|
129
|
+
baseUrl?: string;
|
|
130
|
+
apiKey?: string;
|
|
131
|
+
apiKeyConfig?: string;
|
|
132
|
+
// `apiKey` was minted from Google ADC rather than config, helper, or env.
|
|
133
|
+
apiKeyFromGcloudAdc?: boolean;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export interface LiteLLMMcpTool {
|
|
137
|
+
name: string;
|
|
138
|
+
server_name: string;
|
|
139
|
+
server_id?: string;
|
|
140
|
+
description: string;
|
|
141
|
+
// Absent or `{}` means the proxy supplied no schema; both use the extension-owned envelope.
|
|
142
|
+
input_schema: Record<string, unknown>;
|
|
143
|
+
// True when the proxy supplied an `inputSchema`/`input_schema` that was not a JSON object.
|
|
144
|
+
input_schema_malformed?: boolean;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export interface LiteLLMSkill {
|
|
148
|
+
id?: string;
|
|
149
|
+
name: string;
|
|
150
|
+
description?: string;
|
|
151
|
+
enabled?: boolean;
|
|
152
|
+
source?: Record<string, unknown>;
|
|
153
|
+
version?: string;
|
|
154
|
+
keywords?: string[];
|
|
155
|
+
domain?: string;
|
|
156
|
+
namespace?: string;
|
|
157
|
+
category?: string;
|
|
158
|
+
author?: string;
|
|
159
|
+
homepage?: string;
|
|
160
|
+
input_schema?: Record<string, unknown>;
|
|
161
|
+
code?: string;
|
|
162
|
+
}
|
package/dist/cost.d.ts
DELETED
package/dist/cost.js
DELETED
|
@@ -1,49 +0,0 @@
|
|
|
1
|
-
export function setupLiteLLMCostTracking(pi, providerNames) {
|
|
2
|
-
const knownProviders = new Set(providerNames);
|
|
3
|
-
// Keyed by provider: with multiple LiteLLM providers active, overlapping
|
|
4
|
-
// requests interleave, so a single shared slot lets one provider's response
|
|
5
|
-
// (or a headerless one clearing it) clobber another's pending cost.
|
|
6
|
-
const pendingResponseCosts = new Map();
|
|
7
|
-
pi.on("after_provider_response", (event, ctx) => {
|
|
8
|
-
// Global Pi hook: responses from other providers must not feed LiteLLM cost state.
|
|
9
|
-
if (!ctx.model?.provider || !knownProviders.has(ctx.model.provider))
|
|
10
|
-
return;
|
|
11
|
-
const costHeader = event.headers?.["x-litellm-response-cost"] ?? event.headers?.["X-Litellm-Response-Cost"];
|
|
12
|
-
if (costHeader) {
|
|
13
|
-
const cost = Number.parseFloat(String(costHeader));
|
|
14
|
-
if (!Number.isNaN(cost)) {
|
|
15
|
-
pendingResponseCosts.set(ctx.model.provider, cost);
|
|
16
|
-
return;
|
|
17
|
-
}
|
|
18
|
-
}
|
|
19
|
-
pendingResponseCosts.delete(ctx.model.provider);
|
|
20
|
-
});
|
|
21
|
-
pi.on("message_end", async (event) => {
|
|
22
|
-
if (event.message.role !== "assistant" || !knownProviders.has(event.message.provider))
|
|
23
|
-
return;
|
|
24
|
-
const usage = event.message.usage;
|
|
25
|
-
if (!usage)
|
|
26
|
-
return;
|
|
27
|
-
const totalCost = pendingResponseCosts.get(event.message.provider);
|
|
28
|
-
if (totalCost === undefined)
|
|
29
|
-
return;
|
|
30
|
-
pendingResponseCosts.delete(event.message.provider);
|
|
31
|
-
const totalTokens = usage.input + usage.output + (usage.cacheRead ?? 0) + (usage.cacheWrite ?? 0);
|
|
32
|
-
const fraction = (tokens) => (totalTokens > 0 ? tokens / totalTokens : 0);
|
|
33
|
-
return {
|
|
34
|
-
message: {
|
|
35
|
-
...event.message,
|
|
36
|
-
usage: {
|
|
37
|
-
...usage,
|
|
38
|
-
cost: {
|
|
39
|
-
input: totalCost * fraction(usage.input),
|
|
40
|
-
output: totalCost * fraction(usage.output),
|
|
41
|
-
cacheRead: totalCost * fraction(usage.cacheRead ?? 0),
|
|
42
|
-
cacheWrite: totalCost * fraction(usage.cacheWrite ?? 0),
|
|
43
|
-
total: totalCost,
|
|
44
|
-
},
|
|
45
|
-
},
|
|
46
|
-
},
|
|
47
|
-
};
|
|
48
|
-
});
|
|
49
|
-
}
|
package/dist/discover.d.ts
DELETED
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
import type { Api, Model } from "@earendil-works/pi-ai";
|
|
2
|
-
import type { DiscoveredModel, DiscoveryOptions, DiscoveryResult } from "./types.js";
|
|
3
|
-
export declare function normalizeBaseUrl(input: string, allowInsecureHttp?: boolean): string;
|
|
4
|
-
export declare function isMoonshotModel(modelId: string): boolean;
|
|
5
|
-
export declare function isGpt55Model(modelId: string): boolean;
|
|
6
|
-
export declare function emitsThinkTags(modelId: string): boolean;
|
|
7
|
-
export declare function buildCompat(modelId: string): DiscoveredModel["compat"];
|
|
8
|
-
export declare function enrichCachedModel(model: Model<Api>): Model<Api>;
|
|
9
|
-
export declare function discoverModels(baseUrl: string, apiKey: string, options?: DiscoveryOptions & {
|
|
10
|
-
onProgress?: (message: string) => void;
|
|
11
|
-
silent?: boolean;
|
|
12
|
-
}): Promise<DiscoveryResult>;
|