@inferencesh/sdk 0.1.0 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -7,7 +7,20 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
- ## [0.1.0] - 2024-01-XX
10
+ ## [0.1.1] - 2024-11-30
11
+
12
+ ### Added
13
+
14
+ - Partial data handling for streaming updates (matches Python SDK behavior)
15
+ - `onPartialUpdate` callback option to receive list of changed fields
16
+ - Export `StreamManager` and `PartialDataWrapper` types
17
+
18
+ ### Fixed
19
+
20
+ - Stream updates now properly extract data from server's partial update wrapper
21
+ - Removed unused `onYield` callback
22
+
23
+ ## [0.1.0] - 2024-11-30
11
24
 
12
25
  ### Added
13
26
 
package/README.md CHANGED
@@ -17,6 +17,10 @@ yarn add @inferencesh/sdk
17
17
  pnpm add @inferencesh/sdk
18
18
  ```
19
19
 
20
+ ## Getting an API Key
21
+
22
+ Get your API key from the [inference.sh dashboard](https://app.inference.sh/settings/keys).
23
+
20
24
  ## Quick Start
21
25
 
22
26
  ```typescript
package/dist/client.d.ts CHANGED
@@ -14,6 +14,8 @@ export interface InferenceConfig {
14
14
  export interface RunOptions {
15
15
  /** Callback for real-time status updates */
16
16
  onUpdate?: (update: Task) => void;
17
+ /** Callback for partial updates with list of changed fields */
18
+ onPartialUpdate?: (update: Task, fields: string[]) => void;
17
19
  /** Wait for task completion (default: true) */
18
20
  wait?: boolean;
19
21
  /** Auto-reconnect on connection loss (default: true) */
package/dist/client.js CHANGED
@@ -128,7 +128,7 @@ class Inference {
128
128
  * ```
129
129
  */
130
130
  async run(params, options = {}) {
131
- const { onUpdate, wait = true, autoReconnect = true, maxReconnects = 5, reconnectDelayMs = 1000, } = options;
131
+ const { onUpdate, onPartialUpdate, wait = true, autoReconnect = true, maxReconnects = 5, reconnectDelayMs = 1000, } = options;
132
132
  // Process input data and upload any files
133
133
  const processedInput = await this.processInputData(params.input);
134
134
  const task = await this.request("post", "/run", {
@@ -165,6 +165,13 @@ class Inference {
165
165
  reject(new Error("task cancelled"));
166
166
  }
167
167
  },
168
+ onPartialData: (data, fields) => {
169
+ // Call onPartialUpdate if provided
170
+ if (onPartialUpdate) {
171
+ const stripped = this._stripTask(data);
172
+ onPartialUpdate(stripped, fields);
173
+ }
174
+ },
168
175
  onError: (error) => {
169
176
  reject(error);
170
177
  streamManager.stop();
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,121 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const client_1 = require("./client");
4
+ // Mock fetch globally
5
+ const mockFetch = jest.fn();
6
+ global.fetch = mockFetch;
7
+ describe('Inference', () => {
8
+ beforeEach(() => {
9
+ jest.clearAllMocks();
10
+ });
11
+ describe('constructor', () => {
12
+ it('should create an instance with valid config', () => {
13
+ const client = new client_1.Inference({ apiKey: 'test-api-key' });
14
+ expect(client).toBeInstanceOf(client_1.Inference);
15
+ });
16
+ it('should throw error when apiKey is missing', () => {
17
+ expect(() => new client_1.Inference({ apiKey: '' })).toThrow('API key is required');
18
+ expect(() => new client_1.Inference({})).toThrow('API key is required');
19
+ });
20
+ it('should use default baseUrl when not provided', () => {
21
+ const client = new client_1.Inference({ apiKey: 'test-api-key' });
22
+ expect(client).toBeDefined();
23
+ });
24
+ it('should accept custom baseUrl', () => {
25
+ const client = new client_1.Inference({
26
+ apiKey: 'test-api-key',
27
+ baseUrl: 'https://custom-api.example.com',
28
+ });
29
+ expect(client).toBeDefined();
30
+ });
31
+ });
32
+ describe('run', () => {
33
+ it('should make a POST request to /run', async () => {
34
+ const mockTask = {
35
+ id: 'task-123',
36
+ status: 9, // TaskStatusCompleted
37
+ created_at: new Date().toISOString(),
38
+ updated_at: new Date().toISOString(),
39
+ input: { message: 'hello world' },
40
+ output: { result: 'success' },
41
+ };
42
+ mockFetch.mockResolvedValueOnce({
43
+ json: () => Promise.resolve({ success: true, data: mockTask }),
44
+ });
45
+ const client = new client_1.Inference({ apiKey: 'test-api-key' });
46
+ // Use input that won't trigger base64 detection (contains spaces/special chars)
47
+ const result = await client.run({ app: 'test-app', input: { message: 'hello world!' } }, { wait: false });
48
+ expect(result.id).toBe('task-123');
49
+ expect(mockFetch).toHaveBeenCalledWith(expect.stringContaining('/run'), expect.objectContaining({
50
+ method: 'POST',
51
+ headers: expect.objectContaining({
52
+ Authorization: 'Bearer test-api-key',
53
+ 'Content-Type': 'application/json',
54
+ }),
55
+ }));
56
+ });
57
+ it('should throw error on API failure', async () => {
58
+ mockFetch.mockResolvedValueOnce({
59
+ json: () => Promise.resolve({ success: false, error: { message: 'Invalid app' } }),
60
+ });
61
+ const client = new client_1.Inference({ apiKey: 'test-api-key' });
62
+ await expect(client.run({ app: 'invalid-app', input: { message: 'test!' } }, { wait: false })).rejects.toThrow('Invalid app');
63
+ });
64
+ });
65
+ describe('cancel', () => {
66
+ it('should make a POST request to cancel endpoint', async () => {
67
+ mockFetch.mockResolvedValueOnce({
68
+ json: () => Promise.resolve({ success: true, data: null }),
69
+ });
70
+ const client = new client_1.Inference({ apiKey: 'test-api-key' });
71
+ await client.cancel('task-123');
72
+ expect(mockFetch).toHaveBeenCalledWith(expect.stringContaining('/tasks/task-123/cancel'), expect.objectContaining({ method: 'POST' }));
73
+ });
74
+ });
75
+ describe('backward compatibility', () => {
76
+ it('should export lowercase inference as alias', () => {
77
+ expect(client_1.inference).toBe(client_1.Inference);
78
+ });
79
+ it('should work with lowercase inference', () => {
80
+ const client = new client_1.inference({ apiKey: 'test-api-key' });
81
+ expect(client).toBeInstanceOf(client_1.Inference);
82
+ });
83
+ });
84
+ });
85
+ describe('uploadFile', () => {
86
+ beforeEach(() => {
87
+ jest.clearAllMocks();
88
+ });
89
+ it('should upload a base64 string', async () => {
90
+ const mockFile = {
91
+ id: 'file-123',
92
+ uri: 'https://example.com/file.png',
93
+ upload_url: 'https://upload.example.com/signed-url',
94
+ };
95
+ mockFetch
96
+ .mockResolvedValueOnce({
97
+ json: () => Promise.resolve({ success: true, data: [mockFile] }),
98
+ })
99
+ .mockResolvedValueOnce({ ok: true });
100
+ const client = new client_1.Inference({ apiKey: 'test-api-key' });
101
+ // Use valid base64 that won't be mistaken for regular text
102
+ const result = await client.uploadFile('SGVsbG8gV29ybGQh', {
103
+ filename: 'test.txt',
104
+ contentType: 'text/plain',
105
+ });
106
+ expect(result.uri).toBe('https://example.com/file.png');
107
+ expect(mockFetch).toHaveBeenCalledTimes(2);
108
+ });
109
+ it('should throw error when no upload URL provided', async () => {
110
+ const mockFile = {
111
+ id: 'file-123',
112
+ uri: 'https://example.com/file.png',
113
+ // Missing upload_url
114
+ };
115
+ mockFetch.mockResolvedValueOnce({
116
+ json: () => Promise.resolve({ success: true, data: [mockFile] }),
117
+ });
118
+ const client = new client_1.Inference({ apiKey: 'test-api-key' });
119
+ await expect(client.uploadFile('SGVsbG8gV29ybGQh', { filename: 'test.txt' })).rejects.toThrow('No upload URL provided by the server');
120
+ });
121
+ });
package/dist/index.d.ts CHANGED
@@ -1,3 +1,4 @@
1
1
  export { Inference, inference, InferenceConfig, RunOptions, UploadFileOptions } from './client';
2
+ export { StreamManager, PartialDataWrapper } from './stream';
2
3
  export * from './types';
3
4
  export type { TaskDTO as Task } from './types';
package/dist/index.js CHANGED
@@ -14,10 +14,13 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
14
14
  for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15
15
  };
16
16
  Object.defineProperty(exports, "__esModule", { value: true });
17
- exports.inference = exports.Inference = void 0;
17
+ exports.StreamManager = exports.inference = exports.Inference = void 0;
18
18
  // Main client export
19
19
  var client_1 = require("./client");
20
20
  Object.defineProperty(exports, "Inference", { enumerable: true, get: function () { return client_1.Inference; } });
21
21
  Object.defineProperty(exports, "inference", { enumerable: true, get: function () { return client_1.inference; } });
22
+ // Stream utilities
23
+ var stream_1 = require("./stream");
24
+ Object.defineProperty(exports, "StreamManager", { enumerable: true, get: function () { return stream_1.StreamManager; } });
22
25
  // Types - includes TaskStatus constants and all DTOs
23
26
  __exportStar(require("./types"), exports);
package/dist/stream.d.ts CHANGED
@@ -1,3 +1,8 @@
1
+ /** Partial data structure from server (contains data and list of updated fields) */
2
+ export interface PartialDataWrapper<T> {
3
+ data: T;
4
+ fields: string[];
5
+ }
1
6
  export interface StreamManagerOptions<T> {
2
7
  createEventSource: () => Promise<EventSource | null>;
3
8
  autoReconnect?: boolean;
@@ -6,8 +11,10 @@ export interface StreamManagerOptions<T> {
6
11
  onError?: (error: Error) => void;
7
12
  onStart?: () => void;
8
13
  onStop?: () => void;
14
+ /** Called with the extracted data (handles both full and partial data) */
9
15
  onData?: (data: T) => void;
10
- onYield?: (data: T) => void;
16
+ /** Called specifically for partial updates with data and the list of updated fields */
17
+ onPartialData?: (data: T, fields: string[]) => void;
11
18
  }
12
19
  export declare class StreamManager<T> {
13
20
  private options;
package/dist/stream.js CHANGED
@@ -1,6 +1,17 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.StreamManager = void 0;
4
+ /**
5
+ * Check if the parsed data is a partial data wrapper from the server.
6
+ * The server sends partial updates in the format: { data: T, fields: string[] }
7
+ */
8
+ function isPartialDataWrapper(parsed) {
9
+ return (typeof parsed === 'object' &&
10
+ parsed !== null &&
11
+ 'data' in parsed &&
12
+ 'fields' in parsed &&
13
+ Array.isArray(parsed.fields));
14
+ }
4
15
  class StreamManager {
5
16
  constructor(options) {
6
17
  this.eventSource = null;
@@ -87,8 +98,22 @@ class StreamManager {
87
98
  return;
88
99
  try {
89
100
  const parsed = JSON.parse(e.data);
90
- this.options.onData?.(parsed);
91
- this.options.onYield?.(parsed);
101
+ // Check if this is a partial data wrapper from the server
102
+ if (isPartialDataWrapper(parsed)) {
103
+ // Extract the actual data from the wrapper
104
+ const actualData = parsed.data;
105
+ const fields = parsed.fields;
106
+ // Call onPartialData if provided
107
+ if (this.options.onPartialData) {
108
+ this.options.onPartialData(actualData, fields);
109
+ }
110
+ // Always call onData with the extracted data
111
+ this.options.onData?.(actualData);
112
+ }
113
+ else {
114
+ // Not a partial wrapper, treat as full data
115
+ this.options.onData?.(parsed);
116
+ }
92
117
  }
93
118
  catch (err) {
94
119
  const error = err instanceof Error ? err : new Error("Invalid JSON");
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,168 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const stream_1 = require("./stream");
4
+ describe('StreamManager', () => {
5
+ let mockEventSource;
6
+ beforeEach(() => {
7
+ mockEventSource = {
8
+ onmessage: null,
9
+ onerror: null,
10
+ close: jest.fn(),
11
+ };
12
+ });
13
+ describe('constructor', () => {
14
+ it('should create instance with default options', () => {
15
+ const manager = new stream_1.StreamManager({
16
+ createEventSource: async () => mockEventSource,
17
+ });
18
+ expect(manager).toBeDefined();
19
+ });
20
+ });
21
+ describe('connect', () => {
22
+ it('should call createEventSource', async () => {
23
+ const createEventSource = jest
24
+ .fn()
25
+ .mockResolvedValue(mockEventSource);
26
+ const manager = new stream_1.StreamManager({ createEventSource });
27
+ await manager.connect();
28
+ expect(createEventSource).toHaveBeenCalled();
29
+ });
30
+ it('should call onStart when connected', async () => {
31
+ const onStart = jest.fn();
32
+ const manager = new stream_1.StreamManager({
33
+ createEventSource: async () => mockEventSource,
34
+ onStart,
35
+ });
36
+ await manager.connect();
37
+ expect(onStart).toHaveBeenCalled();
38
+ });
39
+ it('should parse JSON messages and call onData', async () => {
40
+ const onData = jest.fn();
41
+ const manager = new stream_1.StreamManager({
42
+ createEventSource: async () => mockEventSource,
43
+ onData,
44
+ });
45
+ await manager.connect();
46
+ // Simulate receiving a message
47
+ const testData = { status: 'running', id: 'task-123' };
48
+ mockEventSource.onmessage?.({ data: JSON.stringify(testData) });
49
+ expect(onData).toHaveBeenCalledWith(testData);
50
+ });
51
+ it('should extract data from partial data wrapper and call onData', async () => {
52
+ const onData = jest.fn();
53
+ const manager = new stream_1.StreamManager({
54
+ createEventSource: async () => mockEventSource,
55
+ onData,
56
+ });
57
+ await manager.connect();
58
+ // Simulate receiving a partial data wrapper from server
59
+ const innerData = { status: 7, id: 'task-123', logs: ['log1'] };
60
+ const partialWrapper = { data: innerData, fields: ['status', 'logs'] };
61
+ mockEventSource.onmessage?.({ data: JSON.stringify(partialWrapper) });
62
+ // onData should receive the extracted inner data, not the wrapper
63
+ expect(onData).toHaveBeenCalledWith(innerData);
64
+ });
65
+ it('should call onPartialData with data and fields for partial updates', async () => {
66
+ const onData = jest.fn();
67
+ const onPartialData = jest.fn();
68
+ const manager = new stream_1.StreamManager({
69
+ createEventSource: async () => mockEventSource,
70
+ onData,
71
+ onPartialData,
72
+ });
73
+ await manager.connect();
74
+ // Simulate receiving a partial data wrapper
75
+ const innerData = { status: 7, id: 'task-123' };
76
+ const fields = ['status'];
77
+ const partialWrapper = { data: innerData, fields };
78
+ mockEventSource.onmessage?.({ data: JSON.stringify(partialWrapper) });
79
+ expect(onPartialData).toHaveBeenCalledWith(innerData, fields);
80
+ expect(onData).toHaveBeenCalledWith(innerData);
81
+ });
82
+ it('should not call onPartialData for non-partial data', async () => {
83
+ const onData = jest.fn();
84
+ const onPartialData = jest.fn();
85
+ const manager = new stream_1.StreamManager({
86
+ createEventSource: async () => mockEventSource,
87
+ onData,
88
+ onPartialData,
89
+ });
90
+ await manager.connect();
91
+ // Regular data without partial wrapper
92
+ const regularData = { status: 9, id: 'task-123' };
93
+ mockEventSource.onmessage?.({ data: JSON.stringify(regularData) });
94
+ expect(onPartialData).not.toHaveBeenCalled();
95
+ expect(onData).toHaveBeenCalledWith(regularData);
96
+ });
97
+ it('should call onError for invalid JSON', async () => {
98
+ const onError = jest.fn();
99
+ const manager = new stream_1.StreamManager({
100
+ createEventSource: async () => mockEventSource,
101
+ onError,
102
+ });
103
+ await manager.connect();
104
+ mockEventSource.onmessage?.({ data: 'invalid json' });
105
+ expect(onError).toHaveBeenCalled();
106
+ });
107
+ });
108
+ describe('stop', () => {
109
+ it('should close the event source', async () => {
110
+ const onStop = jest.fn();
111
+ const manager = new stream_1.StreamManager({
112
+ createEventSource: async () => mockEventSource,
113
+ onStop,
114
+ });
115
+ await manager.connect();
116
+ manager.stop();
117
+ expect(mockEventSource.close).toHaveBeenCalled();
118
+ expect(onStop).toHaveBeenCalled();
119
+ });
120
+ it('should not process messages after stop', async () => {
121
+ const onData = jest.fn();
122
+ const manager = new stream_1.StreamManager({
123
+ createEventSource: async () => mockEventSource,
124
+ onData,
125
+ });
126
+ await manager.connect();
127
+ manager.stop();
128
+ mockEventSource.onmessage?.({ data: '{"status":"running"}' });
129
+ expect(onData).not.toHaveBeenCalled();
130
+ });
131
+ });
132
+ describe('reconnection', () => {
133
+ it('should attempt reconnection on error when autoReconnect is true', async () => {
134
+ jest.useFakeTimers();
135
+ const createEventSource = jest
136
+ .fn()
137
+ .mockResolvedValue(mockEventSource);
138
+ const manager = new stream_1.StreamManager({
139
+ createEventSource,
140
+ autoReconnect: true,
141
+ reconnectDelayMs: 100,
142
+ });
143
+ await manager.connect();
144
+ expect(createEventSource).toHaveBeenCalledTimes(1);
145
+ // Simulate error
146
+ mockEventSource.onerror?.({});
147
+ // Fast-forward past reconnect delay
148
+ jest.advanceTimersByTime(100);
149
+ await Promise.resolve(); // Flush promises
150
+ expect(createEventSource).toHaveBeenCalledTimes(2);
151
+ jest.useRealTimers();
152
+ });
153
+ it('should not reconnect when autoReconnect is false', async () => {
154
+ const createEventSource = jest
155
+ .fn()
156
+ .mockResolvedValue(mockEventSource);
157
+ const manager = new stream_1.StreamManager({
158
+ createEventSource,
159
+ autoReconnect: false,
160
+ });
161
+ await manager.connect();
162
+ mockEventSource.onerror?.({});
163
+ // Wait a bit
164
+ await new Promise((r) => setTimeout(r, 50));
165
+ expect(createEventSource).toHaveBeenCalledTimes(1);
166
+ });
167
+ });
168
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@inferencesh/sdk",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "Official JavaScript/TypeScript SDK for inference.sh - Run AI models with a simple API",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.mjs",