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
39 changes: 34 additions & 5 deletions src/commands/text/chat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import type {
import { readFileSync } from 'fs';
import { isInteractive } from '../../utils/env';
import { promptText, failIfMissing } from '../../utils/prompt';
import { resolveMaxTokens, resolveThinkingMode, resolveTemperature } from '../../utils/model-defaults';

// ---------------------------------------------------------------------------
// Thinking indicator — dynamic spinner + color-cycling label
Expand Down Expand Up @@ -163,10 +164,11 @@ export default defineCommand({
{ flag: '--message <text>', description: 'Message text (repeatable, prefix role: to set role)', required: true, type: 'array' },
{ flag: '--messages-file <path>', description: 'JSON file with messages array (use - for stdin)' },
{ flag: '--system <text>', description: 'System prompt' },
{ flag: '--max-tokens <n>', description: 'Maximum tokens to generate (default: 4096)', type: 'number' },
{ flag: '--temperature <n>', description: 'Sampling temperature (0.0, 1.0]', type: 'number' },
{ flag: '--top-p <n>', description: 'Nucleus sampling threshold', type: 'number' },
{ flag: '--max-tokens <n>', description: 'Maximum tokens to generate (default: 131072 for M3, 65536 for other models)', type: 'number' },
{ flag: '--temperature <n>', description: 'Sampling temperature [0, 2] (default: 1)', type: 'number' },
{ flag: '--top-p <n>', description: 'Nucleus sampling threshold (default: 0.95 for M3, 0.9 for M2.x per Messages API)', type: 'number' },
{ flag: '--stream', description: 'Stream response tokens (default: on in TTY)' },
{ flag: '--thinking <mode>', description: 'Thinking mode for M3: enabled | disabled | adaptive (default: omitted — thinking disabled per Messages API contract)' },
{ flag: '--tool <json-or-path>', description: 'Tool definition as JSON or file path (repeatable)', type: 'array' },
],
examples: [
Expand Down Expand Up @@ -205,16 +207,43 @@ export default defineCommand({
&& process.stdout.isTTY
);

// Validate --thinking flag before any side effects. Unknown mode is a
// user error; surface it cleanly with CLIError (exit 2 = USAGE).
let thinkingMode: ReturnType<typeof resolveThinkingMode>;
try {
thinkingMode = resolveThinkingMode(flags.thinking);
} catch (err) {
throw new CLIError(
err instanceof Error ? err.message : String(err),
ExitCode.USAGE,
);
}

// Same surface for --temperature. Per Messages API contract, M3 uses
// [0, 2] with default 1; we validate before any network call. We do
// NOT auto-send 1 when the flag is omitted — backward compat: existing
// users get whatever the server defaults to, no body change.
let temperature: ReturnType<typeof resolveTemperature>;
try {
temperature = resolveTemperature(flags.temperature);
} catch (err) {
throw new CLIError(
err instanceof Error ? err.message : String(err),
ExitCode.USAGE,
);
}

const body: ChatRequest = {
model,
messages,
max_tokens: (flags.maxTokens as number) ?? 4096,
max_tokens: resolveMaxTokens(model, flags.maxTokens as number | undefined),
stream: shouldStream,
};

if (system) body.system = system;
if (flags.temperature !== undefined) body.temperature = flags.temperature as number;
if (temperature !== undefined) body.temperature = temperature;
if (flags.topP !== undefined) body.top_p = flags.topP as number;
if (thinkingMode !== undefined) body.thinking = { type: thinkingMode };

if (flags.tool) {
const tools = (flags.tool as string[]).map(t => {
Expand Down
40 changes: 35 additions & 5 deletions src/commands/text/repl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import type { Config } from '../../config/schema';
import type { GlobalFlags } from '../../types/flags';
import type { ChatMessage, ChatRequest, StreamEvent } from '../../types/api';
import { writeFileSync } from 'node:fs';
import { resolveMaxTokens, resolveThinkingMode, resolveTemperature, type ThinkingMode } from '../../utils/model-defaults';

// ---------------------------------------------------------------------------
// ANSI helpers
Expand Down Expand Up @@ -58,6 +59,7 @@ interface ReplState {
maxTokens: number;
temperature: number | undefined;
topP: number | undefined;
thinking: ThinkingMode | undefined;
}

// ---------------------------------------------------------------------------
Expand Down Expand Up @@ -296,9 +298,10 @@ export default defineCommand({
options: [
{ flag: '--model <model>', description: 'Model ID (default: MiniMax-M3)' },
{ flag: '--system <text>', description: 'System prompt' },
{ flag: '--max-tokens <n>', description: 'Maximum tokens per response (default: 4096)', type: 'number' },
{ flag: '--temperature <n>', description: 'Sampling temperature (0.0, 1.0]', type: 'number' },
{ flag: '--top-p <n>', description: 'Nucleus sampling threshold', type: 'number' },
{ flag: '--max-tokens <n>', description: 'Maximum tokens per response (default: 131072 for M3, 65536 for other models)', type: 'number' },
{ flag: '--temperature <n>', description: 'Sampling temperature [0, 2] (default: 1)', type: 'number' },
{ flag: '--top-p <n>', description: 'Nucleus sampling threshold (default: 0.95 for M3, 0.9 for M2.x per Messages API)', type: 'number' },
{ flag: '--thinking <mode>', description: 'Thinking mode for M3: enabled | disabled | adaptive (default: omitted — thinking disabled per Messages API contract)' },
],
examples: [
'mmx text repl',
Expand All @@ -321,14 +324,40 @@ export default defineCommand({
const dim = config.noColor ? '' : '\x1b[2m';
const reset = config.noColor ? '' : '\x1b[0m';

// ---- Validate --thinking up-front (same surface as chat) ----
let thinkingMode: ThinkingMode | undefined;
try {
thinkingMode = resolveThinkingMode(flags.thinking);
} catch (err) {
throw new CLIError(
err instanceof Error ? err.message : String(err),
ExitCode.USAGE,
);
}

// ---- Validate --temperature up-front (same surface as chat) ----
let temperature: number | undefined;
try {
temperature = resolveTemperature(flags.temperature);
} catch (err) {
throw new CLIError(
err instanceof Error ? err.message : String(err),
ExitCode.USAGE,
);
}

// ---- Initialize state ----
const state: ReplState = {
messages: [],
system: flags.system as string | undefined,
model: (flags.model as string) || config.defaultTextModel || 'MiniMax-M3',
maxTokens: (flags.maxTokens as number) ?? 4096,
temperature: flags.temperature !== undefined ? flags.temperature as number : undefined,
maxTokens: resolveMaxTokens(
(flags.model as string) || config.defaultTextModel || 'MiniMax-M3',
flags.maxTokens as number | undefined,
),
temperature,
topP: flags.topP !== undefined ? flags.topP as number : undefined,
thinking: thinkingMode,
};

const bold = config.noColor ? '' : '\x1b[1m';
Expand Down Expand Up @@ -368,6 +397,7 @@ export default defineCommand({
if (state.system) body.system = state.system;
if (state.temperature !== undefined) body.temperature = state.temperature;
if (state.topP !== undefined) body.top_p = state.topP;
if (state.thinking !== undefined) body.thinking = { type: state.thinking };

waitingForResponse = true;
const url = chatEndpoint(config.baseUrl);
Expand Down
19 changes: 16 additions & 3 deletions src/sdk/text/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { chatEndpoint } from "../../client/endpoints";
import { ChatRequest, ChatResponse, StreamEvent } from "../../types/api";
import { SDKError } from "../../errors/base";
import { ExitCode } from "../../errors/codes";
import { resolveMaxTokens } from "../../utils/model-defaults";

export class TextSDK extends Client {
private async *chatStream(body: Partial<ChatRequest>): AsyncGenerator<StreamEvent> {
Expand Down Expand Up @@ -54,10 +55,22 @@ export class TextSDK extends Client {
);
}

return {
const model = params.model ?? 'MiniMax-M3';

const body: ChatRequest = {
...params,
model: params.model ?? 'MiniMax-M3',
max_tokens: params.max_tokens ?? 4096,
model,
max_tokens: resolveMaxTokens(model, params.max_tokens),
} as ChatRequest;

// Pass thinking through only if explicitly provided; never auto-inject.
// Per Messages API contract, thinking is disabled by default when omitted.
if (params.thinking !== undefined) {
body.thinking = params.thinking;
} else {
delete body.thinking;
}

return body;
}
}
1 change: 1 addition & 0 deletions src/types/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ export interface ChatRequest {
stream?: boolean;
tools?: ChatTool[];
tool_choice?: { type: 'auto' | 'any' | 'tool'; name?: string };
thinking?: { type: 'enabled' | 'disabled' | 'adaptive' };
}

export interface ChatResponse {
Expand Down
104 changes: 104 additions & 0 deletions src/utils/model-defaults.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
/**
* Model-aware defaults for the Messages API.
*
* The MiniMax Messages API documents per-model recommendations and maxima for
* `max_tokens`. Different models also support different `thinking` controls.
*
* Centralizing these here keeps the CLI commands and the SDK in lockstep — if
* the API contract changes, the one place to update is this file.
*
* Mirror tests live at test/utils/model-defaults.test.ts.
*/

/**
* Per-model default `max_tokens` when the user does not pass `--max-tokens`.
*
* Per the current Messages API contract:
* - MiniMax-M3: recommended 131072 (128K), max 524288 (512K)
* - other supported models (MiniMax-M2.5, MiniMax-M2.7, etc.): 65536 (64K)
*
* `max_tokens` is an output ceiling, so the recommendation can still be
* overridden downward by the CLI user with `--max-tokens <n>` (e.g. 8192) for
* tighter budgets or response-shape constraints.
*/
export function resolveMaxTokens(model: string, flagValue: number | undefined): number {
if (flagValue !== undefined) return flagValue;
if (model === 'MiniMax-M3') return 131072;
return 65536;
}

/**
* Allowed values for the `--thinking` CLI flag / `ChatRequest.thinking.type`.
*
* Per the Messages API contract, when the `thinking` field is omitted entirely
* the API defaults thinking to disabled for M3. The CLI therefore omits the
* field unless the user passes `--thinking <mode>` explicitly.
*/
export const THINKING_MODES = ['enabled', 'disabled', 'adaptive'] as const;
export type ThinkingMode = (typeof THINKING_MODES)[number];

/**
* Validate and normalize a `--thinking` value. Returns the lowercased mode
* when it matches a known set, or `undefined` when the input is empty/absent.
* Throws on unknown values so the CLI can surface a clean CLIError before
* sending.
*/
export function resolveThinkingMode(value: unknown): ThinkingMode | undefined {
if (value === undefined || value === null || value === '') return undefined;
const v = String(value).toLowerCase();
if ((THINKING_MODES as readonly string[]).includes(v)) return v as ThinkingMode;
throw new Error(
`Invalid --thinking value "${String(value)}". Expected one of: ${THINKING_MODES.join(', ')}.`,
);
}

/**
* Sampling temperature bounds for the Messages API.
*
* M3 documents `[0, 2]` with default `1`. The CLI rejects out-of-range values
* to fail fast on a common typo (e.g. `1.5e1` or a stray negative sign).
*/
export const TEMPERATURE_MIN = 0;
export const TEMPERATURE_MAX = 2;
export const TEMPERATURE_DEFAULT = 1;

/**
* Validate a `--temperature` value. Returns the number when within `[0, 2]`,
* `undefined` when no value was supplied, or throws on out-of-range / non-numeric
* input.
*/
export function resolveTemperature(value: unknown): number | undefined {
if (value === undefined || value === null || value === '') return undefined;
const n = typeof value === 'number' ? value : Number(value);
if (!Number.isFinite(n)) {
throw new Error(`Invalid --temperature value "${String(value)}". Must be a number in [${TEMPERATURE_MIN}, ${TEMPERATURE_MAX}].`);
}
if (n < TEMPERATURE_MIN || n > TEMPERATURE_MAX) {
throw new Error(
`Invalid --temperature value ${n}. Must be in [${TEMPERATURE_MIN}, ${TEMPERATURE_MAX}] (per Messages API contract).`,
);
}
return n;
}

/**
* Per-model default `top_p` (nucleus sampling threshold) recommended by the
* current Messages API contract:
* - MiniMax-M3: 0.95
* - M2.x models (M2.5, M2.7, etc.): 0.9
*
* `top_p` is only sent on the wire when the user explicitly passes
* `--top-p <n>`. The CLI does NOT auto-send the model-aware default — that
* would be a behavior change for existing users who pass nothing today and
* rely on whatever the API defaults to on the server side.
*
* This function returns the recommended value to display in help text and
* to mirror in tests, so the documentation stays aligned with the API
* contract if a future model ships a different default. Unknown models
* fall back to the M2.x default (0.9) as the safer middle ground.
*/
export function resolveTopPDefault(model: string): number {
if (model === 'MiniMax-M3') return 0.95;
return 0.9;
}

Loading
Loading