diff --git a/packages/python-sdk/.gitignore b/packages/python-sdk/.gitignore index db9927b259..ec1f692f4e 100644 --- a/packages/python-sdk/.gitignore +++ b/packages/python-sdk/.gitignore @@ -81,4 +81,7 @@ Thumbs.db # mypy .mypy_cache/ .dmypy.json -dmypy.json \ No newline at end of file +dmypy.json + +# uv +uv.lock \ No newline at end of file diff --git a/packages/python-sdk/pyproject.toml b/packages/python-sdk/pyproject.toml index 850a6b5e76..6812cc1d8a 100644 --- a/packages/python-sdk/pyproject.toml +++ b/packages/python-sdk/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "simstudio-sdk" -version = "0.1.1" +version = "0.1.2" authors = [ {name = "Sim", email = "help@sim.ai"}, ] diff --git a/packages/python-sdk/simstudio/__init__.py b/packages/python-sdk/simstudio/__init__.py index d20b7d7e9c..ec242338ec 100644 --- a/packages/python-sdk/simstudio/__init__.py +++ b/packages/python-sdk/simstudio/__init__.py @@ -13,7 +13,7 @@ import requests -__version__ = "0.1.0" +__version__ = "0.1.2" __all__ = [ "SimStudioClient", "SimStudioError", @@ -64,15 +64,6 @@ class RateLimitInfo: retry_after: Optional[int] = None -@dataclass -class RateLimitStatus: - """Rate limit status for sync/async requests.""" - is_limited: bool - limit: int - remaining: int - reset_at: str - - @dataclass class UsageLimits: """Usage limits and quota information.""" @@ -115,7 +106,6 @@ def _convert_files_to_base64(self, value: Any) -> Any: Recursively processes nested dicts and lists. """ import base64 - import io # Check if this is a file-like object if hasattr(value, 'read') and callable(value.read): @@ -159,7 +149,8 @@ def _convert_files_to_base64(self, value: Any) -> Any: def execute_workflow( self, workflow_id: str, - input_data: Optional[Dict[str, Any]] = None, + input: Optional[Any] = None, + *, timeout: float = 30.0, stream: Optional[bool] = None, selected_outputs: Optional[list] = None, @@ -169,11 +160,13 @@ def execute_workflow( Execute a workflow with optional input data. If async_execution is True, returns immediately with a task ID. - File objects in input_data will be automatically detected and converted to base64. + File objects in input will be automatically detected and converted to base64. Args: workflow_id: The ID of the workflow to execute - input_data: Input data to pass to the workflow (can include file-like objects) + input: Input data to pass to the workflow. Can be a dict (spread at root level), + primitive value (string, number, bool), or list (wrapped in 'input' field). + File-like objects within dicts are automatically converted to base64. timeout: Timeout in seconds (default: 30.0) stream: Enable streaming responses (default: None) selected_outputs: Block outputs to stream (e.g., ["agent1.content"]) @@ -193,8 +186,15 @@ def execute_workflow( headers['X-Execution-Mode'] = 'async' try: - # Build JSON body - spread input at root level, then add API control parameters - body = input_data.copy() if input_data is not None else {} + # Build JSON body - spread dict inputs at root level, wrap primitives/lists in 'input' field + body = {} + if input is not None: + if isinstance(input, dict): + # Dict input: spread at root level (matches curl/API behavior) + body = input.copy() + else: + # Primitive or list input: wrap in 'input' field + body = {'input': input} # Convert any file objects in the input to base64 format body = self._convert_files_to_base64(body) @@ -320,20 +320,18 @@ def validate_workflow(self, workflow_id: str) -> bool: def execute_workflow_sync( self, workflow_id: str, - input_data: Optional[Dict[str, Any]] = None, + input: Optional[Any] = None, + *, timeout: float = 30.0, stream: Optional[bool] = None, selected_outputs: Optional[list] = None ) -> WorkflowExecutionResult: """ - Execute a workflow and poll for completion (useful for long-running workflows). - - Note: Currently, the API is synchronous, so this method just calls execute_workflow. - In the future, if async execution is added, this method can be enhanced. + Execute a workflow synchronously (ensures non-async mode). Args: workflow_id: The ID of the workflow to execute - input_data: Input data to pass to the workflow (can include file-like objects) + input: Input data to pass to the workflow (can include file-like objects) timeout: Timeout for the initial request in seconds stream: Enable streaming responses (default: None) selected_outputs: Block outputs to stream (e.g., ["agent1.content"]) @@ -344,9 +342,14 @@ def execute_workflow_sync( Raises: SimStudioError: If the workflow execution fails """ - # For now, the API is synchronous, so we just execute directly - # In the future, if async execution is added, this method can be enhanced - return self.execute_workflow(workflow_id, input_data, timeout, stream, selected_outputs) + return self.execute_workflow( + workflow_id, + input, + timeout=timeout, + stream=stream, + selected_outputs=selected_outputs, + async_execution=False + ) def set_api_key(self, api_key: str) -> None: """ @@ -410,7 +413,8 @@ def get_job_status(self, task_id: str) -> Dict[str, Any]: def execute_with_retry( self, workflow_id: str, - input_data: Optional[Dict[str, Any]] = None, + input: Optional[Any] = None, + *, timeout: float = 30.0, stream: Optional[bool] = None, selected_outputs: Optional[list] = None, @@ -425,7 +429,7 @@ def execute_with_retry( Args: workflow_id: The ID of the workflow to execute - input_data: Input data to pass to the workflow (can include file-like objects) + input: Input data to pass to the workflow (can include file-like objects) timeout: Timeout in seconds stream: Enable streaming responses selected_outputs: Block outputs to stream @@ -448,11 +452,11 @@ def execute_with_retry( try: return self.execute_workflow( workflow_id, - input_data, - timeout, - stream, - selected_outputs, - async_execution + input, + timeout=timeout, + stream=stream, + selected_outputs=selected_outputs, + async_execution=async_execution ) except SimStudioError as e: if e.code != 'RATE_LIMIT_EXCEEDED': diff --git a/packages/python-sdk/tests/test_client.py b/packages/python-sdk/tests/test_client.py index faa3176a92..8dfdee99b6 100644 --- a/packages/python-sdk/tests/test_client.py +++ b/packages/python-sdk/tests/test_client.py @@ -91,11 +91,9 @@ def test_context_manager(mock_close): """Test SimStudioClient as context manager.""" with SimStudioClient(api_key="test-api-key") as client: assert client.api_key == "test-api-key" - # Should close without error mock_close.assert_called_once() -# Tests for async execution @patch('simstudio.requests.Session.post') def test_async_execution_returns_task_id(mock_post): """Test async execution returns AsyncExecutionResult.""" @@ -115,7 +113,7 @@ def test_async_execution_returns_task_id(mock_post): client = SimStudioClient(api_key="test-api-key") result = client.execute_workflow( "workflow-id", - input_data={"message": "Hello"}, + {"message": "Hello"}, async_execution=True ) @@ -124,7 +122,6 @@ def test_async_execution_returns_task_id(mock_post): assert result.status == "queued" assert result.links["status"] == "/api/jobs/task-123" - # Verify X-Execution-Mode header was set call_args = mock_post.call_args assert call_args[1]["headers"]["X-Execution-Mode"] == "async" @@ -146,7 +143,7 @@ def test_sync_execution_returns_result(mock_post): client = SimStudioClient(api_key="test-api-key") result = client.execute_workflow( "workflow-id", - input_data={"message": "Hello"}, + {"message": "Hello"}, async_execution=False ) @@ -166,13 +163,12 @@ def test_async_header_not_set_when_false(mock_post): mock_post.return_value = mock_response client = SimStudioClient(api_key="test-api-key") - client.execute_workflow("workflow-id", input_data={"message": "Hello"}) + client.execute_workflow("workflow-id", {"message": "Hello"}) call_args = mock_post.call_args assert "X-Execution-Mode" not in call_args[1]["headers"] -# Tests for job status @patch('simstudio.requests.Session.get') def test_get_job_status_success(mock_get): """Test getting job status.""" @@ -222,7 +218,6 @@ def test_get_job_status_not_found(mock_get): assert "Job not found" in str(exc_info.value) -# Tests for retry with rate limiting @patch('simstudio.requests.Session.post') @patch('simstudio.time.sleep') def test_execute_with_retry_success_first_attempt(mock_sleep, mock_post): @@ -238,7 +233,7 @@ def test_execute_with_retry_success_first_attempt(mock_sleep, mock_post): mock_post.return_value = mock_response client = SimStudioClient(api_key="test-api-key") - result = client.execute_with_retry("workflow-id", input_data={"message": "test"}) + result = client.execute_with_retry("workflow-id", {"message": "test"}) assert result.success is True assert mock_post.call_count == 1 @@ -278,7 +273,7 @@ def test_execute_with_retry_retries_on_rate_limit(mock_sleep, mock_post): client = SimStudioClient(api_key="test-api-key") result = client.execute_with_retry( "workflow-id", - input_data={"message": "test"}, + {"message": "test"}, max_retries=3, initial_delay=0.01 ) @@ -307,7 +302,7 @@ def test_execute_with_retry_max_retries_exceeded(mock_sleep, mock_post): with pytest.raises(SimStudioError) as exc_info: client.execute_with_retry( "workflow-id", - input_data={"message": "test"}, + {"message": "test"}, max_retries=2, initial_delay=0.01 ) @@ -333,13 +328,12 @@ def test_execute_with_retry_no_retry_on_other_errors(mock_post): client = SimStudioClient(api_key="test-api-key") with pytest.raises(SimStudioError) as exc_info: - client.execute_with_retry("workflow-id", input_data={"message": "test"}) + client.execute_with_retry("workflow-id", {"message": "test"}) assert "Server error" in str(exc_info.value) assert mock_post.call_count == 1 # No retries -# Tests for rate limit info def test_get_rate_limit_info_returns_none_initially(): """Test rate limit info is None before any API calls.""" client = SimStudioClient(api_key="test-api-key") @@ -362,7 +356,7 @@ def test_get_rate_limit_info_after_api_call(mock_post): mock_post.return_value = mock_response client = SimStudioClient(api_key="test-api-key") - client.execute_workflow("workflow-id", input_data={}) + client.execute_workflow("workflow-id", {}) info = client.get_rate_limit_info() assert info is not None @@ -371,7 +365,6 @@ def test_get_rate_limit_info_after_api_call(mock_post): assert info.reset == 1704067200 -# Tests for usage limits @patch('simstudio.requests.Session.get') def test_get_usage_limits_success(mock_get): """Test getting usage limits.""" @@ -435,7 +428,6 @@ def test_get_usage_limits_unauthorized(mock_get): assert "Invalid API key" in str(exc_info.value) -# Tests for streaming with selectedOutputs @patch('simstudio.requests.Session.post') def test_execute_workflow_with_stream_and_selected_outputs(mock_post): """Test execution with stream and selectedOutputs parameters.""" @@ -449,7 +441,7 @@ def test_execute_workflow_with_stream_and_selected_outputs(mock_post): client = SimStudioClient(api_key="test-api-key") client.execute_workflow( "workflow-id", - input_data={"message": "test"}, + {"message": "test"}, stream=True, selected_outputs=["agent1.content", "agent2.content"] ) @@ -459,4 +451,85 @@ def test_execute_workflow_with_stream_and_selected_outputs(mock_post): assert request_body["message"] == "test" assert request_body["stream"] is True - assert request_body["selectedOutputs"] == ["agent1.content", "agent2.content"] \ No newline at end of file + assert request_body["selectedOutputs"] == ["agent1.content", "agent2.content"] + + +# Tests for primitive and list inputs +@patch('simstudio.requests.Session.post') +def test_execute_workflow_with_string_input(mock_post): + """Test execution with primitive string input wraps in input field.""" + mock_response = Mock() + mock_response.ok = True + mock_response.status_code = 200 + mock_response.json.return_value = {"success": True, "output": {}} + mock_response.headers.get.return_value = None + mock_post.return_value = mock_response + + client = SimStudioClient(api_key="test-api-key") + client.execute_workflow("workflow-id", "NVDA") + + call_args = mock_post.call_args + request_body = call_args[1]["json"] + + assert request_body["input"] == "NVDA" + assert "0" not in request_body # Should not spread string characters + + +@patch('simstudio.requests.Session.post') +def test_execute_workflow_with_number_input(mock_post): + """Test execution with primitive number input wraps in input field.""" + mock_response = Mock() + mock_response.ok = True + mock_response.status_code = 200 + mock_response.json.return_value = {"success": True, "output": {}} + mock_response.headers.get.return_value = None + mock_post.return_value = mock_response + + client = SimStudioClient(api_key="test-api-key") + client.execute_workflow("workflow-id", 42) + + call_args = mock_post.call_args + request_body = call_args[1]["json"] + + assert request_body["input"] == 42 + + +@patch('simstudio.requests.Session.post') +def test_execute_workflow_with_list_input(mock_post): + """Test execution with list input wraps in input field.""" + mock_response = Mock() + mock_response.ok = True + mock_response.status_code = 200 + mock_response.json.return_value = {"success": True, "output": {}} + mock_response.headers.get.return_value = None + mock_post.return_value = mock_response + + client = SimStudioClient(api_key="test-api-key") + client.execute_workflow("workflow-id", ["NVDA", "AAPL", "GOOG"]) + + call_args = mock_post.call_args + request_body = call_args[1]["json"] + + assert request_body["input"] == ["NVDA", "AAPL", "GOOG"] + assert "0" not in request_body # Should not spread list + + +@patch('simstudio.requests.Session.post') +def test_execute_workflow_with_dict_input_spreads_at_root(mock_post): + """Test execution with dict input spreads at root level.""" + mock_response = Mock() + mock_response.ok = True + mock_response.status_code = 200 + mock_response.json.return_value = {"success": True, "output": {}} + mock_response.headers.get.return_value = None + mock_post.return_value = mock_response + + client = SimStudioClient(api_key="test-api-key") + client.execute_workflow("workflow-id", {"ticker": "NVDA", "quantity": 100}) + + call_args = mock_post.call_args + request_body = call_args[1]["json"] + + assert request_body["ticker"] == "NVDA" + assert request_body["quantity"] == 100 + assert "input" not in request_body # Should not wrap in input field \ No newline at end of file diff --git a/packages/ts-sdk/package.json b/packages/ts-sdk/package.json index f40d69516f..649a8b76e9 100644 --- a/packages/ts-sdk/package.json +++ b/packages/ts-sdk/package.json @@ -1,6 +1,6 @@ { "name": "simstudio-ts-sdk", - "version": "0.1.1", + "version": "0.1.2", "description": "Sim SDK - Execute workflows programmatically", "type": "module", "exports": { diff --git a/packages/ts-sdk/src/index.test.ts b/packages/ts-sdk/src/index.test.ts index e8ebbddadb..063a31b323 100644 --- a/packages/ts-sdk/src/index.test.ts +++ b/packages/ts-sdk/src/index.test.ts @@ -119,10 +119,11 @@ describe('SimStudioClient', () => { } vi.mocked(fetch.default).mockResolvedValue(mockResponse as any) - const result = await client.executeWorkflow('workflow-id', { - input: { message: 'Hello' }, - async: true, - }) + const result = await client.executeWorkflow( + 'workflow-id', + { message: 'Hello' }, + { async: true } + ) expect(result).toHaveProperty('taskId', 'task-123') expect(result).toHaveProperty('status', 'queued') @@ -152,10 +153,11 @@ describe('SimStudioClient', () => { } vi.mocked(fetch.default).mockResolvedValue(mockResponse as any) - const result = await client.executeWorkflow('workflow-id', { - input: { message: 'Hello' }, - async: false, - }) + const result = await client.executeWorkflow( + 'workflow-id', + { message: 'Hello' }, + { async: false } + ) expect(result).toHaveProperty('success', true) expect(result).toHaveProperty('output') @@ -177,9 +179,7 @@ describe('SimStudioClient', () => { } vi.mocked(fetch.default).mockResolvedValue(mockResponse as any) - await client.executeWorkflow('workflow-id', { - input: { message: 'Hello' }, - }) + await client.executeWorkflow('workflow-id', { message: 'Hello' }) const calls = vi.mocked(fetch.default).mock.calls expect(calls[0][1]?.headers).not.toHaveProperty('X-Execution-Mode') @@ -256,9 +256,7 @@ describe('SimStudioClient', () => { } vi.mocked(fetch.default).mockResolvedValue(mockResponse as any) - const result = await client.executeWithRetry('workflow-id', { - input: { message: 'test' }, - }) + const result = await client.executeWithRetry('workflow-id', { message: 'test' }) expect(result).toHaveProperty('success', true) expect(vi.mocked(fetch.default)).toHaveBeenCalledTimes(1) @@ -305,7 +303,8 @@ describe('SimStudioClient', () => { const result = await client.executeWithRetry( 'workflow-id', - { input: { message: 'test' } }, + { message: 'test' }, + {}, { maxRetries: 3, initialDelay: 10 } ) @@ -336,7 +335,8 @@ describe('SimStudioClient', () => { await expect( client.executeWithRetry( 'workflow-id', - { input: { message: 'test' } }, + { message: 'test' }, + {}, { maxRetries: 2, initialDelay: 10 } ) ).rejects.toThrow('Rate limit exceeded') @@ -361,9 +361,9 @@ describe('SimStudioClient', () => { vi.mocked(fetch.default).mockResolvedValue(mockResponse as any) - await expect( - client.executeWithRetry('workflow-id', { input: { message: 'test' } }) - ).rejects.toThrow('Server error') + await expect(client.executeWithRetry('workflow-id', { message: 'test' })).rejects.toThrow( + 'Server error' + ) expect(vi.mocked(fetch.default)).toHaveBeenCalledTimes(1) // No retries }) @@ -393,7 +393,7 @@ describe('SimStudioClient', () => { vi.mocked(fetch.default).mockResolvedValue(mockResponse as any) - await client.executeWorkflow('workflow-id', { input: {} }) + await client.executeWorkflow('workflow-id', {}) const info = client.getRateLimitInfo() expect(info).not.toBeNull() @@ -490,11 +490,11 @@ describe('SimStudioClient', () => { vi.mocked(fetch.default).mockResolvedValue(mockResponse as any) - await client.executeWorkflow('workflow-id', { - input: { message: 'test' }, - stream: true, - selectedOutputs: ['agent1.content', 'agent2.content'], - }) + await client.executeWorkflow( + 'workflow-id', + { message: 'test' }, + { stream: true, selectedOutputs: ['agent1.content', 'agent2.content'] } + ) const calls = vi.mocked(fetch.default).mock.calls const requestBody = JSON.parse(calls[0][1]?.body as string) @@ -505,6 +505,134 @@ describe('SimStudioClient', () => { expect(requestBody.selectedOutputs).toEqual(['agent1.content', 'agent2.content']) }) }) + + describe('executeWorkflow - primitive and array inputs', () => { + it('should wrap primitive string input in input field', async () => { + const fetch = await import('node-fetch') + const mockResponse = { + ok: true, + status: 200, + json: vi.fn().mockResolvedValue({ + success: true, + output: {}, + }), + headers: { + get: vi.fn().mockReturnValue(null), + }, + } + + vi.mocked(fetch.default).mockResolvedValue(mockResponse as any) + + await client.executeWorkflow('workflow-id', 'NVDA') + + const calls = vi.mocked(fetch.default).mock.calls + const requestBody = JSON.parse(calls[0][1]?.body as string) + + expect(requestBody).toHaveProperty('input', 'NVDA') + expect(requestBody).not.toHaveProperty('0') // Should not spread string characters + }) + + it('should wrap primitive number input in input field', async () => { + const fetch = await import('node-fetch') + const mockResponse = { + ok: true, + status: 200, + json: vi.fn().mockResolvedValue({ + success: true, + output: {}, + }), + headers: { + get: vi.fn().mockReturnValue(null), + }, + } + + vi.mocked(fetch.default).mockResolvedValue(mockResponse as any) + + await client.executeWorkflow('workflow-id', 42) + + const calls = vi.mocked(fetch.default).mock.calls + const requestBody = JSON.parse(calls[0][1]?.body as string) + + expect(requestBody).toHaveProperty('input', 42) + }) + + it('should wrap array input in input field', async () => { + const fetch = await import('node-fetch') + const mockResponse = { + ok: true, + status: 200, + json: vi.fn().mockResolvedValue({ + success: true, + output: {}, + }), + headers: { + get: vi.fn().mockReturnValue(null), + }, + } + + vi.mocked(fetch.default).mockResolvedValue(mockResponse as any) + + await client.executeWorkflow('workflow-id', ['NVDA', 'AAPL', 'GOOG']) + + const calls = vi.mocked(fetch.default).mock.calls + const requestBody = JSON.parse(calls[0][1]?.body as string) + + expect(requestBody).toHaveProperty('input') + expect(requestBody.input).toEqual(['NVDA', 'AAPL', 'GOOG']) + expect(requestBody).not.toHaveProperty('0') // Should not spread array + }) + + it('should spread object input at root level', async () => { + const fetch = await import('node-fetch') + const mockResponse = { + ok: true, + status: 200, + json: vi.fn().mockResolvedValue({ + success: true, + output: {}, + }), + headers: { + get: vi.fn().mockReturnValue(null), + }, + } + + vi.mocked(fetch.default).mockResolvedValue(mockResponse as any) + + await client.executeWorkflow('workflow-id', { ticker: 'NVDA', quantity: 100 }) + + const calls = vi.mocked(fetch.default).mock.calls + const requestBody = JSON.parse(calls[0][1]?.body as string) + + expect(requestBody).toHaveProperty('ticker', 'NVDA') + expect(requestBody).toHaveProperty('quantity', 100) + expect(requestBody).not.toHaveProperty('input') // Should not wrap in input field + }) + + it('should handle null input as no input (empty body)', async () => { + const fetch = await import('node-fetch') + const mockResponse = { + ok: true, + status: 200, + json: vi.fn().mockResolvedValue({ + success: true, + output: {}, + }), + headers: { + get: vi.fn().mockReturnValue(null), + }, + } + + vi.mocked(fetch.default).mockResolvedValue(mockResponse as any) + + await client.executeWorkflow('workflow-id', null) + + const calls = vi.mocked(fetch.default).mock.calls + const requestBody = JSON.parse(calls[0][1]?.body as string) + + // null treated as "no input" - sends empty body (consistent with Python SDK) + expect(requestBody).toEqual({}) + }) + }) }) describe('SimStudioError', () => { diff --git a/packages/ts-sdk/src/index.ts b/packages/ts-sdk/src/index.ts index 2f4c555d84..e6e7657863 100644 --- a/packages/ts-sdk/src/index.ts +++ b/packages/ts-sdk/src/index.ts @@ -26,7 +26,6 @@ export interface WorkflowStatus { } export interface ExecutionOptions { - input?: any timeout?: number stream?: boolean selectedOutputs?: string[] @@ -117,10 +116,6 @@ export class SimStudioClient { this.baseUrl = normalizeBaseUrl(config.baseUrl || 'https://sim.ai') } - /** - * Execute a workflow with optional input data - * If async is true, returns immediately with a task ID - */ /** * Convert File objects in input to API format (base64) * Recursively processes nested objects and arrays @@ -170,20 +165,25 @@ export class SimStudioClient { return value } + /** + * Execute a workflow with optional input data + * @param workflowId - The ID of the workflow to execute + * @param input - Input data to pass to the workflow (object, primitive, or array) + * @param options - Execution options (timeout, stream, async, etc.) + */ async executeWorkflow( workflowId: string, + input?: any, options: ExecutionOptions = {} ): Promise { const url = `${this.baseUrl}/api/workflows/${workflowId}/execute` - const { input, timeout = 30000, stream, selectedOutputs, async } = options + const { timeout = 30000, stream, selectedOutputs, async } = options try { - // Create a timeout promise const timeoutPromise = new Promise((_, reject) => { setTimeout(() => reject(new Error('TIMEOUT')), timeout) }) - // Build headers - async execution uses X-Execution-Mode header const headers: Record = { 'Content-Type': 'application/json', 'X-API-Key': this.apiKey, @@ -192,10 +192,15 @@ export class SimStudioClient { headers['X-Execution-Mode'] = 'async' } - // Build JSON body - spread input at root level, then add API control parameters - let jsonBody: any = input !== undefined ? { ...input } : {} + let jsonBody: any = {} + if (input !== undefined && input !== null) { + if (typeof input === 'object' && input !== null && !Array.isArray(input)) { + jsonBody = { ...input } + } else { + jsonBody = { input } + } + } - // Convert any File objects in the input to base64 format jsonBody = await this.convertFilesToBase64(jsonBody) if (stream !== undefined) { @@ -213,10 +218,8 @@ export class SimStudioClient { const response = await Promise.race([fetchPromise, timeoutPromise]) - // Extract rate limit headers this.updateRateLimitInfo(response) - // Handle rate limiting with retry if (response.status === 429) { const retryAfter = this.rateLimitInfo?.retryAfter || 1000 throw new SimStudioError( @@ -285,15 +288,18 @@ export class SimStudioClient { } /** - * Execute a workflow and poll for completion (useful for long-running workflows) + * Execute a workflow synchronously (ensures non-async mode) + * @param workflowId - The ID of the workflow to execute + * @param input - Input data to pass to the workflow + * @param options - Execution options (timeout, stream, etc.) */ async executeWorkflowSync( workflowId: string, + input?: any, options: ExecutionOptions = {} ): Promise { - // Ensure sync mode by explicitly setting async to false const syncOptions = { ...options, async: false } - return this.executeWorkflow(workflowId, syncOptions) as Promise + return this.executeWorkflow(workflowId, input, syncOptions) as Promise } /** @@ -361,9 +367,14 @@ export class SimStudioClient { /** * Execute workflow with automatic retry on rate limit + * @param workflowId - The ID of the workflow to execute + * @param input - Input data to pass to the workflow + * @param options - Execution options (timeout, stream, async, etc.) + * @param retryOptions - Retry configuration (maxRetries, delays, etc.) */ async executeWithRetry( workflowId: string, + input?: any, options: ExecutionOptions = {}, retryOptions: RetryOptions = {} ): Promise { @@ -379,7 +390,7 @@ export class SimStudioClient { for (let attempt = 0; attempt <= maxRetries; attempt++) { try { - return await this.executeWorkflow(workflowId, options) + return await this.executeWorkflow(workflowId, input, options) } catch (error: any) { if (!(error instanceof SimStudioError) || error.code !== 'RATE_LIMIT_EXCEEDED') { throw error @@ -387,23 +398,19 @@ export class SimStudioClient { lastError = error - // Don't retry after last attempt if (attempt === maxRetries) { break } - // Use retry-after if provided, otherwise use exponential backoff const waitTime = error.status === 429 && this.rateLimitInfo?.retryAfter ? this.rateLimitInfo.retryAfter : Math.min(delay, maxDelay) - // Add jitter (±25%) const jitter = waitTime * (0.75 + Math.random() * 0.5) await new Promise((resolve) => setTimeout(resolve, jitter)) - // Exponential backoff for next attempt delay *= backoffMultiplier } } @@ -475,5 +482,4 @@ export class SimStudioClient { } } -// Export types and classes export { SimStudioClient as default }