pi-provider-litellm 4.2.0 → 4.3.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 +152 -15
- package/package.json +2 -2
- package/src/discover.ts +26 -6
- package/src/images.ts +258 -0
- package/src/index.ts +709 -110
- package/src/models-store.ts +117 -0
- package/src/provider.ts +148 -33
- package/src/types.ts +4 -0
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reads Pi's persisted catalog, so that a start does not re-prove what Pi already kept.
|
|
3
|
+
*
|
|
4
|
+
* The factory discovers before it returns because Pi's startup refresh runs with `allowNetwork:false` and returns before the
|
|
5
|
+
* network phase, so a provider that waits for Pi has no models when the session picks one (issue #137). That holds on a first
|
|
6
|
+
* run. It does not hold once Pi has persisted the catalog: with discovery switched off and a populated `models-store.json`,
|
|
7
|
+
* Pi offers the stored models and a request works, measured at a 5 ms factory against 1.8 s to discover.
|
|
8
|
+
*
|
|
9
|
+
* So the seed reads the stored catalog and hands those models over, instead of discovering them again. It only reads, and
|
|
10
|
+
* only `models-store.json`, which this repository's AGENTS.md assigns to Pi. Nothing here writes a model cache; the removed
|
|
11
|
+
* `litellm-models.json` is what that rule is about.
|
|
12
|
+
*
|
|
13
|
+
* The models are returned rather than an empty list on purpose. Pi restores its own catalog, so an empty list would do in
|
|
14
|
+
* the ordinary case. It would not do when Pi's active store is somewhere else: `ModelRuntime` takes a `modelsStorePath`, and
|
|
15
|
+
* resolves the default beside whatever `modelsPath` it was given. Every caller inside Pi passes an agent-directory path, but
|
|
16
|
+
* an SDK embedder need not. A catalog left in the default file would then satisfy this check while Pi's active store was
|
|
17
|
+
* empty, and the provider would have no models at all -- the very fault the seed exists to prevent. Returning the models
|
|
18
|
+
* costs nothing and cannot fail that way.
|
|
19
|
+
*
|
|
20
|
+
* Four things make a stored entry unusable, and all are checked here rather than left to Pi:
|
|
21
|
+
*
|
|
22
|
+
* - Age. Only a network refresh writes the store, and `pi -p` never runs one, so without a limit print mode would keep
|
|
23
|
+
* whatever catalog an interactive session last left. Pi's own remote catalogs skip the network for 4 hours after
|
|
24
|
+
* `checkedAt`, and so does this.
|
|
25
|
+
* - A different proxy. Stored models carry the `baseUrl` they were discovered from, so a changed root must still discover.
|
|
26
|
+
* - An older `litellmDiscoveryVersion`. Pi keeps a same-version stored entry over what startup discovery just proved
|
|
27
|
+
* (AGENTS.md), so skipping discovery for an older entry would hide the metadata a new version adds.
|
|
28
|
+
* - An entry Pi cannot restore. Pi's no-network restore publishes the stored models over the seed, so the seed cannot keep
|
|
29
|
+
* a short entry out of a session; it decides the catalog only when that restore fails. The restore runs
|
|
30
|
+
* `enrichCachedModel` (provider.ts), and when that throws Pi restores nothing, so the seed makes the same call and
|
|
31
|
+
* discovers instead.
|
|
32
|
+
*
|
|
33
|
+
* What is deliberately not checked is the credential. Nothing in Pi's store records which key discovered it, and a key scopes
|
|
34
|
+
* which models a LiteLLM proxy shows. Within those 4 hours, a key swapped on the same proxy, or a model added to it, therefore
|
|
35
|
+
* shows the stored catalog until Pi's own network refresh runs: interactive and RPC sessions run one in the background,
|
|
36
|
+
* `pi -p` does not. Recording a key fingerprint would mean writing a file of our own, which is the thing the rule forbids.
|
|
37
|
+
*/
|
|
38
|
+
|
|
39
|
+
import { readFile } from "node:fs/promises";
|
|
40
|
+
import { join } from "node:path";
|
|
41
|
+
import { LITELLM_DISCOVERY_VERSION } from "./backend-identity.js";
|
|
42
|
+
import { enrichCachedModel } from "./discover.js";
|
|
43
|
+
import { LITELLM_IMAGES_API, type LiteLLMImageModel } from "./images.js";
|
|
44
|
+
import { isLiteLLMApi, resolveModelBaseUrl } from "./protocols.js";
|
|
45
|
+
import { imageBaseUrl } from "./provider.js";
|
|
46
|
+
import type { LiteLLMModel } from "./types.js";
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Where Pi keeps it. `FileModelsStore` defaults to `<agentDir>/models-store.json`, and `ModelRuntime` resolves it beside
|
|
50
|
+
* whatever `modelsPath` it was given -- `modelsPath` itself is the model *config* (`models.json`), a different file with a
|
|
51
|
+
* different shape, which is deliberately not read here.
|
|
52
|
+
*/
|
|
53
|
+
const STORE_FILENAME = "models-store.json";
|
|
54
|
+
|
|
55
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
56
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** Pi's own remote catalogs skip the network for 4 hours after `checkedAt` (`REMOTE_CATALOG_REFRESH_INTERVAL_MS`). */
|
|
60
|
+
const STORE_FRESH_MS = 4 * 60 * 60 * 1000;
|
|
61
|
+
|
|
62
|
+
function isImageModel(value: Record<string, unknown>): boolean {
|
|
63
|
+
return value.type === "image" && value.api === LITELLM_IMAGES_API;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function isUsableModel(value: unknown, provider: string, baseUrl: string, allowInsecureHttp?: boolean): boolean {
|
|
67
|
+
if (!isRecord(value) || value.provider !== provider) return false;
|
|
68
|
+
if (value.litellmDiscoveryVersion !== LITELLM_DISCOVERY_VERSION) return false;
|
|
69
|
+
try {
|
|
70
|
+
// A stored baseUrl is per protocol -- `/v1` for the OpenAI APIs and images, the bare root for Messages -- so the root
|
|
71
|
+
// goes through the same resolution rather than being compared to it.
|
|
72
|
+
if (isImageModel(value)) return value.baseUrl === imageBaseUrl(baseUrl, allowInsecureHttp);
|
|
73
|
+
// `api` indexes LITELLM_PROTOCOLS in resolveModelBaseUrl, so an unknown one is a TypeError, not a bad model.
|
|
74
|
+
return isLiteLLMApi(value.api) && value.baseUrl === resolveModelBaseUrl(baseUrl, value.api, allowInsecureHttp);
|
|
75
|
+
} catch {
|
|
76
|
+
return false;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
async function readStore(agentDir: string): Promise<Record<string, unknown> | undefined> {
|
|
81
|
+
try {
|
|
82
|
+
const parsed: unknown = JSON.parse(await readFile(join(agentDir, STORE_FILENAME), "utf8"));
|
|
83
|
+
return isRecord(parsed) ? parsed : undefined;
|
|
84
|
+
} catch {
|
|
85
|
+
// Absent, unreadable, or not JSON: discover instead.
|
|
86
|
+
return undefined;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Pi's stored models for `provider`, when every one of them can be used as it is, and `undefined` otherwise. Never throws;
|
|
92
|
+
* anything it cannot read means "discover".
|
|
93
|
+
*/
|
|
94
|
+
export async function readStoredModels(options: {
|
|
95
|
+
agentDir: string;
|
|
96
|
+
provider: string;
|
|
97
|
+
baseUrl: string;
|
|
98
|
+
allowInsecureHttp?: boolean;
|
|
99
|
+
}): Promise<(LiteLLMModel | LiteLLMImageModel)[] | undefined> {
|
|
100
|
+
const entry = (await readStore(options.agentDir))?.[options.provider];
|
|
101
|
+
if (!isRecord(entry) || !Array.isArray(entry.models) || entry.models.length === 0) return undefined;
|
|
102
|
+
if (typeof entry.checkedAt !== "number" || Date.now() - entry.checkedAt >= STORE_FRESH_MS) return undefined;
|
|
103
|
+
const usable = entry.models.every((model) =>
|
|
104
|
+
isUsableModel(model, options.provider, options.baseUrl, options.allowInsecureHttp),
|
|
105
|
+
);
|
|
106
|
+
if (!usable) return undefined;
|
|
107
|
+
try {
|
|
108
|
+
// Pi's restore path (provider.ts) runs the same call; when it throws, nothing is restored and the seed is the catalog.
|
|
109
|
+
// Image models carry no request policy, so that path restores them as stored.
|
|
110
|
+
return entry.models.map((model) => (isImageModel(model) ? model : enrichCachedModel(model))) as (
|
|
111
|
+
| LiteLLMModel
|
|
112
|
+
| LiteLLMImageModel
|
|
113
|
+
)[];
|
|
114
|
+
} catch {
|
|
115
|
+
return undefined;
|
|
116
|
+
}
|
|
117
|
+
}
|
package/src/provider.ts
CHANGED
|
@@ -2,9 +2,14 @@ import {
|
|
|
2
2
|
type AnyModel,
|
|
3
3
|
type Api,
|
|
4
4
|
type ApiStreamOptions,
|
|
5
|
+
type AssistantImages,
|
|
5
6
|
type Credential,
|
|
6
7
|
createProvider,
|
|
7
8
|
getCurrentTools,
|
|
9
|
+
type ImageApi,
|
|
10
|
+
type ImageModel,
|
|
11
|
+
type ImagesContext,
|
|
12
|
+
type ImagesOptions,
|
|
8
13
|
type Model,
|
|
9
14
|
type Provider,
|
|
10
15
|
type ProviderAuth,
|
|
@@ -13,6 +18,12 @@ import {
|
|
|
13
18
|
} from "@earendil-works/pi-ai";
|
|
14
19
|
import { LITELLM_DISCOVERY_VERSION } from "./backend-identity.js";
|
|
15
20
|
import { enrichCachedModel, normalizeBaseUrl, restoreCachedModelPolicy } from "./discover.js";
|
|
21
|
+
import {
|
|
22
|
+
createLiteLLMImageApis,
|
|
23
|
+
type DiscoveredImageModel,
|
|
24
|
+
LITELLM_IMAGES_API,
|
|
25
|
+
type LiteLLMImageModel,
|
|
26
|
+
} from "./images.js";
|
|
16
27
|
import { createLiteLLMProtocolApis, isLiteLLMApi, LITELLM_API_NAMES, resolveModelBaseUrl } from "./protocols.js";
|
|
17
28
|
import type { DiscoveredModel, DiscoveryResult, LiteLLMApi, LiteLLMModel } from "./types.js";
|
|
18
29
|
|
|
@@ -23,7 +34,7 @@ export type LiteLLMProviderOptions = {
|
|
|
23
34
|
auth: ProviderAuth;
|
|
24
35
|
/** Catalog discovered during activation. Pi's startup refresh never allows network access, so
|
|
25
36
|
* without a seed the provider stays empty until the user opens /model. */
|
|
26
|
-
models?: readonly LiteLLMModel[];
|
|
37
|
+
models?: readonly (LiteLLMModel | LiteLLMImageModel)[];
|
|
27
38
|
allowInsecureHttp?: boolean;
|
|
28
39
|
resolveCredentialRoot: (credential?: Credential, requestBaseUrl?: string, apiKey?: string) => string | undefined;
|
|
29
40
|
discover(credential: Credential, signal?: AbortSignal): Promise<DiscoveryResult & { baseUrl?: string }>;
|
|
@@ -34,6 +45,23 @@ function isChatModel(model: AnyModel): model is Model<Api> {
|
|
|
34
45
|
return (model.type ?? "chat") === "chat";
|
|
35
46
|
}
|
|
36
47
|
|
|
48
|
+
function isLiteLLMImageModel(model: AnyModel): model is LiteLLMImageModel {
|
|
49
|
+
return model.type === "image" && model.api === LITELLM_IMAGES_API;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function imageBaseUrl(root: string, allowInsecureHttp = false): string {
|
|
53
|
+
return `${normalizeBaseUrl(root, allowInsecureHttp)}/v1`;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function toNativeImageModels(
|
|
57
|
+
provider: string,
|
|
58
|
+
baseUrl: string,
|
|
59
|
+
models: readonly DiscoveredImageModel[],
|
|
60
|
+
allowInsecureHttp = false,
|
|
61
|
+
): LiteLLMImageModel[] {
|
|
62
|
+
return models.map((model) => ({ ...model, provider, baseUrl: imageBaseUrl(baseUrl, allowInsecureHttp) }));
|
|
63
|
+
}
|
|
64
|
+
|
|
37
65
|
export function toNativeModels(
|
|
38
66
|
provider: string,
|
|
39
67
|
baseUrl: string,
|
|
@@ -117,6 +145,27 @@ function modelRootError(
|
|
|
117
145
|
}
|
|
118
146
|
}
|
|
119
147
|
|
|
148
|
+
function imageRootError(
|
|
149
|
+
model: ImageModel<ImageApi>,
|
|
150
|
+
active: { root: string; canonical: string; hostname: string },
|
|
151
|
+
allowInsecureHttp = false,
|
|
152
|
+
): Error | undefined {
|
|
153
|
+
let stored: { root: string; canonical: string; hostname: string };
|
|
154
|
+
try {
|
|
155
|
+
stored = proxyRoot(model.baseUrl, "Cached image model", allowInsecureHttp);
|
|
156
|
+
} catch (error) {
|
|
157
|
+
return error instanceof Error ? error : refreshRequired("Cached image model has an invalid LiteLLM model URL");
|
|
158
|
+
}
|
|
159
|
+
if (isPlaceholderHost(stored.hostname)) {
|
|
160
|
+
return refreshRequired("Cached image model uses a placeholder LiteLLM model host");
|
|
161
|
+
}
|
|
162
|
+
if (stored.canonical !== active.canonical) {
|
|
163
|
+
return refreshRequired(
|
|
164
|
+
`Cached image model has stale LiteLLM model root ${stored.root}; active credentials use ${active.root}`,
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
120
169
|
function requestModel(
|
|
121
170
|
provider: string,
|
|
122
171
|
model: Model<LiteLLMApi>,
|
|
@@ -148,6 +197,40 @@ export function createLiteLLMProvider(options: LiteLLMProviderOptions): Provider
|
|
|
148
197
|
reportedAvailabilityDiagnostics.add(message);
|
|
149
198
|
process.stderr.write(`LiteLLM (${options.id}): ${message}\n`);
|
|
150
199
|
};
|
|
200
|
+
type ActiveRoot = { root: string; canonical: string; hostname: string };
|
|
201
|
+
const resolveActiveRoot = (credential: Credential | undefined, hiddenCount: number): ActiveRoot | undefined => {
|
|
202
|
+
try {
|
|
203
|
+
const root = options.resolveCredentialRoot(credential);
|
|
204
|
+
if (!root) {
|
|
205
|
+
if (hiddenCount > 0) {
|
|
206
|
+
reportUnavailable(
|
|
207
|
+
`${hiddenCount} model(s) hidden because no LiteLLM base URL is configured; ` +
|
|
208
|
+
"set LITELLM_BASE_URL or run /login litellm",
|
|
209
|
+
);
|
|
210
|
+
}
|
|
211
|
+
return undefined;
|
|
212
|
+
}
|
|
213
|
+
return activeCredentialRoot(root, options.allowInsecureHttp);
|
|
214
|
+
} catch (error) {
|
|
215
|
+
reportUnavailable(error instanceof Error ? error.message : String(error));
|
|
216
|
+
return undefined;
|
|
217
|
+
}
|
|
218
|
+
};
|
|
219
|
+
const availableChat = (models: readonly Model<LiteLLMApi>[], active: ActiveRoot): Model<LiteLLMApi>[] => {
|
|
220
|
+
const available: Model<LiteLLMApi>[] = [];
|
|
221
|
+
for (const model of models) {
|
|
222
|
+
const error = modelRootError(model, active, options.allowInsecureHttp);
|
|
223
|
+
if (error) {
|
|
224
|
+
reportUnavailable(error.message);
|
|
225
|
+
continue;
|
|
226
|
+
}
|
|
227
|
+
available.push({
|
|
228
|
+
...model,
|
|
229
|
+
baseUrl: resolveModelBaseUrl(active.root, model.api, options.allowInsecureHttp),
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
return available;
|
|
233
|
+
};
|
|
151
234
|
const provider = createProvider<LiteLLMApi>({
|
|
152
235
|
id: options.id,
|
|
153
236
|
name: options.name,
|
|
@@ -157,41 +240,34 @@ export function createLiteLLMProvider(options: LiteLLMProviderOptions): Provider
|
|
|
157
240
|
async fetchModels(context) {
|
|
158
241
|
if (!context.credential) throw new Error("LiteLLM model discovery requires a credential");
|
|
159
242
|
const result = await options.discover(context.credential, context.signal);
|
|
160
|
-
|
|
243
|
+
const root = result.baseUrl ?? options.baseUrl;
|
|
244
|
+
return [
|
|
245
|
+
...toNativeModels(options.id, root, result.models, options.allowInsecureHttp),
|
|
246
|
+
...toNativeImageModels(options.id, root, result.imageModels ?? [], options.allowInsecureHttp),
|
|
247
|
+
];
|
|
161
248
|
},
|
|
162
249
|
filterModels(models, credential) {
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
}
|
|
175
|
-
active = activeCredentialRoot(root, options.allowInsecureHttp);
|
|
176
|
-
} catch (error) {
|
|
177
|
-
reportUnavailable(error instanceof Error ? error.message : String(error));
|
|
178
|
-
return [];
|
|
179
|
-
}
|
|
180
|
-
const available: Model<LiteLLMApi>[] = [];
|
|
181
|
-
for (const model of models) {
|
|
182
|
-
const error = modelRootError(model, active, options.allowInsecureHttp);
|
|
250
|
+
const active = resolveActiveRoot(credential, models.length);
|
|
251
|
+
return active ? availableChat(models, active) : [];
|
|
252
|
+
},
|
|
253
|
+
filterAllModels(models, credential) {
|
|
254
|
+
const active = resolveActiveRoot(credential, models.length);
|
|
255
|
+
if (!active) return [];
|
|
256
|
+
const chat = availableChat(models.filter(isChatModel) as Model<LiteLLMApi>[], active);
|
|
257
|
+
// Image models follow the same rule as chat: a cached root that no longer matches the
|
|
258
|
+
// active credential is hidden until a network refresh proves the new proxy's catalog.
|
|
259
|
+
const images = models.filter(isLiteLLMImageModel).flatMap((model) => {
|
|
260
|
+
const error = imageRootError(model, active, options.allowInsecureHttp);
|
|
183
261
|
if (error) {
|
|
184
262
|
reportUnavailable(error.message);
|
|
185
|
-
|
|
263
|
+
return [];
|
|
186
264
|
}
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
});
|
|
191
|
-
}
|
|
192
|
-
return available;
|
|
265
|
+
return [{ ...model, baseUrl: imageBaseUrl(active.root, options.allowInsecureHttp) }];
|
|
266
|
+
});
|
|
267
|
+
return [...chat, ...images];
|
|
193
268
|
},
|
|
194
269
|
api: createLiteLLMProtocolApis(),
|
|
270
|
+
images: createLiteLLMImageApis(),
|
|
195
271
|
});
|
|
196
272
|
const refreshModels = provider.refreshModels;
|
|
197
273
|
const guardedProvider: Provider<LiteLLMApi> = {
|
|
@@ -220,6 +296,38 @@ export function createLiteLLMProvider(options: LiteLLMProviderOptions): Provider
|
|
|
220
296
|
context,
|
|
221
297
|
requestOptions,
|
|
222
298
|
),
|
|
299
|
+
// Same request-time guard as chat: the credential's root, never a cached model URL, picks the
|
|
300
|
+
// host, so an off-catalog or stale entry cannot send this credential to another proxy.
|
|
301
|
+
generateImages: async (model: ImageModel<ImageApi>, context: ImagesContext, requestOptions?: ImagesOptions) => {
|
|
302
|
+
try {
|
|
303
|
+
const root = options.resolveCredentialRoot(
|
|
304
|
+
undefined,
|
|
305
|
+
requestOptions?.env?.LITELLM_BASE_URL,
|
|
306
|
+
requestOptions?.apiKey,
|
|
307
|
+
);
|
|
308
|
+
if (!root) throw refreshRequired("Active credentials do not identify a LiteLLM model host");
|
|
309
|
+
const active = activeCredentialRoot(root, options.allowInsecureHttp);
|
|
310
|
+
const error = imageRootError(model, active, options.allowInsecureHttp);
|
|
311
|
+
if (error) throw error;
|
|
312
|
+
return await provider.generateImages!(
|
|
313
|
+
{ ...model, provider: options.id, baseUrl: imageBaseUrl(active.root, options.allowInsecureHttp) },
|
|
314
|
+
context,
|
|
315
|
+
requestOptions,
|
|
316
|
+
);
|
|
317
|
+
} catch (error) {
|
|
318
|
+
// `Provider.generateImages` never rejects.
|
|
319
|
+
const failed: AssistantImages = {
|
|
320
|
+
api: model.api,
|
|
321
|
+
provider: model.provider,
|
|
322
|
+
model: model.id,
|
|
323
|
+
output: [],
|
|
324
|
+
stopReason: requestOptions?.signal?.aborted ? "aborted" : "error",
|
|
325
|
+
errorMessage: error instanceof Error ? error.message : String(error),
|
|
326
|
+
timestamp: Date.now(),
|
|
327
|
+
};
|
|
328
|
+
return failed;
|
|
329
|
+
}
|
|
330
|
+
},
|
|
223
331
|
};
|
|
224
332
|
if (!refreshModels) return guardedProvider;
|
|
225
333
|
const seedKey = (model: { id: string; baseUrl: string }) => `${model.id}\0${model.baseUrl}`;
|
|
@@ -227,16 +335,23 @@ export function createLiteLLMProvider(options: LiteLLMProviderOptions): Provider
|
|
|
227
335
|
return {
|
|
228
336
|
...guardedProvider,
|
|
229
337
|
refreshModels: async (context) => {
|
|
230
|
-
// Pi 0.99 persists models of every type;
|
|
231
|
-
//
|
|
338
|
+
// Pi 0.99 persists models of every type; the policy helpers below read chat-only fields, so
|
|
339
|
+
// chat and image entries are restored separately. Other types are not this provider's.
|
|
232
340
|
const storedModels = (context.stored?.models ?? []).filter(isChatModel);
|
|
341
|
+
const storedImageModels = (context.stored?.models ?? []).filter(isLiteLLMImageModel).flatMap((model) => {
|
|
342
|
+
if (model.litellmDiscoveryVersion === LITELLM_DISCOVERY_VERSION) return [model];
|
|
343
|
+
// An image entry carries no request policy to restore; a stale one is only kept when
|
|
344
|
+
// startup discovery proved the same route, otherwise the network refresh replaces it.
|
|
345
|
+
const fresh = seeded.get(seedKey(model));
|
|
346
|
+
return fresh && isLiteLLMImageModel(fresh) ? [fresh] : [];
|
|
347
|
+
});
|
|
233
348
|
let legacyCount = 0;
|
|
234
349
|
const models = storedModels.map((model) => {
|
|
235
350
|
if ((model as LiteLLMModel).litellmDiscoveryVersion !== LITELLM_DISCOVERY_VERSION) {
|
|
236
351
|
// Pi applies stored models over the seed, so a stale entry would otherwise mask what
|
|
237
352
|
// startup discovery just proved for the same route.
|
|
238
353
|
const fresh = seeded.get(seedKey(model));
|
|
239
|
-
if (fresh) return fresh;
|
|
354
|
+
if (fresh && isChatModel(fresh)) return fresh;
|
|
240
355
|
legacyCount++;
|
|
241
356
|
return restoreCachedModelPolicy(model);
|
|
242
357
|
}
|
|
@@ -247,7 +362,7 @@ export function createLiteLLMProvider(options: LiteLLMProviderOptions): Provider
|
|
|
247
362
|
...context,
|
|
248
363
|
// The current pi-ai provider has no freshness gate, but force is the documented refresh contract field.
|
|
249
364
|
force: context.force || (context.allowNetwork && legacyCount > 0),
|
|
250
|
-
stored: context.stored && { ...context.stored, models },
|
|
365
|
+
stored: context.stored && { ...context.stored, models: [...models, ...storedImageModels] },
|
|
251
366
|
});
|
|
252
367
|
} catch (error) {
|
|
253
368
|
if (context.allowNetwork && legacyCount > 0) {
|
package/src/types.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { Model } from "@earendil-works/pi-ai";
|
|
2
2
|
import type { BackendFamily, LITELLM_DISCOVERY_VERSION } from "./backend-identity.js";
|
|
3
|
+
import type { DiscoveredImageModel } from "./images.js";
|
|
3
4
|
import type { ProxyVersion } from "./proxy-version.js";
|
|
4
5
|
|
|
5
6
|
export type DiscoverySource = "model_info" | "models_list" | "health";
|
|
@@ -64,6 +65,8 @@ export type LiteLLMModel = Model<LiteLLMApi> & {
|
|
|
64
65
|
|
|
65
66
|
export interface DiscoveryResult {
|
|
66
67
|
models: DiscoveredModel[];
|
|
68
|
+
/** `mode: "image_generation"` routes from `/model/info`; absent when there are none. */
|
|
69
|
+
imageModels?: DiscoveredImageModel[];
|
|
67
70
|
source: DiscoverySource;
|
|
68
71
|
// Present only when a `/model/info` discovery had to read it and the proxy
|
|
69
72
|
// answered. Absent means unknown, not old: discovery asks only when it must.
|
|
@@ -98,6 +101,7 @@ export interface ModelInfoEntry {
|
|
|
98
101
|
supported_openai_params?: string[];
|
|
99
102
|
input_cost_per_token?: number;
|
|
100
103
|
output_cost_per_token?: number;
|
|
104
|
+
output_cost_per_image_token?: number;
|
|
101
105
|
cache_read_input_token_cost?: number;
|
|
102
106
|
cache_creation_input_token_cost?: number;
|
|
103
107
|
max_input_tokens?: number;
|