Skip to content

Commit cb8bb30

Browse files
committed
Change upload API spec to use the common APIPromise response shape
1 parent 2b16260 commit cb8bb30

File tree

5 files changed

+170
-133
lines changed

5 files changed

+170
-133
lines changed

src/lib/check-file.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ export interface CheckFileReport {
4444
is_check_passed: boolean;
4545
message: string;
4646
found?: boolean | null;
47+
update: any;
4748
file_size?: number | null;
4849
utf8?: boolean | null;
4950
line_type?: boolean | null;
@@ -573,7 +574,9 @@ async function _check_jsonl(file: string, purpose: FilePurpose | string): Promis
573574
} else if (current_format === DatasetFormat.CONVERSATION) {
574575
const message_column = JSONL_REQUIRED_COLUMNS_MAP[DatasetFormat.CONVERSATION][0];
575576
const require_assistant = purpose !== 'eval';
576-
validate_messages(json_line[message_column], idx, require_assistant);
577+
// @ts-ignore - just bad typescript not being great at record access
578+
const messages = json_line[message_column];
579+
validate_messages(messages, idx, require_assistant);
577580
} else {
578581
for (const column of JSONL_REQUIRED_COLUMNS_MAP[current_format]) {
579582
if (typeof json_line[column] !== 'string') {

src/lib/upload.ts

Lines changed: 103 additions & 120 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,13 @@
11
// Upload file to server using /files API
22

33
import { readEnv } from '../internal/utils/env';
4-
import fs from 'fs';
4+
import fs from 'fs/promises';
5+
import { createReadStream } from 'fs';
56
import * as path from 'path';
6-
import { FilePurpose } from '../resources';
7+
import { FilePurpose, FileRetrieveResponse } from '../resources';
78
import { checkFile } from './check-file';
8-
9-
export interface FileResponse {
10-
id: string;
11-
object: string;
12-
type: 'jsonl' | 'parquet';
13-
purpose: 'fine-tune';
14-
filename: string;
15-
bytes: number;
16-
line_count: number;
17-
processed: boolean;
18-
}
9+
import { Together } from '../client';
10+
import { APIPromise } from '../core/api-promise';
1911

2012
export interface ErrorResponse {
2113
message: string;
@@ -27,114 +19,105 @@ const failedUploadMessage = {
2719

2820
const baseURL = readEnv('TOGETHER_API_BASE_URL') || 'https://api.together.xyz/v1';
2921

30-
export async function upload(
22+
export function upload(
23+
client: Together,
3124
fileName: string,
3225
purpose: FilePurpose,
3326
check: boolean = true,
34-
): Promise<FileResponse | ErrorResponse> {
35-
if (!fs.existsSync(fileName)) {
36-
return {
37-
message: 'File does not exists',
38-
};
39-
}
40-
41-
const fileType = path.extname(fileName);
42-
if (fileType !== '.jsonl' && fileType !== '.parquet') {
43-
return {
44-
message: 'File type must be either .jsonl or .parquet',
45-
};
46-
}
47-
48-
if (check) {
49-
const checkResponse = await checkFile(fileName, purpose);
50-
if (!checkResponse.is_check_passed) {
51-
return {
52-
message: checkResponse.message || `verification of ${fileName} failed with some unknown reason`,
53-
};
54-
}
55-
}
56-
57-
// steps to do
58-
// 1. check if file exists
59-
// 2. get signed upload url
60-
// 3. upload file
61-
const apiKey = readEnv('TOGETHER_API_KEY');
62-
63-
if (!apiKey) {
64-
return {
65-
message: 'API key is required',
66-
};
67-
}
68-
69-
const getSigned = baseURL + '/files';
70-
71-
try {
72-
const params = new URLSearchParams({
73-
file_name: fileName,
74-
purpose: purpose,
75-
});
76-
const fullUrl = `${getSigned}?${params}`;
77-
const r = await fetch(fullUrl, {
78-
method: 'POST',
79-
headers: {
80-
'Content-Type': 'application/x-www-form-urlencoded',
81-
Authorization: `Bearer ${apiKey}`,
82-
},
83-
redirect: 'manual',
84-
body: params.toString(),
85-
});
86-
87-
if (r.status !== 302) {
88-
return failedUploadMessage;
89-
}
90-
91-
const uploadUrl = r.headers.get('location') || '';
92-
if (!uploadUrl || uploadUrl === '') {
93-
return failedUploadMessage;
94-
}
95-
const fileId = r.headers.get('x-together-file-id') || '';
96-
if (!fileId || fileId === '') {
97-
return failedUploadMessage;
98-
}
99-
100-
const fileStream = fs.createReadStream(fileName);
101-
const fileSize = fs.statSync(fileName).size;
102-
103-
// upload the file to uploadUrl
104-
const uploadResponse = await fetch(uploadUrl, {
105-
method: 'PUT',
106-
headers: {
107-
'Content-Type': 'application/octet-stream',
108-
'Content-Length': `${fileSize}`,
109-
},
110-
body: fileStream,
111-
});
112-
113-
if (uploadResponse.status !== 200) {
114-
return {
115-
message: `failed to upload file (${uploadResponse.statusText}) status code ${uploadResponse.status}`,
116-
};
117-
}
118-
119-
return {
120-
id: fileId,
121-
object: 'file',
122-
type: 'jsonl',
123-
purpose: 'fine-tune',
124-
filename: fileName,
125-
bytes: fileSize,
126-
line_count: 0,
127-
processed: true,
128-
};
129-
} catch (error) {
130-
if (error instanceof Error && 'status' in error && error.status) {
131-
return {
132-
message: `failed to upload file with status ${error.status}`,
133-
};
134-
}
135-
136-
return {
137-
message: 'failed to upload file',
138-
};
139-
}
27+
): APIPromise<FileRetrieveResponse> {
28+
return new APIPromise<FileRetrieveResponse>(
29+
client,
30+
new Promise(async (resolve, reject) => {
31+
let fileSize = 0;
32+
33+
try {
34+
const stat = await fs.stat(fileName);
35+
fileSize = stat.size;
36+
} catch {
37+
reject(new Error('File does not exists'));
38+
}
39+
40+
const fileType = path.extname(fileName).replace('.', '');
41+
if (fileType !== 'jsonl' && fileType !== 'parquet' && fileType !== 'csv') {
42+
return {
43+
message: 'File type must be either .jsonl, .parquet, or .csv',
44+
};
45+
}
46+
47+
if (check) {
48+
const checkResponse = await checkFile(fileName, purpose);
49+
if (!checkResponse.is_check_passed) {
50+
reject(checkResponse.message || `verification of ${fileName} failed with some unknown reason`);
51+
}
52+
}
53+
54+
try {
55+
const params = new URLSearchParams({
56+
file_name: fileName,
57+
file_type: fileType,
58+
purpose: purpose,
59+
});
60+
const fullUrl = `${baseURL}/files?${params}`;
61+
const r = await fetch(fullUrl, {
62+
method: 'POST',
63+
headers: {
64+
'Content-Type': 'application/x-www-form-urlencoded',
65+
Authorization: `Bearer ${client.apiKey}`,
66+
},
67+
redirect: 'manual',
68+
body: params.toString(),
69+
});
70+
71+
if (r.status !== 302) {
72+
return reject(failedUploadMessage);
73+
}
74+
75+
const uploadUrl = r.headers.get('location') || '';
76+
if (!uploadUrl || uploadUrl === '') {
77+
return reject(failedUploadMessage);
78+
}
79+
const fileId = r.headers.get('x-together-file-id') || '';
80+
if (!fileId || fileId === '') {
81+
return reject(failedUploadMessage);
82+
}
83+
84+
const fileStream = createReadStream(fileName);
85+
86+
// upload the file to uploadUrl
87+
const uploadResponse = await fetch(uploadUrl, {
88+
method: 'PUT',
89+
headers: {
90+
'Content-Type': 'application/octet-stream',
91+
'Content-Length': fileSize.toString(),
92+
},
93+
body: fileStream,
94+
});
95+
96+
// uploadResponse.
97+
98+
if (uploadResponse.status !== 200) {
99+
return reject({
100+
message: `failed to upload file (${uploadResponse.statusText}) status code ${uploadResponse.status}`,
101+
});
102+
}
103+
104+
const data = await client.files.retrieve(fileId).asResponse();
105+
106+
// Forcing the shape into the APIResponse interface
107+
resolve({
108+
controller: new AbortController(),
109+
requestLogID: '',
110+
retryOfRequestLogID: undefined,
111+
startTime: Date.now(),
112+
options: {
113+
method: 'post',
114+
path: '/files',
115+
},
116+
response: data,
117+
});
118+
} catch (error) {
119+
reject(error);
120+
}
121+
}),
122+
);
140123
}

src/resources/files.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -68,8 +68,8 @@ export class Files extends APIResource {
6868
/**
6969
* Upload a file.
7070
*/
71-
upload(file: string, purpose: FilePurpose, check: boolean = true): ReturnType<typeof upload> {
72-
return upload(file, purpose, check);
71+
upload(file: string, purpose: FilePurpose, check: boolean = true): APIPromise<FileRetrieveResponse> {
72+
return upload(this._client, file, purpose, check);
7373
}
7474
}
7575

tests/lib/check-file.test.ts

Lines changed: 2 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -292,10 +292,7 @@ describe('checkFile', () => {
292292
const file = path.join(tmpDir, 'missing_field_in_conversation.jsonl');
293293
const content = [
294294
{
295-
messages: [
296-
{ role: 'user', content: 'Hi' },
297-
{ role: 'assistant' },
298-
],
295+
messages: [{ role: 'user', content: 'Hi' }, { role: 'assistant' }],
299296
},
300297
];
301298
fs.writeFileSync(file, content.map((item) => JSON.stringify(item)).join('\n'));
@@ -309,11 +306,7 @@ describe('checkFile', () => {
309306
const file = path.join(tmpDir, 'wrong_turn_type.jsonl');
310307
const content = [
311308
{
312-
messages: [
313-
'Hi!',
314-
{ role: 'user', content: 'Hi' },
315-
{ role: 'assistant' },
316-
],
309+
messages: ['Hi!', { role: 'user', content: 'Hi' }, { role: 'assistant' }],
317310
},
318311
];
319312
fs.writeFileSync(file, content.map((item) => JSON.stringify(item)).join('\n'));
@@ -427,4 +420,3 @@ describe('checkFile', () => {
427420
});
428421
});
429422
});
430-

tests/lib/upload.test.ts

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
2+
3+
import * as path from 'path';
4+
import * as fs from 'fs/promises';
5+
import { tmpdir } from 'os';
6+
import Together from 'together-ai';
7+
8+
const client = new Together({
9+
apiKey: 'My API Key',
10+
baseURL: process.env['TEST_API_BASE_URL'] ?? 'http://127.0.0.1:4010',
11+
});
12+
13+
describe('Files', () => {
14+
const originalFetch = globalThis.fetch;
15+
const mockFetch = jest.fn();
16+
17+
beforeEach(() => {
18+
mockFetch.mockReset();
19+
globalThis.fetch = mockFetch;
20+
});
21+
22+
afterEach(() => {
23+
globalThis.fetch = originalFetch;
24+
});
25+
26+
test('upload', async () => {
27+
const file = path.join(tmpdir(), 'valid.jsonl');
28+
const content = [{ text: 'Hello, world!' }, { text: 'How are you?' }];
29+
await fs.writeFile(file, content.map((item) => JSON.stringify(item)).join('\n'));
30+
31+
// Mock the Signed URL Response
32+
mockFetch.mockResolvedValueOnce(
33+
new Response('', {
34+
status: 302,
35+
headers: {
36+
location: 's3://bucket/signed-url',
37+
'x-together-file-id': 'signed-url-file-id',
38+
},
39+
}),
40+
);
41+
42+
// Actual file upload response is a 200
43+
mockFetch.mockResolvedValueOnce(
44+
new Response('', {
45+
status: 200,
46+
}),
47+
);
48+
49+
const responsePromise = client.files.upload(file, 'fine-tune', false);
50+
51+
const rawResponse = await responsePromise.asResponse();
52+
expect(rawResponse).toBeInstanceOf(Response);
53+
const response = await responsePromise;
54+
expect(response).not.toBeInstanceOf(Response);
55+
const dataAndResponse = await responsePromise.withResponse();
56+
expect(dataAndResponse.data).toBe(response);
57+
expect(dataAndResponse.response).toBe(rawResponse);
58+
});
59+
});

0 commit comments

Comments
 (0)