@inferencesh/sdk 0.6.7 → 0.6.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +32 -1
- package/README.md +305 -36
- package/dist/agent/actions.test.d.ts +1 -0
- package/dist/agent/actions.test.js +487 -0
- package/dist/agent/api.test.d.ts +1 -0
- package/dist/agent/api.test.js +208 -0
- package/dist/agent/reducer.test.js +4 -0
- package/dist/agent/types.test.d.ts +1 -0
- package/dist/agent/types.test.js +75 -0
- package/dist/api/agents.test.js +289 -35
- package/dist/api/apps.test.d.ts +1 -0
- package/dist/api/apps.test.js +67 -0
- package/dist/api/chats.test.d.ts +1 -0
- package/dist/api/chats.test.js +33 -0
- package/dist/api/engines.test.d.ts +1 -0
- package/dist/api/engines.test.js +55 -0
- package/dist/api/files.test.js +3 -6
- package/dist/api/flow-runs.test.d.ts +1 -0
- package/dist/api/flow-runs.test.js +55 -0
- package/dist/api/flows.test.d.ts +1 -0
- package/dist/api/flows.test.js +43 -0
- package/dist/api/sessions.d.ts +2 -1
- package/dist/api/sessions.js +2 -1
- package/dist/api/sessions.test.d.ts +1 -0
- package/dist/api/sessions.test.js +49 -0
- package/dist/api/tasks.test.js +50 -18
- package/dist/client.test.js +8 -8
- package/dist/http/client.js +5 -26
- package/dist/http/client.test.js +51 -13
- package/dist/proxy/express.test.d.ts +1 -0
- package/dist/proxy/express.test.js +106 -0
- package/dist/proxy/index.test.js +10 -1
- package/dist/stream.test.js +139 -0
- package/dist/tool-builder.test.js +69 -2
- package/dist/types.d.ts +731 -30
- package/dist/types.js +154 -14
- package/package.json +11 -4
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { HttpClient } from '../http/client';
|
|
2
|
+
import { AppsAPI } from './apps';
|
|
3
|
+
const mockFetch = jest.fn();
|
|
4
|
+
global.fetch = mockFetch;
|
|
5
|
+
function mockJsonResponse(body) {
|
|
6
|
+
mockFetch.mockResolvedValueOnce({
|
|
7
|
+
ok: true,
|
|
8
|
+
status: 200,
|
|
9
|
+
text: () => Promise.resolve(JSON.stringify(body)),
|
|
10
|
+
});
|
|
11
|
+
}
|
|
12
|
+
describe('AppsAPI', () => {
|
|
13
|
+
beforeEach(() => {
|
|
14
|
+
jest.clearAllMocks();
|
|
15
|
+
});
|
|
16
|
+
const api = () => new AppsAPI(new HttpClient({ apiKey: 'test-key' }));
|
|
17
|
+
it('should POST /apps/list for list()', async () => {
|
|
18
|
+
const apps = { items: [{ id: 'app-1' }], next_cursor: null };
|
|
19
|
+
mockJsonResponse(apps);
|
|
20
|
+
const result = await api().list({ limit: 10 });
|
|
21
|
+
expect(result).toEqual(apps);
|
|
22
|
+
const [url, init] = mockFetch.mock.calls[0];
|
|
23
|
+
expect(url).toContain('/apps/list');
|
|
24
|
+
expect(init.method).toBe('POST');
|
|
25
|
+
expect(JSON.parse(init.body)).toEqual({ limit: 10 });
|
|
26
|
+
});
|
|
27
|
+
it('should GET /apps/{name} for getByName()', async () => {
|
|
28
|
+
const app = { id: 'app-1', name: 'inference/claude-haiku' };
|
|
29
|
+
mockJsonResponse(app);
|
|
30
|
+
const result = await api().getByName('inference/claude-haiku');
|
|
31
|
+
expect(result).toEqual(app);
|
|
32
|
+
const [url] = mockFetch.mock.calls[0];
|
|
33
|
+
expect(url).toContain('/apps/inference/claude-haiku');
|
|
34
|
+
});
|
|
35
|
+
it('should POST transfer with team_id for transferOwnership()', async () => {
|
|
36
|
+
const app = { id: 'app-1' };
|
|
37
|
+
mockJsonResponse(app);
|
|
38
|
+
await api().transferOwnership('app-1', 'team-99');
|
|
39
|
+
const [url, init] = mockFetch.mock.calls[0];
|
|
40
|
+
expect(url).toContain('/apps/app-1/transfer');
|
|
41
|
+
expect(JSON.parse(init.body)).toEqual({ team_id: 'team-99' });
|
|
42
|
+
});
|
|
43
|
+
it('should POST license payload for saveLicense()', async () => {
|
|
44
|
+
const license = { app_id: 'app-1', license: 'key-abc' };
|
|
45
|
+
mockJsonResponse(license);
|
|
46
|
+
const result = await api().saveLicense('app-1', 'key-abc');
|
|
47
|
+
expect(result).toEqual(license);
|
|
48
|
+
const [, init] = mockFetch.mock.calls[0];
|
|
49
|
+
expect(JSON.parse(init.body)).toEqual({ license: 'key-abc' });
|
|
50
|
+
});
|
|
51
|
+
it('should POST version_id for setCurrentVersion()', async () => {
|
|
52
|
+
const app = { id: 'app-1', current_version_id: 'ver-2' };
|
|
53
|
+
mockJsonResponse(app);
|
|
54
|
+
await api().setCurrentVersion('app-1', 'ver-2');
|
|
55
|
+
const [, init] = mockFetch.mock.calls[0];
|
|
56
|
+
expect(JSON.parse(init.body)).toEqual({ version_id: 'ver-2' });
|
|
57
|
+
});
|
|
58
|
+
it('should POST /apps/{id}/duplicate for duplicate()', async () => {
|
|
59
|
+
const app = { id: 'app-copy' };
|
|
60
|
+
mockJsonResponse(app);
|
|
61
|
+
const result = await api().duplicate('app-1');
|
|
62
|
+
expect(result).toEqual(app);
|
|
63
|
+
const [url, init] = mockFetch.mock.calls[0];
|
|
64
|
+
expect(url).toContain('/apps/app-1/duplicate');
|
|
65
|
+
expect(init.method).toBe('POST');
|
|
66
|
+
});
|
|
67
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { HttpClient } from '../http/client';
|
|
2
|
+
import { ChatsAPI } from './chats';
|
|
3
|
+
const mockFetch = jest.fn();
|
|
4
|
+
global.fetch = mockFetch;
|
|
5
|
+
function mockJsonResponse(body) {
|
|
6
|
+
mockFetch.mockResolvedValueOnce({
|
|
7
|
+
ok: true,
|
|
8
|
+
status: 200,
|
|
9
|
+
text: () => Promise.resolve(JSON.stringify(body)),
|
|
10
|
+
});
|
|
11
|
+
}
|
|
12
|
+
describe('ChatsAPI', () => {
|
|
13
|
+
beforeEach(() => {
|
|
14
|
+
jest.clearAllMocks();
|
|
15
|
+
});
|
|
16
|
+
const api = () => new ChatsAPI(new HttpClient({ apiKey: 'test-key' }));
|
|
17
|
+
it('should GET /chats/{id}/trace for getTrace()', async () => {
|
|
18
|
+
const trace = { chat_id: 'chat-1', spans: [] };
|
|
19
|
+
mockJsonResponse(trace);
|
|
20
|
+
const result = await api().getTrace('chat-1');
|
|
21
|
+
expect(result).toEqual(trace);
|
|
22
|
+
const [url, init] = mockFetch.mock.calls[0];
|
|
23
|
+
expect(url).toContain('/chats/chat-1/trace');
|
|
24
|
+
expect(init.method).toBe('GET');
|
|
25
|
+
});
|
|
26
|
+
it('should DELETE /chats/{id} for delete()', async () => {
|
|
27
|
+
mockJsonResponse(null);
|
|
28
|
+
await api().delete('chat-9');
|
|
29
|
+
const [url, init] = mockFetch.mock.calls[0];
|
|
30
|
+
expect(url).toContain('/chats/chat-9');
|
|
31
|
+
expect(init.method).toBe('DELETE');
|
|
32
|
+
});
|
|
33
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { HttpClient } from '../http/client';
|
|
2
|
+
import { EnginesAPI } from './engines';
|
|
3
|
+
const mockFetch = jest.fn();
|
|
4
|
+
global.fetch = mockFetch;
|
|
5
|
+
function mockJsonResponse(body) {
|
|
6
|
+
mockFetch.mockResolvedValueOnce({
|
|
7
|
+
ok: true,
|
|
8
|
+
status: 200,
|
|
9
|
+
text: () => Promise.resolve(JSON.stringify(body)),
|
|
10
|
+
});
|
|
11
|
+
}
|
|
12
|
+
describe('EnginesAPI', () => {
|
|
13
|
+
beforeEach(() => {
|
|
14
|
+
jest.clearAllMocks();
|
|
15
|
+
});
|
|
16
|
+
const api = () => new EnginesAPI(new HttpClient({ apiKey: 'test-key' }));
|
|
17
|
+
it('should POST resource ids for getForResources()', async () => {
|
|
18
|
+
const engines = [{ id: 'eng-1' }];
|
|
19
|
+
mockJsonResponse(engines);
|
|
20
|
+
const result = await api().getForResources({
|
|
21
|
+
app_ids: ['app-1'],
|
|
22
|
+
agent_ids: ['agent-1'],
|
|
23
|
+
});
|
|
24
|
+
expect(result).toEqual(engines);
|
|
25
|
+
const [url, init] = mockFetch.mock.calls[0];
|
|
26
|
+
expect(url).toContain('/engines/resources');
|
|
27
|
+
expect(JSON.parse(init.body)).toEqual({
|
|
28
|
+
app_ids: ['app-1'],
|
|
29
|
+
agent_ids: ['agent-1'],
|
|
30
|
+
});
|
|
31
|
+
});
|
|
32
|
+
it('should POST /engines/{id}/stop for stop()', async () => {
|
|
33
|
+
mockJsonResponse(null);
|
|
34
|
+
await api().stop('eng-1');
|
|
35
|
+
const [url, init] = mockFetch.mock.calls[0];
|
|
36
|
+
expect(url).toContain('/engines/eng-1/stop');
|
|
37
|
+
expect(init.method).toBe('POST');
|
|
38
|
+
});
|
|
39
|
+
it('should POST /engines/{id}/restart for restart()', async () => {
|
|
40
|
+
mockJsonResponse(null);
|
|
41
|
+
await api().restart('eng-1');
|
|
42
|
+
const [url] = mockFetch.mock.calls[0];
|
|
43
|
+
expect(url).toContain('/engines/eng-1/restart');
|
|
44
|
+
});
|
|
45
|
+
it('should open SSE on /engines/{id}/stream for stream()', async () => {
|
|
46
|
+
const http = new HttpClient({ apiKey: 'test-key' });
|
|
47
|
+
const createEventSource = jest
|
|
48
|
+
.spyOn(http, 'createEventSource')
|
|
49
|
+
.mockResolvedValue(null);
|
|
50
|
+
const engines = new EnginesAPI(http);
|
|
51
|
+
await engines.stream('eng-7');
|
|
52
|
+
expect(createEventSource).toHaveBeenCalledWith('/engines/eng-7/stream');
|
|
53
|
+
createEventSource.mockRestore();
|
|
54
|
+
});
|
|
55
|
+
});
|
package/dist/api/files.test.js
CHANGED
|
@@ -27,7 +27,7 @@ describe('FilesAPI', () => {
|
|
|
27
27
|
upload_url: 'https://upload.example.com/put',
|
|
28
28
|
content_type: 'image/png',
|
|
29
29
|
};
|
|
30
|
-
mockJsonResponse(
|
|
30
|
+
mockJsonResponse([fileRecord]);
|
|
31
31
|
mockFetch.mockResolvedValueOnce({ ok: true, status: 200 });
|
|
32
32
|
const input = {
|
|
33
33
|
prompt: 'draw',
|
|
@@ -41,10 +41,7 @@ describe('FilesAPI', () => {
|
|
|
41
41
|
});
|
|
42
42
|
describe('upload', () => {
|
|
43
43
|
it('should reject invalid data URI format when uploading content', async () => {
|
|
44
|
-
mockJsonResponse({
|
|
45
|
-
success: true,
|
|
46
|
-
data: [{ id: 'file-x', uri: '', upload_url: 'https://upload.example.com/put' }],
|
|
47
|
-
});
|
|
44
|
+
mockJsonResponse([{ id: 'file-x', uri: '', upload_url: 'https://upload.example.com/put' }]);
|
|
48
45
|
await expect(api().upload('data:invalid')).rejects.toThrow('Invalid data URI format');
|
|
49
46
|
});
|
|
50
47
|
it('should decode URL-safe base64 in data URIs', async () => {
|
|
@@ -54,7 +51,7 @@ describe('FilesAPI', () => {
|
|
|
54
51
|
upload_url: 'https://upload.example.com/put',
|
|
55
52
|
content_type: 'text/plain',
|
|
56
53
|
};
|
|
57
|
-
mockJsonResponse(
|
|
54
|
+
mockJsonResponse([fileRecord]);
|
|
58
55
|
mockFetch.mockResolvedValueOnce({ ok: true, status: 200 });
|
|
59
56
|
// "SGVsbG8" is "Hello" in standard base64; URL-safe variant uses '-' instead of '+'
|
|
60
57
|
const dataUri = 'data:text/plain;base64,SGVsbG8';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { HttpClient } from '../http/client';
|
|
2
|
+
import { FlowRunsAPI } from './flow-runs';
|
|
3
|
+
const mockFetch = jest.fn();
|
|
4
|
+
global.fetch = mockFetch;
|
|
5
|
+
function mockJsonResponse(body) {
|
|
6
|
+
mockFetch.mockResolvedValueOnce({
|
|
7
|
+
ok: true,
|
|
8
|
+
status: 200,
|
|
9
|
+
text: () => Promise.resolve(JSON.stringify(body)),
|
|
10
|
+
});
|
|
11
|
+
}
|
|
12
|
+
describe('FlowRunsAPI', () => {
|
|
13
|
+
beforeEach(() => {
|
|
14
|
+
jest.clearAllMocks();
|
|
15
|
+
});
|
|
16
|
+
const api = () => new FlowRunsAPI(new HttpClient({ apiKey: 'test-key' }));
|
|
17
|
+
it('should POST flow and input for create()', async () => {
|
|
18
|
+
const flowRun = { id: 'fr-1', flow: 'flow-1' };
|
|
19
|
+
mockJsonResponse(flowRun);
|
|
20
|
+
const result = await api().create('flow-1', { prompt: 'hi' });
|
|
21
|
+
expect(result).toEqual(flowRun);
|
|
22
|
+
const [, init] = mockFetch.mock.calls[0];
|
|
23
|
+
expect(JSON.parse(init.body)).toEqual({
|
|
24
|
+
flow: 'flow-1',
|
|
25
|
+
input: { prompt: 'hi' },
|
|
26
|
+
});
|
|
27
|
+
});
|
|
28
|
+
it('should POST /flowruns/{id}/cancel for cancel()', async () => {
|
|
29
|
+
mockJsonResponse(null);
|
|
30
|
+
await api().cancel('fr-1');
|
|
31
|
+
const [url, init] = mockFetch.mock.calls[0];
|
|
32
|
+
expect(url).toContain('/flowruns/fr-1/cancel');
|
|
33
|
+
expect(init.method).toBe('POST');
|
|
34
|
+
});
|
|
35
|
+
it('should open SSE on /flowruns/{id}/stream for stream()', async () => {
|
|
36
|
+
const http = new HttpClient({ apiKey: 'test-key' });
|
|
37
|
+
const createEventSource = jest
|
|
38
|
+
.spyOn(http, 'createEventSource')
|
|
39
|
+
.mockResolvedValue(null);
|
|
40
|
+
const flowRuns = new FlowRunsAPI(http);
|
|
41
|
+
await flowRuns.stream('fr-42');
|
|
42
|
+
expect(createEventSource).toHaveBeenCalledWith('/flowruns/fr-42/stream');
|
|
43
|
+
createEventSource.mockRestore();
|
|
44
|
+
});
|
|
45
|
+
it('should open task SSE for streamTasks()', async () => {
|
|
46
|
+
const http = new HttpClient({ apiKey: 'test-key' });
|
|
47
|
+
const createEventSource = jest
|
|
48
|
+
.spyOn(http, 'createEventSource')
|
|
49
|
+
.mockResolvedValue(null);
|
|
50
|
+
const flowRuns = new FlowRunsAPI(http);
|
|
51
|
+
await flowRuns.streamTasks('fr-42');
|
|
52
|
+
expect(createEventSource).toHaveBeenCalledWith('/flowruns/fr-42/tasks/stream');
|
|
53
|
+
createEventSource.mockRestore();
|
|
54
|
+
});
|
|
55
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { HttpClient } from '../http/client';
|
|
2
|
+
import { FlowsAPI } from './flows';
|
|
3
|
+
const mockFetch = jest.fn();
|
|
4
|
+
global.fetch = mockFetch;
|
|
5
|
+
function mockJsonResponse(body) {
|
|
6
|
+
mockFetch.mockResolvedValueOnce({
|
|
7
|
+
ok: true,
|
|
8
|
+
status: 200,
|
|
9
|
+
text: () => Promise.resolve(JSON.stringify(body)),
|
|
10
|
+
});
|
|
11
|
+
}
|
|
12
|
+
describe('FlowsAPI', () => {
|
|
13
|
+
beforeEach(() => {
|
|
14
|
+
jest.clearAllMocks();
|
|
15
|
+
});
|
|
16
|
+
const api = () => new FlowsAPI(new HttpClient({ apiKey: 'test-key' }));
|
|
17
|
+
it('should POST name for create()', async () => {
|
|
18
|
+
const flow = { id: 'flow-1', name: 'My Flow' };
|
|
19
|
+
mockJsonResponse(flow);
|
|
20
|
+
const result = await api().create('My Flow');
|
|
21
|
+
expect(result).toEqual(flow);
|
|
22
|
+
const [, init] = mockFetch.mock.calls[0];
|
|
23
|
+
expect(JSON.parse(init.body)).toEqual({ name: 'My Flow' });
|
|
24
|
+
});
|
|
25
|
+
it('should POST /flows/{id}/app for createApp()', async () => {
|
|
26
|
+
const app = { id: 'app-from-flow' };
|
|
27
|
+
mockJsonResponse(app);
|
|
28
|
+
const result = await api().createApp('flow-1');
|
|
29
|
+
expect(result).toEqual(app);
|
|
30
|
+
const [url] = mockFetch.mock.calls[0];
|
|
31
|
+
expect(url).toContain('/flows/flow-1/app');
|
|
32
|
+
});
|
|
33
|
+
it('should open SSE on /flows/{id}/stream for stream()', async () => {
|
|
34
|
+
const http = new HttpClient({ apiKey: 'test-key' });
|
|
35
|
+
const createEventSource = jest
|
|
36
|
+
.spyOn(http, 'createEventSource')
|
|
37
|
+
.mockResolvedValue(null);
|
|
38
|
+
const flows = new FlowsAPI(http);
|
|
39
|
+
await flows.stream('flow-5');
|
|
40
|
+
expect(createEventSource).toHaveBeenCalledWith('/flows/flow-5/stream');
|
|
41
|
+
createEventSource.mockRestore();
|
|
42
|
+
});
|
|
43
|
+
});
|
package/dist/api/sessions.d.ts
CHANGED
|
@@ -8,7 +8,8 @@ import { AppSessionDTO } from '../types';
|
|
|
8
8
|
*
|
|
9
9
|
* @example
|
|
10
10
|
* ```typescript
|
|
11
|
-
*
|
|
11
|
+
* import { inference } from '@inferencesh/sdk';
|
|
12
|
+
* const client = inference({ apiKey: '...' });
|
|
12
13
|
*
|
|
13
14
|
* // Get session info
|
|
14
15
|
* const info = await client.sessions.get('sess_abc123');
|
package/dist/api/sessions.js
CHANGED
|
@@ -6,7 +6,8 @@
|
|
|
6
6
|
*
|
|
7
7
|
* @example
|
|
8
8
|
* ```typescript
|
|
9
|
-
*
|
|
9
|
+
* import { inference } from '@inferencesh/sdk';
|
|
10
|
+
* const client = inference({ apiKey: '...' });
|
|
10
11
|
*
|
|
11
12
|
* // Get session info
|
|
12
13
|
* const info = await client.sessions.get('sess_abc123');
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { HttpClient } from '../http/client';
|
|
2
|
+
import { SessionsAPI } from './sessions';
|
|
3
|
+
const mockFetch = jest.fn();
|
|
4
|
+
global.fetch = mockFetch;
|
|
5
|
+
function mockJsonResponse(body) {
|
|
6
|
+
mockFetch.mockResolvedValueOnce({
|
|
7
|
+
ok: true,
|
|
8
|
+
status: 200,
|
|
9
|
+
text: () => Promise.resolve(JSON.stringify(body)),
|
|
10
|
+
});
|
|
11
|
+
}
|
|
12
|
+
describe('SessionsAPI', () => {
|
|
13
|
+
beforeEach(() => {
|
|
14
|
+
jest.clearAllMocks();
|
|
15
|
+
});
|
|
16
|
+
const api = () => new SessionsAPI(new HttpClient({ apiKey: 'test-key' }));
|
|
17
|
+
it('should GET /sessions/{id} for get()', async () => {
|
|
18
|
+
const session = { id: 'sess_1', status: 'active' };
|
|
19
|
+
mockJsonResponse(session);
|
|
20
|
+
const result = await api().get('sess_1');
|
|
21
|
+
expect(result).toEqual(session);
|
|
22
|
+
const [url, init] = mockFetch.mock.calls[0];
|
|
23
|
+
expect(url).toContain('/sessions/sess_1');
|
|
24
|
+
expect(init.method).toBe('GET');
|
|
25
|
+
});
|
|
26
|
+
it('should return an empty array when list() response is null', async () => {
|
|
27
|
+
mockJsonResponse(null);
|
|
28
|
+
const result = await api().list();
|
|
29
|
+
expect(result).toEqual([]);
|
|
30
|
+
const [url] = mockFetch.mock.calls[0];
|
|
31
|
+
expect(url).toContain('/sessions');
|
|
32
|
+
});
|
|
33
|
+
it('should POST /sessions/{id}/keepalive for keepalive()', async () => {
|
|
34
|
+
const session = { id: 'sess_2', status: 'active' };
|
|
35
|
+
mockJsonResponse(session);
|
|
36
|
+
const result = await api().keepalive('sess_2');
|
|
37
|
+
expect(result).toEqual(session);
|
|
38
|
+
const [url, init] = mockFetch.mock.calls[0];
|
|
39
|
+
expect(url).toContain('/sessions/sess_2/keepalive');
|
|
40
|
+
expect(init.method).toBe('POST');
|
|
41
|
+
});
|
|
42
|
+
it('should DELETE /sessions/{id} for end()', async () => {
|
|
43
|
+
mockJsonResponse(null);
|
|
44
|
+
await api().end('sess_3');
|
|
45
|
+
const [url, init] = mockFetch.mock.calls[0];
|
|
46
|
+
expect(url).toContain('/sessions/sess_3');
|
|
47
|
+
expect(init.method).toBe('DELETE');
|
|
48
|
+
});
|
|
49
|
+
});
|
package/dist/api/tasks.test.js
CHANGED
|
@@ -35,10 +35,10 @@ describe('TasksAPI.run (polling mode)', () => {
|
|
|
35
35
|
it('should resolve when status polling detects completion', async () => {
|
|
36
36
|
const runningTask = makeTask();
|
|
37
37
|
const completedTask = makeTask({ status: TaskStatusCompleted, output: { ok: true } });
|
|
38
|
-
mockJsonResponse(
|
|
39
|
-
mockJsonResponse({
|
|
40
|
-
mockJsonResponse({
|
|
41
|
-
mockJsonResponse(
|
|
38
|
+
mockJsonResponse(runningTask);
|
|
39
|
+
mockJsonResponse({ status: TaskStatusRunning });
|
|
40
|
+
mockJsonResponse({ status: TaskStatusCompleted });
|
|
41
|
+
mockJsonResponse(completedTask);
|
|
42
42
|
const onUpdate = jest.fn();
|
|
43
43
|
const result = await api().run({ app: 'test-app', input: {} }, { prompt: 'hi' }, { wait: true, stream: false, onUpdate });
|
|
44
44
|
expect(result.status).toBe(TaskStatusCompleted);
|
|
@@ -47,28 +47,42 @@ describe('TasksAPI.run (polling mode)', () => {
|
|
|
47
47
|
it('should reject when polling detects a failed task', async () => {
|
|
48
48
|
const runningTask = makeTask();
|
|
49
49
|
const failedTask = makeTask({ status: TaskStatusFailed, error: 'model error' });
|
|
50
|
-
mockJsonResponse(
|
|
51
|
-
mockJsonResponse({
|
|
52
|
-
mockJsonResponse({
|
|
53
|
-
mockJsonResponse(
|
|
50
|
+
mockJsonResponse(runningTask);
|
|
51
|
+
mockJsonResponse({ status: TaskStatusRunning });
|
|
52
|
+
mockJsonResponse({ status: TaskStatusFailed });
|
|
53
|
+
mockJsonResponse(failedTask);
|
|
54
54
|
await expect(api().run({ app: 'test-app', input: {} }, {}, { wait: true, stream: false })).rejects.toThrow('model error');
|
|
55
55
|
});
|
|
56
56
|
it('should reject when polling detects a cancelled task', async () => {
|
|
57
57
|
const runningTask = makeTask();
|
|
58
58
|
const cancelledTask = makeTask({ status: TaskStatusCancelled });
|
|
59
|
-
mockJsonResponse(
|
|
60
|
-
mockJsonResponse({
|
|
61
|
-
mockJsonResponse({
|
|
62
|
-
mockJsonResponse(
|
|
59
|
+
mockJsonResponse(runningTask);
|
|
60
|
+
mockJsonResponse({ status: TaskStatusRunning });
|
|
61
|
+
mockJsonResponse({ status: TaskStatusCancelled });
|
|
62
|
+
mockJsonResponse(cancelledTask);
|
|
63
63
|
await expect(api().run({ app: 'test-app', input: {} }, {}, { wait: true, stream: false })).rejects.toThrow('task cancelled');
|
|
64
64
|
});
|
|
65
|
+
it('should reject when full task fetch fails after status change', async () => {
|
|
66
|
+
const runningTask = makeTask();
|
|
67
|
+
mockJsonResponse(runningTask);
|
|
68
|
+
mockJsonResponse({ status: TaskStatusRunning });
|
|
69
|
+
mockJsonResponse({ status: TaskStatusCompleted });
|
|
70
|
+
mockFetch.mockRejectedValueOnce(new Error('network down'));
|
|
71
|
+
await expect(api().run({ app: 'test-app', input: {} }, {}, { wait: true, stream: false })).rejects.toThrow('network down');
|
|
72
|
+
});
|
|
73
|
+
it('should reject when status polling fails', async () => {
|
|
74
|
+
const runningTask = makeTask();
|
|
75
|
+
mockJsonResponse(runningTask);
|
|
76
|
+
mockFetch.mockRejectedValueOnce(new Error('status endpoint down'));
|
|
77
|
+
await expect(api().run({ app: 'test-app', input: {} }, {}, { wait: true, stream: false })).rejects.toThrow('status endpoint down');
|
|
78
|
+
});
|
|
65
79
|
it('should parse string terminal statuses from the status endpoint', async () => {
|
|
66
80
|
const runningTask = makeTask();
|
|
67
81
|
const completedTask = makeTask({ status: TaskStatusCompleted });
|
|
68
|
-
mockJsonResponse(
|
|
69
|
-
mockJsonResponse({
|
|
70
|
-
mockJsonResponse({
|
|
71
|
-
mockJsonResponse(
|
|
82
|
+
mockJsonResponse(runningTask);
|
|
83
|
+
mockJsonResponse({ status: TaskStatusRunning });
|
|
84
|
+
mockJsonResponse({ status: 'completed' });
|
|
85
|
+
mockJsonResponse(completedTask);
|
|
72
86
|
const result = await api().run({ app: 'test-app', input: {} }, {}, { wait: true, stream: false });
|
|
73
87
|
expect(result.status).toBe(TaskStatusCompleted);
|
|
74
88
|
});
|
|
@@ -97,7 +111,7 @@ describe('TasksAPI.run (general)', () => {
|
|
|
97
111
|
const streamingApi = () => new TasksAPI(new HttpClient({ apiKey: 'test-key', stream: true }));
|
|
98
112
|
it('should return immediately when wait is false', async () => {
|
|
99
113
|
const task = makeTask();
|
|
100
|
-
mockJsonResponse(
|
|
114
|
+
mockJsonResponse(task);
|
|
101
115
|
const result = await streamingApi().run({ app: 'test-app', input: {} }, { prompt: 'hi' }, { wait: false });
|
|
102
116
|
expect(result.id).toBe('task-1');
|
|
103
117
|
expect(mockFetch).toHaveBeenCalledTimes(1);
|
|
@@ -114,7 +128,7 @@ describe('TasksAPI.run (streaming mode)', () => {
|
|
|
114
128
|
return Promise.resolve({
|
|
115
129
|
ok: true,
|
|
116
130
|
status: 200,
|
|
117
|
-
text: () => Promise.resolve(JSON.stringify(
|
|
131
|
+
text: () => Promise.resolve(JSON.stringify(initialTask)),
|
|
118
132
|
});
|
|
119
133
|
}
|
|
120
134
|
return Promise.resolve(mockNdjsonStream(ndjsonChunks));
|
|
@@ -142,6 +156,24 @@ describe('TasksAPI.run (streaming mode)', () => {
|
|
|
142
156
|
]);
|
|
143
157
|
await expect(api().run({ app: 'test-app', input: {} }, {}, { wait: true })).rejects.toThrow('task cancelled');
|
|
144
158
|
});
|
|
159
|
+
it('should reject when partial stream reports failure', async () => {
|
|
160
|
+
setupStreamMocks([
|
|
161
|
+
`${JSON.stringify({
|
|
162
|
+
data: { status: TaskStatusFailed, id: 'task-1', error: 'partial fail' },
|
|
163
|
+
fields: ['status'],
|
|
164
|
+
})}\n`,
|
|
165
|
+
]);
|
|
166
|
+
await expect(api().run({ app: 'test-app', input: {} }, {}, { wait: true })).rejects.toThrow('partial fail');
|
|
167
|
+
});
|
|
168
|
+
it('should reject when partial stream reports cancellation', async () => {
|
|
169
|
+
setupStreamMocks([
|
|
170
|
+
`${JSON.stringify({
|
|
171
|
+
data: { status: TaskStatusCancelled, id: 'task-1' },
|
|
172
|
+
fields: ['status'],
|
|
173
|
+
})}\n`,
|
|
174
|
+
]);
|
|
175
|
+
await expect(api().run({ app: 'test-app', input: {} }, {}, { wait: true })).rejects.toThrow('task cancelled');
|
|
176
|
+
});
|
|
145
177
|
it('should handle partial updates via onPartialUpdate', async () => {
|
|
146
178
|
setupStreamMocks([
|
|
147
179
|
`${JSON.stringify({
|
package/dist/client.test.js
CHANGED
|
@@ -38,7 +38,7 @@ describe('Inference', () => {
|
|
|
38
38
|
input: { message: 'hello world' },
|
|
39
39
|
output: { result: 'success' },
|
|
40
40
|
};
|
|
41
|
-
const responseData =
|
|
41
|
+
const responseData = mockTask;
|
|
42
42
|
mockFetch.mockResolvedValueOnce({
|
|
43
43
|
ok: true,
|
|
44
44
|
status: 200,
|
|
@@ -58,7 +58,7 @@ describe('Inference', () => {
|
|
|
58
58
|
}));
|
|
59
59
|
});
|
|
60
60
|
it('should throw error on API failure', async () => {
|
|
61
|
-
const responseData = {
|
|
61
|
+
const responseData = { message: 'Invalid app' };
|
|
62
62
|
mockFetch.mockResolvedValueOnce({
|
|
63
63
|
ok: false,
|
|
64
64
|
status: 400,
|
|
@@ -141,7 +141,7 @@ describe('Inference', () => {
|
|
|
141
141
|
});
|
|
142
142
|
describe('cancel', () => {
|
|
143
143
|
it('should make a POST request to cancel endpoint', async () => {
|
|
144
|
-
const responseData =
|
|
144
|
+
const responseData = null;
|
|
145
145
|
mockFetch.mockResolvedValueOnce({
|
|
146
146
|
ok: true,
|
|
147
147
|
status: 200,
|
|
@@ -185,7 +185,7 @@ describe('namespaced APIs', () => {
|
|
|
185
185
|
updated_at: new Date().toISOString(),
|
|
186
186
|
input: { message: 'hello world' },
|
|
187
187
|
};
|
|
188
|
-
const responseData =
|
|
188
|
+
const responseData = mockTask;
|
|
189
189
|
mockFetch.mockResolvedValueOnce({
|
|
190
190
|
ok: true,
|
|
191
191
|
status: 200,
|
|
@@ -202,7 +202,7 @@ describe('namespaced APIs', () => {
|
|
|
202
202
|
});
|
|
203
203
|
it('should get task via tasks.get()', async () => {
|
|
204
204
|
const mockTask = { id: 'task-123', status: 7 };
|
|
205
|
-
const responseData =
|
|
205
|
+
const responseData = mockTask;
|
|
206
206
|
mockFetch.mockResolvedValueOnce({
|
|
207
207
|
ok: true,
|
|
208
208
|
status: 200,
|
|
@@ -215,7 +215,7 @@ describe('namespaced APIs', () => {
|
|
|
215
215
|
expect(mockFetch).toHaveBeenCalledWith(expect.stringContaining('/tasks/task-123'), expect.objectContaining({ method: 'GET' }));
|
|
216
216
|
});
|
|
217
217
|
it('should cancel task via tasks.cancel()', async () => {
|
|
218
|
-
const responseData =
|
|
218
|
+
const responseData = null;
|
|
219
219
|
mockFetch.mockResolvedValueOnce({
|
|
220
220
|
ok: true,
|
|
221
221
|
status: 200,
|
|
@@ -298,7 +298,7 @@ describe('uploadFile', () => {
|
|
|
298
298
|
uri: 'https://example.com/file.png',
|
|
299
299
|
upload_url: 'https://upload.example.com/signed-url',
|
|
300
300
|
};
|
|
301
|
-
const responseData =
|
|
301
|
+
const responseData = [mockFile];
|
|
302
302
|
mockFetch
|
|
303
303
|
.mockResolvedValueOnce({
|
|
304
304
|
ok: true,
|
|
@@ -322,7 +322,7 @@ describe('uploadFile', () => {
|
|
|
322
322
|
uri: 'https://example.com/file.png',
|
|
323
323
|
// Missing upload_url
|
|
324
324
|
};
|
|
325
|
-
const responseData =
|
|
325
|
+
const responseData = [mockFile];
|
|
326
326
|
mockFetch.mockResolvedValueOnce({
|
|
327
327
|
ok: true,
|
|
328
328
|
status: 200,
|
package/dist/http/client.js
CHANGED
|
@@ -16,7 +16,7 @@ export class HttpClient {
|
|
|
16
16
|
this.baseUrl = config.baseUrl || 'https://api.inference.sh';
|
|
17
17
|
this.proxyUrl = config.proxyUrl;
|
|
18
18
|
this.getToken = config.getToken;
|
|
19
|
-
this.customHeaders = { 'X-Client-Source': 'inference-sdk-js/0.5.13', ...config.headers };
|
|
19
|
+
this.customHeaders = { 'X-Client-Source': 'inference-sdk-js/0.5.13', 'X-API-Version': '2', ...config.headers };
|
|
20
20
|
this.credentials = config.credentials || 'include';
|
|
21
21
|
this.onError = config.onError;
|
|
22
22
|
this.streamDefault = config.stream ?? true;
|
|
@@ -114,7 +114,6 @@ export class HttpClient {
|
|
|
114
114
|
}
|
|
115
115
|
const response = await fetch(fetchUrl, fetchOptions);
|
|
116
116
|
const responseText = await response.text();
|
|
117
|
-
// Try to parse as JSON
|
|
118
117
|
let data = null;
|
|
119
118
|
try {
|
|
120
119
|
data = JSON.parse(responseText);
|
|
@@ -122,44 +121,24 @@ export class HttpClient {
|
|
|
122
121
|
catch {
|
|
123
122
|
// Not JSON
|
|
124
123
|
}
|
|
125
|
-
//
|
|
124
|
+
// HTTP errors → RFC 9457 problem+json
|
|
126
125
|
if (!response.ok) {
|
|
127
|
-
// Check for RequirementsNotMetException (412 with errors array)
|
|
128
126
|
if (response.status === 412 && data && 'errors' in data && Array.isArray(data.errors)) {
|
|
129
127
|
throw RequirementsNotMetException.fromResponse(data, response.status);
|
|
130
128
|
}
|
|
131
|
-
// General error handling
|
|
132
129
|
let errorDetail;
|
|
133
130
|
if (data && typeof data === 'object') {
|
|
134
|
-
|
|
135
|
-
if (apiData.error) {
|
|
136
|
-
errorDetail = typeof apiData.error === 'object' ? apiData.error.message : String(apiData.error);
|
|
137
|
-
}
|
|
138
|
-
else if ('message' in data) {
|
|
139
|
-
errorDetail = String(data.message);
|
|
140
|
-
}
|
|
141
|
-
else {
|
|
142
|
-
errorDetail = JSON.stringify(data);
|
|
143
|
-
}
|
|
131
|
+
errorDetail = data.detail || data.title || data.message || JSON.stringify(data);
|
|
144
132
|
}
|
|
145
133
|
else if (responseText) {
|
|
146
134
|
errorDetail = responseText.slice(0, 500);
|
|
147
135
|
}
|
|
148
136
|
throw new InferenceError(response.status, errorDetail || 'Request failed', responseText);
|
|
149
137
|
}
|
|
150
|
-
|
|
151
|
-
if (response.status === 204) {
|
|
138
|
+
if (response.status === 204 || !responseText) {
|
|
152
139
|
return undefined;
|
|
153
140
|
}
|
|
154
|
-
|
|
155
|
-
if (!apiResponse?.success) {
|
|
156
|
-
let errorMessage = apiResponse?.error?.message;
|
|
157
|
-
if (!errorMessage) {
|
|
158
|
-
errorMessage = `Request failed (success=false). Response: ${responseText.slice(0, 500)}`;
|
|
159
|
-
}
|
|
160
|
-
throw new InferenceError(response.status, errorMessage, responseText);
|
|
161
|
-
}
|
|
162
|
-
return (apiResponse.data ?? null);
|
|
141
|
+
return data;
|
|
163
142
|
}
|
|
164
143
|
/**
|
|
165
144
|
* Get URL and headers for NDJSON streaming.
|