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
15 changes: 13 additions & 2 deletions src/commands/file/delete.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import { defineCommand } from '../../command';
import { requestJson } from '../../client/http';
import { fileDeleteEndpoint } from '../../client/endpoints';
import { CLIError } from '../../errors/base';
import { ExitCode } from '../../errors/codes';
import { normalizeFileId } from '../../files/file-id';
import { formatOutput, detectOutputFormat } from '../../output/formatter';
import { isInteractive } from '../../utils/env';
import { promptText, failIfMissing } from '../../utils/prompt';
Expand Down Expand Up @@ -33,18 +36,26 @@ export default defineCommand({
}
}

const normalizedFileId = normalizeFileId(fileId);
if (!normalizedFileId) {
throw new CLIError(
'--file-id must be a positive decimal integer within the int64 range.',
ExitCode.USAGE,
);
}

const format = detectOutputFormat(config.output);

if (config.dryRun) {
process.stdout.write(formatOutput({ request: { delete_file: fileId } }, format) + '\n');
process.stdout.write(formatOutput({ request: { delete_file: normalizedFileId } }, format) + '\n');
return;
}

const url = fileDeleteEndpoint(config.baseUrl);
const response = await requestJson<FileDeleteResponse>(config, {
url,
method: 'POST',
body: { file_id: Number(fileId) },
body: { file_id: normalizedFileId },
});

if (config.quiet) {
Expand Down
24 changes: 24 additions & 0 deletions src/files/file-id.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
const MAX_INT64 = 9_223_372_036_854_775_807n;
const DECIMAL_FILE_ID = /^\d+$/;

/**
* Normalize a positive int64 file ID without passing it through Number.
*
* MiniMax documents file_id as int64, while its JSON examples encode IDs as
* strings. Sending the decimal string preserves all int64 values and remains
* compatible with callers that previously supplied safe integer numbers.
*/
export function normalizeFileId(fileId: string | number | bigint): string | undefined {
if (typeof fileId === 'number') {
if (!Number.isSafeInteger(fileId) || fileId <= 0) return undefined;
return String(fileId);
}

const decimal = typeof fileId === 'bigint' ? fileId.toString() : fileId;
if (!DECIMAL_FILE_ID.test(decimal)) return undefined;

const value = BigInt(decimal);
if (value <= 0n || value > MAX_INT64) return undefined;

return decimal;
}
18 changes: 15 additions & 3 deletions src/sdk/file/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import type {
} from '../../types/api';
import { SDKError } from '../../errors/base';
import { ExitCode } from '../../errors/codes';
import { normalizeFileId } from '../../files/file-id';

export class FileSDK extends Client {
/**
Expand Down Expand Up @@ -54,14 +55,25 @@ export class FileSDK extends Client {
/**
* Delete a file from MiniMax storage by its file ID.
*
* @param fileId - The ID of the file to delete (string or number).
* Decimal strings are sent unchanged to preserve the full int64 range.
* Safe integer numbers remain supported for backwards compatibility.
*
* @param fileId - The positive int64 ID of the file to delete.
*/
async delete(fileId: string | number): Promise<FileDeleteResponse> {
async delete(fileId: string | number | bigint): Promise<FileDeleteResponse> {
const normalizedFileId = normalizeFileId(fileId);
if (!normalizedFileId) {
throw new SDKError(
'fileId must be a positive decimal integer within the int64 range.',
ExitCode.USAGE,
);
}

const url = fileDeleteEndpoint(this.config.baseUrl);
return this.requestJson<FileDeleteResponse>({
url,
method: 'POST',
body: { file_id: Number(fileId) },
body: { file_id: normalizedFileId },
});
}

Expand Down
3 changes: 2 additions & 1 deletion src/types/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -396,7 +396,8 @@ export interface FileListResponse {

export interface FileDeleteResponse {
base_resp: BaseResp;
file_id: number;
/** Decimal string, matching the API examples and preserving int64 precision. */
file_id: string;
}

export interface FileRetrieveResponse {
Expand Down
29 changes: 20 additions & 9 deletions test/commands/file/delete.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,15 +71,26 @@ describe('file delete command', () => {
await deleteCommand.execute(makeConfig({ dryRun: true, output: 'json' }), {
...baseFlags,
dryRun: true,
fileId: 'file-123',
fileId: '123',
});
});

const parsed = JSON.parse(output);
expect(parsed.request.delete_file).toBe('file-123');
expect(parsed.request.delete_file).toBe('123');
});

it('rejects an invalid file ID locally', async () => {
await expect(
deleteCommand.execute(makeConfig({ dryRun: true }), {
...baseFlags,
dryRun: true,
fileId: '123.4',
}),
).rejects.toThrow('--file-id must be a positive decimal integer within the int64 range.');
});

it('sends POST request to delete endpoint', async () => {
it('sends a file ID above Number.MAX_SAFE_INTEGER as a decimal string', async () => {
const fileId = '9223372036854775807';
let method = '';
let body: Record<string, unknown> = {};
server = createMockServer({
Expand All @@ -89,7 +100,7 @@ describe('file delete command', () => {
body = await req.json() as Record<string, unknown>;
return jsonResponse({
base_resp: { status_code: 0, status_msg: '' },
file_id: 123,
file_id: fileId,
});
},
},
Expand All @@ -98,22 +109,22 @@ describe('file delete command', () => {
const output = await captureStdout(async () => {
await deleteCommand.execute(makeConfig({ baseUrl: server.url, output: 'json' }), {
...baseFlags,
fileId: '123',
fileId,
});
});

const parsed = JSON.parse(output);
expect(method).toBe('POST');
expect(body.file_id).toBe(123);
expect(parsed).toEqual({ file_id: 123, deleted: true });
expect(body.file_id).toBe(fileId);
expect(parsed).toEqual({ file_id: fileId, deleted: true });
});

it('prints compact status in quiet mode', async () => {
server = createMockServer({
routes: {
'/v1/files/delete': () => jsonResponse({
base_resp: { status_code: 0, status_msg: '' },
file_id: 123,
file_id: '123',
}),
},
});
Expand All @@ -122,7 +133,7 @@ describe('file delete command', () => {
await deleteCommand.execute(makeConfig({ baseUrl: server.url, quiet: true }), {
...baseFlags,
quiet: true,
fileId: 'file-123',
fileId: '123',
});
});

Expand Down
66 changes: 65 additions & 1 deletion test/sdk/file.test.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,17 @@
import { describe, it, expect } from 'bun:test';
import { describe, it, expect, afterEach } from 'bun:test';
import { FileSDK } from '../../src/sdk/file';
import { existsSync, unlinkSync, writeFileSync } from 'node:fs';
import { join } from 'node:path';
import { tmpdir } from 'node:os';
import { createMockServer, jsonResponse, type MockServer } from '../helpers/mock-server';

describe('FileSDK', () => {
let server: MockServer;

afterEach(() => {
server?.close();
});

it('throws SDKError when file does not exist', async () => {
const sdk = new FileSDK({ apiKey: 'sk-test', region: 'global' });
await expect(sdk.upload('/tmp/nonexistent-file-xxxxx.bin', 'retrieval'))
Expand All @@ -27,4 +34,61 @@ describe('FileSDK', () => {
if (existsSync(tmpFile)) unlinkSync(tmpFile);
}
});

it('preserves a file ID above Number.MAX_SAFE_INTEGER', async () => {
const fileId = '9223372036854775807';
let body: Record<string, unknown> = {};
server = createMockServer({
routes: {
'/v1/files/delete': async (req) => {
body = await req.json() as Record<string, unknown>;
return jsonResponse({
base_resp: { status_code: 0, status_msg: '' },
file_id: fileId,
});
},
},
});

const sdk = new FileSDK({ apiKey: 'sk-test', baseUrl: server.url });
const response = await sdk.delete(fileId);

expect(body.file_id).toBe(fileId);
expect(response.file_id).toBe(fileId);
});

it('accepts bigint IDs without losing precision', async () => {
const fileId = 9_223_372_036_854_775_807n;
let body: Record<string, unknown> = {};
server = createMockServer({
routes: {
'/v1/files/delete': async (req) => {
body = await req.json() as Record<string, unknown>;
return jsonResponse({
base_resp: { status_code: 0, status_msg: '' },
file_id: fileId.toString(),
});
},
},
});

const sdk = new FileSDK({ apiKey: 'sk-test', baseUrl: server.url });
await sdk.delete(fileId);

expect(body.file_id).toBe(fileId.toString());
});

it('rejects invalid and unsafe numeric IDs before making a request', async () => {
const sdk = new FileSDK({ apiKey: 'sk-test', region: 'global' });

await expect(sdk.delete('not-an-id')).rejects.toThrow(
'fileId must be a positive decimal integer within the int64 range.',
);
await expect(sdk.delete(Number.MAX_SAFE_INTEGER + 1)).rejects.toThrow(
'fileId must be a positive decimal integer within the int64 range.',
);
await expect(sdk.delete('9223372036854775808')).rejects.toThrow(
'fileId must be a positive decimal integer within the int64 range.',
);
});
});
Loading