Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
8 changes: 8 additions & 0 deletions src/cost.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,14 @@ export const MODEL_PRICES: Record<string, ModelPrice> = {
"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
Expand Down
86 changes: 78 additions & 8 deletions src/provider-contract.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<ProviderCapabilityFlags>;
}

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<Record<string, ProviderPreset>>;

function builtinProviderPreset(id: string): ProviderPreset | undefined {
return (BUILTIN_PROVIDER_PRESETS as Readonly<Record<string, ProviderPreset>>)[id];
}

// The validated `providers` section: a negotiated contract version, an optional
// default provider id, and the validated entries (ids guaranteed unique).
export interface ProviderContract {
Expand All @@ -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()
Expand Down Expand Up @@ -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)) {
Expand Down Expand Up @@ -263,7 +319,7 @@ export function resolveProviderConfig(
opts: { env?: Record<string, string | undefined> } = {},
): 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`);
}
Expand All @@ -286,7 +342,7 @@ export interface ProviderContractReport {
contractVersion: number;
providerId: string;
endpoint: string;
endpointSource: "settings" | "default";
endpointSource: ProviderEndpointSource;
model: string;
modelCatalog: string[];
credentialVariable: string;
Expand All @@ -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;
Expand Down Expand Up @@ -375,7 +431,7 @@ export interface ProviderReadiness {
state: ProviderReadinessState;
reason: string;
baseUrl: string;
endpointSource: "settings" | "default";
endpointSource: ProviderEndpointSource;
endpointValid: boolean;
credentialVariable: string;
credentialFromSettings: boolean;
Expand All @@ -387,7 +443,7 @@ export function resolveProviderReadiness(
opts: { env?: Record<string, string | undefined> } = {},
): 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;
Expand All @@ -404,14 +460,28 @@ export function resolveProviderReadiness(
state,
reason,
baseUrl,
endpointSource: entry.baseUrl ? "settings" : "default",
endpointSource,
endpointValid,
credentialVariable,
credentialFromSettings: entry.apiKeyEnv !== undefined,
credentialAvailable,
};
}

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
Expand Down
3 changes: 2 additions & 1 deletion src/provider-invocation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ import {
resolveProviderReadiness,
resolveProviderConfig,
type ProviderEntry,
type ProviderEndpointSource,
} from "./provider-contract.js";
import {
clampInvokeTimeout,
Expand Down Expand Up @@ -308,7 +309,7 @@ export interface ProviderInvocationReport {
contractVersion: number;
providerId: string;
endpoint: string;
endpointSource: "settings" | "default";
endpointSource: ProviderEndpointSource;
model: string;
credentialVariable: string;
credentialFromSettings: boolean;
Expand Down
17 changes: 17 additions & 0 deletions tests/unit/cost.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
62 changes: 62 additions & 0 deletions tests/unit/provider-contract.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
PROVIDER_CONTRACT_SCHEMA,
PROVIDER_CONTRACT_VERSION,
SUPPORTED_PROVIDER_CONTRACT_VERSIONS,
BUILTIN_PROVIDER_PRESETS,
parseProviderContract,
selectProviderEntry,
resolveProviderConfig,
Expand Down Expand Up @@ -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"]);
});
});

Expand Down