From 285901b06d31a187b0494ca3d1082a1b72c3f2ab Mon Sep 17 00:00:00 2001 From: PR Bot Date: Mon, 30 Mar 2026 14:16:39 +0800 Subject: [PATCH] feat: add MiniMax as first-class LLM provider Add MiniMax (MiniMax-M2.7, MiniMax-M2.7-highspeed) as a built-in provider alongside OpenAI and Anthropic. MiniMax uses OpenAI-compatible API at api.minimax.io/v1 with automatic temperature clamping to (0, 1]. Changes: - Add "minimax" to provider enum in LLMConfigSchema and AgentLLMOverrideSchema - Handle minimax routing in createLLMClient() with default base URL - Clamp temperature in both chatCompletion() and chatWithTools() for minimax - Update CLI config commands (set-global, init) with minimax provider option - Update .env.example with MiniMax configuration example - Update README (zh/en/ja) with MiniMax quick setup docs - Add 17 unit tests and 3 integration tests --- .env.example | 8 +- README.en.md | 11 +- README.ja.md | 11 +- README.md | 11 +- packages/cli/src/commands/config.ts | 2 +- packages/cli/src/commands/init.ts | 7 +- .../src/__tests__/minimax-integration.test.ts | 57 +++ .../src/__tests__/minimax-provider.test.ts | 355 ++++++++++++++++++ packages/core/src/llm/provider.ts | 28 +- packages/core/src/models/project.ts | 4 +- packages/core/src/utils/config-loader.ts | 2 +- 11 files changed, 474 insertions(+), 22 deletions(-) create mode 100644 packages/core/src/__tests__/minimax-integration.test.ts create mode 100644 packages/core/src/__tests__/minimax-provider.test.ts diff --git a/.env.example b/.env.example index 3e9d3347..bff28c33 100644 --- a/.env.example +++ b/.env.example @@ -1,7 +1,7 @@ # InkOS Environment Configuration # Copy to .env and fill in your values -# LLM Provider (openai, anthropic, custom) +# LLM Provider (openai, anthropic, minimax, custom) INKOS_LLM_PROVIDER=openai # API Base URL (OpenAI-compatible endpoint) @@ -13,6 +13,12 @@ INKOS_LLM_API_KEY=sk-your-key-here # Model name INKOS_LLM_MODEL=gpt-4o +# --- MiniMax example --- +# INKOS_LLM_PROVIDER=minimax +# INKOS_LLM_BASE_URL=https://api.minimax.io/v1 +# INKOS_LLM_API_KEY=your-minimax-api-key +# INKOS_LLM_MODEL=MiniMax-M2.7 + # Notifications (optional) INKOS_TELEGRAM_BOT_TOKEN= INKOS_TELEGRAM_CHAT_ID= diff --git a/README.en.md b/README.en.md index 3e23599d..929556fb 100644 --- a/README.en.md +++ b/README.en.md @@ -49,12 +49,12 @@ Once installed, Claw can invoke InkOS atomic commands and control-surface operat ```bash inkos config set-global \ --lang en \ - --provider \ + --provider \ --base-url \ --api-key \ --model -# provider: openai / anthropic / custom (use custom for OpenAI-compatible proxies) +# provider: openai / anthropic / minimax / custom (use custom for OpenAI-compatible proxies) # base-url: your API provider URL # api-key: your API key # model: your model name @@ -62,6 +62,9 @@ inkos config set-global \ `--lang en` sets English as the default writing language for all projects. Saved to `~/.inkos/.env`. New projects just work without extra config. +> **MiniMax quick setup:** `inkos config set-global --lang en --provider minimax --base-url https://api.minimax.io/v1 --api-key --model MiniMax-M2.7` +> Supported models: `MiniMax-M2.7` (latest, 204K context), `MiniMax-M2.7-highspeed` (204K context). Temperature is auto-clamped to (0, 1]. + **Option 2: Per-project `.env`** ```bash @@ -71,8 +74,8 @@ inkos init my-novel # Initialize project ```bash # Required -INKOS_LLM_PROVIDER= # openai / anthropic / custom (use custom for any OpenAI-compatible API) -INKOS_LLM_BASE_URL= # API endpoint +INKOS_LLM_PROVIDER= # openai / anthropic / minimax / custom (use custom for any OpenAI-compatible API) +INKOS_LLM_BASE_URL= # API endpoint (supports MiniMax, proxies, etc.) INKOS_LLM_API_KEY= # API Key INKOS_LLM_MODEL= # Model name diff --git a/README.ja.md b/README.ja.md index 9e9a5716..1179d987 100644 --- a/README.ja.md +++ b/README.ja.md @@ -49,12 +49,12 @@ npm でインストール済み、またはリポジトリをクローン済み ```bash inkos config set-global \ --lang en \ - --provider \ + --provider \ --base-url \ --api-key \ --model <モデル名> -# provider: openai / anthropic / custom(OpenAI互換プロキシにはcustomを使用) +# provider: openai / anthropic / minimax / custom(OpenAI互換プロキシにはcustomを使用) # base-url: APIプロバイダーURL # api-key: APIキー # model: モデル名 @@ -62,6 +62,9 @@ inkos config set-global \ `--lang en` で英語をすべてのプロジェクトのデフォルト執筆言語に設定。`~/.inkos/.env` に保存されます。新規プロジェクトは追加設定なしですぐに使えます。 +> **MiniMax クイック設定:** `inkos config set-global --provider minimax --base-url https://api.minimax.io/v1 --api-key --model MiniMax-M2.7` +> 対応モデル:`MiniMax-M2.7`(最新、204Kコンテキスト)、`MiniMax-M2.7-highspeed`(204Kコンテキスト)。温度は自動的に (0, 1] に制限されます。 + **方法2:プロジェクトごとの `.env`** ```bash @@ -71,8 +74,8 @@ inkos init my-novel # プロジェクトを初期化 ```bash # 必須 -INKOS_LLM_PROVIDER= # openai / anthropic / custom(OpenAI互換APIにはcustomを使用) -INKOS_LLM_BASE_URL= # APIエンドポイント +INKOS_LLM_PROVIDER= # openai / anthropic / minimax / custom(OpenAI互換APIにはcustomを使用) +INKOS_LLM_BASE_URL= # APIエンドポイント(MiniMax、プロキシ等対応) INKOS_LLM_API_KEY= # APIキー INKOS_LLM_MODEL= # モデル名 diff --git a/README.md b/README.md index 1a28c215..b3c7df64 100644 --- a/README.md +++ b/README.md @@ -48,12 +48,12 @@ clawhub install inkos # 从 ClawHub 安装 InkOS Skill ```bash inkos config set-global \ - --provider \ + --provider \ --base-url \ --api-key <你的 API Key> \ --model <模型名> -# provider: openai / anthropic / custom(兼容 OpenAI 格式的中转站选 custom) +# provider: openai / anthropic / minimax / custom(兼容 OpenAI 格式的中转站选 custom) # base-url: 你的 API 提供商地址 # api-key: 你的 API Key # model: 你的模型名称 @@ -61,6 +61,9 @@ inkos config set-global \ 配置保存在 `~/.inkos/.env`,所有项目共享。之后新建项目不用再配。 +> **MiniMax 快速配置:** `inkos config set-global --provider minimax --base-url https://api.minimax.io/v1 --api-key --model MiniMax-M2.7` +> 支持模型:`MiniMax-M2.7`(最新,204K 上下文)、`MiniMax-M2.7-highspeed`(204K 上下文)。温度自动限制在 (0, 1] 范围。 + **方式二:项目级 `.env`** ```bash @@ -70,8 +73,8 @@ inkos init my-novel # 初始化项目 ```bash # 必填 -INKOS_LLM_PROVIDER= # openai / anthropic / custom(兼容 OpenAI 接口的都选 custom) -INKOS_LLM_BASE_URL= # API 地址(支持中转站、智谱、Gemini 等) +INKOS_LLM_PROVIDER= # openai / anthropic / minimax / custom(兼容 OpenAI 接口的都选 custom) +INKOS_LLM_BASE_URL= # API 地址(支持中转站、MiniMax、智谱、Gemini 等) INKOS_LLM_API_KEY= # API Key INKOS_LLM_MODEL= # 模型名 diff --git a/packages/cli/src/commands/config.ts b/packages/cli/src/commands/config.ts index ccaf65ba..be31d15f 100644 --- a/packages/cli/src/commands/config.ts +++ b/packages/cli/src/commands/config.ts @@ -87,7 +87,7 @@ configCommand configCommand .command("set-global") .description("Set global LLM config (~/.inkos/.env), shared by all projects") - .requiredOption("--provider ", "LLM provider (openai / anthropic)") + .requiredOption("--provider ", "LLM provider (openai / anthropic / minimax)") .requiredOption("--base-url ", "API base URL") .requiredOption("--api-key ", "API key") .requiredOption("--model ", "Model name") diff --git a/packages/cli/src/commands/init.ts b/packages/cli/src/commands/init.ts index 1bdc88df..777b39f6 100644 --- a/packages/cli/src/commands/init.ts +++ b/packages/cli/src/commands/init.ts @@ -90,7 +90,7 @@ export const initCommand = new Command("init") [ "# LLM Configuration", "# Tip: Run 'inkos config set-global' to set once for all projects.", - "# Provider: openai (OpenAI / compatible proxy), anthropic (Anthropic native)", + "# Provider: openai (OpenAI / compatible proxy), anthropic (Anthropic native), minimax (MiniMax)", "INKOS_LLM_PROVIDER=openai", "INKOS_LLM_BASE_URL=", "INKOS_LLM_API_KEY=", @@ -110,6 +110,11 @@ export const initCommand = new Command("init") "# INKOS_LLM_PROVIDER=anthropic", "# INKOS_LLM_BASE_URL=", "# INKOS_LLM_MODEL=", + "", + "# MiniMax example:", + "# INKOS_LLM_PROVIDER=minimax", + "# INKOS_LLM_BASE_URL=https://api.minimax.io/v1", + "# INKOS_LLM_MODEL=MiniMax-M2.7", ].join("\n"), "utf-8", ); diff --git a/packages/core/src/__tests__/minimax-integration.test.ts b/packages/core/src/__tests__/minimax-integration.test.ts new file mode 100644 index 00000000..ad78f346 --- /dev/null +++ b/packages/core/src/__tests__/minimax-integration.test.ts @@ -0,0 +1,57 @@ +/** + * Integration tests for MiniMax LLM provider. + * + * These tests verify the MiniMax integration end-to-end against the real API. + * They require the MINIMAX_API_KEY environment variable to be set. + * + * Run with: + * MINIMAX_API_KEY= npx vitest run src/__tests__/minimax-integration.test.ts + */ +import { describe, expect, it } from "vitest"; +import { createLLMClient, chatCompletion } from "../llm/provider.js"; +import { LLMConfigSchema } from "../models/project.js"; + +const MINIMAX_API_KEY = process.env.MINIMAX_API_KEY; + +const describeIf = MINIMAX_API_KEY ? describe : describe.skip; + +describeIf("MiniMax integration (real API)", () => { + const config = LLMConfigSchema.parse({ + provider: "minimax", + baseUrl: "https://api.minimax.io/v1", + apiKey: MINIMAX_API_KEY ?? "", + model: "MiniMax-M2.7-highspeed", + temperature: 0.7, + maxTokens: 64, + }); + const client = createLLMClient(config); + + it("completes a simple chat request (sync)", async () => { + const syncClient = createLLMClient({ ...config, stream: false }); + const result = await chatCompletion(syncClient, config.model, [ + { role: "user", content: "Say OK and nothing else." }, + ], { maxTokens: 16 }); + + expect(result.content).toBeTruthy(); + expect(result.content.length).toBeGreaterThan(0); + expect(result.usage.totalTokens).toBeGreaterThan(0); + }, 30000); + + it("completes a simple chat request (streaming)", async () => { + const result = await chatCompletion(client, config.model, [ + { role: "user", content: "Say OK and nothing else." }, + ], { maxTokens: 16 }); + + expect(result.content).toBeTruthy(); + expect(result.content.length).toBeGreaterThan(0); + }, 30000); + + it("handles system messages correctly", async () => { + const result = await chatCompletion(client, config.model, [ + { role: "system", content: "You are a helpful assistant. Always reply with exactly one word." }, + { role: "user", content: "What color is the sky?" }, + ], { maxTokens: 16 }); + + expect(result.content).toBeTruthy(); + }, 30000); +}); diff --git a/packages/core/src/__tests__/minimax-provider.test.ts b/packages/core/src/__tests__/minimax-provider.test.ts new file mode 100644 index 00000000..0e52bdd9 --- /dev/null +++ b/packages/core/src/__tests__/minimax-provider.test.ts @@ -0,0 +1,355 @@ +import { describe, expect, it, vi } from "vitest"; +import type OpenAI from "openai"; +import { createLLMClient, chatCompletion, chatWithTools, type LLMClient, type ToolDefinition } from "../llm/provider.js"; +import { LLMConfigSchema } from "../models/project.js"; + +// === Schema validation === + +describe("LLMConfigSchema minimax provider", () => { + it("accepts minimax as a valid provider", () => { + const config = LLMConfigSchema.parse({ + provider: "minimax", + baseUrl: "https://api.minimax.io/v1", + apiKey: "test-key", + model: "MiniMax-M2.7", + }); + expect(config.provider).toBe("minimax"); + expect(config.model).toBe("MiniMax-M2.7"); + }); + + it("defaults temperature to 0.7 for minimax", () => { + const config = LLMConfigSchema.parse({ + provider: "minimax", + baseUrl: "https://api.minimax.io/v1", + apiKey: "test-key", + model: "MiniMax-M2.7", + }); + expect(config.temperature).toBe(0.7); + }); + + it("defaults stream to true for minimax", () => { + const config = LLMConfigSchema.parse({ + provider: "minimax", + baseUrl: "https://api.minimax.io/v1", + apiKey: "test-key", + model: "MiniMax-M2.7", + }); + expect(config.stream).toBe(true); + }); +}); + +// === createLLMClient === + +describe("createLLMClient minimax", () => { + it("creates a minimax client with OpenAI SDK internally", () => { + const config = LLMConfigSchema.parse({ + provider: "minimax", + baseUrl: "https://api.minimax.io/v1", + apiKey: "test-key", + model: "MiniMax-M2.7", + }); + const client = createLLMClient(config); + expect(client.provider).toBe("minimax"); + expect(client._openai).toBeDefined(); + expect(client._anthropic).toBeUndefined(); + expect(client.stream).toBe(true); + }); + + it("uses default base URL when baseUrl matches the MiniMax endpoint", () => { + const config = LLMConfigSchema.parse({ + provider: "minimax", + baseUrl: "https://api.minimax.io/v1", + apiKey: "test-key", + model: "MiniMax-M2.7", + }); + const client = createLLMClient(config); + expect(client._openai?.baseURL).toBe("https://api.minimax.io/v1"); + }); + + it("clamps temperature 0 to 0.01 for minimax", () => { + const config = LLMConfigSchema.parse({ + provider: "minimax", + baseUrl: "https://api.minimax.io/v1", + apiKey: "test-key", + model: "MiniMax-M2.7", + temperature: 0, + }); + const client = createLLMClient(config); + expect(client.defaults.temperature).toBe(0.01); + }); + + it("clamps temperature 1.5 to 1.0 for minimax", () => { + const config = LLMConfigSchema.parse({ + provider: "minimax", + baseUrl: "https://api.minimax.io/v1", + apiKey: "test-key", + model: "MiniMax-M2.7", + temperature: 1.5, + }); + const client = createLLMClient(config); + expect(client.defaults.temperature).toBe(1.0); + }); + + it("keeps valid temperature 0.7 unchanged for minimax", () => { + const config = LLMConfigSchema.parse({ + provider: "minimax", + baseUrl: "https://api.minimax.io/v1", + apiKey: "test-key", + model: "MiniMax-M2.7", + temperature: 0.7, + }); + const client = createLLMClient(config); + expect(client.defaults.temperature).toBe(0.7); + }); + + it("does not clamp temperature for openai provider", () => { + const config = LLMConfigSchema.parse({ + provider: "openai", + baseUrl: "https://api.openai.com/v1", + apiKey: "test-key", + model: "gpt-4o", + temperature: 0, + }); + const client = createLLMClient(config); + expect(client.defaults.temperature).toBe(0); + }); +}); + +// === chatCompletion with minimax client === + +const ZERO_USAGE = { + prompt_tokens: 11, + completion_tokens: 7, + total_tokens: 18, +} as const; + +function makeMiniMaxClient(overrides?: Partial): LLMClient { + const create = vi.fn().mockResolvedValue({ + choices: [{ message: { content: "minimax response" } }], + usage: ZERO_USAGE, + }); + return { + provider: "minimax", + apiFormat: "chat", + stream: false, + _openai: { + chat: { completions: { create } }, + } as unknown as OpenAI, + defaults: { + temperature: 0.7, + maxTokens: 8192, + thinkingBudget: 0, + maxTokensCap: null, + extra: {}, + }, + ...overrides, + }; +} + +describe("chatCompletion with minimax provider", () => { + it("routes minimax through OpenAI Chat API", async () => { + const client = makeMiniMaxClient(); + const result = await chatCompletion(client, "MiniMax-M2.7", [ + { role: "user", content: "Hello" }, + ]); + expect(result.content).toBe("minimax response"); + expect(result.usage.promptTokens).toBe(11); + expect(result.usage.completionTokens).toBe(7); + }); + + it("clamps per-call temperature override for minimax", async () => { + const create = vi.fn().mockResolvedValue({ + choices: [{ message: { content: "ok" } }], + usage: ZERO_USAGE, + }); + const client: LLMClient = { + provider: "minimax", + apiFormat: "chat", + stream: false, + _openai: { + chat: { completions: { create } }, + } as unknown as OpenAI, + defaults: { + temperature: 0.7, + maxTokens: 8192, + thinkingBudget: 0, + maxTokensCap: null, + extra: {}, + }, + }; + + await chatCompletion(client, "MiniMax-M2.7", [ + { role: "user", content: "ping" }, + ], { temperature: 0 }); + + expect(create).toHaveBeenCalledTimes(1); + const callArgs = create.mock.calls[0]?.[0]; + expect(callArgs.temperature).toBe(0.01); + }); + + it("does not clamp temperature for openai provider on per-call override", async () => { + const create = vi.fn().mockResolvedValue({ + choices: [{ message: { content: "ok" } }], + usage: ZERO_USAGE, + }); + const client: LLMClient = { + provider: "openai", + apiFormat: "chat", + stream: false, + _openai: { + chat: { completions: { create } }, + } as unknown as OpenAI, + defaults: { + temperature: 0.7, + maxTokens: 8192, + thinkingBudget: 0, + maxTokensCap: null, + extra: {}, + }, + }; + + await chatCompletion(client, "gpt-4o", [ + { role: "user", content: "ping" }, + ], { temperature: 0 }); + + expect(create.mock.calls[0]?.[0].temperature).toBe(0); + }); + + it("handles streaming with minimax provider", async () => { + const create = vi.fn().mockResolvedValue({ + async *[Symbol.asyncIterator]() { + yield { choices: [{ delta: { content: "mini" } }] }; + yield { choices: [{ delta: { content: "max" } }] }; + yield { choices: [{ delta: {} }], usage: ZERO_USAGE }; + }, + }); + const client: LLMClient = { + provider: "minimax", + apiFormat: "chat", + stream: true, + _openai: { + chat: { completions: { create } }, + } as unknown as OpenAI, + defaults: { + temperature: 0.7, + maxTokens: 8192, + thinkingBudget: 0, + maxTokensCap: null, + extra: {}, + }, + }; + + const result = await chatCompletion(client, "MiniMax-M2.7", [ + { role: "user", content: "ping" }, + ]); + expect(result.content).toBe("minimax"); + expect(create.mock.calls[0]?.[0].stream).toBe(true); + }); +}); + +// === chatWithTools with minimax client === + +describe("chatWithTools with minimax provider", () => { + it("routes tool calls through OpenAI Chat API for minimax", async () => { + const create = vi.fn().mockResolvedValue({ + async *[Symbol.asyncIterator]() { + yield { + choices: [{ + delta: { + content: null, + tool_calls: [{ + index: 0, + id: "call_1", + function: { name: "search", arguments: '{"q":"test"}' }, + }], + }, + }], + }; + }, + }); + const client: LLMClient = { + provider: "minimax", + apiFormat: "chat", + stream: true, + _openai: { + chat: { completions: { create } }, + } as unknown as OpenAI, + defaults: { + temperature: 0.7, + maxTokens: 8192, + thinkingBudget: 0, + maxTokensCap: null, + extra: {}, + }, + }; + + const tools: ToolDefinition[] = [{ + name: "search", + description: "Search the web", + parameters: { type: "object", properties: { q: { type: "string" } } }, + }]; + + const result = await chatWithTools(client, "MiniMax-M2.7", [ + { role: "user", content: "search for cats" }, + ], tools); + + expect(result.toolCalls).toHaveLength(1); + expect(result.toolCalls[0]?.name).toBe("search"); + expect(result.toolCalls[0]?.arguments).toBe('{"q":"test"}'); + }); + + it("clamps temperature in chatWithTools for minimax", async () => { + const create = vi.fn().mockResolvedValue({ + async *[Symbol.asyncIterator]() { + yield { choices: [{ delta: { content: "ok" } }] }; + }, + }); + const client: LLMClient = { + provider: "minimax", + apiFormat: "chat", + stream: true, + _openai: { + chat: { completions: { create } }, + } as unknown as OpenAI, + defaults: { + temperature: 0.7, + maxTokens: 8192, + thinkingBudget: 0, + maxTokensCap: null, + extra: {}, + }, + }; + + await chatWithTools(client, "MiniMax-M2.7", [ + { role: "user", content: "hello" }, + ], [], { temperature: 0 }); + + expect(create.mock.calls[0]?.[0].temperature).toBe(0.01); + }); +}); + +// === MiniMax M2.7-highspeed model === + +describe("MiniMax M2.7-highspeed model", () => { + it("accepts MiniMax-M2.7-highspeed in schema", () => { + const config = LLMConfigSchema.parse({ + provider: "minimax", + baseUrl: "https://api.minimax.io/v1", + apiKey: "test-key", + model: "MiniMax-M2.7-highspeed", + }); + expect(config.model).toBe("MiniMax-M2.7-highspeed"); + }); + + it("creates client with MiniMax-M2.7-highspeed model", () => { + const config = LLMConfigSchema.parse({ + provider: "minimax", + baseUrl: "https://api.minimax.io/v1", + apiKey: "test-key", + model: "MiniMax-M2.7-highspeed", + }); + const client = createLLMClient(config); + expect(client.provider).toBe("minimax"); + expect(client._openai).toBeDefined(); + }); +}); diff --git a/packages/core/src/llm/provider.ts b/packages/core/src/llm/provider.ts index 819c468d..e28ecf18 100644 --- a/packages/core/src/llm/provider.ts +++ b/packages/core/src/llm/provider.ts @@ -70,7 +70,7 @@ export interface LLMMessage { } export interface LLMClient { - readonly provider: "openai" | "anthropic"; + readonly provider: "openai" | "anthropic" | "minimax"; readonly apiFormat: "chat" | "responses"; readonly stream: boolean; readonly _openai?: OpenAI; @@ -111,9 +111,17 @@ export interface ChatWithToolsResult { // === Factory === +/** MiniMax temperature must be in (0, 1]. Clamp to valid range. */ +function clampMiniMaxTemperature(temperature: number): number { + return Math.max(0.01, Math.min(temperature, 1.0)); +} + export function createLLMClient(config: LLMConfig): LLMClient { + const isMiniMax = config.provider === "minimax"; const defaults = { - temperature: config.temperature ?? 0.7, + temperature: isMiniMax + ? clampMiniMaxTemperature(config.temperature ?? 0.7) + : (config.temperature ?? 0.7), maxTokens: config.maxTokens ?? 8192, maxTokensCap: config.maxTokens ?? null, // only cap when user explicitly set maxTokens thinkingBudget: config.thinkingBudget ?? 0, @@ -134,6 +142,16 @@ export function createLLMClient(config: LLMConfig): LLMClient { defaults, }; } + if (config.provider === "minimax") { + const baseURL = config.baseUrl || "https://api.minimax.io/v1"; + return { + provider: "minimax", + apiFormat, + stream, + _openai: new OpenAI({ apiKey: config.apiKey, baseURL }), + defaults, + }; + } // openai or custom — both use OpenAI SDK return { provider: "openai", @@ -249,8 +267,9 @@ export async function chatCompletion( ): Promise { const perCallMax = options?.maxTokens ?? client.defaults.maxTokens; const cap = client.defaults.maxTokensCap; + const rawTemp = options?.temperature ?? client.defaults.temperature; const resolved = { - temperature: options?.temperature ?? client.defaults.temperature, + temperature: client.provider === "minimax" ? clampMiniMaxTemperature(rawTemp) : rawTemp, maxTokens: cap !== null ? Math.min(perCallMax, cap) : perCallMax, extra: client.defaults.extra, }; @@ -345,8 +364,9 @@ export async function chatWithTools( }, ): Promise { try { + const rawTemp = options?.temperature ?? client.defaults.temperature; const resolved = { - temperature: options?.temperature ?? client.defaults.temperature, + temperature: client.provider === "minimax" ? clampMiniMaxTemperature(rawTemp) : rawTemp, maxTokens: options?.maxTokens ?? client.defaults.maxTokens, }; // Tool-calling always uses streaming (only used by agent loop, not by writer/auditor) diff --git a/packages/core/src/models/project.ts b/packages/core/src/models/project.ts index b3d600a5..7f7cb733 100644 --- a/packages/core/src/models/project.ts +++ b/packages/core/src/models/project.ts @@ -1,7 +1,7 @@ import { z } from "zod"; export const LLMConfigSchema = z.object({ - provider: z.enum(["anthropic", "openai", "custom"]), + provider: z.enum(["anthropic", "openai", "minimax", "custom"]), baseUrl: z.string().url(), apiKey: z.string().default(""), model: z.string().min(1), @@ -61,7 +61,7 @@ export type QualityGates = z.infer; export const AgentLLMOverrideSchema = z.object({ model: z.string().min(1), - provider: z.enum(["anthropic", "openai", "custom"]).optional(), + provider: z.enum(["anthropic", "openai", "minimax", "custom"]).optional(), baseUrl: z.string().url().optional(), apiKeyEnv: z.string().optional(), stream: z.boolean().optional(), diff --git a/packages/core/src/utils/config-loader.ts b/packages/core/src/utils/config-loader.ts index 309eaef4..8b4a735c 100644 --- a/packages/core/src/utils/config-loader.ts +++ b/packages/core/src/utils/config-loader.ts @@ -10,7 +10,7 @@ export function isApiKeyOptionalForEndpoint(params: { readonly provider?: string | undefined; readonly baseUrl?: string | undefined; }): boolean { - if (params.provider === "anthropic") { + if (params.provider === "anthropic" || params.provider === "minimax") { return false; } if (!params.baseUrl) {