From b60403ca65227c45fb70a069750b07fc41d549bb Mon Sep 17 00:00:00 2001 From: mriwiki <71009631+Neonity2020@users.noreply.github.com> Date: Tue, 16 Jun 2026 09:37:30 +0800 Subject: [PATCH] Add local Pi fallback for MiniMax-M3 - expose MiniMax-M3 when MiniMax China is available - keep SDK-provided models deduplicated - cover the fallback logic with tests --- .../src/provider/Layers/PiAdapter.test.ts | 52 ++++++++++++++++++- apps/server/src/provider/Layers/PiAdapter.ts | 42 ++++++++++++++- 2 files changed, 91 insertions(+), 3 deletions(-) diff --git a/apps/server/src/provider/Layers/PiAdapter.test.ts b/apps/server/src/provider/Layers/PiAdapter.test.ts index ffe8f40..d0191cd 100644 --- a/apps/server/src/provider/Layers/PiAdapter.test.ts +++ b/apps/server/src/provider/Layers/PiAdapter.test.ts @@ -5,7 +5,7 @@ import type { Api, Model } from "@earendil-works/pi-ai"; import { describe, expect, it } from "vitest"; -import { getPiSupportedThinkingOptions } from "./PiAdapter"; +import { getPiSupportedThinkingOptions, withLocalPiModelAdditions } from "./PiAdapter"; function makePiModel(input: { reasoning: boolean; @@ -17,6 +17,25 @@ function makePiModel(input: { }; } +function makeAvailableModel(input: { + provider: string; + id: string; + name?: string; +}): Model { + return { + id: input.id, + name: input.name ?? input.id, + api: "anthropic-messages", + provider: input.provider, + baseUrl: "https://example.test/anthropic", + reasoning: true, + input: ["text"], + cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, + contextWindow: 128_000, + maxTokens: 8_192, + }; +} + describe("getPiSupportedThinkingOptions", () => { it("hides thinking controls for non-reasoning models", () => { expect(getPiSupportedThinkingOptions(makePiModel({ reasoning: false }))).toEqual([]); @@ -62,3 +81,34 @@ describe("getPiSupportedThinkingOptions", () => { expect(options.map((option) => option.value)).toEqual(["minimal", "low", "medium", "high"]); }); }); + +describe("withLocalPiModelAdditions", () => { + it("adds MiniMax-M3 for MiniMax China when the provider is available", () => { + const models = [makeAvailableModel({ provider: "minimax-cn", id: "MiniMax-M2.7" })]; + + expect(withLocalPiModelAdditions(models, models).map((model) => model.id)).toContain( + "MiniMax-M3", + ); + }); + + it("does not add MiniMax-M3 when MiniMax China is not available", () => { + const models = [makeAvailableModel({ provider: "anthropic", id: "claude-test" })]; + + expect(withLocalPiModelAdditions(models, models).map((model) => model.id)).not.toContain( + "MiniMax-M3", + ); + }); + + it("does not duplicate MiniMax-M3 when the SDK already provides it", () => { + const models = [ + makeAvailableModel({ provider: "minimax-cn", id: "MiniMax-M2.7" }), + makeAvailableModel({ provider: "minimax-cn", id: "MiniMax-M3" }), + ]; + + expect( + withLocalPiModelAdditions(models, models).filter( + (model) => model.provider === "minimax-cn" && model.id === "MiniMax-M3", + ), + ).toHaveLength(1); + }); +}); diff --git a/apps/server/src/provider/Layers/PiAdapter.ts b/apps/server/src/provider/Layers/PiAdapter.ts index 9138206..c4ece6f 100644 --- a/apps/server/src/provider/Layers/PiAdapter.ts +++ b/apps/server/src/provider/Layers/PiAdapter.ts @@ -53,6 +53,25 @@ import { type EventNdjsonLogger, makeEventNdjsonLogger } from "./EventNdjsonLogg const PROVIDER = "pi" as const; const DEFAULT_PI_THINKING_LEVEL: ThinkingLevel = "medium"; +const LOCAL_PI_MODEL_ADDITIONS: ReadonlyArray> = [ + { + id: "MiniMax-M3", + name: "MiniMax-M3", + api: "anthropic-messages", + provider: "minimax-cn", + baseUrl: "https://api.minimaxi.com/anthropic", + reasoning: true, + input: ["text", "image"], + cost: { + input: 0.6, + output: 2.4, + cacheRead: 0.12, + cacheWrite: 0, + }, + contextWindow: 512_000, + maxTokens: 128_000, + }, +]; const PI_THINKING_OPTIONS: ReadonlyArray<{ readonly value: ThinkingLevel; readonly label: string; @@ -198,12 +217,30 @@ function findModelInRegistry( if (parsed.provider) { return ( registry.find(parsed.provider, parsed.id) ?? + LOCAL_PI_MODEL_ADDITIONS.find( + (model) => model.provider === parsed.provider && model.id === parsed.id, + ) ?? createProviderModelFallback(registry, { provider: parsed.provider, id: parsed.id }) ); } return registry .getAll() - .find((model) => model.id === parsed.id || `${model.provider}/${model.id}` === parsed.id); + .find((model) => model.id === parsed.id || `${model.provider}/${model.id}` === parsed.id) ?? + LOCAL_PI_MODEL_ADDITIONS.find( + (model) => model.id === parsed.id || `${model.provider}/${model.id}` === parsed.id, + ); +} + +export function withLocalPiModelAdditions( + models: ReadonlyArray>, + availableModels: ReadonlyArray>, +): ReadonlyArray> { + const keys = new Set(models.map((model) => `${model.provider}/${model.id}`)); + const availableProviders = new Set(availableModels.map((model) => model.provider)); + const additions = LOCAL_PI_MODEL_ADDITIONS.filter( + (model) => availableProviders.has(model.provider) && !keys.has(`${model.provider}/${model.id}`), + ); + return additions.length > 0 ? [...models, ...additions] : models; } function extractResumeSessionFile(resumeCursor: unknown): string | undefined { @@ -1652,7 +1689,8 @@ const makePiAdapter = (options?: PiAdapterLiveOptions) => const agentDir = makeAgentDir(input.agentDir); const registry = getModelRegistry(agentDir); registry.refresh(); - const models = registry.getAvailable().map((model) => { + const availableModels = registry.getAvailable(); + const models = withLocalPiModelAdditions(availableModels, availableModels).map((model) => { const supportedThinkingOptions = getPiSupportedThinkingOptions(model); return { slug: `${model.provider}/${model.id}`,