@inferencesh/sdk 0.6.10 → 0.6.12
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 +6 -0
- package/dist/api/agents.d.ts +10 -0
- package/dist/api/agents.js +17 -3
- package/dist/api/agents.test.js +215 -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 +94 -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 +64 -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 +52 -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/tasks.d.ts +13 -1
- package/dist/api/tasks.js +18 -0
- package/dist/api/tasks.test.js +134 -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 +79 -0
- 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/tool-builder.test.js +11 -1
- package/dist/types.d.ts +110 -5
- package/dist/types.js +58 -2
- package/package.json +4 -4
|
@@ -0,0 +1,79 @@
|
|
|
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
|
+
});
|
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);
|
package/dist/index.d.ts
CHANGED
|
@@ -12,6 +12,14 @@ export { ChatsAPI } from './api/chats';
|
|
|
12
12
|
export { FlowsAPI } from './api/flows';
|
|
13
13
|
export { FlowRunsAPI } from './api/flow-runs';
|
|
14
14
|
export { EnginesAPI } from './api/engines';
|
|
15
|
+
export { KnowledgeAPI, SkillsAPI } from './api/knowledge';
|
|
16
|
+
export { TeamsAPI, MeResponse } from './api/teams';
|
|
17
|
+
export { SecretsAPI } from './api/secrets';
|
|
18
|
+
export { ApiKeysAPI } from './api/api-keys';
|
|
19
|
+
export { IntegrationsAPI } from './api/integrations';
|
|
20
|
+
export { SearchAPI } from './api/search';
|
|
21
|
+
export { ProjectsAPI } from './api/projects';
|
|
22
|
+
export { MCPServersAPI } from './api/mcp-servers';
|
|
15
23
|
export { tool, appTool, agentTool, webhookTool, httpTool, callTool, mcpTool, internalTools, string, number, integer, boolean, enumOf, object, array, optional, } from './tool-builder';
|
|
16
24
|
export type { ClientTool, ClientToolHandler } from './tool-builder';
|
|
17
25
|
export { parseStatus, isTerminalStatus } from './utils';
|
|
@@ -27,6 +35,14 @@ import { ChatsAPI } from './api/chats';
|
|
|
27
35
|
import { FlowsAPI } from './api/flows';
|
|
28
36
|
import { FlowRunsAPI } from './api/flow-runs';
|
|
29
37
|
import { EnginesAPI } from './api/engines';
|
|
38
|
+
import { KnowledgeAPI, SkillsAPI } from './api/knowledge';
|
|
39
|
+
import { TeamsAPI } from './api/teams';
|
|
40
|
+
import { SecretsAPI } from './api/secrets';
|
|
41
|
+
import { ApiKeysAPI } from './api/api-keys';
|
|
42
|
+
import { IntegrationsAPI } from './api/integrations';
|
|
43
|
+
import { SearchAPI } from './api/search';
|
|
44
|
+
import { ProjectsAPI } from './api/projects';
|
|
45
|
+
import { MCPServersAPI } from './api/mcp-servers';
|
|
30
46
|
import { ApiAppRunRequest, TaskDTO as Task, AgentConfigInput as AgentConfig, FileDTO as File } from './types';
|
|
31
47
|
export interface InferenceConfig {
|
|
32
48
|
/** Your inference.sh API key (required unless using proxyUrl) */
|
|
@@ -68,6 +84,15 @@ export declare class Inference {
|
|
|
68
84
|
readonly flows: FlowsAPI;
|
|
69
85
|
readonly flowRuns: FlowRunsAPI;
|
|
70
86
|
readonly engines: EnginesAPI;
|
|
87
|
+
readonly knowledge: KnowledgeAPI;
|
|
88
|
+
readonly skills: SkillsAPI;
|
|
89
|
+
readonly teams: TeamsAPI;
|
|
90
|
+
readonly secrets: SecretsAPI;
|
|
91
|
+
readonly apiKeys: ApiKeysAPI;
|
|
92
|
+
readonly integrations: IntegrationsAPI;
|
|
93
|
+
readonly search: SearchAPI;
|
|
94
|
+
readonly projects: ProjectsAPI;
|
|
95
|
+
readonly mcpServers: MCPServersAPI;
|
|
71
96
|
constructor(config: InferenceConfig | HttpClientConfig);
|
|
72
97
|
/** @internal */
|
|
73
98
|
_request<T>(method: 'get' | 'post' | 'put' | 'delete', endpoint: string, options?: {
|
package/dist/index.js
CHANGED
|
@@ -16,6 +16,14 @@ export { ChatsAPI } from './api/chats';
|
|
|
16
16
|
export { FlowsAPI } from './api/flows';
|
|
17
17
|
export { FlowRunsAPI } from './api/flow-runs';
|
|
18
18
|
export { EnginesAPI } from './api/engines';
|
|
19
|
+
export { KnowledgeAPI, SkillsAPI } from './api/knowledge';
|
|
20
|
+
export { TeamsAPI } from './api/teams';
|
|
21
|
+
export { SecretsAPI } from './api/secrets';
|
|
22
|
+
export { ApiKeysAPI } from './api/api-keys';
|
|
23
|
+
export { IntegrationsAPI } from './api/integrations';
|
|
24
|
+
export { SearchAPI } from './api/search';
|
|
25
|
+
export { ProjectsAPI } from './api/projects';
|
|
26
|
+
export { MCPServersAPI } from './api/mcp-servers';
|
|
19
27
|
// Tool Builder (fluent API)
|
|
20
28
|
export { tool, appTool, agentTool, webhookTool, httpTool, callTool, mcpTool, internalTools, string, number, integer, boolean, enumOf, object, array, optional, } from './tool-builder';
|
|
21
29
|
// Status utilities (handle both int and string status values)
|
|
@@ -35,6 +43,14 @@ import { ChatsAPI } from './api/chats';
|
|
|
35
43
|
import { FlowsAPI } from './api/flows';
|
|
36
44
|
import { FlowRunsAPI } from './api/flow-runs';
|
|
37
45
|
import { EnginesAPI } from './api/engines';
|
|
46
|
+
import { KnowledgeAPI, SkillsAPI } from './api/knowledge';
|
|
47
|
+
import { TeamsAPI } from './api/teams';
|
|
48
|
+
import { SecretsAPI } from './api/secrets';
|
|
49
|
+
import { ApiKeysAPI } from './api/api-keys';
|
|
50
|
+
import { IntegrationsAPI } from './api/integrations';
|
|
51
|
+
import { SearchAPI } from './api/search';
|
|
52
|
+
import { ProjectsAPI } from './api/projects';
|
|
53
|
+
import { MCPServersAPI } from './api/mcp-servers';
|
|
38
54
|
/**
|
|
39
55
|
* Inference.sh SDK Client
|
|
40
56
|
*
|
|
@@ -67,6 +83,15 @@ export class Inference {
|
|
|
67
83
|
this.flows = new FlowsAPI(this.http);
|
|
68
84
|
this.flowRuns = new FlowRunsAPI(this.http);
|
|
69
85
|
this.engines = new EnginesAPI(this.http);
|
|
86
|
+
this.knowledge = new KnowledgeAPI(this.http);
|
|
87
|
+
this.skills = new SkillsAPI(this.http);
|
|
88
|
+
this.teams = new TeamsAPI(this.http);
|
|
89
|
+
this.secrets = new SecretsAPI(this.http);
|
|
90
|
+
this.apiKeys = new ApiKeysAPI(this.http);
|
|
91
|
+
this.integrations = new IntegrationsAPI(this.http);
|
|
92
|
+
this.search = new SearchAPI(this.http);
|
|
93
|
+
this.projects = new ProjectsAPI(this.http);
|
|
94
|
+
this.mcpServers = new MCPServersAPI(this.http);
|
|
70
95
|
}
|
|
71
96
|
// Legacy methods for backward compatibility
|
|
72
97
|
/** @internal */
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { tool, appTool, agentTool, webhookTool, httpTool, callTool, mcpTool, internalTools, string, number, integer, boolean, enumOf, object, array, optional, } from './tool-builder';
|
|
2
|
-
import { ToolTypeClient, ToolTypeApp, ToolTypeAgent, ToolTypeHook, ToolTypeHTTP, ToolTypeMCP, IntegrationProviderGoogle, } from './types';
|
|
2
|
+
import { ToolTypeClient, ToolTypeApp, ToolTypeAgent, ToolTypeHook, ToolTypeHTTP, ToolTypeMCP, IntegrationProviderGoogle, IntegrationProviderGoogleSA, } from './types';
|
|
3
3
|
describe('Schema Helpers', () => {
|
|
4
4
|
describe('string', () => {
|
|
5
5
|
it('creates string schema without description', () => {
|
|
@@ -276,6 +276,16 @@ describe('HTTPToolBuilder (httpTool)', () => {
|
|
|
276
276
|
integration_id: 'int-123',
|
|
277
277
|
});
|
|
278
278
|
});
|
|
279
|
+
it('should attach integration auth for google-sa service account provider', () => {
|
|
280
|
+
const t = httpTool('calendar_read', 'https://api.example.com/calendar')
|
|
281
|
+
.auth({ integration: IntegrationProviderGoogleSA, integrationId: 'sa-int-1' })
|
|
282
|
+
.build();
|
|
283
|
+
expect(t.http?.auth).toEqual({
|
|
284
|
+
type: 'integration',
|
|
285
|
+
provider: IntegrationProviderGoogleSA,
|
|
286
|
+
integration_id: 'sa-int-1',
|
|
287
|
+
});
|
|
288
|
+
});
|
|
279
289
|
it('should attach api key auth with default header', () => {
|
|
280
290
|
const t = httpTool('fetch', 'https://api.example.com').auth({ apiKey: 'KEY' }).build();
|
|
281
291
|
expect(t.http?.auth).toEqual({
|