@inferencesh/sdk 0.6.10 → 0.6.13
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/README.md +1 -1
- package/dist/agent/actions.js +3 -3
- package/dist/agent/actions.test.js +231 -1
- package/dist/agent/api.test.js +18 -1
- package/dist/api/agents.d.ts +10 -0
- package/dist/api/agents.js +17 -3
- package/dist/api/agents.test.js +500 -0
- package/dist/api/api-keys.d.ts +24 -0
- package/dist/api/api-keys.js +26 -0
- package/dist/api/api-keys.test.d.ts +1 -0
- package/dist/api/api-keys.test.js +44 -0
- package/dist/api/apps.js +1 -1
- package/dist/api/apps.test.js +71 -3
- package/dist/api/chats.d.ts +14 -0
- package/dist/api/chats.js +18 -0
- package/dist/api/chats.test.js +52 -0
- package/dist/api/engines.d.ts +12 -0
- package/dist/api/engines.js +18 -0
- package/dist/api/engines.test.js +78 -0
- package/dist/api/files.test.js +183 -0
- package/dist/api/flow-runs.test.js +42 -0
- package/dist/api/flows.d.ts +4 -0
- package/dist/api/flows.js +6 -0
- package/dist/api/flows.test.js +75 -0
- package/dist/api/integrations.d.ts +41 -0
- package/dist/api/integrations.js +56 -0
- package/dist/api/integrations.test.d.ts +1 -0
- package/dist/api/integrations.test.js +109 -0
- package/dist/api/knowledge.d.ts +112 -0
- package/dist/api/knowledge.js +163 -0
- package/dist/api/knowledge.test.d.ts +1 -0
- package/dist/api/knowledge.test.js +238 -0
- package/dist/api/mcp-servers.d.ts +45 -0
- package/dist/api/mcp-servers.js +62 -0
- package/dist/api/mcp-servers.test.d.ts +1 -0
- package/dist/api/mcp-servers.test.js +98 -0
- package/dist/api/projects.d.ts +29 -0
- package/dist/api/projects.js +38 -0
- package/dist/api/projects.test.d.ts +1 -0
- package/dist/api/projects.test.js +61 -0
- package/dist/api/search.d.ts +21 -0
- package/dist/api/search.js +20 -0
- package/dist/api/search.test.d.ts +1 -0
- package/dist/api/search.test.js +39 -0
- package/dist/api/secrets.d.ts +29 -0
- package/dist/api/secrets.js +38 -0
- package/dist/api/secrets.test.d.ts +1 -0
- package/dist/api/secrets.test.js +61 -0
- package/dist/api/sessions.test.js +12 -0
- package/dist/api/tasks.d.ts +13 -1
- package/dist/api/tasks.js +18 -0
- package/dist/api/tasks.test.js +171 -0
- package/dist/api/teams.d.ts +71 -0
- package/dist/api/teams.js +92 -0
- package/dist/api/teams.test.d.ts +1 -0
- package/dist/api/teams.test.js +139 -0
- package/dist/client.test.js +112 -1
- package/dist/http/client.d.ts +5 -0
- package/dist/http/client.js +42 -23
- package/dist/http/client.test.js +245 -0
- package/dist/http/errors.test.js +13 -0
- package/dist/index.d.ts +25 -0
- package/dist/index.js +25 -0
- package/dist/proxy/hono.test.d.ts +1 -0
- package/dist/proxy/hono.test.js +90 -0
- package/dist/proxy/nextjs.test.d.ts +1 -0
- package/dist/proxy/nextjs.test.js +125 -0
- package/dist/proxy/remix.test.d.ts +1 -0
- package/dist/proxy/remix.test.js +66 -0
- package/dist/proxy/svelte.test.d.ts +1 -0
- package/dist/proxy/svelte.test.js +69 -0
- package/dist/tool-builder.test.js +11 -1
- package/dist/types.d.ts +110 -5
- package/dist/types.js +58 -2
- package/package.json +6 -6
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import { HttpClient } from '../http/client';
|
|
2
|
+
import { TeamsAPI } from './teams';
|
|
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('TeamsAPI', () => {
|
|
13
|
+
beforeEach(() => {
|
|
14
|
+
jest.clearAllMocks();
|
|
15
|
+
});
|
|
16
|
+
const api = () => new TeamsAPI(new HttpClient({ apiKey: 'test-key' }));
|
|
17
|
+
it('should GET /me for me()', async () => {
|
|
18
|
+
const me = { user: { id: 'user-1' }, team: { id: 'team-1' } };
|
|
19
|
+
mockJsonResponse(me);
|
|
20
|
+
const result = await api().me();
|
|
21
|
+
expect(result).toEqual(me);
|
|
22
|
+
const [url, init] = mockFetch.mock.calls[0];
|
|
23
|
+
expect(url).toContain('/me');
|
|
24
|
+
expect(init.method).toBe('GET');
|
|
25
|
+
});
|
|
26
|
+
it('should GET /teams for list()', async () => {
|
|
27
|
+
const teams = [{ id: 'team-1', name: 'Acme' }];
|
|
28
|
+
mockJsonResponse(teams);
|
|
29
|
+
const result = await api().list();
|
|
30
|
+
expect(result).toEqual(teams);
|
|
31
|
+
const [url, init] = mockFetch.mock.calls[0];
|
|
32
|
+
expect(url).toContain('/teams');
|
|
33
|
+
expect(init.method).toBe('GET');
|
|
34
|
+
});
|
|
35
|
+
it('should POST /teams for create()', async () => {
|
|
36
|
+
const payload = { name: 'New Team', username: 'new-team' };
|
|
37
|
+
const team = { id: 'team-new', ...payload };
|
|
38
|
+
mockJsonResponse(team);
|
|
39
|
+
const result = await api().create(payload);
|
|
40
|
+
expect(result).toEqual(team);
|
|
41
|
+
const [url, init] = mockFetch.mock.calls[0];
|
|
42
|
+
expect(url).toContain('/teams');
|
|
43
|
+
expect(init.method).toBe('POST');
|
|
44
|
+
expect(JSON.parse(init.body)).toEqual(payload);
|
|
45
|
+
});
|
|
46
|
+
it('should GET /teams/check-username with username param for checkUsername()', async () => {
|
|
47
|
+
mockJsonResponse({ available: true });
|
|
48
|
+
const result = await api().checkUsername('acme-corp');
|
|
49
|
+
expect(result).toEqual({ available: true });
|
|
50
|
+
const [url] = mockFetch.mock.calls[0];
|
|
51
|
+
expect(url).toContain('/teams/check-username');
|
|
52
|
+
expect(url).toContain('username=acme-corp');
|
|
53
|
+
});
|
|
54
|
+
it('should POST role for updateMemberRole()', async () => {
|
|
55
|
+
const member = { user_id: 'user-2', role: 'admin' };
|
|
56
|
+
mockJsonResponse(member);
|
|
57
|
+
await api().updateMemberRole('team-1', 'user-2', 'admin');
|
|
58
|
+
const [url, init] = mockFetch.mock.calls[0];
|
|
59
|
+
expect(url).toContain('/teams/team-1/members/user-2/role');
|
|
60
|
+
expect(JSON.parse(init.body)).toEqual({ role: 'admin' });
|
|
61
|
+
});
|
|
62
|
+
it('should DELETE /teams/{id}/members/{userId} for removeMember()', async () => {
|
|
63
|
+
mockJsonResponse(null);
|
|
64
|
+
await api().removeMember('team-1', 'user-3');
|
|
65
|
+
const [url, init] = mockFetch.mock.calls[0];
|
|
66
|
+
expect(url).toContain('/teams/team-1/members/user-3');
|
|
67
|
+
expect(init.method).toBe('DELETE');
|
|
68
|
+
});
|
|
69
|
+
it('should POST /teams/{id}/invites for createInvite()', async () => {
|
|
70
|
+
const payload = { email: 'dev@example.com', role: 'member' };
|
|
71
|
+
const invite = { id: 'inv-1', ...payload };
|
|
72
|
+
mockJsonResponse(invite);
|
|
73
|
+
const result = await api().createInvite('team-1', payload);
|
|
74
|
+
expect(result).toEqual(invite);
|
|
75
|
+
const [url, init] = mockFetch.mock.calls[0];
|
|
76
|
+
expect(url).toContain('/teams/team-1/invites');
|
|
77
|
+
expect(JSON.parse(init.body)).toEqual(payload);
|
|
78
|
+
});
|
|
79
|
+
it('should GET /teams/{id} for get()', async () => {
|
|
80
|
+
const team = { id: 'team-1', name: 'Acme' };
|
|
81
|
+
mockJsonResponse(team);
|
|
82
|
+
const result = await api().get('team-1');
|
|
83
|
+
expect(result).toEqual(team);
|
|
84
|
+
const [url, init] = mockFetch.mock.calls[0];
|
|
85
|
+
expect(url).toContain('/teams/team-1');
|
|
86
|
+
expect(init.method).toBe('GET');
|
|
87
|
+
});
|
|
88
|
+
it('should POST /teams/{id} for update()', async () => {
|
|
89
|
+
const team = { id: 'team-1', name: 'Acme Updated' };
|
|
90
|
+
mockJsonResponse(team);
|
|
91
|
+
const result = await api().update('team-1', { name: 'Acme Updated' });
|
|
92
|
+
expect(result).toEqual(team);
|
|
93
|
+
const [url, init] = mockFetch.mock.calls[0];
|
|
94
|
+
expect(url).toContain('/teams/team-1');
|
|
95
|
+
expect(JSON.parse(init.body)).toEqual({ name: 'Acme Updated' });
|
|
96
|
+
});
|
|
97
|
+
it('should DELETE /teams/{id} for delete()', async () => {
|
|
98
|
+
mockJsonResponse(null);
|
|
99
|
+
await api().delete('team-1');
|
|
100
|
+
const [url, init] = mockFetch.mock.calls[0];
|
|
101
|
+
expect(url).toContain('/teams/team-1');
|
|
102
|
+
expect(init.method).toBe('DELETE');
|
|
103
|
+
});
|
|
104
|
+
it('should GET /teams/{id}/members for getMembers()', async () => {
|
|
105
|
+
const members = [{ user_id: 'user-1', role: 'owner' }];
|
|
106
|
+
mockJsonResponse(members);
|
|
107
|
+
const result = await api().getMembers('team-1');
|
|
108
|
+
expect(result).toEqual(members);
|
|
109
|
+
const [url, init] = mockFetch.mock.calls[0];
|
|
110
|
+
expect(url).toContain('/teams/team-1/members');
|
|
111
|
+
expect(init.method).toBe('GET');
|
|
112
|
+
});
|
|
113
|
+
it('should POST /teams/{id}/members for addMember()', async () => {
|
|
114
|
+
const payload = { user_id: 'user-4', role: 'member' };
|
|
115
|
+
const member = { ...payload };
|
|
116
|
+
mockJsonResponse(member);
|
|
117
|
+
const result = await api().addMember('team-1', payload);
|
|
118
|
+
expect(result).toEqual(member);
|
|
119
|
+
const [url, init] = mockFetch.mock.calls[0];
|
|
120
|
+
expect(url).toContain('/teams/team-1/members');
|
|
121
|
+
expect(JSON.parse(init.body)).toEqual(payload);
|
|
122
|
+
});
|
|
123
|
+
it('should GET /teams/{id}/invites for listInvites()', async () => {
|
|
124
|
+
const invites = [{ id: 'inv-1', email: 'dev@example.com' }];
|
|
125
|
+
mockJsonResponse(invites);
|
|
126
|
+
const result = await api().listInvites('team-1');
|
|
127
|
+
expect(result).toEqual(invites);
|
|
128
|
+
const [url, init] = mockFetch.mock.calls[0];
|
|
129
|
+
expect(url).toContain('/teams/team-1/invites');
|
|
130
|
+
expect(init.method).toBe('GET');
|
|
131
|
+
});
|
|
132
|
+
it('should DELETE /teams/{id}/invites/{inviteId} for revokeInvite()', async () => {
|
|
133
|
+
mockJsonResponse(null);
|
|
134
|
+
await api().revokeInvite('team-1', 'inv-9');
|
|
135
|
+
const [url, init] = mockFetch.mock.calls[0];
|
|
136
|
+
expect(url).toContain('/teams/team-1/invites/inv-9');
|
|
137
|
+
expect(init.method).toBe('DELETE');
|
|
138
|
+
});
|
|
139
|
+
});
|
package/dist/client.test.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
|
-
import { Inference, inference } from './index';
|
|
1
|
+
import { Inference, inference, createClient } from './index';
|
|
2
2
|
import { RequirementsNotMetException } from './http/errors';
|
|
3
|
+
import { HttpClient } from './http/client';
|
|
4
|
+
import { ChatStatusBusy, ChatStatusIdle } from './types';
|
|
3
5
|
// Mock fetch globally
|
|
4
6
|
const mockFetch = jest.fn();
|
|
5
7
|
global.fetch = mockFetch;
|
|
@@ -138,6 +140,45 @@ describe('Inference', () => {
|
|
|
138
140
|
expect(exception.errors[0].action?.scopes).toEqual(['calendar.readonly', 'calendar.events']);
|
|
139
141
|
}
|
|
140
142
|
});
|
|
143
|
+
it('should process Blob inputs via processInput before calling /apps/run', async () => {
|
|
144
|
+
const fileRecord = {
|
|
145
|
+
id: 'file-blob',
|
|
146
|
+
uri: 'inf://files/blob',
|
|
147
|
+
upload_url: 'https://upload.example.com/put',
|
|
148
|
+
content_type: 'image/png',
|
|
149
|
+
};
|
|
150
|
+
const mockTask = {
|
|
151
|
+
id: 'task-456',
|
|
152
|
+
status: 9,
|
|
153
|
+
created_at: new Date().toISOString(),
|
|
154
|
+
updated_at: new Date().toISOString(),
|
|
155
|
+
input: {},
|
|
156
|
+
output: { result: 'done' },
|
|
157
|
+
};
|
|
158
|
+
mockFetch
|
|
159
|
+
.mockResolvedValueOnce({
|
|
160
|
+
ok: true,
|
|
161
|
+
status: 200,
|
|
162
|
+
text: () => Promise.resolve(JSON.stringify([fileRecord])),
|
|
163
|
+
json: () => Promise.resolve([fileRecord]),
|
|
164
|
+
})
|
|
165
|
+
.mockResolvedValueOnce({ ok: true, status: 200 })
|
|
166
|
+
.mockResolvedValueOnce({
|
|
167
|
+
ok: true,
|
|
168
|
+
status: 200,
|
|
169
|
+
text: () => Promise.resolve(JSON.stringify(mockTask)),
|
|
170
|
+
json: () => Promise.resolve(mockTask),
|
|
171
|
+
});
|
|
172
|
+
const client = new Inference({ apiKey: 'test-api-key' });
|
|
173
|
+
const blob = new Blob(['png-bytes'], { type: 'image/png' });
|
|
174
|
+
const result = await client.run({ app: 'test-app', input: { image: blob } }, { wait: false });
|
|
175
|
+
expect(result.id).toBe('task-456');
|
|
176
|
+
expect(mockFetch).toHaveBeenCalledTimes(3);
|
|
177
|
+
const runCall = mockFetch.mock.calls.find((call) => String(call[0]).includes('/apps/run'));
|
|
178
|
+
expect(runCall).toBeDefined();
|
|
179
|
+
const runBody = JSON.parse(runCall[1].body);
|
|
180
|
+
expect(runBody.input.image).toBe('inf://files/blob');
|
|
181
|
+
});
|
|
141
182
|
});
|
|
142
183
|
describe('cancel', () => {
|
|
143
184
|
it('should make a POST request to cancel endpoint', async () => {
|
|
@@ -153,6 +194,76 @@ describe('Inference', () => {
|
|
|
153
194
|
expect(mockFetch).toHaveBeenCalledWith(expect.stringContaining('/tasks/task-123/cancel'), expect.objectContaining({ method: 'POST' }));
|
|
154
195
|
});
|
|
155
196
|
});
|
|
197
|
+
describe('legacy task helpers', () => {
|
|
198
|
+
it('should delegate getTask() to tasks.get', async () => {
|
|
199
|
+
const mockTask = { id: 'task-legacy', status: 9 };
|
|
200
|
+
mockFetch.mockResolvedValueOnce({
|
|
201
|
+
ok: true,
|
|
202
|
+
status: 200,
|
|
203
|
+
text: () => Promise.resolve(JSON.stringify(mockTask)),
|
|
204
|
+
json: () => Promise.resolve(mockTask),
|
|
205
|
+
});
|
|
206
|
+
const client = new Inference({ apiKey: 'test-api-key' });
|
|
207
|
+
const result = await client.getTask('task-legacy');
|
|
208
|
+
expect(result.id).toBe('task-legacy');
|
|
209
|
+
expect(mockFetch).toHaveBeenCalledWith(expect.stringContaining('/tasks/task-legacy'), expect.objectContaining({ method: 'GET' }));
|
|
210
|
+
});
|
|
211
|
+
it('should delegate streamTask() to tasks.stream', async () => {
|
|
212
|
+
const client = new Inference({ apiKey: 'test-api-key' });
|
|
213
|
+
const createEventSource = jest
|
|
214
|
+
.spyOn(HttpClient.prototype, 'createEventSource')
|
|
215
|
+
.mockResolvedValue(null);
|
|
216
|
+
await client.streamTask('task-stream');
|
|
217
|
+
expect(createEventSource).toHaveBeenCalledWith('/tasks/task-stream/stream');
|
|
218
|
+
createEventSource.mockRestore();
|
|
219
|
+
});
|
|
220
|
+
});
|
|
221
|
+
describe('agent()', () => {
|
|
222
|
+
it('should return an Agent whose sendMessage hits POST /agents/run', async () => {
|
|
223
|
+
mockFetch
|
|
224
|
+
.mockResolvedValueOnce({
|
|
225
|
+
ok: true,
|
|
226
|
+
status: 200,
|
|
227
|
+
text: () => Promise.resolve(JSON.stringify({
|
|
228
|
+
user_message: { id: 'user-1', chat_id: 'chat-1', role: 'user', content: 'hi' },
|
|
229
|
+
assistant_message: { id: 'asst-1', chat_id: 'chat-1', role: 'assistant', content: 'hello' },
|
|
230
|
+
})),
|
|
231
|
+
})
|
|
232
|
+
.mockResolvedValueOnce({
|
|
233
|
+
ok: true,
|
|
234
|
+
status: 200,
|
|
235
|
+
text: () => Promise.resolve(JSON.stringify({ status: ChatStatusBusy })),
|
|
236
|
+
})
|
|
237
|
+
.mockResolvedValueOnce({
|
|
238
|
+
ok: true,
|
|
239
|
+
status: 200,
|
|
240
|
+
text: () => Promise.resolve(JSON.stringify({ id: 'chat-1', status: ChatStatusBusy, chat_messages: [] })),
|
|
241
|
+
})
|
|
242
|
+
.mockResolvedValueOnce({
|
|
243
|
+
ok: true,
|
|
244
|
+
status: 200,
|
|
245
|
+
text: () => Promise.resolve(JSON.stringify({ status: ChatStatusIdle })),
|
|
246
|
+
})
|
|
247
|
+
.mockResolvedValueOnce({
|
|
248
|
+
ok: true,
|
|
249
|
+
status: 200,
|
|
250
|
+
text: () => Promise.resolve(JSON.stringify({ id: 'chat-1', status: ChatStatusIdle, chat_messages: [] })),
|
|
251
|
+
});
|
|
252
|
+
const client = new Inference({ apiKey: 'test-api-key', stream: false, pollIntervalMs: 20 });
|
|
253
|
+
const agentInstance = client.agent('my-agent');
|
|
254
|
+
await agentInstance.sendMessage('hi', { stream: false });
|
|
255
|
+
const runCall = mockFetch.mock.calls.find(([url]) => String(url).includes('/agents/run'));
|
|
256
|
+
expect(runCall).toBeDefined();
|
|
257
|
+
const body = JSON.parse(runCall[1].body);
|
|
258
|
+
expect(body.agent).toBe('my-agent');
|
|
259
|
+
});
|
|
260
|
+
});
|
|
261
|
+
describe('createClient', () => {
|
|
262
|
+
it('should create an Inference instance with extended HttpClient config', () => {
|
|
263
|
+
const client = createClient({ apiKey: 'extended-key', baseUrl: 'https://api.example.com' });
|
|
264
|
+
expect(client).toBeInstanceOf(Inference);
|
|
265
|
+
});
|
|
266
|
+
});
|
|
156
267
|
describe('lowercase factory', () => {
|
|
157
268
|
it('should export lowercase inference factory', () => {
|
|
158
269
|
expect(typeof inference).toBe('function');
|
package/dist/http/client.d.ts
CHANGED
|
@@ -84,6 +84,11 @@ export declare class HttpClient {
|
|
|
84
84
|
url: string;
|
|
85
85
|
headers: Record<string, string>;
|
|
86
86
|
};
|
|
87
|
+
/**
|
|
88
|
+
* Pull a human-readable detail string from a parsed problem+json / API error
|
|
89
|
+
* body (RFC 9457 `detail`/`title`, or a plain `message`).
|
|
90
|
+
*/
|
|
91
|
+
private extractErrorDetail;
|
|
87
92
|
/**
|
|
88
93
|
* Create an EventSource for SSE streaming
|
|
89
94
|
* @deprecated Use getStreamableConfig() with StreamableManager instead
|
package/dist/http/client.js
CHANGED
|
@@ -128,7 +128,7 @@ export class HttpClient {
|
|
|
128
128
|
}
|
|
129
129
|
let errorDetail;
|
|
130
130
|
if (data && typeof data === 'object') {
|
|
131
|
-
errorDetail =
|
|
131
|
+
errorDetail = this.extractErrorDetail(data) ?? JSON.stringify(data);
|
|
132
132
|
}
|
|
133
133
|
else if (responseText) {
|
|
134
134
|
errorDetail = responseText.slice(0, 500);
|
|
@@ -164,6 +164,16 @@ export class HttpClient {
|
|
|
164
164
|
}
|
|
165
165
|
return { url, headers };
|
|
166
166
|
}
|
|
167
|
+
/**
|
|
168
|
+
* Pull a human-readable detail string from a parsed problem+json / API error
|
|
169
|
+
* body (RFC 9457 `detail`/`title`, or a plain `message`).
|
|
170
|
+
*/
|
|
171
|
+
extractErrorDetail(data) {
|
|
172
|
+
if (!data || typeof data !== 'object')
|
|
173
|
+
return undefined;
|
|
174
|
+
const d = data;
|
|
175
|
+
return (d.detail || d.title || d.message);
|
|
176
|
+
}
|
|
167
177
|
/**
|
|
168
178
|
* Create an EventSource for SSE streaming
|
|
169
179
|
* @deprecated Use getStreamableConfig() with StreamableManager instead
|
|
@@ -183,30 +193,39 @@ export class HttpClient {
|
|
|
183
193
|
fetchUrl = targetUrl.toString();
|
|
184
194
|
}
|
|
185
195
|
const resolvedHeaders = this.resolveHeaders();
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
196
|
+
// The EventSource error event omits the response body, so a failed initial
|
|
197
|
+
// connection (e.g. 403 otp_required) is handled here in the fetch wrapper:
|
|
198
|
+
// route it through onError like request() does, and hand the retried
|
|
199
|
+
// response (e.g. after OTP verification) back so the stream connects.
|
|
200
|
+
const doFetch = async (input, init) => {
|
|
201
|
+
const headers = {
|
|
202
|
+
...init?.headers,
|
|
203
|
+
...resolvedHeaders,
|
|
204
|
+
};
|
|
205
|
+
if (isProxyMode) {
|
|
206
|
+
headers['x-inf-target-url'] = targetUrl.toString();
|
|
207
|
+
}
|
|
208
|
+
else {
|
|
209
|
+
const token = this.getAuthToken();
|
|
210
|
+
if (token) {
|
|
211
|
+
headers['Authorization'] = `Bearer ${token}`;
|
|
195
212
|
}
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
213
|
+
}
|
|
214
|
+
const response = await fetch(input, { ...init, headers, credentials: this.credentials });
|
|
215
|
+
if (!response.ok && this.onError) {
|
|
216
|
+
const body = await response.clone().text().catch(() => '');
|
|
217
|
+
let data;
|
|
218
|
+
try {
|
|
219
|
+
data = JSON.parse(body);
|
|
202
220
|
}
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
}
|
|
221
|
+
catch { /* non-JSON body */ }
|
|
222
|
+
const error = new InferenceError(response.status, this.extractErrorDetail(data) ?? 'Request failed', body);
|
|
223
|
+
const handled = await this.onError(error, () => doFetch(input, init));
|
|
224
|
+
return handled ?? response;
|
|
225
|
+
}
|
|
226
|
+
return response;
|
|
227
|
+
};
|
|
228
|
+
return Promise.resolve(new EventSource(fetchUrl, { fetch: doFetch }));
|
|
210
229
|
}
|
|
211
230
|
}
|
|
212
231
|
/**
|
package/dist/http/client.test.js
CHANGED
|
@@ -114,6 +114,32 @@ describe('HttpClient', () => {
|
|
|
114
114
|
expect(calledUrl).toContain('ids=');
|
|
115
115
|
expect(decodeURIComponent(calledUrl)).toContain('["a","b"]');
|
|
116
116
|
});
|
|
117
|
+
it('should serialize object query params as JSON', async () => {
|
|
118
|
+
mockJsonResponse([]);
|
|
119
|
+
await client().request('get', '/tasks', {
|
|
120
|
+
params: { filter: { status: 'active', team_id: 'team-1' } },
|
|
121
|
+
});
|
|
122
|
+
const calledUrl = mockFetch.mock.calls[0][0];
|
|
123
|
+
expect(calledUrl).toContain('filter=');
|
|
124
|
+
expect(decodeURIComponent(calledUrl)).toContain('"status":"active"');
|
|
125
|
+
expect(decodeURIComponent(calledUrl)).toContain('"team_id":"team-1"');
|
|
126
|
+
});
|
|
127
|
+
it('should omit Authorization when getToken returns null', async () => {
|
|
128
|
+
mockJsonResponse({ id: 'task-1' });
|
|
129
|
+
const tokenClient = new HttpClient({ getToken: () => null });
|
|
130
|
+
await tokenClient.request('get', '/tasks/task-1');
|
|
131
|
+
const [, init] = mockFetch.mock.calls[0];
|
|
132
|
+
const headers = init.headers;
|
|
133
|
+
expect(headers.Authorization).toBeUndefined();
|
|
134
|
+
});
|
|
135
|
+
it('should omit Authorization when getToken returns undefined', async () => {
|
|
136
|
+
mockJsonResponse({ id: 'task-1' });
|
|
137
|
+
const tokenClient = new HttpClient({ getToken: () => undefined });
|
|
138
|
+
await tokenClient.request('get', '/tasks/task-1');
|
|
139
|
+
const [, init] = mockFetch.mock.calls[0];
|
|
140
|
+
const headers = init.headers;
|
|
141
|
+
expect(headers.Authorization).toBeUndefined();
|
|
142
|
+
});
|
|
117
143
|
it('should use top-level message field in HTTP error responses', async () => {
|
|
118
144
|
mockFetch.mockResolvedValueOnce({
|
|
119
145
|
ok: false,
|
|
@@ -125,6 +151,99 @@ describe('HttpClient', () => {
|
|
|
125
151
|
message: expect.stringContaining('service unavailable'),
|
|
126
152
|
});
|
|
127
153
|
});
|
|
154
|
+
it('should use getToken for Authorization on regular requests', async () => {
|
|
155
|
+
mockJsonResponse({ id: 'task-1' });
|
|
156
|
+
const tokenClient = new HttpClient({ getToken: () => 'dynamic-key' });
|
|
157
|
+
await tokenClient.request('get', '/tasks/task-1');
|
|
158
|
+
const [, init] = mockFetch.mock.calls[0];
|
|
159
|
+
const headers = init.headers;
|
|
160
|
+
expect(headers.Authorization).toBe('Bearer dynamic-key');
|
|
161
|
+
});
|
|
162
|
+
it('should resolve function-valued headers on each request', async () => {
|
|
163
|
+
mockJsonResponse({ id: 'task-1' });
|
|
164
|
+
mockJsonResponse({ id: 'task-2' });
|
|
165
|
+
let teamId = 'team-a';
|
|
166
|
+
const client = new HttpClient({
|
|
167
|
+
apiKey: 'test-key',
|
|
168
|
+
headers: {
|
|
169
|
+
'X-Team-ID': () => teamId,
|
|
170
|
+
'X-Request-Id': () => 'req-1',
|
|
171
|
+
},
|
|
172
|
+
});
|
|
173
|
+
await client.request('get', '/tasks/task-1');
|
|
174
|
+
teamId = 'team-b';
|
|
175
|
+
await client.request('get', '/tasks/task-2');
|
|
176
|
+
const firstHeaders = mockFetch.mock.calls[0][1]?.headers;
|
|
177
|
+
const secondHeaders = mockFetch.mock.calls[1][1]?.headers;
|
|
178
|
+
expect(firstHeaders['X-Team-ID']).toBe('team-a');
|
|
179
|
+
expect(secondHeaders['X-Team-ID']).toBe('team-b');
|
|
180
|
+
expect(firstHeaders['X-Request-Id']).toBe('req-1');
|
|
181
|
+
});
|
|
182
|
+
it('should omit headers when a resolver returns undefined', async () => {
|
|
183
|
+
mockJsonResponse({ id: 'task-1' });
|
|
184
|
+
const client = new HttpClient({
|
|
185
|
+
apiKey: 'test-key',
|
|
186
|
+
headers: {
|
|
187
|
+
'X-Team-ID': () => undefined,
|
|
188
|
+
'X-Custom': 'static',
|
|
189
|
+
},
|
|
190
|
+
});
|
|
191
|
+
await client.request('get', '/tasks/task-1');
|
|
192
|
+
const headers = mockFetch.mock.calls[0][1]?.headers;
|
|
193
|
+
expect(headers['X-Team-ID']).toBeUndefined();
|
|
194
|
+
expect(headers['X-Custom']).toBe('static');
|
|
195
|
+
});
|
|
196
|
+
it('should send X-API-Version 2 and X-Client-Source on every request', async () => {
|
|
197
|
+
mockJsonResponse({ id: 'task-1' });
|
|
198
|
+
await client().request('get', '/tasks/task-1');
|
|
199
|
+
const [, init] = mockFetch.mock.calls[0];
|
|
200
|
+
const headers = init.headers;
|
|
201
|
+
expect(headers['X-API-Version']).toBe('2');
|
|
202
|
+
expect(headers['X-Client-Source']).toMatch(/inference-sdk-js\//);
|
|
203
|
+
});
|
|
204
|
+
it('should prefer RFC 9457 detail over title in error responses', async () => {
|
|
205
|
+
mockFetch.mockResolvedValueOnce({
|
|
206
|
+
ok: false,
|
|
207
|
+
status: 422,
|
|
208
|
+
text: () => Promise.resolve(JSON.stringify({
|
|
209
|
+
type: 'about:blank',
|
|
210
|
+
title: 'Validation failed',
|
|
211
|
+
detail: 'app field is required',
|
|
212
|
+
})),
|
|
213
|
+
});
|
|
214
|
+
await expect(client().request('post', '/apps')).rejects.toMatchObject({
|
|
215
|
+
name: 'InferenceError',
|
|
216
|
+
message: expect.stringContaining('app field is required'),
|
|
217
|
+
});
|
|
218
|
+
});
|
|
219
|
+
it('should fall back to RFC 9457 title when detail is absent', async () => {
|
|
220
|
+
mockFetch.mockResolvedValueOnce({
|
|
221
|
+
ok: false,
|
|
222
|
+
status: 403,
|
|
223
|
+
text: () => Promise.resolve(JSON.stringify({ type: 'about:blank', title: 'Forbidden' })),
|
|
224
|
+
});
|
|
225
|
+
await expect(client().request('get', '/tasks/1')).rejects.toMatchObject({
|
|
226
|
+
name: 'InferenceError',
|
|
227
|
+
message: expect.stringContaining('Forbidden'),
|
|
228
|
+
});
|
|
229
|
+
});
|
|
230
|
+
it('should use raw response text when error body is not JSON', async () => {
|
|
231
|
+
mockFetch.mockResolvedValueOnce({
|
|
232
|
+
ok: false,
|
|
233
|
+
status: 502,
|
|
234
|
+
text: () => Promise.resolve('Bad Gateway from upstream'),
|
|
235
|
+
});
|
|
236
|
+
await expect(client().request('get', '/tasks/1')).rejects.toMatchObject({
|
|
237
|
+
name: 'InferenceError',
|
|
238
|
+
message: expect.stringContaining('Bad Gateway from upstream'),
|
|
239
|
+
});
|
|
240
|
+
});
|
|
241
|
+
it('should not unwrap legacy v1 APIResponse envelopes', async () => {
|
|
242
|
+
const v1Envelope = { success: true, data: { id: 'task-legacy' } };
|
|
243
|
+
mockJsonResponse(v1Envelope);
|
|
244
|
+
const result = await client().request('get', '/tasks/legacy');
|
|
245
|
+
expect(result).toEqual(v1Envelope);
|
|
246
|
+
});
|
|
128
247
|
});
|
|
129
248
|
describe('getStreamableConfig', () => {
|
|
130
249
|
it('should include bearer token in direct mode', () => {
|
|
@@ -146,6 +265,14 @@ describe('HttpClient', () => {
|
|
|
146
265
|
}).getStreamableConfig('/tasks/task-1/stream');
|
|
147
266
|
expect(config.headers.Authorization).toBe('Bearer dynamic-token');
|
|
148
267
|
});
|
|
268
|
+
it('should include resolved dynamic headers for streaming requests', () => {
|
|
269
|
+
const config = new HttpClient({
|
|
270
|
+
apiKey: 'secret-key',
|
|
271
|
+
headers: { 'X-Team-ID': () => 'team-stream' },
|
|
272
|
+
}).getStreamableConfig('/tasks/task-1/stream');
|
|
273
|
+
expect(config.headers['X-Team-ID']).toBe('team-stream');
|
|
274
|
+
expect(config.headers.Authorization).toBe('Bearer secret-key');
|
|
275
|
+
});
|
|
149
276
|
});
|
|
150
277
|
describe('createEventSource', () => {
|
|
151
278
|
beforeEach(() => {
|
|
@@ -186,5 +313,123 @@ describe('HttpClient', () => {
|
|
|
186
313
|
}),
|
|
187
314
|
}));
|
|
188
315
|
});
|
|
316
|
+
it('should attach resolved dynamic headers on SSE custom fetch', async () => {
|
|
317
|
+
let capturedFetch;
|
|
318
|
+
MockEventSource.mockImplementation((_url, options) => {
|
|
319
|
+
capturedFetch = options?.fetch;
|
|
320
|
+
return { close: jest.fn(), onmessage: null, onerror: null };
|
|
321
|
+
});
|
|
322
|
+
mockFetch.mockResolvedValueOnce({ ok: true, status: 200 });
|
|
323
|
+
const client = new HttpClient({
|
|
324
|
+
apiKey: 'sse-key',
|
|
325
|
+
headers: { 'X-Team-ID': () => 'team-sse' },
|
|
326
|
+
});
|
|
327
|
+
await client.createEventSource('/tasks/task-1/stream');
|
|
328
|
+
await capturedFetch('https://api.inference.sh/tasks/task-1/stream', { headers: {} });
|
|
329
|
+
expect(mockFetch).toHaveBeenCalledWith('https://api.inference.sh/tasks/task-1/stream', expect.objectContaining({
|
|
330
|
+
headers: expect.objectContaining({
|
|
331
|
+
'X-Team-ID': 'team-sse',
|
|
332
|
+
Authorization: 'Bearer sse-key',
|
|
333
|
+
}),
|
|
334
|
+
}));
|
|
335
|
+
});
|
|
336
|
+
it('should use getToken for Authorization on SSE custom fetch', async () => {
|
|
337
|
+
let capturedFetch;
|
|
338
|
+
MockEventSource.mockImplementation((_url, options) => {
|
|
339
|
+
capturedFetch = options?.fetch;
|
|
340
|
+
return { close: jest.fn(), onmessage: null, onerror: null };
|
|
341
|
+
});
|
|
342
|
+
mockFetch.mockResolvedValueOnce({ ok: true, status: 200 });
|
|
343
|
+
const client = new HttpClient({ getToken: () => 'session-token' });
|
|
344
|
+
await client.createEventSource('/tasks/task-1/stream');
|
|
345
|
+
await capturedFetch('https://api.inference.sh/tasks/task-1/stream', { headers: {} });
|
|
346
|
+
expect(mockFetch).toHaveBeenCalledWith('https://api.inference.sh/tasks/task-1/stream', expect.objectContaining({
|
|
347
|
+
headers: expect.objectContaining({
|
|
348
|
+
Authorization: 'Bearer session-token',
|
|
349
|
+
}),
|
|
350
|
+
}));
|
|
351
|
+
});
|
|
352
|
+
it('should omit Authorization on SSE fetch when getToken returns null', async () => {
|
|
353
|
+
let capturedFetch;
|
|
354
|
+
MockEventSource.mockImplementation((_url, options) => {
|
|
355
|
+
capturedFetch = options?.fetch;
|
|
356
|
+
return { close: jest.fn(), onmessage: null, onerror: null };
|
|
357
|
+
});
|
|
358
|
+
mockFetch.mockResolvedValueOnce({ ok: true, status: 200 });
|
|
359
|
+
const client = new HttpClient({ getToken: () => null });
|
|
360
|
+
await client.createEventSource('/tasks/task-1/stream');
|
|
361
|
+
await capturedFetch('https://api.inference.sh/tasks/task-1/stream', { headers: {} });
|
|
362
|
+
const [, init] = mockFetch.mock.calls[0];
|
|
363
|
+
const headers = init.headers;
|
|
364
|
+
expect(headers.Authorization).toBeUndefined();
|
|
365
|
+
});
|
|
366
|
+
it('should return EventSource without awaiting the initial fetch', async () => {
|
|
367
|
+
MockEventSource.mockImplementation(() => ({ close: jest.fn() }));
|
|
368
|
+
const client = new HttpClient({ apiKey: 'sse-key' });
|
|
369
|
+
const eventSource = await client.createEventSource('/tasks/task-1/stream');
|
|
370
|
+
expect(eventSource).toBeDefined();
|
|
371
|
+
expect(MockEventSource).toHaveBeenCalled();
|
|
372
|
+
expect(mockFetch).not.toHaveBeenCalled();
|
|
373
|
+
});
|
|
374
|
+
function mockFailedResponse(status, body) {
|
|
375
|
+
return {
|
|
376
|
+
ok: false,
|
|
377
|
+
status,
|
|
378
|
+
clone: () => ({
|
|
379
|
+
text: () => Promise.resolve(body),
|
|
380
|
+
}),
|
|
381
|
+
};
|
|
382
|
+
}
|
|
383
|
+
it('should route failed initial SSE response through onError and return retried response', async () => {
|
|
384
|
+
let capturedFetch;
|
|
385
|
+
MockEventSource.mockImplementation((_url, options) => {
|
|
386
|
+
capturedFetch = options?.fetch;
|
|
387
|
+
return { close: jest.fn(), onmessage: null, onerror: null };
|
|
388
|
+
});
|
|
389
|
+
const onError = jest.fn(async (_error, retry) => retry());
|
|
390
|
+
const client = new HttpClient({ apiKey: 'sse-key', onError });
|
|
391
|
+
mockFetch
|
|
392
|
+
.mockResolvedValueOnce(mockFailedResponse(403, JSON.stringify({ detail: 'otp_required' })))
|
|
393
|
+
.mockResolvedValueOnce({ ok: true, status: 200 });
|
|
394
|
+
await client.createEventSource('/tasks/task-1/stream');
|
|
395
|
+
const response = await capturedFetch('https://api.inference.sh/tasks/task-1/stream', {});
|
|
396
|
+
expect(onError).toHaveBeenCalledTimes(1);
|
|
397
|
+
const [error] = onError.mock.calls[0];
|
|
398
|
+
expect(error).toBeInstanceOf(InferenceError);
|
|
399
|
+
expect(error.statusCode).toBe(403);
|
|
400
|
+
expect(error.message).toContain('otp_required');
|
|
401
|
+
expect(response).toEqual({ ok: true, status: 200 });
|
|
402
|
+
expect(mockFetch).toHaveBeenCalledTimes(2);
|
|
403
|
+
});
|
|
404
|
+
it('should return original failed response when onError does not retry', async () => {
|
|
405
|
+
let capturedFetch;
|
|
406
|
+
MockEventSource.mockImplementation((_url, options) => {
|
|
407
|
+
capturedFetch = options?.fetch;
|
|
408
|
+
return { close: jest.fn(), onmessage: null, onerror: null };
|
|
409
|
+
});
|
|
410
|
+
const failed = mockFailedResponse(401, JSON.stringify({ title: 'Unauthorized' }));
|
|
411
|
+
const onError = jest.fn(async () => undefined);
|
|
412
|
+
const client = new HttpClient({ apiKey: 'sse-key', onError });
|
|
413
|
+
mockFetch.mockResolvedValueOnce(failed);
|
|
414
|
+
await client.createEventSource('/tasks/task-1/stream');
|
|
415
|
+
const response = await capturedFetch('https://api.inference.sh/tasks/task-1/stream', {});
|
|
416
|
+
expect(onError).toHaveBeenCalledTimes(1);
|
|
417
|
+
expect(response).toBe(failed);
|
|
418
|
+
expect(mockFetch).toHaveBeenCalledTimes(1);
|
|
419
|
+
});
|
|
420
|
+
it('should return failed response when no onError handler is configured', async () => {
|
|
421
|
+
let capturedFetch;
|
|
422
|
+
MockEventSource.mockImplementation((_url, options) => {
|
|
423
|
+
capturedFetch = options?.fetch;
|
|
424
|
+
return { close: jest.fn(), onmessage: null, onerror: null };
|
|
425
|
+
});
|
|
426
|
+
const failed = mockFailedResponse(503, 'service unavailable');
|
|
427
|
+
const client = new HttpClient({ apiKey: 'sse-key' });
|
|
428
|
+
mockFetch.mockResolvedValueOnce(failed);
|
|
429
|
+
await client.createEventSource('/tasks/task-1/stream');
|
|
430
|
+
const response = await capturedFetch('https://api.inference.sh/tasks/task-1/stream', {});
|
|
431
|
+
expect(response).toBe(failed);
|
|
432
|
+
expect(mockFetch).toHaveBeenCalledTimes(1);
|
|
433
|
+
});
|
|
189
434
|
});
|
|
190
435
|
});
|
package/dist/http/errors.test.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { InferenceError, RequirementsNotMetException, SessionEndedError, SessionExpiredError, SessionNotFoundError, WorkerLostError, isInferenceError, isRequirementsNotMetException, isSessionError, } from './errors';
|
|
2
|
+
import { RequirementTypeIntegration, RequirementTypeScope, RequirementTypeSecret, } from '../types';
|
|
2
3
|
describe('error type guards', () => {
|
|
3
4
|
it('isRequirementsNotMetException should match instances and plain objects', () => {
|
|
4
5
|
const instance = new RequirementsNotMetException([
|
|
@@ -43,6 +44,18 @@ describe('error classes', () => {
|
|
|
43
44
|
expect(err.statusCode).toBe(412);
|
|
44
45
|
expect(err.message).toBe('requirements not met');
|
|
45
46
|
});
|
|
47
|
+
it('RequirementsNotMetException should preserve typed RequirementType values', () => {
|
|
48
|
+
const errors = [
|
|
49
|
+
{ type: RequirementTypeSecret, key: 'API_KEY', message: 'Missing secret' },
|
|
50
|
+
{ type: RequirementTypeIntegration, key: 'google', message: 'Not connected' },
|
|
51
|
+
{ type: RequirementTypeScope, key: 'calendar.readonly', message: 'Missing scope' },
|
|
52
|
+
];
|
|
53
|
+
const err = new RequirementsNotMetException(errors);
|
|
54
|
+
expect(err.errors[0].type).toBe('secret');
|
|
55
|
+
expect(err.errors[1].type).toBe('integration');
|
|
56
|
+
expect(err.errors[2].type).toBe('scope');
|
|
57
|
+
expect(isRequirementsNotMetException(err)).toBe(true);
|
|
58
|
+
});
|
|
46
59
|
it('session error subclasses should expose sessionId and statusCode', () => {
|
|
47
60
|
expect(new SessionNotFoundError('sess-a').sessionId).toBe('sess-a');
|
|
48
61
|
expect(new SessionNotFoundError('sess-a').statusCode).toBe(404);
|