From acf1b7db7f0cfa92851cf45bf9ee135a2779f69f Mon Sep 17 00:00:00 2001 From: octo-patch <266937838+octo-patch@users.noreply.github.com> Date: Fri, 4 Sep 2026 03:00:41 +0800 Subject: [PATCH] feat(providers): add MiniMax regional presets --- README.md | 7 +++ src/cost.ts | 8 +++ src/provider-contract.ts | 86 +++++++++++++++++++++++++--- src/provider-invocation.ts | 3 +- tests/unit/cost.test.ts | 17 ++++++ tests/unit/provider-contract.test.ts | 62 ++++++++++++++++++++ 6 files changed, 174 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 94aa4a3..5fe0ef0 100644 --- a/README.md +++ b/README.md @@ -1598,6 +1598,13 @@ availability is reported (`credentialAvailable`) but not gated, so the contract stays inspectable even when the credential is not currently exported. Exit code: `0` on success, `2` on a contract/usage error. +MiniMax has built-in global and China entries, including +the current `MiniMax-M3` and `MiniMax-M2.7` catalog. Declare +`{ "id": "minimax" }` for the global endpoint or +`{ "id": "minimax-cn" }` for the China endpoint, then export the +credential as `MINIMAX_API_KEY`. An explicit `baseUrl`, `model`, +`models`, or `apiKeyEnv` in the entry still overrides the corresponding preset. + ```text Provider: alt Contract: oh-my-cli.provider-contract v1 (settings contract version 1) diff --git a/src/cost.ts b/src/cost.ts index e0c55b4..a58033b 100644 --- a/src/cost.ts +++ b/src/cost.ts @@ -23,6 +23,14 @@ export const MODEL_PRICES: Record = { "gpt-4o-mini": { promptPerM: 0.15, completionPerM: 0.6 }, "gpt-4.1": { promptPerM: 2, completionPerM: 8 }, "gpt-4.1-mini": { promptPerM: 0.4, completionPerM: 1.6 }, + "MiniMax-M3": { + promptPerM: 0.6, + completionPerM: 2.4, + }, + "MiniMax-M2.7": { + promptPerM: 0.3, + completionPerM: 1.2, + }, }; // Conservative fallback used when the model is not in the table, so cost is diff --git a/src/provider-contract.ts b/src/provider-contract.ts index 08361f3..cbc3230 100644 --- a/src/provider-contract.ts +++ b/src/provider-contract.ts @@ -64,6 +64,40 @@ export interface ProviderEntry { capabilities?: ProviderCapabilityFlags; } +export type ProviderEndpointSource = "settings" | "builtin" | "default"; + +// Built-in entries keep common provider configuration concise while preserving +// the contract's trust boundary: users still opt in by declaring the provider +// id, and credentials remain in a named environment variable. +export interface ProviderPreset { + baseUrl: string; + model: string; + models: readonly string[]; + apiKeyEnv: string; + capabilities: Readonly; +} + +export const BUILTIN_PROVIDER_PRESETS = { + "minimax": { + baseUrl: "https://api.minimax.io/v1", + model: "MiniMax-M3", + models: ["MiniMax-M3", "MiniMax-M2.7"], + apiKeyEnv: "MINIMAX_API_KEY", + capabilities: { vision: true, tools: true, streaming: true }, + }, + "minimax-cn": { + baseUrl: "https://api.minimaxi.com/v1", + model: "MiniMax-M3", + models: ["MiniMax-M3", "MiniMax-M2.7"], + apiKeyEnv: "MINIMAX_API_KEY", + capabilities: { vision: true, tools: true, streaming: true }, + }, +} as const satisfies Readonly>; + +function builtinProviderPreset(id: string): ProviderPreset | undefined { + return (BUILTIN_PROVIDER_PRESETS as Readonly>)[id]; +} + // The validated `providers` section: a negotiated contract version, an optional // default provider id, and the validated entries (ids guaranteed unique). export interface ProviderContract { @@ -83,7 +117,7 @@ const CapabilitySchema = z const ProviderEntrySchema = z.object({ id: z.string().min(1, "providers.entries[].id must be a non-empty string"), baseUrl: z.string().url("providers.entries[].baseUrl must be a valid URL").optional(), - model: z.string().min(1, "providers.entries[].model is required"), + model: z.string().min(1, "providers.entries[].model is required").optional(), models: z.array(z.string().min(1, "providers.entries[].models entries must be non-empty")).optional(), apiKeyEnv: z .string() @@ -206,11 +240,33 @@ export function parseProviderContract(section: unknown): ProviderContract { const issues = result.error.issues.map((i) => i.message).join("; "); throw new Error(`Settings error: ${issues}`); } + const preset = builtinProviderPreset(result.data.id); + const model = result.data.model ?? preset?.model; + if (model === undefined) { + throw new Error("Settings error: providers.entries[].model is required"); + } if (seen.has(result.data.id)) { throw new Error(`Settings error: duplicate provider id "${result.data.id}"`); } seen.add(result.data.id); - entries.push(result.data); + entries.push({ + id: result.data.id, + model, + ...(result.data.baseUrl !== undefined ? { baseUrl: result.data.baseUrl } : {}), + ...(result.data.models !== undefined + ? { models: result.data.models } + : preset + ? { models: [...preset.models] } + : {}), + ...(result.data.apiKeyEnv !== undefined + ? { apiKeyEnv: result.data.apiKeyEnv } + : preset + ? { apiKeyEnv: preset.apiKeyEnv } + : {}), + ...(result.data.capabilities !== undefined || preset + ? { capabilities: { ...preset?.capabilities, ...result.data.capabilities } } + : {}), + }); }); if (def !== undefined && !seen.has(def)) { @@ -263,7 +319,7 @@ export function resolveProviderConfig( opts: { env?: Record } = {}, ): Config { const env = opts.env ?? process.env; - const baseUrl = entry.baseUrl ?? DEFAULT_BASE_URL; + const { baseUrl } = resolveProviderEndpoint(entry); if (!isValidUrl(baseUrl)) { throw new Error(`Provider error: provider "${entry.id}" base URL is not a valid URL`); } @@ -286,7 +342,7 @@ export interface ProviderContractReport { contractVersion: number; providerId: string; endpoint: string; - endpointSource: "settings" | "default"; + endpointSource: ProviderEndpointSource; model: string; modelCatalog: string[]; credentialVariable: string; @@ -301,7 +357,7 @@ export function buildProviderContractReport(facts: { contractVersion: number; entry: ProviderEntry; baseUrl: string; - endpointSource: "settings" | "default"; + endpointSource: ProviderEndpointSource; credentialVariable: string; credentialFromSettings: boolean; credentialAvailable: boolean; @@ -375,7 +431,7 @@ export interface ProviderReadiness { state: ProviderReadinessState; reason: string; baseUrl: string; - endpointSource: "settings" | "default"; + endpointSource: ProviderEndpointSource; endpointValid: boolean; credentialVariable: string; credentialFromSettings: boolean; @@ -387,7 +443,7 @@ export function resolveProviderReadiness( opts: { env?: Record } = {}, ): ProviderReadiness { const env = opts.env ?? process.env; - const baseUrl = entry.baseUrl ?? DEFAULT_BASE_URL; + const { baseUrl, endpointSource } = resolveProviderEndpoint(entry); const endpointValid = isValidUrl(baseUrl); const credentialVariable = entry.apiKeyEnv ?? "OPENAI_API_KEY"; const credentialAvailable = envValue(env, credentialVariable) !== undefined; @@ -404,7 +460,7 @@ export function resolveProviderReadiness( state, reason, baseUrl, - endpointSource: entry.baseUrl ? "settings" : "default", + endpointSource, endpointValid, credentialVariable, credentialFromSettings: entry.apiKeyEnv !== undefined, @@ -412,6 +468,20 @@ export function resolveProviderReadiness( }; } +function resolveProviderEndpoint(entry: ProviderEntry): { + baseUrl: string; + endpointSource: ProviderEndpointSource; +} { + if (entry.baseUrl !== undefined) { + return { baseUrl: entry.baseUrl, endpointSource: "settings" }; + } + const preset = builtinProviderPreset(entry.id); + if (preset !== undefined) { + return { baseUrl: preset.baseUrl, endpointSource: "builtin" }; + } + return { baseUrl: DEFAULT_BASE_URL, endpointSource: "default" }; +} + // Read the user settings file, negotiate the provider contract, select one // provider, and build the redacted report. Throws a redacted error when no // `providers` section exists or the contract is invalid. Credential availability diff --git a/src/provider-invocation.ts b/src/provider-invocation.ts index 8d4e736..286386d 100644 --- a/src/provider-invocation.ts +++ b/src/provider-invocation.ts @@ -36,6 +36,7 @@ import { resolveProviderReadiness, resolveProviderConfig, type ProviderEntry, + type ProviderEndpointSource, } from "./provider-contract.js"; import { clampInvokeTimeout, @@ -308,7 +309,7 @@ export interface ProviderInvocationReport { contractVersion: number; providerId: string; endpoint: string; - endpointSource: "settings" | "default"; + endpointSource: ProviderEndpointSource; model: string; credentialVariable: string; credentialFromSettings: boolean; diff --git a/tests/unit/cost.test.ts b/tests/unit/cost.test.ts index 83d1cd7..9991c83 100644 --- a/tests/unit/cost.test.ts +++ b/tests/unit/cost.test.ts @@ -15,6 +15,23 @@ describe("lookupModelPrice", () => { expect(price).toEqual(MODEL_PRICES["gpt-4o"]); }); + it("matches the current MiniMax models", () => { + expect(lookupModelPrice("MiniMax-M3")).toEqual({ + known: true, + price: { + promptPerM: 0.6, + completionPerM: 2.4, + }, + }); + expect(lookupModelPrice("MiniMax-M2.7")).toEqual({ + known: true, + price: { + promptPerM: 0.3, + completionPerM: 1.2, + }, + }); + }); + it("matches a dated variant by longest known prefix", () => { const dated = lookupModelPrice("gpt-4o-2024-08-06"); expect(dated.known).toBe(true); diff --git a/tests/unit/provider-contract.test.ts b/tests/unit/provider-contract.test.ts index 5034d90..b7fb8dc 100644 --- a/tests/unit/provider-contract.test.ts +++ b/tests/unit/provider-contract.test.ts @@ -6,6 +6,7 @@ import { PROVIDER_CONTRACT_SCHEMA, PROVIDER_CONTRACT_VERSION, SUPPORTED_PROVIDER_CONTRACT_VERSIONS, + BUILTIN_PROVIDER_PRESETS, parseProviderContract, selectProviderEntry, resolveProviderConfig, @@ -139,6 +140,67 @@ describe("parseProviderContract: trust boundary and validation", () => { expect(() => parseProviderContract({ contractVersion: 1, entries: [{ id: "a", model: "" }] }), ).toThrow(/model is required/); + expect(() => + parseProviderContract({ contractVersion: 1, entries: [{ id: "a" }] }), + ).toThrow(/model is required/); + }); +}); + +describe("built-in MiniMax provider entries", () => { + it("resolves the global endpoint and current model catalog from the provider id", () => { + const contract = parseProviderContract({ + contractVersion: 1, + entries: [{ id: "minimax" }], + }); + const entry = contract.entries[0]; + expect(entry.model).toBe("MiniMax-M3"); + expect(entry.models).toEqual(["MiniMax-M3", "MiniMax-M2.7"]); + expect(entry.apiKeyEnv).toBe("MINIMAX_API_KEY"); + expect(entry.capabilities).toEqual({ vision: true, tools: true, streaming: true }); + + const config = resolveProviderConfig(entry, { + env: { MINIMAX_API_KEY: "test-key" }, + }); + expect(config.baseUrl).toBe("https://api.minimax.io/v1"); + expect(config.model).toBe("MiniMax-M3"); + }); + + it("resolves the China endpoint from the regional provider id", () => { + const contract = parseProviderContract({ + contractVersion: 1, + entries: [{ id: "minimax-cn" }], + }); + const entry = contract.entries[0]; + const config = resolveProviderConfig(entry, { + env: { MINIMAX_API_KEY: "test-key" }, + }); + expect(config.baseUrl).toBe("https://api.minimaxi.com/v1"); + expect(BUILTIN_PROVIDER_PRESETS["minimax-cn"].model).toBe("MiniMax-M3"); + }); + + it("keeps explicit entry fields above the built-in defaults", () => { + const contract = parseProviderContract({ + contractVersion: 1, + entries: [{ id: "minimax", baseUrl: "https://override.example/v1", model: "custom-model" }], + }); + const config = resolveProviderConfig(contract.entries[0], { + env: { MINIMAX_API_KEY: "test-key" }, + }); + expect(config.baseUrl).toBe("https://override.example/v1"); + expect(config.model).toBe("custom-model"); + }); + + it("reports built-in endpoint provenance without exposing its path", () => { + const settings = writeSettings({ + providers: { contractVersion: 1, entries: [{ id: "minimax" }] }, + }); + const report = collectProviderContract({ + settingsPath: settings, + env: { MINIMAX_API_KEY: "test-key" }, + }); + expect(report.endpoint).toBe("api.minimax.io"); + expect(report.endpointSource).toBe("builtin"); + expect(report.modelCatalog).toEqual(["MiniMax-M3", "MiniMax-M2.7"]); }); });