diff --git a/src/commands/file/delete.ts b/src/commands/file/delete.ts index 8bb2c160..8b46290a 100644 --- a/src/commands/file/delete.ts +++ b/src/commands/file/delete.ts @@ -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'; @@ -33,10 +36,18 @@ 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; } @@ -44,7 +55,7 @@ export default defineCommand({ const response = await requestJson(config, { url, method: 'POST', - body: { file_id: Number(fileId) }, + body: { file_id: normalizedFileId }, }); if (config.quiet) { diff --git a/src/files/file-id.ts b/src/files/file-id.ts new file mode 100644 index 00000000..d42331e1 --- /dev/null +++ b/src/files/file-id.ts @@ -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; +} diff --git a/src/sdk/file/index.ts b/src/sdk/file/index.ts index 211ebb59..3d1aa615 100644 --- a/src/sdk/file/index.ts +++ b/src/sdk/file/index.ts @@ -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 { /** @@ -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 { + async delete(fileId: string | number | bigint): Promise { + 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({ url, method: 'POST', - body: { file_id: Number(fileId) }, + body: { file_id: normalizedFileId }, }); } diff --git a/src/types/api.ts b/src/types/api.ts index badf5073..bb2d8cdf 100644 --- a/src/types/api.ts +++ b/src/types/api.ts @@ -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 { diff --git a/test/commands/file/delete.test.ts b/test/commands/file/delete.test.ts index 938c3a77..79abdcb5 100644 --- a/test/commands/file/delete.test.ts +++ b/test/commands/file/delete.test.ts @@ -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 = {}; server = createMockServer({ @@ -89,7 +100,7 @@ describe('file delete command', () => { body = await req.json() as Record; return jsonResponse({ base_resp: { status_code: 0, status_msg: '' }, - file_id: 123, + file_id: fileId, }); }, }, @@ -98,14 +109,14 @@ 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 () => { @@ -113,7 +124,7 @@ describe('file delete command', () => { routes: { '/v1/files/delete': () => jsonResponse({ base_resp: { status_code: 0, status_msg: '' }, - file_id: 123, + file_id: '123', }), }, }); @@ -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', }); }); diff --git a/test/sdk/file.test.ts b/test/sdk/file.test.ts index 506ce3ec..bfb8f786 100644 --- a/test/sdk/file.test.ts +++ b/test/sdk/file.test.ts @@ -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')) @@ -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 = {}; + server = createMockServer({ + routes: { + '/v1/files/delete': async (req) => { + body = await req.json() as Record; + 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 = {}; + server = createMockServer({ + routes: { + '/v1/files/delete': async (req) => { + body = await req.json() as Record; + 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.', + ); + }); });