diff --git a/changelog/unreleased/2026-08-03-minimax-m3-preset.md b/changelog/unreleased/2026-08-03-minimax-m3-preset.md new file mode 100644 index 00000000..4fe883ee --- /dev/null +++ b/changelog/unreleased/2026-08-03-minimax-m3-preset.md @@ -0,0 +1,19 @@ +# Built-in MiniMax M3 model preset + +The built-in model catalog gains a MiniMax provider group with one direct `MiniMax-M3` preset, ready to use with only an API key. + +## Core + +The new `minimax-token-plan` group (label "MiniMax", listed between Moonshot and Custom) carries `MiniMax-M3` with a 1,000,000-token context window and vision support. The preset pins AgentHub's `minimax-m3` Responses client and inlines the direct endpoint `https://api.minimax.io/v1`, so it holds no secret and accepts either a Token Plan Subscription Key or a pay-as-you-go API key. + +Pricing is omitted because MiniMax doubles cache-read, input, and output rates above 512K input tokens, while the catalog cannot represent tiered pricing. Env-var fallback mirrors AgentHub's exact routing: only `MiniMax-M3` with the `minimax-m3` client resolves to `MINIMAX_API_KEY` / `MINIMAX_BASE_URL`; lookalike and unsupported MiniMax ids remain unroutable. + +Requires `@prismshadow/agenthub` >= 0.4.2, the first release that ships the `minimax-m3` Responses client. + +## Web App + +The provider logo set gains MiniMax's official stream-lines mark, flattened to currentColor monochrome — the same recognition-purposes treatment as the other vendor brand marks. + +## Docs + +The bilingual model and configuration pages document the new group, its credential resolution, omitted tiered pricing, and the M3 thinking mapping where `none` becomes `reasoning.effort = "none"`. diff --git a/changelog/unreleased/README.md b/changelog/unreleased/README.md index 1bbef0a9..f250ccbb 100644 --- a/changelog/unreleased/README.md +++ b/changelog/unreleased/README.md @@ -13,3 +13,5 @@ - [2026-08-06] Models: Thinking Machines Lab's Inkling joins on OpenRouter and Fireworks AI, Fireworks AI gains DeepSeek V4 Flash 0731, and the OpenRouter + SiliconFlow GLM-5.1 gateway listings are delisted (Z.AI direct stays; existing Project configs unaffected); OpenRouter prices refreshed from the models API on 2026-08-07 (Inkling cached input $0.17, four drifted rows corrected); agenthub-models skill v11. ([details](2026-08-06-model-catalog-inkling-dsv4-flash-0731.md)) - [2026-08-06] Release tooling: repo versions realigned with the shipped 0.2.1, and the release workflow now refuses a tag push whose version does not match `package.json` (the drift that made every dev build nag about updates); the bump is documented as a release-prep step. ([details](2026-08-06-release-version-guard.md)) + +- [2026-08-03] Core & Web App: an official `MiniMax-M3` preset (1M context, vision) pins AgentHub's `minimax-m3` Responses client and direct endpoint, uses `MINIMAX_*` credential fallback, omits unsupported tiered pricing, and adds the MiniMax provider glyph plus bilingual docs (requires AgentHub >= 0.4.2). ([details](2026-08-03-minimax-m3-preset.md)) diff --git a/packages/core/src/state/model-catalog.ts b/packages/core/src/state/model-catalog.ts index 7d5cdcba..c4464145 100644 --- a/packages/core/src/state/model-catalog.ts +++ b/packages/core/src/state/model-catalog.ts @@ -1,7 +1,7 @@ /** * Built-in model catalog (single source of truth): official chat models that AgentHub can * auto-route, shared by core's default config, server's initial config, and web/cli display. - * Data verified as of 2026-07-10 (Qwen Token Plan entries: 2026-07-20, per the plan's docs). + * Data verified as of 2026-07-10 (Qwen Token Plan entries: 2026-07-20; MiniMax Token Plan: 2026-08-03, per each provider's docs). * Docs: packages/docs/content/models.{zh,en}.md (site path /docs/models) documents the * provider groups and credential resolution described here. * @@ -11,9 +11,10 @@ * - cache_write: the vendor's "cache write" price (e.g. Anthropic uses 1.25 x input); vendors * without a separate cache-write fee use the standard input price; * - output: output price (thinking + reply). - * OpenAI charges extra for >272K input and Gemini 3.1 Pro for >200K input under official - * long-context pricing; this catalog only records the base tier (the cost center uses a - * single rate, so long-context usage will be underestimated). + * OpenAI charges extra for >272K input and Gemini 3.1 Pro for >200K input; this catalog records + * their base tier (the cost center uses a single rate, so long-context usage will be + * underestimated). MiniMax M3 pricing is omitted because all three rates double above 512K + * input tokens and the catalog cannot represent tiered pricing. * * Scope: excludes deepseek-chat / deepseek-reasoner legacy aliases that AgentHub cannot * auto-route (deprecated 2026-07-24), glm-5v-turbo (image input unsupported by AgentHub's GLM @@ -22,7 +23,8 @@ * generation / TTS), and Bedrock. Direct-vendor ids are auto-routed by AgentHub and leave * client_type unset; the five gateway groups (OpenRouter, Fireworks AI, SiliconFlow, Qwen * Token Plan, Qwen Pay-As-You-Go) can't be auto-routed, so they set `client_type: "openai"` - * and inline their preset base URL. + * and inline their preset base URL. The MiniMax M3 preset pins AgentHub's first-party + * `minimax-m3` protocol and direct API endpoint. * * This file imports no Node built-ins (type-only imports only), so it can be bundled directly * for the browser. @@ -60,25 +62,27 @@ export interface ModelCatalogEntry { pricing?: ModelPricing; /** Whether image input (vision modality) is supported. */ supportsVision: boolean; - /** AgentHub client protocol: required for models whose id can't be auto-routed (e.g. OpenRouter gateway models). */ + /** AgentHub client protocol: required when an id cannot be auto-routed or a shared protocol must be pinned. */ clientType?: string; - /** Preset base URL (gateway models): inlined into the model entry so the user only needs to supply an API key. */ + /** Preset base URL: inlined into gateway and direct MiniMax entries so only an API key is required. */ baseUrl?: string; } -/** Each gateway's OpenAI-compatible endpoint (preset base URL for gateway models; also used as the provider's gatewayBaseUrl). */ +/** Preset provider endpoints; only OpenAI-compatible gateways expose theirs as gatewayBaseUrl. */ const OPENROUTER_BASE_URL = "https://openrouter.ai/api/v1"; const SILICONFLOW_BASE_URL = "https://api.siliconflow.cn/v1"; const QWEN_TOKEN_PLAN_BASE_URL = "https://token-plan.cn-beijing.maas.aliyuncs.com/compatible-mode/v1"; const QWEN_PAYG_BASE_URL = "https://dashscope.aliyuncs.com/compatible-mode/v1"; const FIREWORKS_BASE_URL = "https://api.fireworks.ai/inference/v1"; +const MINIMAX_BASE_URL = "https://api.minimax.io/v1"; /** * Provider list (web model page groups in this order): DeepSeek first (the default model's * provider), followed by the five gateways (OpenRouter, Fireworks AI, SiliconFlow, Qwen Token * Plan, Qwen Pay-As-You-Go), then the first-party providers Google Gemini, Anthropic, OpenAI, - * Z.AI (GLM) and Moonshot (Kimi); custom groups custom OpenAI-protocol models and comes last. + * Z.AI (GLM), Moonshot (Kimi), and MiniMax; custom groups custom OpenAI-protocol models and + * comes last. */ export const MODEL_PROVIDERS: ModelProviderInfo[] = [ { @@ -179,6 +183,14 @@ export const MODEL_PROVIDERS: ModelProviderInfo[] = [ apiKeyUrl: "https://platform.kimi.com/console/api-keys", modelsUrl: "https://platform.kimi.com/docs/pricing", }, + { + id: "minimax-token-plan", + label: "MiniMax", + envKey: "MINIMAX_API_KEY", + envBaseUrlKey: "MINIMAX_BASE_URL", + apiKeyUrl: "https://platform.minimax.io/subscribe/token-plan?tab=api-enterprise", + modelsUrl: "https://platform.minimax.io/docs/guides/models-intro", + }, { id: "custom", label: "Custom", envKey: "OPENAI_API_KEY", envBaseUrlKey: "OPENAI_BASE_URL" }, ]; @@ -815,6 +827,17 @@ export const MODEL_CATALOG: ModelCatalogEntry[] = [ clientType: "openai", baseUrl: QWEN_PAYG_BASE_URL, }, + // -- MiniMax (direct M3 Responses client). Pricing is omitted because all rates double above + // 512K input tokens and the catalog cannot represent tiered pricing. -- + { + modelId: "MiniMax-M3", + displayName: "MiniMax M3", + provider: "minimax-token-plan", + contextWindow: 1000000, + supportsVision: true, + clientType: "minimax-m3", + baseUrl: MINIMAX_BASE_URL, + }, // -- Google Gemini (official USD pricing) -- { modelId: "gemini-3.6-flash", @@ -1030,14 +1053,15 @@ export interface ModelEnvInfo { /** * Resolves the env var fallback for a model: mirrors AgentHub's - * AutoLLMClient routing rules (verified against agenthub v0.4.1 autoClient.ts) - an explicit - * client_type takes priority, otherwise routes to a client by lowercase substring match on - * model_id, returning the var pair that client reads; branch order matches AutoLLMClient. + * AutoLLMClient routing rules - an explicit client_type takes priority; otherwise the lowercase + * model_id is matched by the same exact or family-specific rules, returning the var pair that + * client reads. Branch order matches AutoLLMClient. * Returns undefined on no match (AgentHub will reject that id: it needs an explicit * client_type, or should be added under custom / a self-built group via the OpenAI protocol). */ export function resolveModelEnv(modelId: string, clientType?: string): ModelEnvInfo | undefined { - const t = (clientType || modelId).toLowerCase(); + const explicitClientType = clientType?.toLowerCase(); + const t = explicitClientType || modelId.toLowerCase(); const env = (prefix: string): ModelEnvInfo => ({ envKey: `${prefix}_API_KEY`, envBaseUrlKey: `${prefix}_BASE_URL`, @@ -1054,6 +1078,9 @@ export function resolveModelEnv(modelId: string, clientType?: string): ModelEnvI // agenthub 0.4.1 routes kimi-k3 to its own client, which reads the same MOONSHOT_* pair. if (t.includes("kimi-k3")) return env("MOONSHOT"); if (t.includes("kimi-k2.5") || t.includes("kimi-k2.6")) return env("MOONSHOT"); + if (t === "minimax-m3" && modelId.toLowerCase() === "minimax-m3") { + return env("MINIMAX"); + } if (t.includes("deepseek-v4")) return env("DEEPSEEK"); if (t.includes("openai")) return env("OPENAI"); return undefined; @@ -1064,8 +1091,8 @@ export function resolveModelEnv(modelId: string, clientType?: string): ModelEnvI * config, avoiding duplicate hand-written copies). `provider` and `model_id` are persisted as * separate fields (`model_id` is the plain upstream id); models whose upstream id can be * auto-routed by AgentHub leave client_type unset; gateway models (OpenRouter / SiliconFlow) - * explicitly set client_type=openai and inline a preset base_url (no secrets included, so the - * user only needs to supply an API key). + * explicitly set client_type=openai and inline a preset base_url. The direct MiniMax M3 entry + * also pins its protocol and endpoint. No secrets are included, so only an API key is needed. */ export function presetModelEntries(): ModelEntry[] { return MODEL_CATALOG.map((m) => ({ diff --git a/packages/core/test/model-catalog.test.ts b/packages/core/test/model-catalog.test.ts index cdbadaee..a7656e5d 100644 --- a/packages/core/test/model-catalog.test.ts +++ b/packages/core/test/model-catalog.test.ts @@ -13,6 +13,8 @@ import { resolveModelEnv, } from "../src/state/index.js"; +const UNPRICED = new Set(["minimax-token-plan\0MiniMax-M3"]); + describe("model-catalog", () => { it("(provider, model_id) pairs are unique; DeepSeek comes first (the default model's provider)", () => { // Bare model ids may repeat across providers (a gateway reselling a vendor model keeps the @@ -22,8 +24,8 @@ describe("model-catalog", () => { expect(new Set(pairs).size).toBe(pairs.length); const ids = MODEL_CATALOG.map((m) => m.modelId); expect(MODEL_CATALOG[0]!.provider).toBe("deepseek"); - // Group order: DeepSeek first, followed by the OpenRouter, SiliconFlow, and Qwen Token - // Plan gateways, then Google Gemini before Anthropic, with custom last. + // Group order: DeepSeek first, followed by the gateway and first-party groups, with + // MiniMax immediately before custom. expect(MODEL_PROVIDERS.map((p) => p.id)).toEqual([ "deepseek", "openrouter", @@ -36,9 +38,12 @@ describe("model-catalog", () => { "openai", "zhipu", "moonshot", + "minimax-token-plan", "custom", ]); expect(providerInfo("siliconflow")!.label).toBe("SiliconFlow"); + expect(providerInfo("minimax-token-plan")!.label).toBe("MiniMax"); + expect(providerInfo("minimax-token-plan")!.envKey).toBe("MINIMAX_API_KEY"); // The catalog no longer includes GLM-5-Turbo. expect(ids).not.toContain("glm-5-turbo"); // The OpenRouter and SiliconFlow gateway listings of GLM-5.1 were delisted 2026-08-06; @@ -73,12 +78,14 @@ describe("model-catalog", () => { } }); - it("every entry is priced (free-tier rows store a genuine $0); context_window is a positive integer", () => { - // As of 2026-08-03 no catalog entry ships unpriced: the last holdouts (the three - // SiliconFlow Pro//Qwen/ rows) got their official CNY list prices. Unpriced remains a - // legal state for user-added models only. + it("priced entries have valid three-bucket pricing; documented unpriced entries omit it; context_window is a positive integer", () => { for (const m of MODEL_CATALOG) { - if (m.modelId.endsWith(":free") || m.modelId === "openrouter/free") { + const key = `${m.provider}\0${m.modelId}`; + if (UNPRICED.has(key)) { + // MiniMax M3 doubles every rate above 512K input tokens; a single-rate catalog entry + // would be misleading, so pricing remains unknown until tiered rates are supported. + expect(m.pricing, key).toBeUndefined(); + } else if (m.modelId.endsWith(":free") || m.modelId === "openrouter/free") { // Free-tier gateway model (:free variants and the openrouter/free router): a genuine // $0 price (not "unknown"), so costs compute to 0. expect(m.pricing, m.modelId).toBeDefined(); @@ -128,9 +135,10 @@ describe("model-catalog", () => { expect(catalogEntryFor("qwen-token-plan", "glm-5.2")?.contextWindow).toBe(1048576); expect(catalogEntryFor("deepseek", "deepseek-v4-pro")?.provider).toBe("deepseek"); expect(catalogEntryFor("qwen-token-plan", "deepseek-v4-pro")?.provider).toBe("qwen-token-plan"); + expect(catalogEntryFor("minimax-token-plan", "MiniMax-M3")?.displayName).toBe("MiniMax M3"); }); - it("presetModelEntries: provider and bare upstream model_id are separate fields; gateway models inline base_url", () => { + it("presetModelEntries: provider and bare upstream model_id are separate fields; preset endpoints are inlined", () => { const entries = presetModelEntries(); expect(entries).toHaveLength(MODEL_CATALOG.length); for (const [i, entry] of entries.entries()) { @@ -140,9 +148,9 @@ describe("model-catalog", () => { expect(entry.context_window).toBe(cat.contextWindow); expect(entry.pricing).toEqual(cat.pricing); expect(entry.vision).toBe(cat.supportsVision ? undefined : false); - // Models that AgentHub can auto-route leave client_type unset; OpenRouter gateway models set it to openai. + // Gateway and direct MiniMax presets pin a client protocol; other direct models auto-route. expect(entry.client_type).toBe(cat.clientType); - // Gateway models inline a preset base URL (no credentials); other models carry no credential at all. + // Gateway and direct MiniMax models inline a preset base URL; no entry carries credentials. expect(entry.base_url).toBe(cat.baseUrl); expect(entry.api_key).toBeUndefined(); // The concatenated storage id and request_model_id have been removed and no longer appear. @@ -252,6 +260,21 @@ describe("model-catalog", () => { expect(m.clientType).toBe("openai"); expect(m.baseUrl).toBe("https://dashscope.aliyuncs.com/compatible-mode/v1"); } + const minimax = MODEL_CATALOG.filter((m) => m.provider === "minimax-token-plan"); + expect( + minimax.map((m) => [ + m.modelId, + m.contextWindow, + m.supportsVision, + m.clientType, + m.baseUrl, + m.pricing, + ]), + ).toEqual([ + ["MiniMax-M3", 1000000, true, "minimax-m3", "https://api.minimax.io/v1", undefined], + ]); + expect(providerInfo("minimax-token-plan")!.envBaseUrlKey).toBe("MINIMAX_BASE_URL"); + expect(providerInfo("minimax-token-plan")!.gatewayBaseUrl).toBeUndefined(); // Routed through AgentHub's OpenAI client -> when the credential is left blank it reads OPENAI_API_KEY (not the provider's own env var name). for (const id of [ "openrouter", @@ -317,10 +340,10 @@ describe("model-catalog", () => { expect(catalogEntryFor("openrouter", "google/gemini-3.5-flash")!.contextWindow).toBe(1048576); expect(catalogEntryFor("google", "gemini-3.5-flash")!.contextWindow).toBe(1048576); - // In preset entries, exactly the gateway models (and only them) inline base_url (no credentials). + // In preset entries, every gateway model and the direct MiniMax client inline base_url (no credentials). const withBaseUrl = presetModelEntries().filter((e) => e.base_url !== undefined); expect(withBaseUrl.map((e) => [e.provider, e.model_id]).sort()).toEqual( - gateway.map((m) => [m.provider, m.modelId]).sort(), + [...gateway, ...minimax].map((m) => [m.provider, m.modelId]).sort(), ); }); @@ -408,7 +431,7 @@ describe("model-catalog", () => { }); describe("resolveModelEnv (PRN-021: env fallback resolved by AgentHub routing rules)", () => { - it("first-party provider ids route by substring to the provider client's env var", () => { + it("first-party model ids route to the provider client's env var", () => { expect(resolveModelEnv("deepseek-v4-pro")?.envKey).toBe("DEEPSEEK_API_KEY"); expect(resolveModelEnv("claude-opus-4-8")?.envKey).toBe("ANTHROPIC_API_KEY"); expect(resolveModelEnv("claude-sonnet-4-6")?.envKey).toBe("ANTHROPIC_API_KEY"); @@ -424,27 +447,32 @@ describe("resolveModelEnv (PRN-021: env fallback resolved by AgentHub routing ru expect(resolveModelEnv("gemini-3.5-flash-lite")?.envKey).toBe("GEMINI_API_KEY"); expect(resolveModelEnv("claude-fable-5")?.envKey).toBe("ANTHROPIC_API_KEY"); expect(resolveModelEnv("claude-sonnet-5")?.envKey).toBe("ANTHROPIC_API_KEY"); + expect(resolveModelEnv("MiniMax-M3")?.envKey).toBe("MINIMAX_API_KEY"); + expect(resolveModelEnv("MiniMax-M3")?.envBaseUrlKey).toBe("MINIMAX_BASE_URL"); }); - it("explicit client_type beats id: the openai protocol always uses OPENAI_* (independent of grouping)", () => { + it("explicit client_type selects the protocol, while model-scoped clients still validate the id", () => { expect(resolveModelEnv("deepseek-v4-pro", "openai")?.envKey).toBe("OPENAI_API_KEY"); expect(resolveModelEnv("zai-org/GLM-5.2", "openai")?.envKey).toBe("OPENAI_API_KEY"); + expect(resolveModelEnv("MiniMax-M3", "minimax-m3")?.envBaseUrlKey).toBe("MINIMAX_BASE_URL"); + expect(resolveModelEnv("custom-model", "minimax-m3")).toBeUndefined(); }); it("unroutable ids return undefined (AgentHub would reject; needs explicit client_type or an OpenAI-protocol grouping)", () => { expect(resolveModelEnv("totally-unknown-model")).toBeUndefined(); expect(resolveModelEnv("xiaomi/mimo-v2.5")).toBeUndefined(); + expect(resolveModelEnv("minimax-m3-preview")).toBeUndefined(); + expect(resolveModelEnv("MiniMax-M4")).toBeUndefined(); + expect(resolveModelEnv("MiniMax-M4", "minimax-m3")).toBeUndefined(); }); - it("catalog invariant: entries without client_type route by id with envKey matching the provider; gateway entries resolve to OPENAI_* via client_type", () => { + it("catalog invariant: each model's resolved client uses its provider's documented environment variables", () => { for (const m of MODEL_CATALOG) { const env = resolveModelEnv(m.modelId, m.clientType); + const provider = providerInfo(m.provider)!; expect(env, `${m.provider}/${m.modelId}`).toBeDefined(); - if (m.clientType === undefined) { - expect(env!.envKey, m.modelId).toBe(providerInfo(m.provider)!.envKey); - } else { - expect(env!.envKey, m.modelId).toBe("OPENAI_API_KEY"); - } + expect(env!.envKey, m.modelId).toBe(provider.envKey); + expect(env!.envBaseUrlKey, m.modelId).toBe(provider.envBaseUrlKey); } }); @@ -477,6 +505,9 @@ describe("resolveModelEnv (PRN-021: env fallback resolved by AgentHub routing ru expect(modelHomepageUrl("deepseek", "deepseek-v4-pro")).toBe( "https://api-docs.deepseek.com/quick_start/pricing", ); + expect(modelHomepageUrl("minimax-token-plan", "MiniMax-M3")).toBe( + "https://platform.minimax.io/docs/guides/models-intro", + ); // Z.AI and Moonshot have per-model pages (Moonshot drops the dot: kimi-k2.6 -> chat-k26). expect(modelHomepageUrl("zhipu", "glm-5.2")).toBe("https://docs.z.ai/guides/llm/glm-5.2"); expect(modelHomepageUrl("moonshot", "kimi-k2.6")).toBe( diff --git a/packages/docs/content/configuration.en.md b/packages/docs/content/configuration.en.md index 5d7eade4..98b18a07 100644 --- a/packages/docs/content/configuration.en.md +++ b/packages/docs/content/configuration.en.md @@ -27,18 +27,19 @@ These configure PenguinHarness itself, so `PORT`, `HOST`, `PENGUIN_WEB_DIST` and ### Provider credential variables -When a model entry has no inline `api_key`, the AgentHub gateway falls back to the provider's environment variable; the `*_BASE_URL` variants override the base URL the same way: +When a model entry has no inline `api_key`, AgentHub falls back to the provider's environment variable. A `*_BASE_URL` value is used only when the entry does not already inline `base_url`: | Provider | API key | Base URL | | --- | --- | --- | | deepseek | `DEEPSEEK_API_KEY` | `DEEPSEEK_BASE_URL` | | anthropic | `ANTHROPIC_API_KEY` | `ANTHROPIC_BASE_URL` | | openai, openrouter, fireworks, siliconflow, qwen-token-plan, qwen-pay-as-you-go, custom | `OPENAI_API_KEY` | `OPENAI_BASE_URL` | +| minimax-token-plan | `MINIMAX_API_KEY` | `MINIMAX_BASE_URL` | | google | `GEMINI_API_KEY` | `GEMINI_BASE_URL` | | zhipu | `ZAI_API_KEY` | `ZAI_BASE_URL` | | moonshot | `MOONSHOT_API_KEY` | `MOONSHOT_BASE_URL` | -The openrouter, fireworks, siliconflow, qwen-token-plan, qwen-pay-as-you-go, and custom groups speak the OpenAI-compatible protocol, hence the shared `OPENAI_*` variables. Provider groups and the built-in model catalog are covered in [Models & Providers](/models). +The openrouter, fireworks, siliconflow, qwen-token-plan, qwen-pay-as-you-go, and custom groups speak the OpenAI-compatible protocol, hence the shared `OPENAI_*` variables. The direct MiniMax M3 Responses client uses `MINIMAX_*`; the built-in MiniMax preset already pins the official endpoint. Provider groups and the built-in model catalog are covered in [Models & Providers](/models). ## Project config @@ -64,7 +65,7 @@ Model entry (`[[models]]`) fields: | `max_tokens` | Per-model max output tokens; overrides the Agent's `model.max_tokens` when set, omitted = inherit it | | `pricing` | Three price buckets `cache_read` / `cache_write` / `output`, in USD per million Tokens (`unit = "usd_per_mtok"`) | | `api_key` | Inline credential; when empty, falls back to the provider environment variable | -| `base_url` | Custom base URL; preset for gateway models | +| `base_url` | Custom base URL; preset by the built-in catalog for gateways and direct MiniMax models | | `created_at` | Write timestamp of `api_key` (ISO 8601; a display field maintained by the interface layer) | ```toml diff --git a/packages/docs/content/configuration.zh.md b/packages/docs/content/configuration.zh.md index dc75fc24..3dfeeb20 100644 --- a/packages/docs/content/configuration.zh.md +++ b/packages/docs/content/configuration.zh.md @@ -27,18 +27,19 @@ CLI 与服务端启动时会自动加载工作目录下的 `.env` 文件。 ### Provider 凭证环境变量 -当模型条目未内联 `api_key` 时,AgentHub 网关按 Provider 回退读取对应环境变量;`*_BASE_URL` 变体同理覆盖 Base URL: +当模型条目未内联 `api_key` 时,AgentHub 按 Provider 回退读取对应环境变量;仅当条目未内联 `base_url` 时才使用 `*_BASE_URL`: | Provider | API Key | Base URL | | --- | --- | --- | | deepseek | `DEEPSEEK_API_KEY` | `DEEPSEEK_BASE_URL` | | anthropic | `ANTHROPIC_API_KEY` | `ANTHROPIC_BASE_URL` | | openai、openrouter、fireworks、siliconflow、qwen-token-plan、qwen-pay-as-you-go、custom | `OPENAI_API_KEY` | `OPENAI_BASE_URL` | +| minimax-token-plan | `MINIMAX_API_KEY` | `MINIMAX_BASE_URL` | | google | `GEMINI_API_KEY` | `GEMINI_BASE_URL` | | zhipu | `ZAI_API_KEY` | `ZAI_BASE_URL` | | moonshot | `MOONSHOT_API_KEY` | `MOONSHOT_BASE_URL` | -openrouter、fireworks、siliconflow、qwen-token-plan、qwen-pay-as-you-go 与 custom 分组走 OpenAI 兼容协议,因此复用 `OPENAI_*` 变量。Provider 分组与内置模型目录见[模型与 Provider](/models)。 +openrouter、fireworks、siliconflow、qwen-token-plan、qwen-pay-as-you-go 与 custom 分组走 OpenAI 兼容协议,因此复用 `OPENAI_*` 变量。直连 MiniMax M3 Responses 客户端使用 `MINIMAX_*`;内置 MiniMax 预设已固定官方端点。Provider 分组与内置模型目录见[模型与 Provider](/models)。 ## Project 配置 @@ -64,7 +65,7 @@ openrouter、fireworks、siliconflow、qwen-token-plan、qwen-pay-as-you-go 与 | `max_tokens` | 单模型最大输出 Token;设置后覆盖 Agent 的 `model.max_tokens`,缺省则继承 | | `pricing` | 三档价格 `cache_read` / `cache_write` / `output`,单位 USD 每百万 Token(`unit = "usd_per_mtok"`) | | `api_key` | 内联凭证;留空回退到 Provider 环境变量 | -| `base_url` | 自定义 Base URL;网关模型预置 | +| `base_url` | 自定义 Base URL;内置目录会为网关与直连 MiniMax 模型预置 | | `created_at` | `api_key` 写入时间(ISO 8601,界面维护的展示字段) | ```toml diff --git a/packages/docs/content/models.en.md b/packages/docs/content/models.en.md index bc7dda4c..ad2e0443 100644 --- a/packages/docs/content/models.en.md +++ b/packages/docs/content/models.en.md @@ -69,13 +69,14 @@ Built-in groups and their env-var fallbacks (catalog source: `packages/core/src/ | openai | `OPENAI_API_KEY` | | | zhipu | `ZAI_API_KEY` | | | moonshot | `MOONSHOT_API_KEY` | | +| minimax-token-plan (MiniMax) | `MINIMAX_API_KEY` | Direct MiniMax M3 Responses client (`client_type = "minimax-m3"`): `MiniMax-M3` with a 1,000,000-token context window and vision; preset base URL `https://api.minimax.io/v1`; accepts a Token Plan Subscription Key or pay-as-you-go API key | | custom | `OPENAI_API_KEY` | Any OpenAI-protocol endpoint | -The gateway groups (openrouter / fireworks / siliconflow / qwen-token-plan / qwen-pay-as-you-go) go through AgentHub's OpenAI client, so with blank credentials they read `OPENAI_API_KEY` — not a gateway-specific variable. +The gateway groups (openrouter / fireworks / siliconflow / qwen-token-plan / qwen-pay-as-you-go) go through AgentHub's OpenAI client, so with blank credentials they read `OPENAI_API_KEY` — not a gateway-specific variable. The direct MiniMax M3 client reads `MINIMAX_API_KEY`. The built-in MiniMax preset pins `https://api.minimax.io/v1`; `MINIMAX_BASE_URL` is consulted only for entries without an inline `base_url`. M3 pricing is omitted because MiniMax doubles every rate above 512K input tokens and the catalog cannot represent tiered pricing. The preset catalog also carries OpenRouter's free tier: `:free` model variants (e.g. `inclusionai/ling-3.0-flash:free`, `nvidia/nemotron-3-ultra-550b-a55b:free`) and the `openrouter/free` unified Free Models Router. They cost nothing, but are subject to OpenRouter's free-tier rate limits and data policy. -Some models in the preset catalog: deepseek-v4-pro / deepseek-v4-flash, gemini-3.1-pro-preview, claude-opus-4-8 / claude-sonnet-4-6, gpt-5.5, glm-5.2, kimi-k2.6, qwen3.8-max (not exhaustive). +Some models in the preset catalog: deepseek-v4-pro / deepseek-v4-flash, MiniMax-M3, gemini-3.1-pro-preview, claude-opus-4-8 / claude-sonnet-4-6, gpt-5.5, glm-5.2, kimi-k2.6, qwen3.8-max (not exhaustive). ## Local / self-hosted OpenAI-compatible endpoints (e.g. vLLM) @@ -86,6 +87,8 @@ A local inference server is just a `custom` entry: `client_type = "openai"`, `ba ## Thinking levels +For MiniMax M3, `none` maps directly to `reasoning.effort = "none"`. + Five levels: `none | low | medium | high | xhigh`, configured per Agent as `model.thinking_level` in `system_config.yaml`, default medium. The Web pickers offer `low` and above only (many models cannot disable thinking; `none` stays a valid stored value and still displays). The chat draft view offers a quick picker next to the model selector: a picked level is written back to the selected Agent's setting immediately (the switched-to level becomes that Agent's new default and applies from the next session). Inside an active session the thinking level is a **per-turn parameter**: the composer's picker lists only the levels and starts out showing the Agent config's level — while the user hasn't picked one it auto-follows the config (sends omit the level, so config edits keep taking effect); once picked, the level sticks for that session and rides on every subsequent send (it applies to that session's subsequent Tasks only and never writes back to the Agent config). See [Configuration](/configuration). ## Models decoupled from Agents diff --git a/packages/docs/content/models.zh.md b/packages/docs/content/models.zh.md index b97e9cd6..c9531672 100644 --- a/packages/docs/content/models.zh.md +++ b/packages/docs/content/models.zh.md @@ -69,13 +69,14 @@ api_key = "sk-..." | openai | `OPENAI_API_KEY` | | | zhipu | `ZAI_API_KEY` | | | moonshot | `MOONSHOT_API_KEY` | | +| minimax-token-plan(MiniMax) | `MINIMAX_API_KEY` | 直连 MiniMax M3 Responses 客户端(`client_type = "minimax-m3"`):`MiniMax-M3` 支持 1,000,000 Token 上下文和视觉输入;预置 base URL `https://api.minimax.io/v1`;接受 Token Plan Subscription Key 或按量付费 API Key | | custom | `OPENAI_API_KEY` | 任意 OpenAI 协议端点 | -网关分组(openrouter / fireworks / siliconflow / qwen-token-plan / qwen-pay-as-you-go)经 AgentHub 的 OpenAI 客户端请求,因此凭证留空时读取的是 `OPENAI_API_KEY`,而非网关自己的变量名。 +网关分组(openrouter / fireworks / siliconflow / qwen-token-plan / qwen-pay-as-you-go)经 AgentHub 的 OpenAI 客户端请求,因此凭证留空时读取的是 `OPENAI_API_KEY`,而非网关自己的变量名。直连 MiniMax M3 客户端读取 `MINIMAX_API_KEY`。内置 MiniMax 预设固定使用 `https://api.minimax.io/v1`;仅当模型条目未内联 `base_url` 时才读取 `MINIMAX_BASE_URL`。MiniMax 在输入超过 512K Token 后将各档价格翻倍,而当前目录无法表达阶梯定价,因此不记录 M3 价格。 预置目录还收录了 OpenRouter 的免费档:`:free` 模型变体(如 `inclusionai/ling-3.0-flash:free`、`nvidia/nemotron-3-ultra-550b-a55b:free`)与统一路由 `openrouter/free`(Free Models Router),零成本可用,但受 OpenRouter 免费档速率限制与数据政策约束。 -预置目录中的部分模型:deepseek-v4-pro / deepseek-v4-flash、gemini-3.1-pro-preview、claude-opus-4-8 / claude-sonnet-4-6、gpt-5.5、glm-5.2、kimi-k2.6、qwen3.8-max 等(非完整清单)。 +预置目录中的部分模型:deepseek-v4-pro / deepseek-v4-flash、MiniMax-M3、gemini-3.1-pro-preview、claude-opus-4-8 / claude-sonnet-4-6、gpt-5.5、glm-5.2、kimi-k2.6、qwen3.8-max 等(非完整清单)。 ## 本地 / 自建 OpenAI 兼容端点(如 vLLM) @@ -86,6 +87,8 @@ api_key = "sk-..." ## 思考等级 +对于 MiniMax M3,`none` 会直接映射为 `reasoning.effort = "none"`。 + 思考等级共五档:`none | low | medium | high | xhigh`,按 Agent 在 `system_config.yaml` 的 `model.thinking_level` 配置,默认 medium。Web 拾取器只提供 `low` 及以上档位(多数模型不支持关闭思考;`none` 仍是合法的已存值,能正常显示)。对话草稿页在模型选择器旁提供快捷拾取器:选定档位立即写回所选 Agent 的该项配置(切换后的档位即成为该 Agent 的新默认,自下一个 Session 生效)。进行中的会话里,思考等级是**逐轮参数**:输入区拾取器只列出各档位,初始即显示 Agent 配置的档位——用户未手动选择时自动跟随配置下发(请求不携带档位,配置的修改持续生效);选定某档后即固定为该会话的档位,随之后每次发送携带(仅作用于该会话的后续 Task,不写回 Agent 配置)。见 [配置参考](/configuration)。 ## 模型与 Agent 解耦 diff --git a/packages/web/src/components/ui/provider-logo.tsx b/packages/web/src/components/ui/provider-logo.tsx index 7566bba0..2bbbac1f 100644 --- a/packages/web/src/components/ui/provider-logo.tsx +++ b/packages/web/src/components/ui/provider-logo.tsx @@ -3,9 +3,9 @@ * light and dark themes). * * Anthropic / OpenAI / Google Gemini / DeepSeek / Moonshot AI / OpenRouter / - * SiliconFlow / Qwen Token Plan / Qwen Pay-As-You-Go / Fireworks AI use each - * vendor's brand mark (for recognition purposes, not under trademark license; - * Qwen's official gradient wordmark is flattened to currentColor monochrome); + * SiliconFlow / Qwen Token Plan / Qwen Pay-As-You-Go / Fireworks AI / MiniMax use + * each vendor's brand mark (for recognition purposes, not under trademark license; + * Qwen's official gradient wordmark is flattened to currentColor monochrome). * Z.AI uses a simplified geometric approximation of its branded glyph (not an * exact reproduction of the trademark); custom models use a generic cube. All * are pure paths, no external image assets. @@ -92,6 +92,15 @@ const GLYPHS: Record = { }, "qwen-token-plan": QWEN_GLYPH, "qwen-pay-as-you-go": QWEN_GLYPH, + "minimax-token-plan": { + // The official MiniMax stream-lines mark (evenodd fill preserved from the brand SVG). + path: ( + + ), + }, fireworks: { // The official Fireworks AI burst mark (three strokes of the wordmark's icon). viewBox: "0 0 638 315", diff --git a/packages/web/test/model-grouping.test.ts b/packages/web/test/model-grouping.test.ts index 101f04e5..553c9af3 100644 --- a/packages/web/test/model-grouping.test.ts +++ b/packages/web/test/model-grouping.test.ts @@ -26,6 +26,7 @@ const rows: ModelRowLike[] = [ { provider: "anthropic", modelId: "claude-sonnet-4-6", displayName: "Claude Sonnet 4.6" }, { provider: "anthropic", modelId: "claude-opus-4-8", displayName: "Claude Opus 4.8" }, { provider: "moonshot", modelId: "kimi-k2.6", displayName: "Kimi K2.6" }, + { provider: "minimax-token-plan", modelId: "MiniMax-M3", displayName: "MiniMax M3" }, { provider: "custom", modelId: "my-proxy-model" }, { provider: "unknown-vendor", modelId: "weird-model" }, // provider not in the catalog → custom-built group ]; @@ -45,11 +46,13 @@ describe("matchesQuery", () => { }); it("custom models without a display name match by id and the Custom group name", () => { - expect(matchesQuery(rows[3]!, "proxy")).toBe(true); - expect(matchesQuery(rows[3]!, "custom")).toBe(true); + const customRow = rows.find((row) => row.provider === "custom")!; + const customBuiltRow = rows.find((row) => row.provider === "unknown-vendor")!; + expect(matchesQuery(customRow, "proxy")).toBe(true); + expect(matchesQuery(customRow, "custom")).toBe(true); // Custom-built groups are searchable by their group name (raw provider value); no longer folded into the Custom bucket. - expect(matchesQuery(rows[4]!, "unknown-vendor")).toBe(true); - expect(matchesQuery(rows[4]!, "custom")).toBe(false); + expect(matchesQuery(customBuiltRow, "unknown-vendor")).toBe(true); + expect(matchesQuery(customBuiltRow, "custom")).toBe(false); }); }); @@ -59,17 +62,20 @@ describe("groupModelRows", () => { expect(groups.map((g) => g.provider.id)).toEqual([ "anthropic", "moonshot", + "minimax-token-plan", "custom", "unknown-vendor", ]); expect(groups[0]!.rows.map((r) => r.modelId)).toEqual(["claude-sonnet-4-6", "claude-opus-4-8"]); - expect(groups[2]!.rows.map((r) => r.modelId)).toEqual(["my-proxy-model"]); + expect(groups[2]!.provider.label).toBe("MiniMax"); + expect(groups[2]!.rows.map((r) => r.modelId)).toEqual(["MiniMax-M3"]); + expect(groups[3]!.rows.map((r) => r.modelId)).toEqual(["my-proxy-model"]); // Custom-built group: synthesized provider info — label is the group name, OpenAI-protocol semantics (env falls back to OPENAI_*). - expect(groups[3]!.provider.label).toBe("unknown-vendor"); - expect(groups[3]!.provider.envKey).toBe("OPENAI_API_KEY"); - expect(groups[3]!.rows.map((r) => r.modelId)).toEqual(["weird-model"]); - // Group order matches the MODEL_PROVIDERS definition: DeepSeek first, the two gateways right after, - // Google Gemini before Anthropic, custom last. + expect(groups[4]!.provider.label).toBe("unknown-vendor"); + expect(groups[4]!.provider.envKey).toBe("OPENAI_API_KEY"); + expect(groups[4]!.rows.map((r) => r.modelId)).toEqual(["weird-model"]); + // Group order matches MODEL_PROVIDERS: DeepSeek and gateway groups first, then first-party + // providers, with MiniMax immediately before custom. expect(MODEL_PROVIDERS.map((p) => p.id)).toEqual([ "deepseek", "openrouter", @@ -82,9 +88,11 @@ describe("groupModelRows", () => { "openai", "zhipu", "moonshot", + "minimax-token-plan", "custom", ]); expect(MODEL_PROVIDERS.find((p) => p.id === "siliconflow")!.label).toBe("SiliconFlow"); + expect(MODEL_PROVIDERS.find((p) => p.id === "minimax-token-plan")!.label).toBe("MiniMax"); }); it("the custom group always shows without a search query (returned even when empty, hosting the add entry point)", () => {