fix(sdk): improve input handling and separate input from options (#2993)

* fix(sdk): improve input handling and separate input from options

* fix(sdk): treat null as no input for consistency with Python SDK
This commit is contained in:
Waleed
2026-01-25 00:50:09 -08:00
committed by GitHub
parent 1952b196a0
commit dc0ed842c4
7 changed files with 314 additions and 100 deletions
+4 -1
View File
@@ -81,4 +81,7 @@ Thumbs.db
# mypy
.mypy_cache/
.dmypy.json
dmypy.json
dmypy.json
# uv
uv.lock
+1 -1
View File
@@ -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"},
]
+36 -32
View File
@@ -13,7 +13,7 @@ import os
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 @@ class SimStudioClient:
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 @@ class SimStudioClient:
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 @@ class SimStudioClient:
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 @@ class SimStudioClient:
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 @@ class SimStudioClient:
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 @@ class SimStudioClient:
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 @@ class SimStudioClient:
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 @@ class SimStudioClient:
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 @@ class SimStudioClient:
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':
+91 -18
View File
@@ -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"]
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
+1 -1
View File
@@ -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": {
+153 -25
View File
@@ -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', () => {
+28 -22
View File
@@ -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<WorkflowExecutionResult | AsyncExecutionResult> {
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<never>((_, reject) => {
setTimeout(() => reject(new Error('TIMEOUT')), timeout)
})
// Build headers - async execution uses X-Execution-Mode header
const headers: Record<string, string> = {
'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<WorkflowExecutionResult> {
// Ensure sync mode by explicitly setting async to false
const syncOptions = { ...options, async: false }
return this.executeWorkflow(workflowId, syncOptions) as Promise<WorkflowExecutionResult>
return this.executeWorkflow(workflowId, input, syncOptions) as Promise<WorkflowExecutionResult>
}
/**
@@ -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<WorkflowExecutionResult | AsyncExecutionResult> {
@@ -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 }