pi-provider-litellm 2.0.2 → 2.0.3

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/dist/discover.js CHANGED
@@ -85,6 +85,8 @@ function catalogLookupIds(id) {
85
85
  const match = /^(?:claude-)?(opus|sonnet|haiku)-(\d+)-(\d+)$/.exec(anthropicAlias);
86
86
  if (match)
87
87
  lookupIds.add(`claude-${match[1]}-${match[2]}-${match[3]}`);
88
+ if (anthropicAlias === "fable-5")
89
+ lookupIds.add("claude-fable-5");
88
90
  return [...lookupIds];
89
91
  }
90
92
  function findCatalogModelInProvider(provider, lookupIds) {
package/dist/index.js CHANGED
@@ -3,7 +3,7 @@ import { readFile } from "node:fs/promises";
3
3
  import { join } from "node:path";
4
4
  import { getAgentDir } from "@earendil-works/pi-coding-agent";
5
5
  import { setupLiteLLMCostTracking } from "./cost.js";
6
- import { discoverModels, isGpt55Model, normalizeBaseUrl, shouldSuppressReasoningContent } from "./discover.js";
6
+ import { discoverModels, isGpt55Model, isMoonshotModel, normalizeBaseUrl, shouldSuppressReasoningContent, } from "./discover.js";
7
7
  import { getGcloudToken, getGcloudTokenCacheKey, getGcloudTokenCommand, isGcloudTokenAuthEnabled, } from "./gcloud-token.js";
8
8
  import { getSessionIdFromFile } from "./litellm.js";
9
9
  import { createMcpToolDefinitions } from "./mcp-tools.js";
@@ -579,6 +579,56 @@ function createProviderAuth(definition) {
579
579
  function isReasoningItem(item) {
580
580
  return typeof item === "object" && item !== null && item.type === "reasoning";
581
581
  }
582
+ // Returns the original array reference when no normalization is needed.
583
+ function normalizeStrictToolMessages(messages) {
584
+ const normalized = [];
585
+ let imageParts = [];
586
+ let changed = false;
587
+ const flushImages = () => {
588
+ if (imageParts.length === 0)
589
+ return;
590
+ normalized.push({
591
+ role: "user",
592
+ content: [{ type: "text", text: "Attached image(s) from tool result:" }, ...imageParts],
593
+ });
594
+ imageParts = [];
595
+ };
596
+ for (const message of messages) {
597
+ if (message.role !== "tool")
598
+ flushImages();
599
+ if (message.role === "assistant" &&
600
+ Array.isArray(message.tool_calls) &&
601
+ message.tool_calls.length > 0 &&
602
+ message.content == null) {
603
+ normalized.push({ ...message, content: "" });
604
+ changed = true;
605
+ }
606
+ else if (message.role === "tool" && Array.isArray(message.content)) {
607
+ const previousImageCount = imageParts.length;
608
+ const content = message.content
609
+ .map((part) => {
610
+ if (typeof part === "string")
611
+ return part;
612
+ if (part?.type === "text")
613
+ return part.text || "";
614
+ if (part?.type === "image_url")
615
+ imageParts.push(part);
616
+ return "";
617
+ })
618
+ .join("");
619
+ normalized.push({
620
+ ...message,
621
+ content: content || (imageParts.length > previousImageCount ? "(see attached image)" : "(no tool output)"),
622
+ });
623
+ changed = true;
624
+ }
625
+ else {
626
+ normalized.push(message);
627
+ }
628
+ }
629
+ flushImages();
630
+ return changed ? normalized : messages;
631
+ }
582
632
  // Reasoning fields LiteLLM forwards to chat-completions providers. The Moonshot
583
633
  // path defaults them off; the gpt-5.5 tool path strips them entirely.
584
634
  const REASONING_SUPPRESSION_DEFAULTS = {
@@ -587,6 +637,7 @@ const REASONING_SUPPRESSION_DEFAULTS = {
587
637
  merge_reasoning_content_in_choices: true,
588
638
  thinking: { type: "disabled" },
589
639
  };
640
+ const GEMINI_MODEL_PATTERN = /(?:^|[-_/.:])gemini(?=$|[-_/.:])/i;
590
641
  function prepareLiteLLMRequestPayload(payload, modelId, api, sessionId) {
591
642
  let next;
592
643
  const update = (key, value) => {
@@ -632,6 +683,39 @@ function prepareLiteLLMRequestPayload(payload, modelId, api, sessionId) {
632
683
  next.input = input.filter((item) => !isReasoningItem(item));
633
684
  }
634
685
  }
686
+ // Moonshot/Kimi applies strict OpenAI schema validation: assistant tool calls
687
+ // must carry string content, and tool results must be plain text.
688
+ if (modelId && isMoonshotModel(modelId)) {
689
+ const messages = (next ?? payload).messages;
690
+ if (Array.isArray(messages)) {
691
+ const normalized = normalizeStrictToolMessages(messages);
692
+ if (normalized !== messages) {
693
+ next ??= { ...payload };
694
+ next.messages = normalized;
695
+ }
696
+ }
697
+ }
698
+ if (modelId && GEMINI_MODEL_PATTERN.test(modelId)) {
699
+ const currentPayload = next ?? payload;
700
+ if (typeof currentPayload.reasoning_effort === "string") {
701
+ const lower = currentPayload.reasoning_effort.toLowerCase();
702
+ if (currentPayload.reasoning_effort !== lower) {
703
+ next ??= { ...payload };
704
+ next.reasoning_effort = lower;
705
+ }
706
+ }
707
+ const reasoningPayload = next ?? payload;
708
+ if (isPlainObject(reasoningPayload.reasoning) && typeof reasoningPayload.reasoning.effort === "string") {
709
+ const lower = reasoningPayload.reasoning.effort.toLowerCase();
710
+ if (reasoningPayload.reasoning.effort !== lower) {
711
+ next ??= { ...payload };
712
+ next.reasoning = {
713
+ ...next.reasoning,
714
+ effort: lower,
715
+ };
716
+ }
717
+ }
718
+ }
635
719
  if (sessionId) {
636
720
  next ??= { ...payload };
637
721
  next.litellm_session_id = sessionId;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-provider-litellm",
3
- "version": "2.0.2",
3
+ "version": "2.0.3",
4
4
  "description": "LiteLLM proxy provider extension for Pi",
5
5
  "type": "module",
6
6
  "license": "MIT",