pi-provider-litellm 3.2.0 → 3.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 +55 -9
- package/package.json +6 -6
- package/src/backend-identity.ts +17 -1
- package/src/discover.ts +180 -41
- package/src/index.ts +530 -150
- package/src/mcp-tools.ts +191 -9
- package/src/model-groups.ts +12 -11
- package/src/provider.ts +27 -12
- package/src/proxy-version.ts +68 -0
- package/src/skills.ts +10 -0
- package/src/types.ts +16 -0
package/src/discover.ts
CHANGED
|
@@ -2,7 +2,12 @@ import { isIP } from "node:net";
|
|
|
2
2
|
import type { Api, Model } from "@earendil-works/pi-ai";
|
|
3
3
|
import { getModels, getProviders } from "@earendil-works/pi-ai/compat";
|
|
4
4
|
import type { BuiltinProvider } from "@earendil-works/pi-ai/providers/all";
|
|
5
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
LITELLM_DISCOVERY_VERSION,
|
|
7
|
+
resolveBackendIdentity,
|
|
8
|
+
resolveCatalogProvider,
|
|
9
|
+
routesOnlyThrough,
|
|
10
|
+
} from "./backend-identity.js";
|
|
6
11
|
import {
|
|
7
12
|
type CatalogResolution,
|
|
8
13
|
catalogResolution,
|
|
@@ -20,6 +25,7 @@ import {
|
|
|
20
25
|
type SemanticModel,
|
|
21
26
|
wireString,
|
|
22
27
|
} from "./model-groups.js";
|
|
28
|
+
import { type ProxyVersion, type ProxyVersionFloor, probeProxyVersion, proxyVersionAtLeast } from "./proxy-version.js";
|
|
23
29
|
import { loadPublicCatalog, type PublicCatalog, type PublicCatalogRecord } from "./public-catalog.js";
|
|
24
30
|
import { intersectThinkingLevelMaps } from "./thinking-levels.js";
|
|
25
31
|
import type {
|
|
@@ -28,6 +34,7 @@ import type {
|
|
|
28
34
|
DiscoveryOptions,
|
|
29
35
|
DiscoveryResult,
|
|
30
36
|
HealthResponse,
|
|
37
|
+
LiteLLMModel,
|
|
31
38
|
LiteLLMModelPolicy,
|
|
32
39
|
ModelInfoEntry,
|
|
33
40
|
ModelInfoResponse,
|
|
@@ -134,10 +141,43 @@ export function completionsCompat(
|
|
|
134
141
|
return { supportsStore: false };
|
|
135
142
|
}
|
|
136
143
|
|
|
137
|
-
|
|
144
|
+
// Before v1.103.0 LiteLLM has no native Responses config for the `azure_ai`
|
|
145
|
+
// provider, so every azure_ai deployment reaches `/v1/responses` through the
|
|
146
|
+
// Chat Completions bridge. That bridge crashes on Azure's empty-`choices` stream
|
|
147
|
+
// chunks with "list index out of range" (BerriAI/litellm#34455), while the same
|
|
148
|
+
// deployment streams fine on `/v1/chat/completions`. v1.103.0 guards the bridge
|
|
149
|
+
// and serves azure_ai natively where the host allows it.
|
|
150
|
+
const AZURE_AI_RESPONSES_FLOOR: ProxyVersionFloor = [1, 103, 0];
|
|
151
|
+
|
|
152
|
+
function isAzureAiDeployment(entry: ModelInfoEntry): boolean {
|
|
153
|
+
const adapter = wireString(entry.litellm_params?.custom_llm_provider)?.trim().toLowerCase();
|
|
154
|
+
const configuredModel = wireString(entry.litellm_params?.model)?.trim().toLowerCase();
|
|
155
|
+
return adapter === "azure_ai" || /^azure_ai\//.test(configuredModel ?? "");
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function listsResponsesEndpoint(entry: ModelInfoEntry): boolean | undefined {
|
|
138
159
|
const endpoints = entry.model_info?.supported_endpoints;
|
|
139
|
-
|
|
160
|
+
return Array.isArray(endpoints) ? endpoints.some((endpoint) => endpoint === "/v1/responses") : undefined;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// An azure_ai `supported_endpoints` list is copied from the model cost map and
|
|
164
|
+
// describes the model, not the transport LiteLLM uses, so on its own it cannot
|
|
165
|
+
// authorize Responses. These are the only deployments whose transport the proxy
|
|
166
|
+
// version decides; discovery asks for the version only when one is published.
|
|
167
|
+
function transportAwaitsProxyVersion(entry: ModelInfoEntry): boolean {
|
|
168
|
+
return (
|
|
169
|
+
isAzureAiDeployment(entry) &&
|
|
170
|
+
listsResponsesEndpoint(entry) === true &&
|
|
171
|
+
normalizedMode(entry.model_info?.mode) !== "responses"
|
|
172
|
+
);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function supportsResponses(entry: ModelInfoEntry, proxyVersion?: ProxyVersion): boolean {
|
|
176
|
+
const listsResponses = listsResponsesEndpoint(entry);
|
|
177
|
+
if (listsResponses === false) return false;
|
|
140
178
|
if (normalizedMode(entry.model_info?.mode) === "responses") return true;
|
|
179
|
+
if (transportAwaitsProxyVersion(entry) && !proxyVersionAtLeast(proxyVersion, AZURE_AI_RESPONSES_FLOOR)) return false;
|
|
180
|
+
if (listsResponses === true) return true;
|
|
141
181
|
|
|
142
182
|
const identity = resolveBackendIdentity(entry);
|
|
143
183
|
if (identity?.family !== "openai") return false;
|
|
@@ -157,12 +197,16 @@ function supportsResponses(entry: ModelInfoEntry): boolean {
|
|
|
157
197
|
return !azureAdapter;
|
|
158
198
|
}
|
|
159
199
|
|
|
160
|
-
export function modelProtocol(
|
|
200
|
+
export function modelProtocol(
|
|
201
|
+
modelId: string,
|
|
202
|
+
modeOrEntry?: string | null | ModelInfoEntry,
|
|
203
|
+
proxyVersion?: ProxyVersion,
|
|
204
|
+
): ModelProtocol {
|
|
161
205
|
const selectedEntry =
|
|
162
206
|
typeof modeOrEntry === "object" && modeOrEntry !== null
|
|
163
207
|
? { ...modeOrEntry, model_name: undefined }
|
|
164
208
|
: { model_name: modelId, model_info: { mode: modeOrEntry } };
|
|
165
|
-
return supportsResponses(selectedEntry)
|
|
209
|
+
return supportsResponses(selectedEntry, proxyVersion)
|
|
166
210
|
? { api: "openai-responses", compat: responsesCompat(modelId) }
|
|
167
211
|
: { api: "openai-completions", compat: completionsCompat(modelId) };
|
|
168
212
|
}
|
|
@@ -324,10 +368,20 @@ function messagesCompatOf(model: Model<Api>): MessagesBackendCompat | undefined
|
|
|
324
368
|
const carried: MessagesBackendCompat = {};
|
|
325
369
|
if (compat?.forceAdaptiveThinking !== undefined) carried.forceAdaptiveThinking = compat.forceAdaptiveThinking;
|
|
326
370
|
if (compat?.supportsTemperature !== undefined) carried.supportsTemperature = compat.supportsTemperature;
|
|
327
|
-
if (compat?.supportsStrictTools !== undefined) carried.supportsStrictTools = compat.supportsStrictTools;
|
|
328
371
|
return carried;
|
|
329
372
|
}
|
|
330
373
|
|
|
374
|
+
const ANTHROPIC_ROUTES: ReadonlySet<string> = new Set(["anthropic"]);
|
|
375
|
+
|
|
376
|
+
function messagesStrictToolEvidence(entry: ModelInfoEntry, model: Model<Api>): boolean | undefined {
|
|
377
|
+
// The Anthropic catalog describes the Anthropic API, so only deployments whose
|
|
378
|
+
// declared routing names Anthropic keep its grant. A hosted route (Bedrock,
|
|
379
|
+
// Vertex) can reject tools[].strict for the same model, and LiteLLM reports no
|
|
380
|
+
// per-route strict-tool capability, so it stays unknown.
|
|
381
|
+
if (!routesOnlyThrough(entry, ANTHROPIC_ROUTES) || model.api !== "anthropic-messages") return undefined;
|
|
382
|
+
return (model as Model<"anthropic-messages">).compat?.supportsStrictTools;
|
|
383
|
+
}
|
|
384
|
+
|
|
331
385
|
function anthropicBackendLookupIds(id: string): string[] {
|
|
332
386
|
const routed = (id.split("/").pop() ?? id).toLowerCase();
|
|
333
387
|
const base = routed.replace(/^(?:[a-z0-9-]+\.)*anthropic[./]/, "");
|
|
@@ -353,7 +407,7 @@ const CLAUDE_MODEL_PATTERN = /(?:^|[./_-])(?:claude|opus|sonnet|haiku|fable)(?:$
|
|
|
353
407
|
|
|
354
408
|
function nativeMessagesCatalog(
|
|
355
409
|
entry: ModelInfoEntry,
|
|
356
|
-
): Pick<CatalogResolution, "messagesCompat" | "messagesThinkingLevelMap"> {
|
|
410
|
+
): Pick<CatalogResolution, "messagesCompat" | "messagesStrictTools" | "messagesThinkingLevelMap"> {
|
|
357
411
|
const adapter = wireString(entry.model_info?.litellm_provider)?.trim().toLowerCase();
|
|
358
412
|
if (!adapter || !CLAUDE_CAPABLE_ADAPTERS.has(adapter) || deploymentFamily(entry) !== "claude") return {};
|
|
359
413
|
const candidates = [entry.litellm_params?.model, entry.model_info?.base_model]
|
|
@@ -366,7 +420,13 @@ function nativeMessagesCatalog(
|
|
|
366
420
|
if (models.some((model) => !model) || new Set(models.map((model) => model?.id)).size !== 1) return {};
|
|
367
421
|
const model = models[0]!;
|
|
368
422
|
const compat = messagesCompatOf(model);
|
|
369
|
-
return compat
|
|
423
|
+
return compat
|
|
424
|
+
? {
|
|
425
|
+
messagesCompat: compat,
|
|
426
|
+
messagesStrictTools: messagesStrictToolEvidence(entry, model),
|
|
427
|
+
messagesThinkingLevelMap: model.thinkingLevelMap,
|
|
428
|
+
}
|
|
429
|
+
: {};
|
|
370
430
|
}
|
|
371
431
|
|
|
372
432
|
const ADAPTER_CATALOG_PROVIDERS: Readonly<Record<string, BuiltinProvider>> = {
|
|
@@ -601,6 +661,7 @@ function mapFromModelInfoGroup(
|
|
|
601
661
|
defaultedContextRoutes?: Set<string>;
|
|
602
662
|
denyLevels?: boolean;
|
|
603
663
|
allowMessages?: boolean;
|
|
664
|
+
proxyVersion?: ProxyVersion;
|
|
604
665
|
} = {},
|
|
605
666
|
): DiscoveredModel | undefined {
|
|
606
667
|
const reduced = reduceModelGroup(entries, (entry) => {
|
|
@@ -614,7 +675,7 @@ function mapFromModelInfoGroup(
|
|
|
614
675
|
const model = new Set(generations).size === 1 && family !== "conflicting" ? generations[0] : undefined;
|
|
615
676
|
return {
|
|
616
677
|
...catalog,
|
|
617
|
-
...(options.allowMessages === false ? { messagesCompat: undefined } : {}),
|
|
678
|
+
...(options.allowMessages === false ? { messagesCompat: undefined, messagesStrictTools: undefined } : {}),
|
|
618
679
|
...(family ? { semanticFamily: family } : {}),
|
|
619
680
|
...(model ? { semanticModel: model } : {}),
|
|
620
681
|
};
|
|
@@ -623,7 +684,7 @@ function mapFromModelInfoGroup(
|
|
|
623
684
|
if (reduced.contextWindowDefaulted) options.defaultedContextRoutes?.add(reduced.id);
|
|
624
685
|
if (reduced.catalogAuthorityAmbiguous) options.ambiguousRoutes?.push(reduced.id);
|
|
625
686
|
if (reduced.deploymentFamilies.includes("conflicting")) options.conflictingFamilyRoutes?.push(reduced.id);
|
|
626
|
-
const protocols = entries.map((entry) => modelProtocol(reduced.id, entry));
|
|
687
|
+
const protocols = entries.map((entry) => modelProtocol(reduced.id, entry, options.proxyVersion));
|
|
627
688
|
const protocol = protocols.find((candidate) => candidate.api === "openai-completions") ?? protocols[0]!;
|
|
628
689
|
const api = reduced.api === "anthropic-messages" ? reduced.api : protocol.api;
|
|
629
690
|
const families = new Set(entries.map((entry) => resolveBackendIdentity({ ...entry, model_name: undefined })?.family));
|
|
@@ -645,10 +706,17 @@ function mapFromModelInfoGroup(
|
|
|
645
706
|
family === undefined ? undefined : completionsCompat(reduced.id, family),
|
|
646
707
|
),
|
|
647
708
|
);
|
|
709
|
+
const messagesCompat =
|
|
710
|
+
api === "anthropic-messages"
|
|
711
|
+
? {
|
|
712
|
+
...reduced.messagesCompat,
|
|
713
|
+
...(reduced.messagesStrictTools !== undefined ? { supportsStrictTools: reduced.messagesStrictTools } : {}),
|
|
714
|
+
}
|
|
715
|
+
: undefined;
|
|
648
716
|
const policy = closeSerializerPolicy({
|
|
649
717
|
api,
|
|
650
718
|
reasoning,
|
|
651
|
-
vendorCompat: api === "anthropic-messages" ?
|
|
719
|
+
vendorCompat: api === "anthropic-messages" ? messagesCompat : vendorCompat,
|
|
652
720
|
semanticCompat: reduced.acceptsResponsesReasoningControl
|
|
653
721
|
? { ...reasoningPolicy?.compat, supportsReasoningEffort: true }
|
|
654
722
|
: reasoningPolicy?.compat,
|
|
@@ -692,12 +760,23 @@ function mapFromModelInfoGroup(
|
|
|
692
760
|
: {}),
|
|
693
761
|
...(families.size === 1 && family ? { litellmBackendFamily: family } : {}),
|
|
694
762
|
...(modelPolicy ? { litellmPolicy: modelPolicy } : {}),
|
|
695
|
-
|
|
763
|
+
// closeSerializerPolicy derives compat from `api`, a correlation its return type cannot carry.
|
|
764
|
+
} as DiscoveredModel;
|
|
696
765
|
}
|
|
697
766
|
|
|
698
767
|
// An evidence-free fallback entry has no deployment or adapter evidence. Its route name
|
|
699
768
|
// authorizes nothing; the Pi catalog entry for its id supplies its protocol and presentation
|
|
700
769
|
// metadata, while an unknown id stays on Chat Completions until /model/info can be read.
|
|
770
|
+
// Whether a group that will be published has its transport decided by the proxy
|
|
771
|
+
// version. Mapping without collectors leaves no diagnostic behind, and whether a
|
|
772
|
+
// group is published does not depend on the version.
|
|
773
|
+
function publishesVersionGatedTransport(
|
|
774
|
+
group: readonly ModelInfoEntry[],
|
|
775
|
+
publicCatalog: PublicCatalog | undefined,
|
|
776
|
+
): boolean {
|
|
777
|
+
return group.some(transportAwaitsProxyVersion) && mapFromModelInfoGroup(group, publicCatalog) !== undefined;
|
|
778
|
+
}
|
|
779
|
+
|
|
701
780
|
function catalogProtocol(modelId: string, catalogModel: Model<Api> | undefined): ModelProtocol {
|
|
702
781
|
return catalogModel?.api === "openai-responses"
|
|
703
782
|
? { api: "openai-responses", compat: responsesCompat(modelId) }
|
|
@@ -781,6 +860,14 @@ async function discoverFromHealth(
|
|
|
781
860
|
const publicCatalog = deployments.some((deployment) => deployment && !deployment.synthetic)
|
|
782
861
|
? await loadDiscoveryPublicCatalog(options)
|
|
783
862
|
: undefined;
|
|
863
|
+
const proxyVersion = [...groups.values()].some((group) =>
|
|
864
|
+
publishesVersionGatedTransport(
|
|
865
|
+
group.map(({ entry }) => entry),
|
|
866
|
+
publicCatalog,
|
|
867
|
+
),
|
|
868
|
+
)
|
|
869
|
+
? await probeProxyVersion(base, apiKey, options)
|
|
870
|
+
: undefined;
|
|
784
871
|
const incompatibleModeRoutes: string[] = [];
|
|
785
872
|
const ambiguousRoutes: string[] = [];
|
|
786
873
|
const conflictingFamilyRoutes: string[] = [];
|
|
@@ -799,6 +886,7 @@ async function discoverFromHealth(
|
|
|
799
886
|
withheldRepairRoutes,
|
|
800
887
|
denyLevels: group.some(({ denyLevels }) => denyLevels),
|
|
801
888
|
allowMessages: false,
|
|
889
|
+
proxyVersion,
|
|
802
890
|
});
|
|
803
891
|
})
|
|
804
892
|
.filter((model): model is DiscoveredModel => model !== undefined);
|
|
@@ -874,16 +962,14 @@ function wildcardPatternSpecificity(pattern: string): readonly [number, number]
|
|
|
874
962
|
return [escaped.length, complexity];
|
|
875
963
|
}
|
|
876
964
|
|
|
877
|
-
//
|
|
878
|
-
//
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
model: DiscoveredModel,
|
|
965
|
+
// The rows of the most-specific published wildcard route LiteLLM would select for a
|
|
966
|
+
// concrete id, using its pattern order, with the id substituted into each row.
|
|
967
|
+
function selectedWildcardRows(
|
|
968
|
+
modelId: string,
|
|
882
969
|
wildcardRows: readonly ModelInfoEntry[],
|
|
883
970
|
publishedWildcardIds: ReadonlySet<string>,
|
|
884
|
-
|
|
885
|
-
)
|
|
886
|
-
const matchingRows = wildcardRows.filter((row) => row.model_name && wildcardMatches(row.model_name, model.id));
|
|
971
|
+
): ModelInfoEntry[] | undefined {
|
|
972
|
+
const matchingRows = wildcardRows.filter((row) => row.model_name && wildcardMatches(row.model_name, modelId));
|
|
887
973
|
const selectedPattern = matchingRows
|
|
888
974
|
.map((row) => row.model_name!)
|
|
889
975
|
.sort((left, right) => {
|
|
@@ -893,10 +979,24 @@ function applyWildcardEvidence(
|
|
|
893
979
|
return rightLength - leftLength || leftComplexity - rightComplexity || left.localeCompare(right);
|
|
894
980
|
})[0];
|
|
895
981
|
if (!selectedPattern || !publishedWildcardIds.has(selectedPattern)) return undefined;
|
|
896
|
-
|
|
982
|
+
return matchingRows
|
|
897
983
|
.filter((row) => row.model_name === selectedPattern)
|
|
898
|
-
.map((row) => resolveWildcardRow(row,
|
|
899
|
-
|
|
984
|
+
.map((row) => resolveWildcardRow(row, modelId));
|
|
985
|
+
}
|
|
986
|
+
|
|
987
|
+
// A concrete id expanded from a wildcard route inherits the deployment evidence from the
|
|
988
|
+
// most-specific matching route, using LiteLLM's pattern order. Rows sharing that route still
|
|
989
|
+
// vote together, so any deployment needing Chat keeps the child on Chat.
|
|
990
|
+
function applyWildcardEvidence(
|
|
991
|
+
model: DiscoveredModel,
|
|
992
|
+
wildcardRows: readonly ModelInfoEntry[],
|
|
993
|
+
publishedWildcardIds: ReadonlySet<string>,
|
|
994
|
+
publicCatalog: PublicCatalog | undefined,
|
|
995
|
+
proxyVersion: ProxyVersion | undefined,
|
|
996
|
+
): DiscoveredModel | undefined {
|
|
997
|
+
const parents = selectedWildcardRows(model.id, wildcardRows, publishedWildcardIds);
|
|
998
|
+
if (!parents) return undefined;
|
|
999
|
+
const selected = mapFromModelInfoGroup(parents, publicCatalog, { proxyVersion });
|
|
900
1000
|
if (!selected) return undefined;
|
|
901
1001
|
const policies = [model.litellmPolicy, selected.litellmPolicy].filter((policy) => policy !== undefined);
|
|
902
1002
|
const combinedPolicy =
|
|
@@ -926,7 +1026,7 @@ function applyWildcardEvidence(
|
|
|
926
1026
|
...(selected.litellmBackendFamily ? { litellmBackendFamily: selected.litellmBackendFamily } : {}),
|
|
927
1027
|
...(combinedPolicy ? { litellmPolicy: combinedPolicy } : {}),
|
|
928
1028
|
...(selected.litellmResponsesReasoningControl ? { litellmResponsesReasoningControl: true as const } : {}),
|
|
929
|
-
};
|
|
1029
|
+
} as DiscoveredModel;
|
|
930
1030
|
}
|
|
931
1031
|
|
|
932
1032
|
function mapFromWildcardExpansion(
|
|
@@ -1033,6 +1133,18 @@ export async function discoverModels(
|
|
|
1033
1133
|
groups.set(route, group);
|
|
1034
1134
|
}
|
|
1035
1135
|
const publicCatalog = await loadDiscoveryPublicCatalog(options);
|
|
1136
|
+
// A wildcard route publishes nothing until `/v1/models` expands it, so only
|
|
1137
|
+
// exact routes can call for the proxy version this early.
|
|
1138
|
+
let proxyVersionRead = false;
|
|
1139
|
+
const readProxyVersion = (): Promise<ProxyVersion | undefined> => {
|
|
1140
|
+
proxyVersionRead = true;
|
|
1141
|
+
return probeProxyVersion(base, apiKey, options);
|
|
1142
|
+
};
|
|
1143
|
+
let proxyVersion = [...groups].some(
|
|
1144
|
+
([route, group]) => !route.includes("*") && publishesVersionGatedTransport(group, publicCatalog),
|
|
1145
|
+
)
|
|
1146
|
+
? await readProxyVersion()
|
|
1147
|
+
: undefined;
|
|
1036
1148
|
const ambiguousRoutes: string[] = [];
|
|
1037
1149
|
const incompatibleModeRoutes: string[] = [];
|
|
1038
1150
|
const conflictingFamilyRoutes: string[] = [];
|
|
@@ -1047,6 +1159,7 @@ export async function discoverModels(
|
|
|
1047
1159
|
conflictingFamilyRoutes,
|
|
1048
1160
|
withheldRepairRoutes,
|
|
1049
1161
|
defaultedContextRoutes,
|
|
1162
|
+
proxyVersion,
|
|
1050
1163
|
}),
|
|
1051
1164
|
deploymentFamilies: group.map(deploymentFamily),
|
|
1052
1165
|
};
|
|
@@ -1084,19 +1197,33 @@ export async function discoverModels(
|
|
|
1084
1197
|
const listResult = await fetchJson<ModelsListResponse>(`${base}/v1/models`, apiKey, options);
|
|
1085
1198
|
if (listResult.ok && wildcards.length > 0) {
|
|
1086
1199
|
const seen = new Set(models.map((model) => model.id));
|
|
1087
|
-
const
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1200
|
+
const candidates = (listResult.data.data ?? []).filter((entry) => {
|
|
1201
|
+
const id = wireString(entry.id);
|
|
1202
|
+
return (
|
|
1203
|
+
id === undefined ||
|
|
1204
|
+
(!seen.has(id) && !droppedExactIds.has(id) && !droppedWildcards.some((route) => wildcardMatches(route, id)))
|
|
1205
|
+
);
|
|
1206
|
+
});
|
|
1207
|
+
let sources = wildcards;
|
|
1208
|
+
const expansionAwaitsProxyVersion = candidates.some((entry) => {
|
|
1209
|
+
const id = wireString(entry.id);
|
|
1210
|
+
if (!id || id.includes("*")) return false;
|
|
1211
|
+
const parents = selectedWildcardRows(id, wildcardRows, publishedWildcardIds);
|
|
1212
|
+
return parents !== undefined && publishesVersionGatedTransport(parents, publicCatalog);
|
|
1213
|
+
});
|
|
1214
|
+
if (!proxyVersionRead && expansionAwaitsProxyVersion) {
|
|
1215
|
+
proxyVersion = await readProxyVersion();
|
|
1216
|
+
// The parents were reduced before the version was known; an expansion
|
|
1217
|
+
// must inherit from parents reduced under the version it is published with.
|
|
1218
|
+
sources = wildcardRoutes.flatMap(({ route, deploymentFamilies }) => {
|
|
1219
|
+
const model = mapFromModelInfoGroup(groups.get(route)!, publicCatalog, { proxyVersion });
|
|
1220
|
+
return model ? [{ model, deploymentFamilies }] : [];
|
|
1221
|
+
});
|
|
1222
|
+
}
|
|
1223
|
+
const expanded = candidates
|
|
1224
|
+
.map((entry) => mapFromWildcardExpansion(entry, sources, withheldRepairRoutes))
|
|
1098
1225
|
.filter((model): model is DiscoveredModel => model !== undefined)
|
|
1099
|
-
.map((model) => applyWildcardEvidence(model, wildcardRows, publishedWildcardIds, publicCatalog))
|
|
1226
|
+
.map((model) => applyWildcardEvidence(model, wildcardRows, publishedWildcardIds, publicCatalog, proxyVersion))
|
|
1100
1227
|
.filter((model): model is DiscoveredModel => model !== undefined);
|
|
1101
1228
|
models = [...models, ...expanded];
|
|
1102
1229
|
}
|
|
@@ -1122,7 +1249,7 @@ export async function discoverModels(
|
|
|
1122
1249
|
"set model_info.max_input_tokens on every deployment in each route",
|
|
1123
1250
|
);
|
|
1124
1251
|
}
|
|
1125
|
-
return { source: "model_info", models };
|
|
1252
|
+
return { source: "model_info", models, ...(proxyVersion ? { proxyVersion } : {}) };
|
|
1126
1253
|
}
|
|
1127
1254
|
if (![401, 403, 404].includes(infoResult.status)) {
|
|
1128
1255
|
throw new Error(`/model/info returned ${infoResult.status}`);
|
|
@@ -1188,10 +1315,22 @@ function hasMoonshotCompatEvidence(compat: Model<Api>["compat"]): boolean {
|
|
|
1188
1315
|
}
|
|
1189
1316
|
|
|
1190
1317
|
export function restoreCachedModelPolicy(model: Model<Api>): Model<Api> {
|
|
1191
|
-
const cached = model as
|
|
1192
|
-
|
|
1193
|
-
const
|
|
1194
|
-
|
|
1318
|
+
const cached = model as LiteLLMModel;
|
|
1319
|
+
let restored: LiteLLMModel = cached;
|
|
1320
|
+
const messagesCompat = cached.compat as Model<"anthropic-messages">["compat"];
|
|
1321
|
+
if (
|
|
1322
|
+
cached.api === "anthropic-messages" &&
|
|
1323
|
+
cached.litellmDiscoveryVersion !== LITELLM_DISCOVERY_VERSION &&
|
|
1324
|
+
messagesCompat?.supportsStrictTools !== undefined
|
|
1325
|
+
) {
|
|
1326
|
+
const { supportsStrictTools: _staleStrictTools, ...compat } = messagesCompat;
|
|
1327
|
+
// Older discovery copied this value from the direct Anthropic catalog even
|
|
1328
|
+
// for hosted routes. Without the original deployment rows it cannot be
|
|
1329
|
+
// re-proven offline, so fail closed until the forced network refresh below.
|
|
1330
|
+
restored = { ...cached, compat: Object.keys(compat).length > 0 ? compat : undefined };
|
|
1331
|
+
}
|
|
1332
|
+
if (restored.litellmPolicy || !hasMoonshotCompatEvidence(restored.compat)) return restored;
|
|
1333
|
+
return { ...restored, litellmPolicy: moonshotPolicy(restored.id) } as LiteLLMModel;
|
|
1195
1334
|
}
|
|
1196
1335
|
|
|
1197
1336
|
function hasResponsesReasoningControl(model: Model<Api>): boolean {
|