@inferencesh/sdk 0.6.8 → 0.6.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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
  });
@@ -149,4 +147,44 @@ describe('HttpClient', () => {
149
147
  expect(config.headers.Authorization).toBe('Bearer dynamic-token');
150
148
  });
151
149
  });
150
+ describe('createEventSource', () => {
151
+ beforeEach(() => {
152
+ MockEventSource.mockReset();
153
+ });
154
+ it('should attach Bearer token in direct mode via custom fetch', async () => {
155
+ let capturedFetch;
156
+ MockEventSource.mockImplementation((_url, options) => {
157
+ capturedFetch = options?.fetch;
158
+ return { close: jest.fn(), onmessage: null, onerror: null };
159
+ });
160
+ mockFetch.mockResolvedValueOnce({ ok: true, status: 200 });
161
+ const client = new HttpClient({ apiKey: 'sse-key' });
162
+ await client.createEventSource('/tasks/task-1/stream');
163
+ expect(capturedFetch).toBeDefined();
164
+ await capturedFetch('https://api.inference.sh/tasks/task-1/stream', { headers: {} });
165
+ expect(mockFetch).toHaveBeenCalledWith('https://api.inference.sh/tasks/task-1/stream', expect.objectContaining({
166
+ headers: expect.objectContaining({
167
+ Authorization: 'Bearer sse-key',
168
+ }),
169
+ }));
170
+ });
171
+ it('should route through proxy with target URL header on custom fetch', async () => {
172
+ let capturedFetch;
173
+ MockEventSource.mockImplementation((url, options) => {
174
+ capturedFetch = options?.fetch;
175
+ expect(url).toContain('https://proxy.example.com');
176
+ expect(url).toContain('__inf_target=');
177
+ return { close: jest.fn(), onmessage: null, onerror: null };
178
+ });
179
+ mockFetch.mockResolvedValueOnce({ ok: true, status: 200 });
180
+ const client = new HttpClient({ proxyUrl: 'https://proxy.example.com' });
181
+ await client.createEventSource('/tasks/task-1/stream');
182
+ await capturedFetch('https://proxy.example.com', { headers: {} });
183
+ expect(mockFetch).toHaveBeenCalledWith('https://proxy.example.com', expect.objectContaining({
184
+ headers: expect.objectContaining({
185
+ 'x-inf-target-url': 'https://api.inference.sh/tasks/task-1/stream',
186
+ }),
187
+ }));
188
+ });
189
+ });
152
190
  });
@@ -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;