pi-provider-litellm 3.3.0 → 3.4.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 +3 -1
- package/package.json +1 -1
- package/src/backend-identity.ts +1 -1
- package/src/discover.ts +67 -8
- package/src/index.ts +19 -6
- package/src/model-groups.ts +4 -1
- package/src/types.ts +9 -1
package/README.md
CHANGED
|
@@ -205,7 +205,7 @@ adding transport-specific fields to request bodies.
|
|
|
205
205
|
|
|
206
206
|
## Model transport
|
|
207
207
|
|
|
208
|
-
A `/model/info` route group uses native Anthropic `/v1/messages` only when every deployment explicitly reports Chat mode, identifies a Claude backend through an Anthropic-, Bedrock-, or Vertex-capable adapter, and every declared backend identity resolves the same Anthropic compatibility policy. A `claude-<name>-<major>-<minor>` backend id resolves through its `claude-<name>-<major>` entry when the exact id is absent; an unknown generation stays on Chat Completions. When `supported_endpoints` is
|
|
208
|
+
A `/model/info` route group uses native Anthropic `/v1/messages` only when every deployment explicitly reports Chat mode, identifies a Claude backend through an Anthropic-, Bedrock-, or Vertex-capable adapter, and every declared backend identity resolves the same Anthropic compatibility policy. A `claude-<name>-<major>-<minor>` backend id resolves through its `claude-<name>-<major>` entry when the exact id is absent; an unknown generation stays on Chat Completions. When `supported_endpoints` is a list, it must include `/v1/messages`; an explicit endpoint list without it keeps the group on Chat Completions. A `null` value, which LiteLLM v1.100 and later report for models absent from their model map, counts as absent. An unresolved routing or base-model identity denies native Messages rather than being discarded from the unanimity check. Mixed-generation Claude groups can use Messages when their serializer policies match, with reasoning levels restricted to their common supported set. Groups with incompatible policies, mixed families, or unknown backend evidence remain on Chat Completions; unanimous explicit Responses mode takes precedence. Routes discovered through `/health` never select native Messages: Messages candidates are downgraded to Chat Completions with reasoning controls closed, while correlated Responses detail remains on Responses. Evidence-free `/v1/models` and wholly health-only `/health` groups take transport and presentation metadata from the bounded Pi catalog lookup: `openai-responses` when the catalog says so, otherwise Chat Completions. They expose no speculative reasoning selector and never select native Messages. Catalog identity such as Amazon Bedrock remains separate from the selected wire protocol.
|
|
209
209
|
|
|
210
210
|
Native Messages authenticates with `x-api-key`; every transport carries the `x-litellm-session-id` header described above. Strict JSON-schema tools are enabled separately from Messages transport and thinking compatibility: every deployment must affirm support for the actual route/API. Generic `supports_response_schema` and `supports_native_structured_output` metadata does not grant strict tools. Only deployments that LiteLLM reports with the `anthropic` adapter carry that evidence today. Claude served through Bedrock or Vertex therefore uses ordinary tool serialization, and a tool that requires strict sampling fails before the request is sent.
|
|
211
211
|
|
|
@@ -405,6 +405,8 @@ Reasoning levels require deployment evidence for the parameter Pi sends: `reason
|
|
|
405
405
|
|
|
406
406
|
LiteLLM omits `supported_openai_params` for a deployment its model map does not describe. Such a deployment keeps the `reasoning_effort` carrier when its `model_info` explicitly sets `supports_reasoning: true`, which is the operator's opt-in. A `supported_openai_params` list that omits `reasoning_effort` still denies effort levels unless `litellm_params.allowed_openai_params` adds it. Kimi and DeepSeek generations are excluded because their contracts name their own carriers. Evidence-free fallback models retain catalog presentation metadata but expose no speculative selector.
|
|
407
407
|
|
|
408
|
+
GPT-5.5 and later reject `reasoning_effort` together with function tools on `/v1/chat/completions`. On a Chat route where any deployment's `model_info.base_model` or `litellm_params.model` names such a backend, tool requests turn reasoning off instead of failing or being answered by a router fallback: `reasoning_effort` is set to the model's declared off value, or removed when it declares none. GPT-6 and later fall back to their default effort when the field is omitted, so they get `none` either way. A model with no backend evidence falls back to its route name. Requests without tools keep their reasoning level. A model that cannot turn reasoning off, such as GPT-6 Astra, cannot call tools on Chat Completions at all. To keep reasoning on tool turns, serve the route through `/v1/responses` (see [Protocols and prompt caching](#protocols-and-prompt-caching)).
|
|
409
|
+
|
|
408
410
|
Kimi K2.5/K2.6 use an on/off thinking switch; K2.7 Code/Highspeed is always thinking. Kimi K3 and DeepSeek V4 use public effort evidence with LiteLLM overrides. Responses levels are translated to valid `reasoning.effort` values; Chat-only compatibility fields never reach Responses models. Legacy cached level maps remain usable while discovery refreshes, and stored Moonshot compatibility can restore safe display normalization.
|
|
409
411
|
|
|
410
412
|
The development probe runs against minimized snapshots with `npm run probe:proxy -- --snapshot tests/fixtures/proxy/prod-model-info-2026-09-04.json`. A bounded live matrix is available with `--live --base-url <proxy-url> --max-requests 100`; its request bodies use the discovered carrier and request policy.
|
package/package.json
CHANGED
package/src/backend-identity.ts
CHANGED
package/src/discover.ts
CHANGED
|
@@ -93,17 +93,31 @@ export function normalizeBaseUrl(input: string, allowInsecureHttp = false): stri
|
|
|
93
93
|
const ANTHROPIC_MODEL_PATTERN = /(?:^|[-_/.:])(?:anthropic\/|(?:claude|fable|opus|sonnet|haiku)(?=$|[-_/.:]))/i;
|
|
94
94
|
const MOONSHOT_MODEL_PATTERN = /^(moonshotai\/|moonshot\/|kimi[-/])/i;
|
|
95
95
|
const FORCED_THINKING_MODEL_PATTERN = /(?:^|[-/])thinking(?:[-/]|$)/i;
|
|
96
|
-
//
|
|
97
|
-
//
|
|
98
|
-
//
|
|
99
|
-
const
|
|
96
|
+
// GPT ids appear bare, prefixed, or dated (`gpt-6-sol`, `llm-gateway/gpt-5.5`,
|
|
97
|
+
// `gpt-5.5-20260504143601`); match them all so the tool+reasoning workaround
|
|
98
|
+
// survives renames. `gpt-4o` and `gpt-oss` carry no generation number here.
|
|
99
|
+
const GPT_GENERATION_PATTERN = /(?:^|\/)gpt-(\d+)(?:\.(\d+))?(?=$|[-.])/i;
|
|
100
100
|
|
|
101
101
|
export function isMoonshotModel(modelId: string): boolean {
|
|
102
102
|
return MOONSHOT_MODEL_PATTERN.test(modelId);
|
|
103
103
|
}
|
|
104
104
|
|
|
105
|
-
|
|
106
|
-
|
|
105
|
+
function gptGeneration(modelId: string): [major: number, minor: number] | undefined {
|
|
106
|
+
const match = GPT_GENERATION_PATTERN.exec(modelId);
|
|
107
|
+
return match ? [Number(match[1]), Number(match[2] ?? 0)] : undefined;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// GPT-5.5 and later reject `reasoning_effort` alongside function tools on
|
|
111
|
+
// /v1/chat/completions ("use /v1/responses").
|
|
112
|
+
export function isGpt55OrNewerModel(modelId: string): boolean {
|
|
113
|
+
const generation = gptGeneration(modelId);
|
|
114
|
+
return generation !== undefined && (generation[0] > 5 || (generation[0] === 5 && generation[1] >= 5));
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// GPT-6 also rejects an omitted effort there: it falls back to its default effort.
|
|
118
|
+
export function isGpt6OrNewerModel(modelId: string): boolean {
|
|
119
|
+
const generation = gptGeneration(modelId);
|
|
120
|
+
return generation !== undefined && generation[0] >= 6;
|
|
107
121
|
}
|
|
108
122
|
|
|
109
123
|
function shouldSuppressReasoningContent(modelId: string): boolean {
|
|
@@ -736,13 +750,29 @@ function mapFromModelInfoGroup(
|
|
|
736
750
|
!hasConflictingFamily &&
|
|
737
751
|
(reduced.deploymentFamilies.includes("kimi") || (unlabeled && isMoonshotModel(reduced.id)));
|
|
738
752
|
if (moonshotEvidence && !unanimousMoonshot) options.withheldRepairRoutes?.push(reduced.id);
|
|
739
|
-
const
|
|
753
|
+
const familyPolicy = requestPolicy(
|
|
740
754
|
reduced.id,
|
|
741
755
|
reduced.deploymentFamilies,
|
|
742
756
|
reduced.normalizeThinkTags,
|
|
743
757
|
reduced.suppressReasoningVisibility,
|
|
744
758
|
unanimousMoonshot,
|
|
745
759
|
);
|
|
760
|
+
// One GPT-5.5+ deployment is enough: a tool request routed to it fails with
|
|
761
|
+
// reasoning attached, while dropping reasoning elsewhere only loses effort.
|
|
762
|
+
const chatBackendIds =
|
|
763
|
+
api === "openai-completions"
|
|
764
|
+
? entries.flatMap((entry) => resolveBackendIdentity({ ...entry, model_name: undefined })?.modelId ?? [])
|
|
765
|
+
: [];
|
|
766
|
+
const modelPolicy: LiteLLMModelPolicy | undefined = chatBackendIds.some(isGpt55OrNewerModel)
|
|
767
|
+
? {
|
|
768
|
+
normalizeStrictToolMessages: false,
|
|
769
|
+
normalizeThinkTags: false,
|
|
770
|
+
suppressReasoningVisibility: false,
|
|
771
|
+
...familyPolicy,
|
|
772
|
+
dropToolReasoning: true,
|
|
773
|
+
...(chatBackendIds.some(isGpt6OrNewerModel) ? { explicitToolReasoningOff: true } : {}),
|
|
774
|
+
}
|
|
775
|
+
: familyPolicy;
|
|
746
776
|
return {
|
|
747
777
|
id: reduced.id,
|
|
748
778
|
// Reduced groups never borrow the ` (no metadata)` sentinel, which authorizes
|
|
@@ -1008,6 +1038,10 @@ function applyWildcardEvidence(
|
|
|
1008
1038
|
...(policies.every((policy) => policy.normalizeGeminiReasoningEffort)
|
|
1009
1039
|
? { normalizeGeminiReasoningEffort: true as const }
|
|
1010
1040
|
: {}),
|
|
1041
|
+
...(policies.some((policy) => policy.dropToolReasoning) ? { dropToolReasoning: true as const } : {}),
|
|
1042
|
+
...(policies.some((policy) => policy.explicitToolReasoningOff)
|
|
1043
|
+
? { explicitToolReasoningOff: true as const }
|
|
1044
|
+
: {}),
|
|
1011
1045
|
}
|
|
1012
1046
|
: undefined;
|
|
1013
1047
|
const {
|
|
@@ -1062,6 +1096,12 @@ function mapFromWildcardExpansion(
|
|
|
1062
1096
|
...(modelPolicies.every((policy) => policy?.normalizeGeminiReasoningEffort === true)
|
|
1063
1097
|
? { normalizeGeminiReasoningEffort: true as const }
|
|
1064
1098
|
: {}),
|
|
1099
|
+
...(api === "openai-completions" && modelPolicies.some((policy) => policy?.dropToolReasoning === true)
|
|
1100
|
+
? { dropToolReasoning: true as const }
|
|
1101
|
+
: {}),
|
|
1102
|
+
...(api === "openai-completions" && modelPolicies.some((policy) => policy?.explicitToolReasoningOff === true)
|
|
1103
|
+
? { explicitToolReasoningOff: true as const }
|
|
1104
|
+
: {}),
|
|
1065
1105
|
}
|
|
1066
1106
|
: !hasFamilyEvidence && isMoonshotModel(id)
|
|
1067
1107
|
? moonshotPolicy(id)
|
|
@@ -1329,7 +1369,26 @@ export function restoreCachedModelPolicy(model: Model<Api>): Model<Api> {
|
|
|
1329
1369
|
// re-proven offline, so fail closed until the forced network refresh below.
|
|
1330
1370
|
restored = { ...cached, compat: Object.keys(compat).length > 0 ? compat : undefined };
|
|
1331
1371
|
}
|
|
1332
|
-
if (restored.litellmPolicy
|
|
1372
|
+
if (restored.litellmPolicy) return restored;
|
|
1373
|
+
// The cache no longer holds the deployment rows that prove a GPT-5.5+ backend, so an
|
|
1374
|
+
// OpenAI-family Chat entry fails toward the tool-reasoning workaround until refreshed.
|
|
1375
|
+
if (
|
|
1376
|
+
restored.api === "openai-completions" &&
|
|
1377
|
+
restored.litellmBackendFamily === "openai" &&
|
|
1378
|
+
isGpt55OrNewerModel(restored.id)
|
|
1379
|
+
) {
|
|
1380
|
+
return {
|
|
1381
|
+
...restored,
|
|
1382
|
+
litellmPolicy: {
|
|
1383
|
+
normalizeStrictToolMessages: false,
|
|
1384
|
+
normalizeThinkTags: false,
|
|
1385
|
+
suppressReasoningVisibility: false,
|
|
1386
|
+
dropToolReasoning: true,
|
|
1387
|
+
...(isGpt6OrNewerModel(restored.id) ? { explicitToolReasoningOff: true } : {}),
|
|
1388
|
+
},
|
|
1389
|
+
} as LiteLLMModel;
|
|
1390
|
+
}
|
|
1391
|
+
if (!hasMoonshotCompatEvidence(restored.compat)) return restored;
|
|
1333
1392
|
return { ...restored, litellmPolicy: moonshotPolicy(restored.id) } as LiteLLMModel;
|
|
1334
1393
|
}
|
|
1335
1394
|
|
package/src/index.ts
CHANGED
|
@@ -19,7 +19,7 @@ import { Type } from "@earendil-works/pi-ai";
|
|
|
19
19
|
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
20
20
|
import { getAgentDir, readStoredCredential } from "@earendil-works/pi-coding-agent";
|
|
21
21
|
import { setupLiteLLMCostTracking } from "./cost.js";
|
|
22
|
-
import { discoverModels,
|
|
22
|
+
import { discoverModels, isGpt6OrNewerModel, isGpt55OrNewerModel, normalizeBaseUrl } from "./discover.js";
|
|
23
23
|
import { getGcloudToken, hasGcloudAdcCredentials, isGcloudTokenAuthEnabled } from "./gcloud-token.js";
|
|
24
24
|
import {
|
|
25
25
|
createMcpToolDefinitions,
|
|
@@ -1360,7 +1360,8 @@ async function refreshWithBackoff(
|
|
|
1360
1360
|
return credentials;
|
|
1361
1361
|
}
|
|
1362
1362
|
pkceTransientRefreshBackoff.delete(credentials.refresh);
|
|
1363
|
-
|
|
1363
|
+
// A transient failure leaves the stored credential intact, and a new login would need the same endpoint.
|
|
1364
|
+
if (!result.ok) throw new Error(result.transient ? result.message : `${result.message}; run /login litellm again`);
|
|
1364
1365
|
return { ...credentials, ...result.token };
|
|
1365
1366
|
}
|
|
1366
1367
|
|
|
@@ -1713,12 +1714,13 @@ export function prepareLiteLLMRequestPayload(
|
|
|
1713
1714
|
for (const [key, value] of Object.entries(REASONING_VISIBILITY_DEFAULTS)) update(key, value);
|
|
1714
1715
|
}
|
|
1715
1716
|
|
|
1716
|
-
//
|
|
1717
|
-
// Drop
|
|
1717
|
+
// GPT-5.5 and later reject reasoning alongside function tools on Chat Completions.
|
|
1718
|
+
// Drop it until the gateway serves /v1/responses for the route. Discovery decides
|
|
1719
|
+
// from deployment evidence; only a model with no backend evidence uses its route name.
|
|
1720
|
+
const routeOnlyId = model?.litellmBackendFamily === undefined ? modelId : undefined;
|
|
1718
1721
|
if (
|
|
1719
1722
|
openAIApi === "openai-completions" &&
|
|
1720
|
-
|
|
1721
|
-
isGpt55Model(modelId) &&
|
|
1723
|
+
(modelPolicy?.dropToolReasoning === true || (routeOnlyId !== undefined && isGpt55OrNewerModel(routeOnlyId))) &&
|
|
1722
1724
|
Array.isArray(payload.tools) &&
|
|
1723
1725
|
payload.tools.length > 0
|
|
1724
1726
|
) {
|
|
@@ -1727,6 +1729,17 @@ export function prepareLiteLLMRequestPayload(
|
|
|
1727
1729
|
next ??= { ...payload };
|
|
1728
1730
|
delete next[key];
|
|
1729
1731
|
}
|
|
1732
|
+
// Send the model's own off effort when it declares one. GPT-6 also rejects an
|
|
1733
|
+
// omitted effort (it falls back to its default), and its error names `none` as
|
|
1734
|
+
// the fix, so it gets `none` even without carrier evidence for an off level.
|
|
1735
|
+
const declaredOff = model?.thinkingLevelMap?.off;
|
|
1736
|
+
const explicitOff =
|
|
1737
|
+
modelPolicy?.explicitToolReasoningOff === true || (routeOnlyId !== undefined && isGpt6OrNewerModel(routeOnlyId));
|
|
1738
|
+
const off = typeof declaredOff === "string" ? declaredOff : explicitOff ? "none" : undefined;
|
|
1739
|
+
if (off !== undefined && (next ?? payload).reasoning_effort !== off) {
|
|
1740
|
+
next ??= { ...payload };
|
|
1741
|
+
next.reasoning_effort = off;
|
|
1742
|
+
}
|
|
1730
1743
|
const include = (next ?? payload).include;
|
|
1731
1744
|
if (Array.isArray(include) && include.includes("reasoning.encrypted_content")) {
|
|
1732
1745
|
next ??= { ...payload };
|
package/src/model-groups.ts
CHANGED
|
@@ -798,9 +798,12 @@ export function reduceModelGroup(
|
|
|
798
798
|
// Strict tools require affirmative evidence from every routable deployment.
|
|
799
799
|
const messagesStrictTools =
|
|
800
800
|
catalogs.length > 0 && catalogs.every((catalog) => catalog?.messagesStrictTools === true) ? true : undefined;
|
|
801
|
+
// LiteLLM v1.100+ reports `supported_endpoints: null` for every model its model map does not
|
|
802
|
+
// list, so null means unknown, like an absent field. Only a list without Messages, or a
|
|
803
|
+
// malformed non-null value, withholds the transport.
|
|
801
804
|
const messagesEndpointAllowed = deployments.every((entry) => {
|
|
802
805
|
const endpoints = entry.model_info?.supported_endpoints;
|
|
803
|
-
return endpoints
|
|
806
|
+
return endpoints == null || (Array.isArray(endpoints) && endpoints.includes("/v1/messages"));
|
|
804
807
|
});
|
|
805
808
|
const api = candidateModes.every((mode) => mode === "responses")
|
|
806
809
|
? "openai-responses"
|
package/src/types.ts
CHANGED
|
@@ -28,6 +28,14 @@ export interface LiteLLMModelPolicy {
|
|
|
28
28
|
// Gemini-backed deployments require lowercase effort values. Discovery carries
|
|
29
29
|
// this evidence so the request hook does not infer a backend from the route name.
|
|
30
30
|
normalizeGeminiReasoningEffort?: boolean;
|
|
31
|
+
// GPT-5.5 and later reject `reasoning_effort` alongside function tools on
|
|
32
|
+
// /v1/chat/completions. Discovery sets this for Chat routes from deployment
|
|
33
|
+
// evidence so tool requests drop reasoning instead of failing over.
|
|
34
|
+
dropToolReasoning?: boolean;
|
|
35
|
+
// GPT-6 falls back to its default effort when `reasoning_effort` is omitted, and
|
|
36
|
+
// that is rejected alongside tools too, so these tool requests send `none` unless
|
|
37
|
+
// the model declares its own off effort.
|
|
38
|
+
explicitToolReasoningOff?: boolean;
|
|
31
39
|
}
|
|
32
40
|
|
|
33
41
|
export type DiscoveredModelFor<TApi extends LiteLLMApi> = Omit<Model<TApi>, "provider" | "baseUrl"> & {
|
|
@@ -86,7 +94,7 @@ export interface ModelInfoEntry {
|
|
|
86
94
|
mode?: string | null;
|
|
87
95
|
base_model?: string;
|
|
88
96
|
litellm_provider?: string;
|
|
89
|
-
supported_endpoints?: string[];
|
|
97
|
+
supported_endpoints?: string[] | null;
|
|
90
98
|
supported_openai_params?: string[];
|
|
91
99
|
input_cost_per_token?: number;
|
|
92
100
|
output_cost_per_token?: number;
|