Skip to content

Commit 1e1c108

Browse files
NianJiuZstclaude
andcommitted
feat: add FileSDK module for file storage operations
Add a new `file` property to MiniMaxSDK that exposes: - upload(filePath, purpose?) — upload a local file via multipart/form-data - list() — list all files in storage - delete(fileId) — delete a file by ID - retrieve(fileId) — get file metadata and optional download URL The module reuses existing endpoint definitions and API types from the shared client layer. FormData construction and file existence checks mirror the CLI's file upload command behavior. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent d433807 commit 1e1c108

3 files changed

Lines changed: 110 additions & 0 deletions

File tree

src/sdk/file/index.ts

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
import { existsSync } from 'node:fs';
2+
import { readFile } from 'node:fs/promises';
3+
import { resolve, basename } from 'node:path';
4+
import { Client } from '../client';
5+
import {
6+
fileUploadEndpoint,
7+
fileListEndpoint,
8+
fileDeleteEndpoint,
9+
fileRetrieveEndpoint,
10+
} from '../../client/endpoints';
11+
import type {
12+
FileUploadResponse,
13+
FileListResponse,
14+
FileDeleteResponse,
15+
FileRetrieveResponse,
16+
} from '../../types/api';
17+
import { SDKError } from '../../errors/base';
18+
import { ExitCode } from '../../errors/codes';
19+
20+
export class FileSDK extends Client {
21+
/**
22+
* Upload a file to MiniMax storage.
23+
*
24+
* @param filePath - Absolute or relative path to the file on disk.
25+
* @param purpose - File purpose, defaults to `"retrieval"`.
26+
*/
27+
async upload(filePath: string, purpose = 'retrieval'): Promise<FileUploadResponse> {
28+
const fullPath = resolve(filePath);
29+
if (!existsSync(fullPath)) {
30+
throw new SDKError(`File not found: ${fullPath}`, ExitCode.USAGE);
31+
}
32+
33+
const fileData = await readFile(fullPath);
34+
const fileName = basename(fullPath);
35+
36+
const formData = new FormData();
37+
formData.append('file', new Blob([fileData]), fileName);
38+
formData.append('purpose', purpose);
39+
40+
const url = fileUploadEndpoint(this.config.baseUrl);
41+
return this.requestJson<FileUploadResponse>({
42+
url,
43+
method: 'POST',
44+
body: formData,
45+
});
46+
}
47+
48+
/** List all files in MiniMax storage. */
49+
async list(): Promise<FileListResponse> {
50+
const url = fileListEndpoint(this.config.baseUrl);
51+
return this.requestJson<FileListResponse>({ url, method: 'GET' });
52+
}
53+
54+
/**
55+
* Delete a file from MiniMax storage by its file ID.
56+
*
57+
* @param fileId - The ID of the file to delete (string or number).
58+
*/
59+
async delete(fileId: string | number): Promise<FileDeleteResponse> {
60+
const url = fileDeleteEndpoint(this.config.baseUrl);
61+
return this.requestJson<FileDeleteResponse>({
62+
url,
63+
method: 'POST',
64+
body: { file_id: Number(fileId) },
65+
});
66+
}
67+
68+
/**
69+
* Retrieve metadata (and optional download URL) for a file.
70+
*
71+
* @param fileId - The ID of the file to retrieve.
72+
*/
73+
async retrieve(fileId: string): Promise<FileRetrieveResponse> {
74+
const url = fileRetrieveEndpoint(this.config.baseUrl, fileId);
75+
return this.requestJson<FileRetrieveResponse>({ url, method: 'GET' });
76+
}
77+
}

src/sdk/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { MusicSDK } from "./music";
66
import { SearchSDK } from "./search";
77
import { VisionSDK } from "./vision";
88
import { QuotaSDK } from "./quota";
9+
import { FileSDK } from "./file";
910
import { Client } from "./client";
1011
import { MiniMaxSDKOptions } from "./types";
1112

@@ -18,6 +19,7 @@ export class MiniMaxSDK extends Client {
1819
readonly search: SearchSDK;
1920
readonly vision: VisionSDK;
2021
readonly quota: QuotaSDK;
22+
readonly file: FileSDK;
2123

2224
constructor(options: MiniMaxSDKOptions) {
2325
super(options);
@@ -29,5 +31,6 @@ export class MiniMaxSDK extends Client {
2931
this.search = new SearchSDK(options);
3032
this.vision = new VisionSDK(options);
3133
this.quota = new QuotaSDK(options);
34+
this.file = new FileSDK(options);
3235
}
3336
}

test/sdk/file.test.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import { describe, it, expect } from 'bun:test';
2+
import { FileSDK } from '../../src/sdk/file';
3+
import { existsSync, unlinkSync, writeFileSync } from 'node:fs';
4+
import { join } from 'node:path';
5+
import { tmpdir } from 'node:os';
6+
7+
describe('FileSDK', () => {
8+
it('throws SDKError when file does not exist', async () => {
9+
const sdk = new FileSDK({ apiKey: 'sk-test', region: 'global' });
10+
await expect(sdk.upload('/tmp/nonexistent-file-xxxxx.bin', 'retrieval'))
11+
.rejects
12+
.toThrow('File not found');
13+
});
14+
15+
it('gets past file existence check for a valid file', async () => {
16+
const tmpFile = join(tmpdir(), 'mmx-sdk-test-upload.txt');
17+
writeFileSync(tmpFile, 'hello world');
18+
19+
try {
20+
const sdk = new FileSDK({ apiKey: 'sk-test', region: 'global' });
21+
await sdk.upload(tmpFile, 'retrieval');
22+
// Should not reach here (no mock server), but if it does, fail informatively
23+
} catch (err) {
24+
// Must NOT be "File not found" — proves file existence check passed
25+
expect((err as Error).message).not.toContain('File not found');
26+
} finally {
27+
if (existsSync(tmpFile)) unlinkSync(tmpFile);
28+
}
29+
});
30+
});

0 commit comments

Comments
 (0)