@inferencesh/sdk 0.6.8 → 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.
Files changed (76) hide show
  1. package/CHANGELOG.md +7 -1
  2. package/README.md +70 -1
  3. package/dist/agent/actions.js +3 -3
  4. package/dist/agent/actions.test.js +89 -0
  5. package/dist/agent/api.test.js +86 -8
  6. package/dist/api/agents.d.ts +10 -0
  7. package/dist/api/agents.js +17 -3
  8. package/dist/api/agents.test.js +303 -92
  9. package/dist/api/api-keys.d.ts +24 -0
  10. package/dist/api/api-keys.js +26 -0
  11. package/dist/api/api-keys.test.d.ts +1 -0
  12. package/dist/api/api-keys.test.js +44 -0
  13. package/dist/api/apps.js +1 -1
  14. package/dist/api/apps.test.d.ts +1 -0
  15. package/dist/api/apps.test.js +135 -0
  16. package/dist/api/chats.d.ts +14 -0
  17. package/dist/api/chats.js +18 -0
  18. package/dist/api/chats.test.d.ts +1 -0
  19. package/dist/api/chats.test.js +85 -0
  20. package/dist/api/engines.d.ts +12 -0
  21. package/dist/api/engines.js +18 -0
  22. package/dist/api/engines.test.d.ts +1 -0
  23. package/dist/api/engines.test.js +133 -0
  24. package/dist/api/files.test.js +186 -6
  25. package/dist/api/flow-runs.test.d.ts +1 -0
  26. package/dist/api/flow-runs.test.js +97 -0
  27. package/dist/api/flows.d.ts +4 -0
  28. package/dist/api/flows.js +6 -0
  29. package/dist/api/flows.test.d.ts +1 -0
  30. package/dist/api/flows.test.js +118 -0
  31. package/dist/api/integrations.d.ts +41 -0
  32. package/dist/api/integrations.js +56 -0
  33. package/dist/api/integrations.test.d.ts +1 -0
  34. package/dist/api/integrations.test.js +109 -0
  35. package/dist/api/knowledge.d.ts +112 -0
  36. package/dist/api/knowledge.js +163 -0
  37. package/dist/api/knowledge.test.d.ts +1 -0
  38. package/dist/api/knowledge.test.js +94 -0
  39. package/dist/api/mcp-servers.d.ts +45 -0
  40. package/dist/api/mcp-servers.js +62 -0
  41. package/dist/api/mcp-servers.test.d.ts +1 -0
  42. package/dist/api/mcp-servers.test.js +64 -0
  43. package/dist/api/projects.d.ts +29 -0
  44. package/dist/api/projects.js +38 -0
  45. package/dist/api/projects.test.d.ts +1 -0
  46. package/dist/api/projects.test.js +52 -0
  47. package/dist/api/search.d.ts +21 -0
  48. package/dist/api/search.js +20 -0
  49. package/dist/api/search.test.d.ts +1 -0
  50. package/dist/api/search.test.js +39 -0
  51. package/dist/api/secrets.d.ts +29 -0
  52. package/dist/api/secrets.js +38 -0
  53. package/dist/api/secrets.test.d.ts +1 -0
  54. package/dist/api/secrets.test.js +61 -0
  55. package/dist/api/sessions.test.js +4 -4
  56. package/dist/api/tasks.d.ts +13 -1
  57. package/dist/api/tasks.js +18 -0
  58. package/dist/api/tasks.test.js +174 -22
  59. package/dist/api/teams.d.ts +71 -0
  60. package/dist/api/teams.js +92 -0
  61. package/dist/api/teams.test.d.ts +1 -0
  62. package/dist/api/teams.test.js +79 -0
  63. package/dist/client.test.js +8 -8
  64. package/dist/http/client.d.ts +5 -0
  65. package/dist/http/client.js +46 -48
  66. package/dist/http/client.test.js +296 -13
  67. package/dist/http/errors.test.js +13 -0
  68. package/dist/index.d.ts +25 -0
  69. package/dist/index.js +25 -0
  70. package/dist/proxy/express.test.d.ts +1 -0
  71. package/dist/proxy/express.test.js +106 -0
  72. package/dist/proxy/index.test.js +10 -1
  73. package/dist/tool-builder.test.js +11 -1
  74. package/dist/types.d.ts +793 -23
  75. package/dist/types.js +180 -8
  76. package/package.json +4 -4
@@ -1,7 +1,10 @@
1
1
  import { HttpClient, createHttpClient } from './client';
2
2
  import { InferenceError, RequirementsNotMetException } from './errors';
3
+ import { EventSource } from 'eventsource';
4
+ jest.mock('eventsource');
3
5
  const mockFetch = jest.fn();
4
6
  global.fetch = mockFetch;
7
+ const MockEventSource = EventSource;
5
8
  function mockJsonResponse(body, status = 200, ok = true) {
6
9
  mockFetch.mockResolvedValueOnce({
7
10
  ok,
@@ -38,17 +41,12 @@ describe('HttpClient', () => {
38
41
  describe('request', () => {
39
42
  const client = () => new HttpClient({ apiKey: 'test-key' });
40
43
  it('should return parsed data on success', async () => {
41
- mockJsonResponse({ success: true, data: { id: 'task-1' } });
44
+ mockJsonResponse({ id: 'task-1' });
42
45
  const result = await client().request('get', '/tasks/task-1');
43
46
  expect(result).toEqual({ id: 'task-1' });
44
47
  });
45
- it('should return null when success is true but data field is omitted', async () => {
46
- mockJsonResponse({ success: true });
47
- const result = await client().request('post', '/tasks/task-1/cancel');
48
- expect(result).toBeNull();
49
- });
50
- it('should return null when success is true with explicit data: null', async () => {
51
- mockJsonResponse({ success: true, data: null });
48
+ it('should return null for null response body', async () => {
49
+ mockJsonResponse(null);
52
50
  const result = await client().request('post', '/tasks/task-1/cancel');
53
51
  expect(result).toBeNull();
54
52
  });
@@ -61,8 +59,8 @@ describe('HttpClient', () => {
61
59
  const result = await client().request('delete', '/tasks/task-1');
62
60
  expect(result).toBeUndefined();
63
61
  });
64
- it('should throw InferenceError when success is false', async () => {
65
- mockJsonResponse({ success: false, error: { message: 'Invalid request' } });
62
+ it('should throw InferenceError on non-ok response', async () => {
63
+ mockJsonResponse({ message: 'Invalid request' }, 400, false);
66
64
  const err = await client().request('get', '/tasks/1').catch((e) => e);
67
65
  expect(err).toBeInstanceOf(InferenceError);
68
66
  expect(err.message).toContain('Invalid request');
@@ -91,7 +89,7 @@ describe('HttpClient', () => {
91
89
  .mockResolvedValueOnce({
92
90
  ok: true,
93
91
  status: 200,
94
- text: () => Promise.resolve(JSON.stringify({ success: true, data: { ok: true } })),
92
+ text: () => Promise.resolve(JSON.stringify({ ok: true })),
95
93
  });
96
94
  const result = await httpClient.request('get', '/tasks/1');
97
95
  expect(result).toEqual({ ok: true });
@@ -99,7 +97,7 @@ describe('HttpClient', () => {
99
97
  });
100
98
  it('should route through proxy with x-inf-target-url header', async () => {
101
99
  const proxyClient = new HttpClient({ proxyUrl: 'https://proxy.example.com' });
102
- mockJsonResponse({ success: true, data: { id: '1' } });
100
+ mockJsonResponse({ id: '1' });
103
101
  await proxyClient.request('get', '/tasks/1');
104
102
  expect(mockFetch).toHaveBeenCalledWith('https://proxy.example.com', expect.objectContaining({
105
103
  headers: expect.objectContaining({
@@ -108,7 +106,7 @@ describe('HttpClient', () => {
108
106
  }));
109
107
  });
110
108
  it('should serialize array query params as JSON', async () => {
111
- mockJsonResponse({ success: true, data: [] });
109
+ mockJsonResponse([]);
112
110
  await client().request('get', '/tasks', {
113
111
  params: { ids: ['a', 'b'] },
114
112
  });
@@ -116,6 +114,32 @@ describe('HttpClient', () => {
116
114
  expect(calledUrl).toContain('ids=');
117
115
  expect(decodeURIComponent(calledUrl)).toContain('["a","b"]');
118
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
+ });
119
143
  it('should use top-level message field in HTTP error responses', async () => {
120
144
  mockFetch.mockResolvedValueOnce({
121
145
  ok: false,
@@ -127,6 +151,99 @@ describe('HttpClient', () => {
127
151
  message: expect.stringContaining('service unavailable'),
128
152
  });
129
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
+ });
130
247
  });
131
248
  describe('getStreamableConfig', () => {
132
249
  it('should include bearer token in direct mode', () => {
@@ -148,5 +265,171 @@ describe('HttpClient', () => {
148
265
  }).getStreamableConfig('/tasks/task-1/stream');
149
266
  expect(config.headers.Authorization).toBe('Bearer dynamic-token');
150
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
+ });
276
+ });
277
+ describe('createEventSource', () => {
278
+ beforeEach(() => {
279
+ MockEventSource.mockReset();
280
+ });
281
+ it('should attach Bearer token in direct mode via custom fetch', async () => {
282
+ let capturedFetch;
283
+ MockEventSource.mockImplementation((_url, options) => {
284
+ capturedFetch = options?.fetch;
285
+ return { close: jest.fn(), onmessage: null, onerror: null };
286
+ });
287
+ mockFetch.mockResolvedValueOnce({ ok: true, status: 200 });
288
+ const client = new HttpClient({ apiKey: 'sse-key' });
289
+ await client.createEventSource('/tasks/task-1/stream');
290
+ expect(capturedFetch).toBeDefined();
291
+ await capturedFetch('https://api.inference.sh/tasks/task-1/stream', { headers: {} });
292
+ expect(mockFetch).toHaveBeenCalledWith('https://api.inference.sh/tasks/task-1/stream', expect.objectContaining({
293
+ headers: expect.objectContaining({
294
+ Authorization: 'Bearer sse-key',
295
+ }),
296
+ }));
297
+ });
298
+ it('should route through proxy with target URL header on custom fetch', async () => {
299
+ let capturedFetch;
300
+ MockEventSource.mockImplementation((url, options) => {
301
+ capturedFetch = options?.fetch;
302
+ expect(url).toContain('https://proxy.example.com');
303
+ expect(url).toContain('__inf_target=');
304
+ return { close: jest.fn(), onmessage: null, onerror: null };
305
+ });
306
+ mockFetch.mockResolvedValueOnce({ ok: true, status: 200 });
307
+ const client = new HttpClient({ proxyUrl: 'https://proxy.example.com' });
308
+ await client.createEventSource('/tasks/task-1/stream');
309
+ await capturedFetch('https://proxy.example.com', { headers: {} });
310
+ expect(mockFetch).toHaveBeenCalledWith('https://proxy.example.com', expect.objectContaining({
311
+ headers: expect.objectContaining({
312
+ 'x-inf-target-url': 'https://api.inference.sh/tasks/task-1/stream',
313
+ }),
314
+ }));
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
+ });
151
434
  });
152
435
  });
@@ -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 */
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,106 @@
1
+ import { INF_TARGET_HEADER } from './index';
2
+ import { createHandler } from './express';
3
+ function createMockResponse() {
4
+ const res = {
5
+ statusCode: 200,
6
+ headers: {},
7
+ body: undefined,
8
+ chunks: [],
9
+ status(code) {
10
+ this.statusCode = code;
11
+ return this;
12
+ },
13
+ setHeader(name, value) {
14
+ this.headers[name] = value;
15
+ },
16
+ json(data) {
17
+ this.body = data;
18
+ return this;
19
+ },
20
+ send(data) {
21
+ this.body = data;
22
+ return this;
23
+ },
24
+ write(chunk) {
25
+ this.chunks.push(chunk);
26
+ },
27
+ end() {
28
+ return;
29
+ },
30
+ };
31
+ return res;
32
+ }
33
+ describe('express createHandler', () => {
34
+ const originalFetch = global.fetch;
35
+ beforeEach(() => {
36
+ jest.clearAllMocks();
37
+ process.env.INFERENCE_API_KEY = 'express-test-key';
38
+ });
39
+ afterEach(() => {
40
+ global.fetch = originalFetch;
41
+ });
42
+ it('should return 400 when target URL is missing', async () => {
43
+ const handler = createHandler();
44
+ const req = {
45
+ method: 'POST',
46
+ body: {},
47
+ headers: {},
48
+ query: {},
49
+ };
50
+ const res = createMockResponse();
51
+ await handler(req, res, jest.fn());
52
+ expect(res.statusCode).toBe(400);
53
+ expect(res.body).toEqual({
54
+ error: `Missing ${INF_TARGET_HEADER} header or __inf_target query param`,
55
+ });
56
+ });
57
+ it('should proxy JSON responses through res.json()', async () => {
58
+ global.fetch = jest.fn().mockResolvedValue(new Response(JSON.stringify({ ok: true }), {
59
+ status: 200,
60
+ headers: { 'content-type': 'application/json' },
61
+ }));
62
+ const handler = createHandler();
63
+ const target = 'https://api.inference.sh/v1/run';
64
+ const req = {
65
+ method: 'POST',
66
+ body: { prompt: 'hi' },
67
+ headers: { [INF_TARGET_HEADER]: target },
68
+ query: {},
69
+ };
70
+ const res = createMockResponse();
71
+ await handler(req, res, jest.fn());
72
+ expect(res.statusCode).toBe(200);
73
+ expect(res.body).toEqual({ ok: true });
74
+ expect(global.fetch).toHaveBeenCalledWith(target, expect.objectContaining({
75
+ headers: expect.objectContaining({
76
+ authorization: 'Bearer express-test-key',
77
+ }),
78
+ }));
79
+ });
80
+ it('should stream SSE responses with res.write()', async () => {
81
+ const encoder = new TextEncoder();
82
+ const stream = new ReadableStream({
83
+ start(controller) {
84
+ controller.enqueue(encoder.encode('data: {"x":1}\n\n'));
85
+ controller.close();
86
+ },
87
+ });
88
+ global.fetch = jest.fn().mockResolvedValue(new Response(stream, {
89
+ status: 200,
90
+ headers: { 'content-type': 'text/event-stream' },
91
+ }));
92
+ const handler = createHandler();
93
+ const target = 'https://api.inference.sh/v1/stream';
94
+ const req = {
95
+ method: 'POST',
96
+ body: {},
97
+ headers: { [INF_TARGET_HEADER]: target },
98
+ query: {},
99
+ };
100
+ const res = createMockResponse();
101
+ await handler(req, res, jest.fn());
102
+ expect(res.statusCode).toBe(200);
103
+ expect(res.chunks.length).toBeGreaterThan(0);
104
+ expect(res.body).toBeUndefined();
105
+ });
106
+ });
@@ -1,4 +1,4 @@
1
- import { INF_TARGET_HEADER, INF_TARGET_PARAM, processProxyRequest, } from './index';
1
+ import { INF_TARGET_HEADER, INF_TARGET_PARAM, headersToRecord, processProxyRequest, } from './index';
2
2
  function createTestAdapter(overrides = {}) {
3
3
  const state = { status: 0 };
4
4
  const adapter = {
@@ -26,6 +26,15 @@ function createTestAdapter(overrides = {}) {
26
26
  };
27
27
  return adapter;
28
28
  }
29
+ describe('headersToRecord', () => {
30
+ it('should convert Headers to a plain record', () => {
31
+ const headers = new Headers({ 'x-test': 'value', 'content-type': 'application/json' });
32
+ expect(headersToRecord(headers)).toEqual({
33
+ 'x-test': 'value',
34
+ 'content-type': 'application/json',
35
+ });
36
+ });
37
+ });
29
38
  describe('processProxyRequest', () => {
30
39
  const originalFetch = global.fetch;
31
40
  const originalApiKey = process.env.INFERENCE_API_KEY;
@@ -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({