diff --git a/README.md b/README.md
index accc066..4feea0a 100644
--- a/README.md
+++ b/README.md
@@ -158,6 +158,195 @@ mmx update
mmx update latest
```
+## SDK Usage
+
+You can also use MiniMax programmatically via the TypeScript SDK.
+
+### Installation
+
+```bash
+npm install mmx-cli
+```
+
+### Basic Usage
+
+```typescript
+import { MiniMaxSDK } from 'mmx-cli/sdk';
+
+const sdk = new MiniMaxSDK({
+ apiKey: 'sk-xxxxx',
+ region: 'global', // or 'cn'
+});
+```
+
+### Text Chat
+
+```typescript
+// Non-streaming
+const response = await sdk.text.chat({
+ model: 'MiniMax-M2.7',
+ messages: [{ role: 'user', content: 'Hello!' }],
+ max_tokens: 4096,
+});
+
+// Streaming
+const stream = await sdk.text.chat({
+ model: 'MiniMax-M2.7',
+ messages: [{ role: 'user', content: 'Write a poem' }],
+ stream: true,
+});
+
+for await (const event of stream) {
+ console.log(event.choices[0]?.delta?.content);
+}
+```
+
+### Image Generation
+
+```typescript
+const result = await sdk.image.generate({
+ model: 'image-01',
+ prompt: 'A cat in a spacesuit',
+ width: 1024,
+ height: 1024,
+ n: 1,
+});
+```
+
+### Video Generation
+
+```typescript
+// Synchronous (waits for completion)
+const video = await sdk.video.generate({
+ model: 'MiniMax-Hailuo-2.3',
+ prompt: 'Ocean waves at sunset',
+});
+
+// Asynchronous (returns task ID immediately)
+const { taskId } = await sdk.video.generate({
+ prompt: 'A robot painting',
+ async: true,
+});
+
+// Check task status
+const task = await sdk.video.getTask({ taskId });
+
+// Download video
+const { size, save, downloadUrl } = await sdk.video.download({
+ fileId: '176844028768320',
+ outPath: './video.mp4',
+});
+```
+
+### Speech Synthesis
+
+```typescript
+// Non-streaming
+const speech = await sdk.speech.synthesize({
+ model: 'speech-2.8-hd',
+ text: 'Hello, world!',
+ voice_setting: { voice_id: 'English_expressive_narrator' },
+ audio_setting: { format: 'mp3', sample_rate: 32000, bitrate: 128000, channel: 1 },
+});
+
+// Streaming
+const stream = await sdk.speech.synthesize({
+ text: 'Stream me',
+ stream: true,
+});
+
+for await (const chunk of stream) {
+ // Process audio chunks
+}
+
+// List available voices
+const voices = await sdk.speech.voices();
+const englishVoices = await sdk.speech.voices('en');
+```
+
+### Music Generation
+
+```typescript
+// With lyrics
+const music = await sdk.music.generate({
+ model: 'music-2.6',
+ prompt: 'Upbeat pop song',
+ lyrics: '[verse] La da dee, sunny day',
+ output_format: 'hex',
+});
+
+// Instrumental
+const instrumental = await sdk.music.generate({
+ prompt: 'Cinematic orchestral',
+ instrumental: true,
+});
+
+// Auto-generate lyrics
+const autoLyrics = await sdk.music.generate({
+ prompt: 'Indie folk, melancholic, rainy night',
+ lyrics_optimizer: true,
+});
+
+// Streaming
+const stream = await sdk.music.generate({
+ prompt: 'Upbeat pop',
+ lyrics: '[verse] Hello world',
+ stream: true,
+});
+
+for await (const chunk of stream) {
+ // Process audio chunks
+}
+
+// Structured prompt
+const structured = await sdk.music.generate({
+ prompt: 'A beautiful song',
+ vocals: 'warm male baritone',
+ genre: 'jazz',
+ mood: 'relaxing',
+ instruments: 'piano, saxophone',
+ bpm: 120,
+ key: 'C major',
+});
+```
+
+### Vision (Image Description)
+
+```typescript
+const result = await sdk.vision.describe({
+ image: 'https://example.com/photo.jpg',
+ prompt: 'What breed is this dog?',
+});
+
+console.log(result.content);
+```
+
+### Web Search
+
+```typescript
+const results = await sdk.search.query('MiniMax AI latest news');
+
+for (const item of results.organic) {
+ console.log(item.title, item.link, item.snippet);
+}
+```
+
+### Quota Information
+
+```typescript
+const quota = await sdk.quota.info();
+console.log(quota);
+```
+
+### Custom Base URL
+
+```typescript
+const sdk = new MiniMaxSDK({
+ apiKey: 'sk-xxxxx',
+ baseUrl: 'https://api.minimax.io', // custom endpoint
+});
+```
+
## Thanks to
diff --git a/README_CN.md b/README_CN.md
index 40b14eb..d197394 100644
--- a/README_CN.md
+++ b/README_CN.md
@@ -158,6 +158,195 @@ mmx update
mmx update latest
```
+## SDK 使用
+
+你也可以通过 TypeScript SDK 以编程方式使用 MiniMax。
+
+### 安装
+
+```bash
+npm install mmx-cli
+```
+
+### 基础用法
+
+```typescript
+import { MiniMaxSDK } from 'mmx-cli/sdk';
+
+const sdk = new MiniMaxSDK({
+ apiKey: 'sk-xxxxx',
+ region: 'global', // 或 'cn'
+});
+```
+
+### 文本对话
+
+```typescript
+// 非流式
+const response = await sdk.text.chat({
+ model: 'MiniMax-M2.7',
+ messages: [{ role: 'user', content: '你好!' }],
+ max_tokens: 4096,
+});
+
+// 流式
+const stream = await sdk.text.chat({
+ model: 'MiniMax-M2.7',
+ messages: [{ role: 'user', content: '写一首诗' }],
+ stream: true,
+});
+
+for await (const event of stream) {
+ console.log(event.choices[0]?.delta?.content);
+}
+```
+
+### 图像生成
+
+```typescript
+const result = await sdk.image.generate({
+ model: 'image-01',
+ prompt: '一只穿宇航服的猫',
+ width: 1024,
+ height: 1024,
+ n: 1,
+});
+```
+
+### 视频生成
+
+```typescript
+// 同步(等待完成)
+const video = await sdk.video.generate({
+ model: 'MiniMax-Hailuo-2.3',
+ prompt: '海浪拍打礁石',
+});
+
+// 异步(立即返回任务 ID)
+const { taskId } = await sdk.video.generate({
+ prompt: '机器人作画',
+ async: true,
+});
+
+// 查询任务状态
+const task = await sdk.video.getTask({ taskId });
+
+// 下载视频
+const { size, save, downloadUrl } = await sdk.video.download({
+ fileId: '176844028768320',
+ outPath: './video.mp4',
+});
+```
+
+### 语音合成
+
+```typescript
+// 非流式
+const speech = await sdk.speech.synthesize({
+ model: 'speech-2.8-hd',
+ text: '你好,世界!',
+ voice_setting: { voice_id: 'English_expressive_narrator' },
+ audio_setting: { format: 'mp3', sample_rate: 32000, bitrate: 128000, channel: 1 },
+});
+
+// 流式
+const stream = await sdk.speech.synthesize({
+ text: '流式输出',
+ stream: true,
+});
+
+for await (const chunk of stream) {
+ // 处理音频块
+}
+
+// 获取可用音色列表
+const voices = await sdk.speech.voices();
+const chineseVoices = await sdk.speech.voices('zh');
+```
+
+### 音乐生成
+
+```typescript
+// 带歌词
+const music = await sdk.music.generate({
+ model: 'music-2.6',
+ prompt: '欢快的流行乐',
+ lyrics: '[主歌] 啦啦啦,阳光照',
+ output_format: 'hex',
+});
+
+// 纯音乐
+const instrumental = await sdk.music.generate({
+ prompt: '史诗管弦乐',
+ instrumental: true,
+});
+
+// 自动生词
+const autoLyrics = await sdk.music.generate({
+ prompt: '忧郁的独立民谣,雨夜',
+ lyrics_optimizer: true,
+});
+
+// 流式
+const stream = await sdk.music.generate({
+ prompt: '欢快的流行乐',
+ lyrics: '[主歌] 你好世界',
+ stream: true,
+});
+
+for await (const chunk of stream) {
+ // 处理音频块
+}
+
+// 结构化提示词
+const structured = await sdk.music.generate({
+ prompt: '一首优美的歌曲',
+ vocals: '温暖的男中音',
+ genre: '爵士',
+ mood: '放松',
+ instruments: '钢琴,萨克斯',
+ bpm: 120,
+ key: 'C 大调',
+});
+```
+
+### 图像理解
+
+```typescript
+const result = await sdk.vision.describe({
+ image: 'https://example.com/photo.jpg',
+ prompt: '这是什么品种的狗?',
+});
+
+console.log(result.content);
+```
+
+### 网络搜索
+
+```typescript
+const results = await sdk.search.query('MiniMax AI 最新动态');
+
+for (const item of results.organic) {
+ console.log(item.title, item.link, item.snippet);
+}
+```
+
+### 配额信息
+
+```typescript
+const quota = await sdk.quota.info();
+console.log(quota);
+```
+
+### 自定义基础 URL
+
+```typescript
+const sdk = new MiniMaxSDK({
+ apiKey: 'sk-xxxxx',
+ baseUrl: 'https://api.minimax.io', // 自定义端点
+});
+```
+
## 贡献者
diff --git a/build.ts b/build.ts
index 20cab98..4e16d78 100644
--- a/build.ts
+++ b/build.ts
@@ -1,8 +1,10 @@
import { readFileSync, writeFileSync } from 'fs';
+import dts from 'bun-plugin-dts';
const pkg = JSON.parse(readFileSync('package.json', 'utf-8'));
const VERSION = process.env.VERSION ?? pkg.version;
const OUT = 'dist/mmx.mjs';
+const SDK_OUT = 'dist/sdk.mjs';
await Bun.build({
entrypoints: ['src/main.ts'],
@@ -18,3 +20,16 @@ writeFileSync(OUT, Buffer.concat([Buffer.from('#!/usr/bin/env node\n'), content]
const size = (content.length / 1024).toFixed(0);
console.log(`dist/mmx.mjs ${size}KB`);
+
+await Bun.build({
+ entrypoints: ['src/sdk/index.ts'],
+ outdir: 'dist',
+ naming: 'sdk.mjs',
+ target: 'node',
+ minify: true,
+ plugins: [dts()],
+});
+
+const sdkContent = readFileSync(SDK_OUT);
+const sdkSize = (sdkContent.length / 1024).toFixed(0);
+console.log(`dist/sdk.mjs ${sdkSize}KB`);
diff --git a/bun.lock b/bun.lock
index 8e711f3..7d732be 100644
--- a/bun.lock
+++ b/bun.lock
@@ -6,6 +6,8 @@
"name": "minimax-cli",
"dependencies": {
"@clack/prompts": "^0.7.0",
+ "bun-plugin-dts": "^0.4.0",
+ "es-toolkit": "^1.46.1",
},
"devDependencies": {
"@eslint/js": "^9.0.0",
@@ -81,6 +83,8 @@
"ajv": ["ajv@6.14.0", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw=="],
+ "ansi-regex": ["ansi-regex@5.0.1", "https://registry.npmmirror.com/ansi-regex/-/ansi-regex-5.0.1.tgz", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
+
"ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
"argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="],
@@ -89,16 +93,22 @@
"brace-expansion": ["brace-expansion@1.1.12", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg=="],
+ "bun-plugin-dts": ["bun-plugin-dts@0.4.0", "https://registry.npmmirror.com/bun-plugin-dts/-/bun-plugin-dts-0.4.0.tgz", { "dependencies": { "common-path-prefix": "^3.0.0", "dts-bundle-generator": "^9.5.1", "get-tsconfig": "^4.13.6" } }, "sha512-g/pHy9SuhnUw+E+bHnJvADOnnZlEIci3nvZY8EuQEMwkpC4V4Kmoa2nG9nfda4jmjj+0POlCRCjdqXrL9gjYtA=="],
+
"bun-types": ["bun-types@1.3.11", "", { "dependencies": { "@types/node": "*" } }, "sha512-1KGPpoxQWl9f6wcZh57LvrPIInQMn2TQ7jsgxqpRzg+l0QPOFvJVH7HmvHo/AiPgwXy+/Thf6Ov3EdVn1vOabg=="],
"callsites": ["callsites@3.1.0", "", {}, "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ=="],
"chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="],
+ "cliui": ["cliui@8.0.1", "https://registry.npmmirror.com/cliui/-/cliui-8.0.1.tgz", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.1", "wrap-ansi": "^7.0.0" } }, "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ=="],
+
"color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="],
"color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="],
+ "common-path-prefix": ["common-path-prefix@3.0.0", "https://registry.npmmirror.com/common-path-prefix/-/common-path-prefix-3.0.0.tgz", {}, "sha512-QE33hToZseCH3jS0qN96O/bSh3kaw/h+Tq7ngyY9eWDUnTlTNUyqfqvCXioLe5Na5jFsL78ra/wuBU4iuEgd4w=="],
+
"concat-map": ["concat-map@0.0.1", "", {}, "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="],
"cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="],
@@ -107,6 +117,14 @@
"deep-is": ["deep-is@0.1.4", "", {}, "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ=="],
+ "dts-bundle-generator": ["dts-bundle-generator@9.5.1", "https://registry.npmmirror.com/dts-bundle-generator/-/dts-bundle-generator-9.5.1.tgz", { "dependencies": { "typescript": ">=5.0.2", "yargs": "^17.6.0" }, "bin": { "dts-bundle-generator": "dist/bin/dts-bundle-generator.js" } }, "sha512-DxpJOb2FNnEyOzMkG11sxO2dmxPjthoVWxfKqWYJ/bI/rT1rvTMktF5EKjAYrRZu6Z6t3NhOUZ0sZ5ZXevOfbA=="],
+
+ "emoji-regex": ["emoji-regex@8.0.0", "https://registry.npmmirror.com/emoji-regex/-/emoji-regex-8.0.0.tgz", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="],
+
+ "es-toolkit": ["es-toolkit@1.46.1", "https://registry.npmmirror.com/es-toolkit/-/es-toolkit-1.46.1.tgz", {}, "sha512-5eNtXOs3tbfxXOj04tjjseeWkRWaoCjdEI+96DgwzZoe6c9juL49pXlzAFTI72aWC9Y8p7168g6XIKjh7k6pyQ=="],
+
+ "escalade": ["escalade@3.2.0", "https://registry.npmmirror.com/escalade/-/escalade-3.2.0.tgz", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="],
+
"escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="],
"eslint": ["eslint@9.39.4", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", "@eslint/config-array": "^0.21.2", "@eslint/config-helpers": "^0.4.2", "@eslint/core": "^0.17.0", "@eslint/eslintrc": "^3.3.5", "@eslint/js": "9.39.4", "@eslint/plugin-kit": "^0.4.1", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", "ajv": "^6.14.0", "chalk": "^4.0.0", "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", "eslint-scope": "^8.4.0", "eslint-visitor-keys": "^4.2.1", "espree": "^10.4.0", "esquery": "^1.5.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^8.0.0", "find-up": "^5.0.0", "glob-parent": "^6.0.2", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "json-stable-stringify-without-jsonify": "^1.0.1", "lodash.merge": "^4.6.2", "minimatch": "^3.1.5", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, "peerDependencies": { "jiti": "*" }, "optionalPeers": ["jiti"], "bin": { "eslint": "bin/eslint.js" } }, "sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ=="],
@@ -141,6 +159,10 @@
"flatted": ["flatted@3.4.2", "", {}, "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA=="],
+ "get-caller-file": ["get-caller-file@2.0.5", "https://registry.npmmirror.com/get-caller-file/-/get-caller-file-2.0.5.tgz", {}, "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg=="],
+
+ "get-tsconfig": ["get-tsconfig@4.14.0", "https://registry.npmmirror.com/get-tsconfig/-/get-tsconfig-4.14.0.tgz", { "dependencies": { "resolve-pkg-maps": "^1.0.0" } }, "sha512-yTb+8DXzDREzgvYmh6s9vHsSVCHeC0G3PI5bEXNBHtmshPnO+S5O7qgLEOn0I5QvMy6kpZN8K1NKGyilLb93wA=="],
+
"glob-parent": ["glob-parent@6.0.2", "", { "dependencies": { "is-glob": "^4.0.3" } }, "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A=="],
"globals": ["globals@14.0.0", "", {}, "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ=="],
@@ -155,6 +177,8 @@
"is-extglob": ["is-extglob@2.1.1", "", {}, "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="],
+ "is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "https://registry.npmmirror.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="],
+
"is-glob": ["is-glob@4.0.3", "", { "dependencies": { "is-extglob": "^2.1.1" } }, "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg=="],
"isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="],
@@ -201,8 +225,12 @@
"punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="],
+ "require-directory": ["require-directory@2.1.1", "https://registry.npmmirror.com/require-directory/-/require-directory-2.1.1.tgz", {}, "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q=="],
+
"resolve-from": ["resolve-from@4.0.0", "", {}, "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g=="],
+ "resolve-pkg-maps": ["resolve-pkg-maps@1.0.0", "https://registry.npmmirror.com/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", {}, "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw=="],
+
"semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="],
"shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="],
@@ -211,6 +239,10 @@
"sisteransi": ["sisteransi@1.0.5", "", {}, "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg=="],
+ "string-width": ["string-width@4.2.3", "https://registry.npmmirror.com/string-width/-/string-width-4.2.3.tgz", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="],
+
+ "strip-ansi": ["strip-ansi@6.0.1", "https://registry.npmmirror.com/strip-ansi/-/strip-ansi-6.0.1.tgz", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
+
"strip-json-comments": ["strip-json-comments@3.1.1", "", {}, "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig=="],
"supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="],
@@ -233,6 +265,14 @@
"word-wrap": ["word-wrap@1.2.5", "", {}, "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA=="],
+ "wrap-ansi": ["wrap-ansi@7.0.0", "https://registry.npmmirror.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="],
+
+ "y18n": ["y18n@5.0.8", "https://registry.npmmirror.com/y18n/-/y18n-5.0.8.tgz", {}, "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA=="],
+
+ "yargs": ["yargs@17.7.2", "https://registry.npmmirror.com/yargs/-/yargs-17.7.2.tgz", { "dependencies": { "cliui": "^8.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", "string-width": "^4.2.3", "y18n": "^5.0.5", "yargs-parser": "^21.1.1" } }, "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w=="],
+
+ "yargs-parser": ["yargs-parser@21.1.1", "https://registry.npmmirror.com/yargs-parser/-/yargs-parser-21.1.1.tgz", {}, "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw=="],
+
"yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="],
"@clack/prompts/is-unicode-supported": ["is-unicode-supported@2.1.0", "", { "bundled": true }, "sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ=="],
diff --git a/package.json b/package.json
index ba9efc7..f26f706 100644
--- a/package.json
+++ b/package.json
@@ -10,8 +10,17 @@
"mmx": "dist/mmx.mjs"
},
"files": [
- "dist/mmx.mjs"
+ "dist/mmx.mjs",
+ "dist/sdk.mjs",
+ "dist/index.d.ts"
],
+ "exports": {
+ "./sdk": {
+ "import": "./dist/sdk.mjs",
+ "types": "./dist/index.d.ts"
+ },
+ "./package.json": "./package.json"
+ },
"scripts": {
"dev": "bun run src/main.ts",
"build": "bun run build.ts",
@@ -23,7 +32,9 @@
"test:watch": "bun test --watch"
},
"dependencies": {
- "@clack/prompts": "^0.7.0"
+ "@clack/prompts": "^0.7.0",
+ "bun-plugin-dts": "^0.4.0",
+ "es-toolkit": "^1.46.1"
},
"devDependencies": {
"@eslint/js": "^9.0.0",
diff --git a/src/commands/speech/voices.ts b/src/commands/speech/voices.ts
index df87469..f1424ee 100644
--- a/src/commands/speech/voices.ts
+++ b/src/commands/speech/voices.ts
@@ -13,7 +13,7 @@ function extractLanguage(voiceId: string): string {
return match ? match[1] : voiceId;
}
-function filterByLanguage(voices: SystemVoiceInfo[], language: string): SystemVoiceInfo[] {
+export function filterByLanguage(voices: SystemVoiceInfo[], language: string): SystemVoiceInfo[] {
const lang = language.toLowerCase();
return voices.filter(v => {
const voiceLang = extractLanguage(v.voice_id).toLowerCase();
diff --git a/src/commands/vision/describe.ts b/src/commands/vision/describe.ts
index c7d9d36..c2bbd8c 100644
--- a/src/commands/vision/describe.ts
+++ b/src/commands/vision/describe.ts
@@ -24,7 +24,7 @@ const MIME_TYPES: Record = {
const MAX_IMAGE_SIZE_BYTES = 50 * 1024 * 1024; // 50 MB limit
-async function toDataUri(image: string): Promise {
+export async function toDataUri(image: string): Promise {
if (image.startsWith('data:')) return image;
if (image.startsWith('http://') || image.startsWith('https://')) {
diff --git a/src/errors/base.ts b/src/errors/base.ts
index 0bb03ba..bab9fc4 100644
--- a/src/errors/base.ts
+++ b/src/errors/base.ts
@@ -21,3 +21,15 @@ export class CLIError extends Error {
};
}
}
+
+export class SDKError extends CLIError {
+ readonly exitCode: ExitCode;
+ readonly hint?: string;
+
+ constructor(message: string, exitCode: ExitCode = ExitCode.GENERAL, hint?: string) {
+ super(message);
+ this.name = 'SDKError';
+ this.exitCode = exitCode;
+ this.hint = hint;
+ }
+}
diff --git a/src/sdk/client.ts b/src/sdk/client.ts
new file mode 100644
index 0000000..f4ada4f
--- /dev/null
+++ b/src/sdk/client.ts
@@ -0,0 +1,33 @@
+import { loadConfig } from "../config/loader";
+import { Config, Region } from "../config/schema";
+import { request as requestClient, requestJson as requestJsonClient, RequestOpts } from "../client/http";
+import { MiniMaxSDKOptions } from "./types";
+
+export class Client {
+ protected config: Config;
+
+ constructor(options: MiniMaxSDKOptions) {
+ const { apiKey, region, baseUrl } = options;
+ this.config = loadConfig({
+ apiKey,
+ baseUrl,
+ region,
+ quiet: true,
+ verbose: false,
+ noColor: true,
+ yes: false,
+ dryRun: false,
+ help: false,
+ nonInteractive: false,
+ async: false,
+ });
+ }
+
+ protected request(opts: RequestOpts) {
+ return requestClient(this.config, opts);
+ }
+
+ protected requestJson(opts: RequestOpts): Promise {
+ return requestJsonClient(this.config, opts);
+ }
+}
diff --git a/src/sdk/image/index.ts b/src/sdk/image/index.ts
new file mode 100644
index 0000000..b7bb578
--- /dev/null
+++ b/src/sdk/image/index.ts
@@ -0,0 +1,40 @@
+import { Client } from "../client";
+import { imageEndpoint } from "../../client/endpoints";
+import { ImageRequest, ImageResponse } from "../../types/api";
+import { ModelPartial } from "../types";
+import { SDKError } from "../../errors/base";
+import { ExitCode } from "../../errors/codes";
+import { toMerged } from 'es-toolkit/object';
+
+export class ImageSDK extends Client {
+ async generate(request: ModelPartial): Promise {
+ const body = this.validateParams(request);
+ const url = imageEndpoint(this.config.baseUrl);
+
+ return await this.requestJson({
+ url,
+ method: "POST",
+ body,
+ });
+ }
+
+ private validateParams(params: Partial): ImageRequest {
+ const {width, height} = params;
+ for (const [name, val] of Object.entries({width, height})) {
+ if (!val) {
+ throw new SDKError(`${name} is required`, ExitCode.USAGE);
+ }
+ if (val < 512 || val > 2048) {
+ throw new SDKError(`${name} must be between 512 and 2048, got ${val}.`, ExitCode.USAGE);
+ }
+ if (val % 8 !== 0) {
+ throw new SDKError(`${name} must be a multiple of 8, got ${val}.`, ExitCode.USAGE);
+ }
+ }
+
+ return toMerged({
+ model: "image-01",
+ n: 1,
+ }, params) as ImageRequest;
+ }
+}
diff --git a/src/sdk/index.ts b/src/sdk/index.ts
new file mode 100644
index 0000000..41c0d16
--- /dev/null
+++ b/src/sdk/index.ts
@@ -0,0 +1,33 @@
+import { TextSDK } from "./text";
+import { SpeechSDK } from "./speech";
+import { ImageSDK } from "./image";
+import { VideoSDK } from "./video";
+import { MusicSDK } from "./music";
+import { SearchSDK } from "./search";
+import { VisionSDK } from "./vision";
+import { QuotaSDK } from "./quota";
+import { Client } from "./client";
+import { MiniMaxSDKOptions } from "./types";
+
+export class MiniMaxSDK extends Client {
+ readonly text: TextSDK;
+ readonly speech: SpeechSDK;
+ readonly image: ImageSDK;
+ readonly video: VideoSDK;
+ readonly music: MusicSDK;
+ readonly search: SearchSDK;
+ readonly vision: VisionSDK;
+ readonly quota: QuotaSDK;
+
+ constructor(options: MiniMaxSDKOptions) {
+ super(options);
+ this.text = new TextSDK(options);
+ this.speech = new SpeechSDK(options);
+ this.image = new ImageSDK(options);
+ this.video = new VideoSDK(options);
+ this.music = new MusicSDK(options);
+ this.search = new SearchSDK(options);
+ this.vision = new VisionSDK(options);
+ this.quota = new QuotaSDK(options);
+ }
+}
diff --git a/src/sdk/music/index.ts b/src/sdk/music/index.ts
new file mode 100644
index 0000000..c779f6d
--- /dev/null
+++ b/src/sdk/music/index.ts
@@ -0,0 +1,171 @@
+import { Client } from "../client";
+import { musicEndpoint } from "../../client/endpoints";
+import { MusicRequest, MusicResponse } from "../../types/api";
+import { ModelPartial } from "../types";
+import { SDKError } from "../../errors/base";
+import { ExitCode } from "../../errors/codes";
+import { toMerged } from "es-toolkit";
+import { musicGenerateModel } from "../../commands/music/models";
+
+export interface MusicGenerateRequest extends MusicRequest {
+ /** Vocal style, e.g. "warm male baritone", "bright female soprano", "duet with harmonies" */
+ vocals?: string;
+ /** Music genre, e.g. folk, pop, jazz */
+ genre?: string;
+ /** Mood or emotion, e.g. warm, melancholic, uplifting */
+ mood?: string;
+ /** Instruments to feature, e.g. "acoustic guitar, piano" */
+ instruments?: string;
+ /** Tempo description, e.g. fast, slow, moderate */
+ tempo?: string;
+ /** Exact tempo in beats per minute */
+ bpm?: number;
+ /** Musical key, e.g. C major, A minor, G sharp */
+ key?: string;
+ /** Elements to avoid in the generated music */
+ avoid?: string;
+ /** Use case context, e.g. "background music for video", "theme song" */
+ use_case?: string;
+ /** Song structure, e.g. "verse-chorus-verse-bridge-chorus" */
+ structure?: string;
+ /** Reference tracks or artists, e.g. "similar to Ed Sheeran, Taylor Swift" */
+ references?: string;
+ /** Additional fine-grained requirements not covered above */
+ extra?: string;
+ /** Generate instrumental music (no vocals) */
+ instrumental?: boolean;
+ /** Use case */
+ useCase?: string;
+}
+
+export class MusicSDK extends Client {
+ private async *generateStream(body: ModelPartial, url: string): AsyncGenerator> {
+ const res = await this.request({
+ url,
+ method: 'POST',
+ body,
+ stream: true,
+ });
+
+ const reader = res.body?.getReader();
+ if (!reader) {
+ throw new SDKError('No response body', ExitCode.GENERAL);
+ };
+
+ try {
+ while (true) {
+ const { done, value } = await reader.read();
+ if (done) break;
+ yield value;
+ }
+ } finally {
+ reader.releaseLock();
+ }
+ }
+
+ async generate(request: ModelPartial & { stream: true }): Promise>>;
+ async generate(request: ModelPartial): Promise;
+ async generate(request: ModelPartial): Promise>> {
+ const body = this.validateParams(request);
+
+ const url = musicEndpoint(this.config.baseUrl);
+
+ if (request.stream) {
+ return this.generateStream(body, url);
+ }
+
+ return await this.requestJson({
+ url,
+ method: 'POST',
+ body,
+ });
+ }
+
+ private buildPrompt(request: ModelPartial) {
+ const structuredParts: string[] = [];
+ if (request.vocals) structuredParts.push(`Vocals: ${request.vocals as string}`);
+ if (request.genre) structuredParts.push(`Genre: ${request.genre as string}`);
+ if (request.mood) structuredParts.push(`Mood: ${request.mood as string}`);
+ if (request.instruments) structuredParts.push(`Instruments: ${request.instruments as string}`);
+ if (request.tempo) structuredParts.push(`Tempo: ${request.tempo as string}`);
+ if (request.bpm) structuredParts.push(`BPM: ${request.bpm as number}`);
+ if (request.key) structuredParts.push(`Key: ${request.key as string}`);
+ if (request.avoid) structuredParts.push(`Avoid: ${request.avoid as string}`);
+ if (request.useCase) structuredParts.push(`Use case: ${request.useCase as string}`);
+ if (request.structure) structuredParts.push(`Structure: ${request.structure as string}`);
+ if (request.references) structuredParts.push(`References: ${request.references as string}`);
+ if (request.extra) structuredParts.push(`Extra: ${request.extra as string}`);
+
+ let lyrics = request.lyrics;
+ let prompt = request.prompt;
+
+ if (request.instrumental || !lyrics || lyrics === '无歌词' || lyrics === 'no lyrics') {
+ lyrics = '[intro] [outro]';
+ structuredParts.push('Style: instrumental, no vocals, pure music');
+ }
+
+ if (structuredParts.length > 0) {
+ const structured = structuredParts.join('. ');
+ prompt = prompt ? `${prompt}. ${structured}` : structured;
+ }
+ return prompt;
+ }
+
+ private validateParams(params: ModelPartial) {
+ const {
+ model, output_format, stream, prompt, lyrics, is_instrumental, lyrics_optimizer,
+ } = params;
+ if (is_instrumental && lyrics) {
+ throw new SDKError('Cannot use is_instrumental with lyrics', ExitCode.USAGE);
+ }
+
+ if (lyrics_optimizer && (lyrics || is_instrumental)) {
+ throw new SDKError('Cannot use lyrics_optimizer with lyrics or is_instrumental', ExitCode.USAGE);
+ }
+
+ if (!prompt && !lyrics && !is_instrumental && !lyrics_optimizer) {
+ throw new SDKError('At least one of prompt or lyrics or is_instrumental or lyrics_optimizer is required', ExitCode.USAGE);
+ }
+
+ if (!is_instrumental && !lyrics_optimizer && !lyrics?.trim()) {
+ throw new SDKError('lyrics is required', ExitCode.USAGE);
+ }
+
+ const VALID_MODELS = ['music-2.6', 'music-2.6-free', 'music-2.5+', 'music-2.5'];
+ if (model && !VALID_MODELS.includes(model)) {
+ throw new SDKError(
+ `Invalid model: ${model}. Valid models are ${VALID_MODELS.join(', ')}.`,
+ ExitCode.USAGE,
+ );
+ }
+
+ const VALID_OUTPUT_FORMATS = ['hex', 'url'];
+ if (output_format && !VALID_OUTPUT_FORMATS.includes(output_format)) {
+ throw new SDKError(
+ `Invalid output format: ${output_format}. Valid formats are ${VALID_OUTPUT_FORMATS.join(', ')}.`,
+ ExitCode.USAGE,
+ );
+ }
+ if (stream && output_format === 'url') {
+ throw new SDKError(
+ `stream and output_format url cannot be used together. Streaming requires hex format.`,
+ ExitCode.USAGE,
+ );
+ }
+
+ const targetPrompt = this.buildPrompt(params);
+
+ return toMerged({
+ model: musicGenerateModel(this.config),
+ audio_setting: {
+ format: 'mp3',
+ sample_rate: 44100,
+ bitrate: 256000,
+ },
+ output_format: 'hex',
+ }, {
+ ...params,
+ prompt: targetPrompt,
+ });
+ }
+}
diff --git a/src/sdk/quota/index.ts b/src/sdk/quota/index.ts
new file mode 100644
index 0000000..b48b359
--- /dev/null
+++ b/src/sdk/quota/index.ts
@@ -0,0 +1,12 @@
+import { Client } from "../client";
+import { quotaEndpoint } from "../../client/endpoints";
+import type { QuotaResponse } from "../../types/api";
+
+export class QuotaSDK extends Client {
+ async info(): Promise {
+ const url = quotaEndpoint(this.config.baseUrl);
+ const res = await this.requestJson({ url });
+
+ return res;
+ }
+}
diff --git a/src/sdk/search/index.ts b/src/sdk/search/index.ts
new file mode 100644
index 0000000..090af8d
--- /dev/null
+++ b/src/sdk/search/index.ts
@@ -0,0 +1,26 @@
+import { Client } from "../client";
+import { searchEndpoint } from "../../client/endpoints";
+
+interface SearchResult {
+ title: string;
+ link: string;
+ snippet: string;
+ date: string;
+}
+
+export interface SearchResponse {
+ organic: SearchResult[];
+}
+
+export class SearchSDK extends Client {
+ async query(query: string): Promise {
+ const url = searchEndpoint(this.config.baseUrl);
+ const res = await this.requestJson({
+ url,
+ method: 'POST',
+ body: { q: query },
+ });
+
+ return res;
+ }
+}
diff --git a/src/sdk/speech/index.ts b/src/sdk/speech/index.ts
new file mode 100644
index 0000000..56b8412
--- /dev/null
+++ b/src/sdk/speech/index.ts
@@ -0,0 +1,90 @@
+import { Client } from "../client";
+import { speechEndpoint, voicesEndpoint } from "../../client/endpoints";
+import { SpeechRequest, SpeechResponse, VoiceListResponse } from "../../types/api";
+import { parseSSE } from "../../client/stream";
+import { filterByLanguage } from "../../commands/speech/voices";
+import { SDKError } from "../../errors/base";
+import { ExitCode } from "../../errors/codes";
+import { toMerged } from "es-toolkit/object";
+import { ModelPartial } from "../types";
+
+export class SpeechSDK extends Client {
+ async synthesize(request: ModelPartial & { stream: true }): Promise>;
+ async synthesize(request: ModelPartial): Promise;
+ async synthesize(request: ModelPartial): Promise> {
+ const body = this.validateParams(request);
+
+ const url = speechEndpoint(this.config.baseUrl);
+
+ if (body.stream) {
+ return this.synthesizeStream(body, url);
+ }
+
+ const res = await this.requestJson({
+ url,
+ method: "POST",
+ body,
+ });
+
+ return res;
+ }
+
+ private async *synthesizeStream(body: SpeechRequest, url: string): AsyncGenerator {
+ const res = await this.request({
+ url,
+ method: "POST",
+ body,
+ stream: true,
+ });
+
+ for await (const event of parseSSE(res)) {
+ if (!event.data || event.data === '[DONE]') break;
+ try {
+ const parsed = JSON.parse(event.data) as SpeechResponse;
+ yield parsed;
+ } catch (err) {
+ throw new SDKError(
+ `Failed to parse stream chunk: ${err instanceof Error ? err.message : String(err)}`,
+ ExitCode.GENERAL,
+ );
+ }
+ }
+ }
+
+ async voices(language?: string) {
+ const url = voicesEndpoint(this.config.baseUrl);
+
+ const res = await this.requestJson({
+ url,
+ method: "POST",
+ body: { voice_type: 'system' },
+ });
+
+ const voices = res.system_voice ?? [];
+ if (language) {
+ const filtered = filterByLanguage(voices, language);
+ return filtered;
+ }
+ return voices;
+ }
+
+ private validateParams(params: Partial): SpeechRequest {
+ if (!params.text) {
+ throw new SDKError('text is required', ExitCode.USAGE);
+ }
+
+ return toMerged({
+ model: "speech-2.8-hd",
+ voice_setting: {
+ voice_id:"English_expressive_narrator",
+ },
+ audio_setting: {
+ format: "mp3",
+ sample_rate: 32000,
+ bitrate: 128000,
+ channel: 1,
+ },
+ output_format: 'hex',
+ }, params) as SpeechRequest;
+ }
+}
diff --git a/src/sdk/text/index.ts b/src/sdk/text/index.ts
new file mode 100644
index 0000000..8295de8
--- /dev/null
+++ b/src/sdk/text/index.ts
@@ -0,0 +1,75 @@
+import { Client } from "../client";
+import { chatEndpoint } from "../../client/endpoints";
+import { ChatRequest, ChatResponse, StreamEvent } from "../../types/api";
+import { parseSSE } from "../../client/stream";
+import { SDKError } from "../../errors/base";
+import { ExitCode } from "../../errors/codes";
+
+export class TextSDK extends Client {
+ private async *chatStream(body: Partial): AsyncGenerator {
+ const url = chatEndpoint(this.config.baseUrl);
+
+ const res = await this.request({
+ url,
+ method: 'POST',
+ body,
+ stream: true,
+ authStyle: 'x-api-key',
+ });
+
+ const contentType = res.headers.get('content-type') || '';
+
+ if (!contentType.includes('text/event-stream') && !contentType.includes('stream')) {
+ throw new SDKError(
+ `Expected SSE stream but got content-type "${contentType}". Server may be experiencing issues.`,
+ ExitCode.GENERAL,
+ );
+ }
+
+ for await (const event of parseSSE(res)) {
+ if (event.data === '[DONE]') break;
+ try {
+ const parsed = JSON.parse(event.data) as StreamEvent;
+ yield parsed;
+ } catch(err) {
+ throw new SDKError(
+ `Failed to parse stream chunk: ${err instanceof Error ? err.message : String(err)}`,
+ ExitCode.GENERAL,
+ );
+ }
+ }
+ }
+
+ async chat(request: Partial & { stream: true }): Promise>;
+ async chat(request: Partial): Promise;
+ async chat(request: Partial): Promise> {
+ const body = this.validateParams(request);
+ const url = chatEndpoint(this.config.baseUrl);
+
+ if (body.stream) {
+ return this.chatStream(body);
+ }
+
+ return await this.requestJson({
+ url,
+ method: 'POST',
+ body,
+ authStyle: 'x-api-key',
+ });
+ }
+
+ private validateParams(params: Partial): ChatRequest {
+ if (params.messages?.length === 0) {
+ throw new SDKError(
+ 'At least one message is required.',
+ ExitCode.USAGE,
+ );
+ }
+
+ return {
+ ...params,
+ model: params.model ?? 'MiniMax-M2.7',
+ max_tokens: params.max_tokens ?? 4096,
+ } as ChatRequest;
+ }
+}
diff --git a/src/sdk/types.ts b/src/sdk/types.ts
new file mode 100644
index 0000000..a62df27
--- /dev/null
+++ b/src/sdk/types.ts
@@ -0,0 +1,11 @@
+import { Region } from "../config/schema";
+
+export interface MiniMaxSDKOptions {
+ apiKey?: string;
+ baseUrl?: string;
+ region?: Region;
+}
+
+export type ModelPartial = 'model' extends keyof T
+ ? Omit & { model?: T['model'] }
+ : T;
diff --git a/src/sdk/video/index.ts b/src/sdk/video/index.ts
new file mode 100644
index 0000000..cb1fb31
--- /dev/null
+++ b/src/sdk/video/index.ts
@@ -0,0 +1,106 @@
+import { Client } from "../client";
+import { fileRetrieveEndpoint, videoGenerateEndpoint, videoTaskEndpoint } from "../../client/endpoints";
+import { FileRetrieveResponse, VideoRequest, VideoResponse, VideoTaskResponse } from "../../types/api";
+import { ModelPartial } from "../types";
+import { poll } from "../../polling/poll";
+import { downloadFile } from "../../files/download";
+import { SDKError } from "../../errors/base";
+import { ExitCode } from "../../errors/codes";
+import { toMerged } from 'es-toolkit/object';
+
+export interface VideoAsyncGenerateRequest extends ModelPartial {
+ async?: boolean;
+ pollInterval?: number;
+ timeout?: number;
+}
+
+export interface VideoDownloadRequest {
+ fileId: string;
+ outPath: string;
+}
+
+export class VideoSDK extends Client {
+ async generate(request: VideoAsyncGenerateRequest & { async: true }): Promise<{taskId: string}>;
+ async generate(request: ModelPartial): Promise;
+ async generate(request: VideoAsyncGenerateRequest): Promise {
+ const body = this.validateParams(request);
+ const url = videoGenerateEndpoint(this.config.baseUrl);
+ const res = await this.requestJson({
+ url,
+ method: "POST",
+ body,
+ });
+
+ const taskId = res.task_id;
+ if (request.async) {
+ return {taskId};
+ }
+
+ const taskUrl = videoTaskEndpoint(this.config.baseUrl, taskId);
+ const result = await poll(this.config, {
+ url: taskUrl,
+ intervalSec: request.pollInterval ?? 5,
+ timeoutSec: request.timeout ?? this.config.timeout,
+ isComplete: (d) => (d as VideoTaskResponse).status === 'Success',
+ isFailed: (d) => (d as VideoTaskResponse).status === 'Failed',
+ getStatus: (d) => (d as VideoTaskResponse).status,
+ });
+
+ return result;
+ }
+
+ async getTask({taskId}: {taskId: string}): Promise {
+ const url = videoTaskEndpoint(this.config.baseUrl, taskId);
+ return await this.requestJson({ url });
+ }
+
+ async download(request: VideoDownloadRequest) {
+ const url = fileRetrieveEndpoint(this.config.baseUrl, request.fileId);
+ const fileInfo = await this.requestJson({ url });
+ const downloadUrl = fileInfo.file?.download_url;
+ if (!downloadUrl) {
+ throw new Error('No download URL available for this file.');
+ }
+ const { size } = await downloadFile(downloadUrl, request.outPath, { quiet: true });
+ return {
+ size,
+ save: request.outPath,
+ downloadUrl,
+ }
+ }
+
+ private validateParams(request: VideoAsyncGenerateRequest): VideoRequest {
+ const { prompt, model, first_frame_image, last_frame_image, subject_reference } = request;
+
+ if (!prompt) {
+ throw new SDKError('prompt is required', ExitCode.USAGE);
+ }
+
+ const resolvedModel = model ?? 'MiniMax-Hailuo-2.3';
+
+ if (resolvedModel === 'MiniMax-Hailuo-2.3-Fast' && !first_frame_image) {
+ throw new SDKError(
+ 'MiniMax-Hailuo-2.3-Fast only supports I2V (image-to-video). Provide first_frame_image.',
+ ExitCode.USAGE,
+ );
+ }
+
+ if (last_frame_image && !first_frame_image) {
+ throw new SDKError(
+ 'last_frame_image requires first_frame_image (SEF mode).',
+ ExitCode.USAGE,
+ );
+ }
+
+ if (last_frame_image && subject_reference) {
+ throw new SDKError(
+ 'last_frame_image and subject_reference cannot be used together (SEF and S2V are different modes).',
+ ExitCode.USAGE,
+ );
+ }
+
+ return toMerged({
+ model: resolvedModel,
+ }, request)
+ }
+}
diff --git a/src/sdk/vision/index.ts b/src/sdk/vision/index.ts
new file mode 100644
index 0000000..daa72c3
--- /dev/null
+++ b/src/sdk/vision/index.ts
@@ -0,0 +1,30 @@
+import { Client } from "../client";
+import { vlmEndpoint } from "../../client/endpoints";
+import { toDataUri } from "../../commands/vision/describe";
+
+export interface VlmResponse {
+ content: string;
+}
+
+export interface ImageDescribeRequest {
+ prompt?: string;
+ image: string;
+}
+
+export class VisionSDK extends Client {
+ async describe(request: ImageDescribeRequest): Promise {
+ const body = {
+ prompt: request.prompt || 'Describe the image.',
+ image_url: await toDataUri(request.image),
+ };
+
+ const url = vlmEndpoint(this.config.baseUrl);
+ const res = await this.requestJson({
+ url,
+ method: 'POST',
+ body,
+ });
+
+ return res;
+ }
+}
diff --git a/src/types/api.ts b/src/types/api.ts
index fc80937..47ae2cf 100644
--- a/src/types/api.ts
+++ b/src/types/api.ts
@@ -98,7 +98,7 @@ export interface SpeechRequest {
vol?: number;
pitch?: number;
};
- audio_setting: {
+ audio_setting?: {
format?: string;
sample_rate?: number;
bitrate?: number;
diff --git a/test/sdk/image.test.ts b/test/sdk/image.test.ts
new file mode 100644
index 0000000..7a6851b
--- /dev/null
+++ b/test/sdk/image.test.ts
@@ -0,0 +1,39 @@
+import { describe, it, expect, afterEach } from 'bun:test';
+import { createMockServer, jsonResponse, type MockServer } from '../helpers/mock-server';
+import { MiniMaxSDK } from '../../src/sdk';
+
+describe('MiniMaxSDK.image', () => {
+ let server: MockServer;
+
+ afterEach(() => {
+ server?.close();
+ });
+
+ it('should generate image successfully', async () => {
+ server = createMockServer({
+ routes: {
+ '/v1/image_generation': () => jsonResponse({
+ base_resp: { status_code: 0, status_msg: 'success' },
+ data: {
+ task_id: 'img-123',
+ success_count: 1,
+ failed_count: 0,
+ },
+ }),
+ },
+ });
+
+ const sdk = new MiniMaxSDK({
+ apiKey: 'test-key',
+ baseUrl: server.url,
+ });
+
+ const result = await sdk.image.generate({
+ prompt: 'A beautiful sunset',
+ width: 1024,
+ height: 1024,
+ });
+
+ expect(result.data.task_id).toBe('img-123');
+ });
+});
diff --git a/test/sdk/music.test.ts b/test/sdk/music.test.ts
new file mode 100644
index 0000000..4b822e0
--- /dev/null
+++ b/test/sdk/music.test.ts
@@ -0,0 +1,34 @@
+import { describe, it, expect, afterEach } from 'bun:test';
+import { createMockServer, jsonResponse, type MockServer } from '../helpers/mock-server';
+import { MiniMaxSDK } from '../../src/sdk';
+
+describe('MiniMaxSDK.music', () => {
+ let server: MockServer;
+
+ afterEach(() => {
+ server?.close();
+ });
+
+ it('should generate music successfully', async () => {
+ server = createMockServer({
+ routes: {
+ '/v1/music_generation': () => jsonResponse({
+ data: { audio_url: 'https://example.com/music.mp3' },
+ base_resp: { status_code: 0, status_msg: 'success' },
+ }),
+ },
+ });
+
+ const sdk = new MiniMaxSDK({
+ apiKey: 'test-key',
+ baseUrl: server.url,
+ });
+
+ const result = await sdk.music.generate({
+ lyrics: 'no lyrics',
+ instrumental: true,
+ });
+
+ expect(result.data.audio_url).toBe('https://example.com/music.mp3');
+ });
+});
diff --git a/test/sdk/quota.test.ts b/test/sdk/quota.test.ts
new file mode 100644
index 0000000..4268b77
--- /dev/null
+++ b/test/sdk/quota.test.ts
@@ -0,0 +1,49 @@
+import { describe, it, expect, afterEach, mock } from 'bun:test';
+import { jsonResponse } from '../helpers/mock-server';
+import { MiniMaxSDK } from '../../src/sdk';
+
+describe('MiniMaxSDK.quota', () => {
+ it('should get quota info successfully', async () => {
+ const mockFetch = mock(async (url: string) => {
+ if (url.includes('/v1/token_plan/remains')) {
+ return new Response(JSON.stringify({
+ model_remains: [
+ {
+ model_name: 'MiniMax-M2.7',
+ start_time: 0,
+ end_time: 9999999999,
+ remains_time: 1000,
+ current_interval_total_count: 1000,
+ current_interval_usage_count: 500,
+ current_weekly_total_count: 5000,
+ current_weekly_usage_count: 2000,
+ weekly_start_time: 0,
+ weekly_end_time: 9999999999,
+ weekly_remains_time: 3000,
+ },
+ ],
+ }), {
+ status: 200,
+ headers: { 'Content-Type': 'application/json' },
+ });
+ }
+ return new Response('Not found', { status: 404 });
+ });
+
+ const originalFetch = globalThis.fetch;
+ globalThis.fetch = mockFetch as unknown as typeof fetch;
+
+ try {
+ const sdk = new MiniMaxSDK({
+ apiKey: 'test-key',
+ });
+
+ const result = await sdk.quota.info();
+
+ expect(result.model_remains).toHaveLength(1);
+ expect(result.model_remains[0].model_name).toBe('MiniMax-M2.7');
+ } finally {
+ globalThis.fetch = originalFetch;
+ }
+ });
+});
diff --git a/test/sdk/search.test.ts b/test/sdk/search.test.ts
new file mode 100644
index 0000000..182d0ce
--- /dev/null
+++ b/test/sdk/search.test.ts
@@ -0,0 +1,33 @@
+import { describe, it, expect, afterEach } from 'bun:test';
+import { createMockServer, jsonResponse, type MockServer } from '../helpers/mock-server';
+import { MiniMaxSDK } from '../../src/sdk';
+
+describe('MiniMaxSDK.search', () => {
+ let server: MockServer;
+
+ afterEach(() => {
+ server?.close();
+ });
+
+ it('should search successfully', async () => {
+ server = createMockServer({
+ routes: {
+ '/v1/coding_plan/search': () => jsonResponse({
+ organic: [
+ { title: 'Test Result', link: 'https://example.com', snippet: 'A test snippet', date: '2024-01-01' },
+ ],
+ }),
+ },
+ });
+
+ const sdk = new MiniMaxSDK({
+ apiKey: 'test-key',
+ baseUrl: server.url,
+ });
+
+ const result = await sdk.search.query('test query');
+
+ expect(result.organic).toHaveLength(1);
+ expect(result.organic[0].title).toBe('Test Result');
+ });
+});
diff --git a/test/sdk/speech.test.ts b/test/sdk/speech.test.ts
new file mode 100644
index 0000000..7a29309
--- /dev/null
+++ b/test/sdk/speech.test.ts
@@ -0,0 +1,56 @@
+import { describe, it, expect, afterEach } from 'bun:test';
+import { createMockServer, jsonResponse, type MockServer } from '../helpers/mock-server';
+import { MiniMaxSDK } from '../../src/sdk';
+
+describe('MiniMaxSDK.speech', () => {
+ let server: MockServer;
+
+ afterEach(() => {
+ server?.close();
+ });
+
+ it('should synthesize speech successfully', async () => {
+ server = createMockServer({
+ routes: {
+ '/v1/t2a_v2': () => jsonResponse({
+ data: { audio: 'base64audio' },
+ base_resp: { status_code: 0, status_msg: 'success' },
+ }),
+ },
+ });
+
+ const sdk = new MiniMaxSDK({
+ apiKey: 'test-key',
+ baseUrl: server.url,
+ });
+
+ const result = await sdk.speech.synthesize({
+ text: 'Hello world',
+ });
+
+ expect(result.data.audio).toBe('base64audio');
+ });
+
+ it('should get voices list', async () => {
+ server = createMockServer({
+ routes: {
+ '/v1/get_voice': () => jsonResponse({
+ system_voice: [
+ { voice_id: 'voice-1', voice_name: 'Voice 1', description: [] },
+ ],
+ base_resp: { status_code: 0, status_msg: 'success' },
+ }),
+ },
+ });
+
+ const sdk = new MiniMaxSDK({
+ apiKey: 'test-key',
+ baseUrl: server.url,
+ });
+
+ const voices = await sdk.speech.voices();
+
+ expect(voices).toHaveLength(1);
+ expect(voices[0].voice_id).toBe('voice-1');
+ });
+});
diff --git a/test/sdk/text.test.ts b/test/sdk/text.test.ts
new file mode 100644
index 0000000..9219f22
--- /dev/null
+++ b/test/sdk/text.test.ts
@@ -0,0 +1,38 @@
+import { describe, it, expect, afterEach } from 'bun:test';
+import { createMockServer, jsonResponse, type MockServer } from '../helpers/mock-server';
+import { MiniMaxSDK } from '../../src/sdk';
+
+describe('MiniMaxSDK.text', () => {
+ let server: MockServer;
+
+ afterEach(() => {
+ server?.close();
+ });
+
+ it('should chat successfully', async () => {
+ server = createMockServer({
+ routes: {
+ '/anthropic/v1/messages': () => jsonResponse({
+ id: 'msg-123',
+ type: 'message',
+ role: 'assistant',
+ content: [{ type: 'text', text: 'Hello!' }],
+ model: 'MiniMax-M2.7',
+ stop_reason: 'end_turn',
+ usage: { input_tokens: 10, output_tokens: 5 },
+ }),
+ },
+ });
+
+ const sdk = new MiniMaxSDK({
+ apiKey: 'test-key',
+ baseUrl: server.url,
+ });
+
+ const result = await sdk.text.chat({
+ messages: [{ role: 'user', content: 'Hello' }],
+ });
+
+ expect(result.id).toBe('msg-123');
+ });
+});
diff --git a/test/sdk/video.test.ts b/test/sdk/video.test.ts
new file mode 100644
index 0000000..58e4aa0
--- /dev/null
+++ b/test/sdk/video.test.ts
@@ -0,0 +1,55 @@
+import { describe, it, expect, afterEach } from 'bun:test';
+import { createMockServer, jsonResponse, type MockServer } from '../helpers/mock-server';
+import { MiniMaxSDK } from '../../src/sdk';
+
+describe('MiniMaxSDK.video', () => {
+ let server: MockServer;
+
+ afterEach(() => {
+ server?.close();
+ });
+
+ it('should generate video async successfully', async () => {
+ server = createMockServer({
+ routes: {
+ '/v1/video_generation': () => jsonResponse({
+ task_id: 'vid-123',
+ base_resp: { status_code: 0, status_msg: 'success' },
+ }),
+ },
+ });
+
+ const sdk = new MiniMaxSDK({
+ apiKey: 'test-key',
+ baseUrl: server.url,
+ });
+
+ const result = await sdk.video.generate({
+ prompt: 'A cat walking',
+ async: true,
+ });
+
+ expect(result.taskId).toBe('vid-123');
+ });
+
+ it('should get task status', async () => {
+ server = createMockServer({
+ routes: {
+ '/v1/query/video_generation': () => jsonResponse({
+ task_id: 'vid-123',
+ status: 'Success',
+ base_resp: { status_code: 0, status_msg: 'success' },
+ }),
+ },
+ });
+
+ const sdk = new MiniMaxSDK({
+ apiKey: 'test-key',
+ baseUrl: server.url,
+ });
+
+ const result = await sdk.video.getTask({ taskId: 'vid-123' });
+
+ expect(result.status).toBe('Success');
+ });
+});
diff --git a/test/sdk/vision.test.ts b/test/sdk/vision.test.ts
new file mode 100644
index 0000000..827dc32
--- /dev/null
+++ b/test/sdk/vision.test.ts
@@ -0,0 +1,32 @@
+import { describe, it, expect, afterEach } from 'bun:test';
+import { createMockServer, jsonResponse, type MockServer } from '../helpers/mock-server';
+import { MiniMaxSDK } from '../../src/sdk';
+
+describe('MiniMaxSDK.vision', () => {
+ let server: MockServer;
+
+ afterEach(() => {
+ server?.close();
+ });
+
+ it('should describe image successfully', async () => {
+ server = createMockServer({
+ routes: {
+ '/v1/coding_plan/vlm': () => jsonResponse({
+ content: 'A beautiful sunset over the ocean',
+ }),
+ },
+ });
+
+ const sdk = new MiniMaxSDK({
+ apiKey: 'test-key',
+ baseUrl: server.url,
+ });
+
+ const result = await sdk.vision.describe({
+ image: 'data:image/jpeg;base64,dGVzdA==',
+ });
+
+ expect(result.content).toBe('A beautiful sunset over the ocean');
+ });
+});