chore: import upstream snapshot with attribution
Publish CLI Package / publish-npm (push) Waiting to run
Publish Python SDK / publish-pypi (push) Waiting to run
Publish TypeScript SDK / publish-npm (push) Waiting to run
CI / Migrate Dev DB (push) Has been skipped
CI / Detect Version (push) Has been cancelled
CI / Migrate DB (push) Has been cancelled
CI / Build Dev ECR (./docker/app.Dockerfile, ECR_APP) (push) Has been cancelled
CI / Build Dev ECR (./docker/db.Dockerfile, ECR_MIGRATIONS) (push) Has been cancelled
CI / Build Dev ECR (./docker/pii.Dockerfile, ECR_PII) (push) Has been cancelled
CI / Build Dev ECR (./docker/realtime.Dockerfile, ECR_REALTIME) (push) Has been cancelled
CI / Deploy Trigger.dev (Dev) (push) Has been cancelled
CI / Build AMD64 (./docker/app.Dockerfile, ECR_APP, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build AMD64 (./docker/db.Dockerfile, ECR_MIGRATIONS, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build AMD64 (./docker/pii.Dockerfile, ECR_PII, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build AMD64 (./docker/realtime.Dockerfile, ECR_REALTIME, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/app.Dockerfile, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/db.Dockerfile, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/pii.Dockerfile, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/realtime.Dockerfile, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Check Docs Changes (push) Has been cancelled
CI / Process Docs (push) Has been cancelled
CI / Create GitHub Release (push) Has been cancelled
CI / Test and Build (push) Has been cancelled
Publish CLI Package / publish-npm (push) Waiting to run
Publish Python SDK / publish-pypi (push) Waiting to run
Publish TypeScript SDK / publish-npm (push) Waiting to run
CI / Migrate Dev DB (push) Has been skipped
CI / Detect Version (push) Has been cancelled
CI / Migrate DB (push) Has been cancelled
CI / Build Dev ECR (./docker/app.Dockerfile, ECR_APP) (push) Has been cancelled
CI / Build Dev ECR (./docker/db.Dockerfile, ECR_MIGRATIONS) (push) Has been cancelled
CI / Build Dev ECR (./docker/pii.Dockerfile, ECR_PII) (push) Has been cancelled
CI / Build Dev ECR (./docker/realtime.Dockerfile, ECR_REALTIME) (push) Has been cancelled
CI / Deploy Trigger.dev (Dev) (push) Has been cancelled
CI / Build AMD64 (./docker/app.Dockerfile, ECR_APP, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build AMD64 (./docker/db.Dockerfile, ECR_MIGRATIONS, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build AMD64 (./docker/pii.Dockerfile, ECR_PII, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build AMD64 (./docker/realtime.Dockerfile, ECR_REALTIME, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/app.Dockerfile, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/db.Dockerfile, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/pii.Dockerfile, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/realtime.Dockerfile, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Check Docs Changes (push) Has been cancelled
CI / Process Docs (push) Has been cancelled
CI / Create GitHub Release (push) Has been cancelled
CI / Test and Build (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,43 @@
|
||||
# Dependencies
|
||||
node_modules/
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
|
||||
# Build output
|
||||
dist/
|
||||
build/
|
||||
|
||||
# Coverage reports
|
||||
coverage/
|
||||
*.lcov
|
||||
|
||||
# Cache directories
|
||||
.turbo/
|
||||
.cache/
|
||||
.parcel-cache/
|
||||
|
||||
# Environment variables
|
||||
.env
|
||||
.env.local
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
|
||||
# IDE
|
||||
.vscode/
|
||||
.idea/
|
||||
*.swp
|
||||
*.swo
|
||||
|
||||
# OS
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# TypeScript
|
||||
*.tsbuildinfo
|
||||
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
@@ -0,0 +1,536 @@
|
||||
# Sim TypeScript SDK
|
||||
|
||||
The official TypeScript/JavaScript SDK for [Sim](https://sim.ai), allowing you to execute workflows programmatically from your applications.
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
npm install simstudio-ts-sdk
|
||||
# or
|
||||
yarn add simstudio-ts-sdk
|
||||
# or
|
||||
bun add simstudio-ts-sdk
|
||||
```
|
||||
|
||||
## Quick Start
|
||||
|
||||
```typescript
|
||||
import { SimStudioClient } from 'simstudio-ts-sdk';
|
||||
|
||||
// Initialize the client
|
||||
const client = new SimStudioClient({
|
||||
apiKey: 'your-api-key-here',
|
||||
baseUrl: 'https://sim.ai' // optional, defaults to https://sim.ai
|
||||
});
|
||||
|
||||
// Execute a workflow
|
||||
try {
|
||||
const result = await client.executeWorkflow('workflow-id');
|
||||
console.log('Workflow executed successfully:', result);
|
||||
} catch (error) {
|
||||
console.error('Workflow execution failed:', error);
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### SimStudioClient
|
||||
|
||||
#### Constructor
|
||||
|
||||
```typescript
|
||||
new SimStudioClient(config: SimStudioConfig)
|
||||
```
|
||||
|
||||
- `config.apiKey` (string): Your Sim API key
|
||||
- `config.baseUrl` (string, optional): Base URL for the Sim API (defaults to `https://sim.ai`)
|
||||
|
||||
#### Methods
|
||||
|
||||
##### executeWorkflow(workflowId, input?, options?)
|
||||
|
||||
Execute a workflow with optional input data.
|
||||
|
||||
```typescript
|
||||
// With object input (spread at root level of request body)
|
||||
const result = await client.executeWorkflow('workflow-id', {
|
||||
message: 'Hello, world!'
|
||||
});
|
||||
|
||||
// With primitive input (wrapped as { input: value })
|
||||
const result = await client.executeWorkflow('workflow-id', 'NVDA');
|
||||
|
||||
// With options
|
||||
const result = await client.executeWorkflow('workflow-id', { message: 'Hello' }, {
|
||||
timeout: 60000
|
||||
});
|
||||
```
|
||||
|
||||
**Parameters:**
|
||||
- `workflowId` (string): The ID of the workflow to execute
|
||||
- `input` (any, optional): Input data to pass to the workflow. Objects are spread at the root level, primitives/arrays are wrapped in `{ input: value }`. File objects are automatically converted to base64.
|
||||
- `options` (ExecutionOptions, optional):
|
||||
- `timeout` (number): Timeout in milliseconds (default: 30000)
|
||||
- `stream` (boolean): Enable streaming responses
|
||||
- `selectedOutputs` (string[]): Block outputs to stream (e.g., `["agent1.content"]`)
|
||||
- `async` (boolean): Execute asynchronously and return execution ID
|
||||
|
||||
**Returns:** `Promise<WorkflowExecutionResult | AsyncExecutionResult>`
|
||||
|
||||
##### getWorkflowStatus(workflowId)
|
||||
|
||||
Get the status of a workflow (deployment status, etc.).
|
||||
|
||||
```typescript
|
||||
const status = await client.getWorkflowStatus('workflow-id');
|
||||
console.log('Is deployed:', status.isDeployed);
|
||||
```
|
||||
|
||||
**Parameters:**
|
||||
- `workflowId` (string): The ID of the workflow
|
||||
|
||||
**Returns:** `Promise<WorkflowStatus>`
|
||||
|
||||
##### validateWorkflow(workflowId)
|
||||
|
||||
Validate that a workflow is ready for execution.
|
||||
|
||||
```typescript
|
||||
const isReady = await client.validateWorkflow('workflow-id');
|
||||
if (isReady) {
|
||||
// Workflow is deployed and ready
|
||||
}
|
||||
```
|
||||
|
||||
**Parameters:**
|
||||
- `workflowId` (string): The ID of the workflow
|
||||
|
||||
**Returns:** `Promise<boolean>`
|
||||
|
||||
##### executeWorkflowSync(workflowId, input?, options?)
|
||||
|
||||
Execute a workflow and poll for completion (useful for long-running workflows).
|
||||
|
||||
```typescript
|
||||
const result = await client.executeWorkflowSync('workflow-id', { data: 'some input' }, {
|
||||
timeout: 60000
|
||||
});
|
||||
```
|
||||
|
||||
**Parameters:**
|
||||
- `workflowId` (string): The ID of the workflow to execute
|
||||
- `input` (any, optional): Input data to pass to the workflow
|
||||
- `options` (ExecutionOptions, optional):
|
||||
- `timeout` (number): Timeout for the initial request in milliseconds
|
||||
|
||||
**Returns:** `Promise<WorkflowExecutionResult>`
|
||||
|
||||
##### getJobStatus(jobId)
|
||||
|
||||
Get the status of an async job.
|
||||
|
||||
```typescript
|
||||
const status = await client.getJobStatus('job-id-from-async-execution');
|
||||
console.log('Job status:', status);
|
||||
```
|
||||
|
||||
**Parameters:**
|
||||
- `jobId` (string): The job ID returned from async execution
|
||||
|
||||
**Returns:** `Promise<any>`
|
||||
|
||||
##### executeWithRetry(workflowId, input?, options?, retryOptions?)
|
||||
|
||||
Execute a workflow with automatic retry on rate limit errors.
|
||||
|
||||
```typescript
|
||||
const result = await client.executeWithRetry('workflow-id', { message: 'Hello' }, {
|
||||
timeout: 30000
|
||||
}, {
|
||||
maxRetries: 3,
|
||||
initialDelay: 1000,
|
||||
maxDelay: 30000,
|
||||
backoffMultiplier: 2
|
||||
});
|
||||
```
|
||||
|
||||
**Parameters:**
|
||||
- `workflowId` (string): The ID of the workflow to execute
|
||||
- `input` (any, optional): Input data to pass to the workflow
|
||||
- `options` (ExecutionOptions, optional): Execution options
|
||||
- `retryOptions` (RetryOptions, optional):
|
||||
- `maxRetries` (number): Maximum retry attempts (default: 3)
|
||||
- `initialDelay` (number): Initial delay in ms (default: 1000)
|
||||
- `maxDelay` (number): Maximum delay in ms (default: 30000)
|
||||
- `backoffMultiplier` (number): Backoff multiplier (default: 2)
|
||||
|
||||
**Returns:** `Promise<WorkflowExecutionResult | AsyncExecutionResult>`
|
||||
|
||||
##### getRateLimitInfo()
|
||||
|
||||
Get current rate limit information from the last API response.
|
||||
|
||||
```typescript
|
||||
const rateInfo = client.getRateLimitInfo();
|
||||
if (rateInfo) {
|
||||
console.log('Remaining requests:', rateInfo.remaining);
|
||||
}
|
||||
```
|
||||
|
||||
**Returns:** `RateLimitInfo | null`
|
||||
|
||||
##### getUsageLimits()
|
||||
|
||||
Get current usage limits and quota information.
|
||||
|
||||
```typescript
|
||||
const limits = await client.getUsageLimits();
|
||||
console.log('Current usage:', limits.usage);
|
||||
```
|
||||
|
||||
**Returns:** `Promise<UsageLimits>`
|
||||
|
||||
##### setApiKey(apiKey)
|
||||
|
||||
Update the API key.
|
||||
|
||||
```typescript
|
||||
client.setApiKey('new-api-key');
|
||||
```
|
||||
|
||||
##### setBaseUrl(baseUrl)
|
||||
|
||||
Update the base URL.
|
||||
|
||||
```typescript
|
||||
client.setBaseUrl('https://my-custom-domain.com');
|
||||
```
|
||||
|
||||
## Types
|
||||
|
||||
### WorkflowExecutionResult
|
||||
|
||||
```typescript
|
||||
interface WorkflowExecutionResult {
|
||||
success: boolean;
|
||||
output?: any;
|
||||
error?: string;
|
||||
logs?: any[];
|
||||
metadata?: {
|
||||
duration?: number;
|
||||
executionId?: string;
|
||||
[key: string]: any;
|
||||
};
|
||||
traceSpans?: any[];
|
||||
totalDuration?: number;
|
||||
}
|
||||
```
|
||||
|
||||
### LargeValueRef
|
||||
|
||||
Oversized execution values may be returned as a versioned reference inside `output`, `logs`, streaming events, or async job status responses.
|
||||
The `key` field is an opaque execution-scoped server storage pointer, not a client-readable download URL.
|
||||
|
||||
```typescript
|
||||
interface LargeValueRef {
|
||||
__simLargeValueRef: true;
|
||||
version: 1;
|
||||
id: string;
|
||||
kind: 'array' | 'object' | 'string' | 'json';
|
||||
size: number;
|
||||
key?: string;
|
||||
executionId?: string;
|
||||
preview?: unknown;
|
||||
}
|
||||
```
|
||||
|
||||
### WorkflowStatus
|
||||
|
||||
```typescript
|
||||
interface WorkflowStatus {
|
||||
isDeployed: boolean;
|
||||
deployedAt?: string;
|
||||
needsRedeployment: boolean;
|
||||
}
|
||||
```
|
||||
|
||||
### SimStudioError
|
||||
|
||||
```typescript
|
||||
class SimStudioError extends Error {
|
||||
code?: string;
|
||||
status?: number;
|
||||
}
|
||||
```
|
||||
|
||||
### AsyncExecutionResult
|
||||
|
||||
```typescript
|
||||
interface AsyncExecutionResult {
|
||||
success: boolean;
|
||||
jobId: string;
|
||||
statusUrl: string;
|
||||
executionId?: string;
|
||||
message: string;
|
||||
async: true;
|
||||
}
|
||||
```
|
||||
|
||||
### RateLimitInfo
|
||||
|
||||
```typescript
|
||||
interface RateLimitInfo {
|
||||
limit: number;
|
||||
remaining: number;
|
||||
reset: number;
|
||||
retryAfter?: number;
|
||||
}
|
||||
```
|
||||
|
||||
### UsageLimits
|
||||
|
||||
```typescript
|
||||
interface UsageLimits {
|
||||
success: boolean;
|
||||
rateLimit: {
|
||||
sync: {
|
||||
isLimited: boolean;
|
||||
limit: number;
|
||||
remaining: number;
|
||||
resetAt: string;
|
||||
};
|
||||
async: {
|
||||
isLimited: boolean;
|
||||
limit: number;
|
||||
remaining: number;
|
||||
resetAt: string;
|
||||
};
|
||||
authType: string;
|
||||
};
|
||||
usage: {
|
||||
currentPeriodCost: number;
|
||||
limit: number;
|
||||
plan: string;
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
### ExecutionOptions
|
||||
|
||||
```typescript
|
||||
interface ExecutionOptions {
|
||||
timeout?: number;
|
||||
stream?: boolean;
|
||||
selectedOutputs?: string[];
|
||||
async?: boolean;
|
||||
}
|
||||
```
|
||||
|
||||
### RetryOptions
|
||||
|
||||
```typescript
|
||||
interface RetryOptions {
|
||||
maxRetries?: number;
|
||||
initialDelay?: number;
|
||||
maxDelay?: number;
|
||||
backoffMultiplier?: number;
|
||||
}
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
### Basic Workflow Execution
|
||||
|
||||
```typescript
|
||||
import { SimStudioClient } from 'simstudio-ts-sdk';
|
||||
|
||||
const client = new SimStudioClient({
|
||||
apiKey: process.env.SIM_API_KEY!
|
||||
});
|
||||
|
||||
async function runWorkflow() {
|
||||
try {
|
||||
// Check if workflow is ready
|
||||
const isReady = await client.validateWorkflow('my-workflow-id');
|
||||
if (!isReady) {
|
||||
throw new Error('Workflow is not deployed or ready');
|
||||
}
|
||||
|
||||
// Execute the workflow
|
||||
const result = await client.executeWorkflow('my-workflow-id', {
|
||||
message: 'Process this data',
|
||||
userId: '12345'
|
||||
});
|
||||
|
||||
if (result.success) {
|
||||
console.log('Output:', result.output);
|
||||
console.log('Duration:', result.metadata?.duration);
|
||||
} else {
|
||||
console.error('Workflow failed:', result.error);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error:', error);
|
||||
}
|
||||
}
|
||||
|
||||
runWorkflow();
|
||||
```
|
||||
|
||||
### Error Handling
|
||||
|
||||
```typescript
|
||||
import { SimStudioClient, SimStudioError } from 'simstudio-ts-sdk';
|
||||
|
||||
const client = new SimStudioClient({
|
||||
apiKey: process.env.SIM_API_KEY!
|
||||
});
|
||||
|
||||
async function executeWithErrorHandling() {
|
||||
try {
|
||||
const result = await client.executeWorkflow('workflow-id');
|
||||
return result;
|
||||
} catch (error) {
|
||||
if (error instanceof SimStudioError) {
|
||||
switch (error.code) {
|
||||
case 'UNAUTHORIZED':
|
||||
console.error('Invalid API key');
|
||||
break;
|
||||
case 'TIMEOUT':
|
||||
console.error('Workflow execution timed out');
|
||||
break;
|
||||
case 'USAGE_LIMIT_EXCEEDED':
|
||||
console.error('Usage limit exceeded');
|
||||
break;
|
||||
case 'INVALID_JSON':
|
||||
console.error('Invalid JSON in request body');
|
||||
break;
|
||||
default:
|
||||
console.error('Workflow error:', error.message);
|
||||
}
|
||||
} else {
|
||||
console.error('Unexpected error:', error);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Environment Configuration
|
||||
|
||||
```typescript
|
||||
// Using environment variables
|
||||
const client = new SimStudioClient({
|
||||
apiKey: process.env.SIM_API_KEY!,
|
||||
baseUrl: process.env.SIM_BASE_URL // optional
|
||||
});
|
||||
```
|
||||
|
||||
### File Upload
|
||||
|
||||
File objects are automatically detected and converted to base64 format. Include them in your input under the field name matching your workflow's API trigger input format:
|
||||
|
||||
The SDK converts File objects to this format:
|
||||
```typescript
|
||||
{
|
||||
type: 'file',
|
||||
data: 'data:mime/type;base64,base64data',
|
||||
name: 'filename',
|
||||
mime: 'mime/type'
|
||||
}
|
||||
```
|
||||
|
||||
Alternatively, you can manually provide files using the URL format:
|
||||
```typescript
|
||||
{
|
||||
type: 'url',
|
||||
data: 'https://example.com/file.pdf',
|
||||
name: 'file.pdf',
|
||||
mime: 'application/pdf'
|
||||
}
|
||||
```
|
||||
|
||||
```typescript
|
||||
import { SimStudioClient } from 'simstudio-ts-sdk';
|
||||
import fs from 'fs';
|
||||
|
||||
const client = new SimStudioClient({
|
||||
apiKey: process.env.SIM_API_KEY!
|
||||
});
|
||||
|
||||
// Node.js: Read file and create File object
|
||||
const fileBuffer = fs.readFileSync('./document.pdf');
|
||||
const file = new File([fileBuffer], 'document.pdf', { type: 'application/pdf' });
|
||||
|
||||
// Include files under the field name from your API trigger's input format
|
||||
const result = await client.executeWorkflow('workflow-id', {
|
||||
documents: [file], // Field name must match your API trigger's file input field
|
||||
instructions: 'Process this document'
|
||||
});
|
||||
|
||||
// Browser: From file input
|
||||
const handleFileUpload = async (event: Event) => {
|
||||
const inputEl = event.target as HTMLInputElement;
|
||||
const files = Array.from(inputEl.files || []);
|
||||
|
||||
const result = await client.executeWorkflow('workflow-id', {
|
||||
attachments: files, // Field name must match your API trigger's file input field
|
||||
query: 'Analyze these files'
|
||||
});
|
||||
};
|
||||
```
|
||||
|
||||
## Getting Your API Key
|
||||
|
||||
1. Log in to your [Sim](https://sim.ai) account
|
||||
2. Navigate to your workflow
|
||||
3. Click on "Deploy" to deploy your workflow
|
||||
4. Select or create an API key during the deployment process
|
||||
5. Copy the API key to use in your application
|
||||
|
||||
## Development
|
||||
|
||||
### Running Tests
|
||||
|
||||
To run the tests locally:
|
||||
|
||||
1. Clone the repository and navigate to the TypeScript SDK directory:
|
||||
```bash
|
||||
cd packages/ts-sdk
|
||||
```
|
||||
|
||||
2. Install dependencies:
|
||||
```bash
|
||||
bun install
|
||||
```
|
||||
|
||||
3. Run the tests:
|
||||
```bash
|
||||
bun run test
|
||||
```
|
||||
|
||||
### Building
|
||||
|
||||
Build the TypeScript SDK:
|
||||
|
||||
```bash
|
||||
bun run build
|
||||
```
|
||||
|
||||
This will compile TypeScript files to JavaScript and generate type declarations in the `dist/` directory.
|
||||
|
||||
### Development Mode
|
||||
|
||||
For development with auto-rebuild:
|
||||
|
||||
```bash
|
||||
bun run dev
|
||||
```
|
||||
|
||||
## Requirements
|
||||
|
||||
- Node.js 18+
|
||||
- TypeScript 5.0+ (for TypeScript projects)
|
||||
|
||||
## License
|
||||
|
||||
Apache-2.0
|
||||
@@ -0,0 +1,164 @@
|
||||
import { SimStudioClient, SimStudioError } from '../src/index'
|
||||
|
||||
// Example 1: Basic workflow execution
|
||||
async function basicExample() {
|
||||
const client = new SimStudioClient({
|
||||
apiKey: process.env.SIM_API_KEY!,
|
||||
baseUrl: 'https://sim.ai',
|
||||
})
|
||||
|
||||
try {
|
||||
// Execute a workflow without input
|
||||
const result = await client.executeWorkflow('your-workflow-id')
|
||||
|
||||
if (result.success) {
|
||||
console.log('✅ Workflow executed successfully!')
|
||||
console.log('Output:', result.output)
|
||||
console.log('Duration:', result.metadata?.duration, 'ms')
|
||||
} else {
|
||||
console.log('❌ Workflow failed:', result.error)
|
||||
}
|
||||
} catch (error) {
|
||||
if (error instanceof SimStudioError) {
|
||||
console.error('SDK Error:', error.message, 'Code:', error.code)
|
||||
} else {
|
||||
console.error('Unexpected error:', error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Example 2: Workflow execution with input data
|
||||
async function withInputExample() {
|
||||
const client = new SimStudioClient({
|
||||
apiKey: process.env.SIM_API_KEY!,
|
||||
})
|
||||
|
||||
try {
|
||||
const result = await client.executeWorkflow('your-workflow-id', {
|
||||
input: {
|
||||
message: 'Hello from SDK!',
|
||||
userId: '12345',
|
||||
data: {
|
||||
type: 'analysis',
|
||||
parameters: {
|
||||
includeMetadata: true,
|
||||
format: 'json',
|
||||
},
|
||||
},
|
||||
},
|
||||
timeout: 60000, // 60 seconds
|
||||
})
|
||||
|
||||
if (result.success) {
|
||||
console.log('✅ Workflow executed successfully!')
|
||||
console.log('Output:', result.output)
|
||||
if (result.metadata?.duration) {
|
||||
console.log('Duration:', result.metadata.duration, 'ms')
|
||||
}
|
||||
} else {
|
||||
console.log('❌ Workflow failed:', result.error)
|
||||
}
|
||||
} catch (error) {
|
||||
if (error instanceof SimStudioError) {
|
||||
console.error('SDK Error:', error.message, 'Code:', error.code)
|
||||
} else {
|
||||
console.error('Unexpected error:', error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Example 3: Workflow validation and status checking
|
||||
async function statusExample() {
|
||||
const client = new SimStudioClient({
|
||||
apiKey: process.env.SIM_API_KEY!,
|
||||
})
|
||||
|
||||
try {
|
||||
// Check if workflow is ready
|
||||
const isReady = await client.validateWorkflow('your-workflow-id')
|
||||
console.log('Workflow ready:', isReady)
|
||||
|
||||
// Get detailed status
|
||||
const status = await client.getWorkflowStatus('your-workflow-id')
|
||||
console.log('Status:', {
|
||||
deployed: status.isDeployed,
|
||||
needsRedeployment: status.needsRedeployment,
|
||||
deployedAt: status.deployedAt,
|
||||
})
|
||||
|
||||
if (status.isDeployed) {
|
||||
// Execute the workflow
|
||||
const result = await client.executeWorkflow('your-workflow-id')
|
||||
|
||||
if (result.success) {
|
||||
console.log('✅ Workflow executed successfully!')
|
||||
console.log('Output:', result.output)
|
||||
} else {
|
||||
console.log('❌ Workflow failed:', result.error)
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
if (error instanceof SimStudioError) {
|
||||
console.error('SDK Error:', error.message, 'Code:', error.code)
|
||||
} else {
|
||||
console.error('Unexpected error:', error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Example 4: Workflow execution with streaming
|
||||
async function streamingExample() {
|
||||
const client = new SimStudioClient({
|
||||
apiKey: process.env.SIM_API_KEY!,
|
||||
})
|
||||
|
||||
try {
|
||||
const result = await client.executeWorkflow('your-workflow-id', {
|
||||
input: {
|
||||
message: 'Count to five',
|
||||
},
|
||||
stream: true,
|
||||
selectedOutputs: ['agent1.content'], // Use blockName.attribute format
|
||||
timeout: 60000,
|
||||
})
|
||||
|
||||
if (result.success) {
|
||||
console.log('✅ Workflow executed successfully!')
|
||||
console.log('Output:', result.output)
|
||||
console.log('Duration:', result.metadata?.duration, 'ms')
|
||||
} else {
|
||||
console.log('❌ Workflow failed:', result.error)
|
||||
}
|
||||
} catch (error) {
|
||||
if (error instanceof SimStudioError) {
|
||||
console.error('SDK Error:', error.message, 'Code:', error.code)
|
||||
} else {
|
||||
console.error('Unexpected error:', error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Run examples
|
||||
if (require.main === module) {
|
||||
async function runExamples() {
|
||||
console.log('🚀 Running Sim SDK Examples\n')
|
||||
|
||||
try {
|
||||
await basicExample()
|
||||
console.log('\n✅ Basic example completed')
|
||||
|
||||
await withInputExample()
|
||||
console.log('\n✅ Input example completed')
|
||||
|
||||
await statusExample()
|
||||
console.log('\n✅ Status example completed')
|
||||
|
||||
await streamingExample()
|
||||
console.log('\n✅ Streaming example completed')
|
||||
} catch (error) {
|
||||
console.error('Error running examples:', error)
|
||||
}
|
||||
}
|
||||
|
||||
runExamples()
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
{
|
||||
"name": "simstudio-ts-sdk",
|
||||
"version": "0.1.2",
|
||||
"description": "Sim SDK - Execute workflows programmatically",
|
||||
"type": "module",
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./dist/index.d.ts",
|
||||
"import": "./dist/index.js"
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"build": "tsc",
|
||||
"type-check": "tsc --noEmit",
|
||||
"lint": "biome check --write --unsafe .",
|
||||
"lint:check": "biome check .",
|
||||
"format": "biome format --write .",
|
||||
"format:check": "biome format .",
|
||||
"dev:watch": "tsc --watch",
|
||||
"prepublishOnly": "bun run build",
|
||||
"test": "vitest run",
|
||||
"test:watch": "vitest"
|
||||
},
|
||||
"files": [
|
||||
"dist"
|
||||
],
|
||||
"keywords": [
|
||||
"simstudio",
|
||||
"ai",
|
||||
"workflow",
|
||||
"sdk",
|
||||
"api",
|
||||
"automation",
|
||||
"typescript"
|
||||
],
|
||||
"author": "Sim",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"node-fetch": "^3.3.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@sim/tsconfig": "workspace:*",
|
||||
"@types/node": "^20.5.1",
|
||||
"@vitest/coverage-v8": "^4.1.0",
|
||||
"typescript": "^7.0.2",
|
||||
"vitest": "^4.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16"
|
||||
},
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/simstudioai/sim.git",
|
||||
"directory": "packages/ts-sdk"
|
||||
},
|
||||
"homepage": "https://sim.ai",
|
||||
"bugs": {
|
||||
"url": "https://github.com/simstudioai/sim/issues"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,658 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { SimStudioClient, SimStudioError } from './index'
|
||||
|
||||
vi.mock('node-fetch', () => ({
|
||||
default: vi.fn(),
|
||||
}))
|
||||
|
||||
describe('SimStudioClient', () => {
|
||||
let client: SimStudioClient
|
||||
|
||||
beforeEach(() => {
|
||||
client = new SimStudioClient({
|
||||
apiKey: 'test-api-key',
|
||||
baseUrl: 'https://test.sim.ai',
|
||||
})
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
describe('constructor', () => {
|
||||
it('should create a client with correct configuration', () => {
|
||||
expect(client).toBeInstanceOf(SimStudioClient)
|
||||
})
|
||||
|
||||
it('should use default base URL when not provided', () => {
|
||||
const defaultClient = new SimStudioClient({
|
||||
apiKey: 'test-api-key',
|
||||
})
|
||||
expect(defaultClient).toBeInstanceOf(SimStudioClient)
|
||||
})
|
||||
})
|
||||
|
||||
describe('setApiKey', () => {
|
||||
it('should update the API key', () => {
|
||||
const newApiKey = 'new-api-key'
|
||||
client.setApiKey(newApiKey)
|
||||
|
||||
// Verify the method exists
|
||||
expect(client.setApiKey).toBeDefined()
|
||||
// Verify the API key was actually updated
|
||||
expect((client as any).apiKey).toBe(newApiKey)
|
||||
})
|
||||
})
|
||||
|
||||
describe('setBaseUrl', () => {
|
||||
it('should update the base URL', () => {
|
||||
const newBaseUrl = 'https://new.sim.ai'
|
||||
client.setBaseUrl(newBaseUrl)
|
||||
expect((client as any).baseUrl).toBe(newBaseUrl)
|
||||
})
|
||||
|
||||
it('should strip trailing slash from base URL', () => {
|
||||
const urlWithSlash = 'https://test.sim.ai/'
|
||||
client.setBaseUrl(urlWithSlash)
|
||||
// Verify the trailing slash was actually stripped
|
||||
expect((client as any).baseUrl).toBe('https://test.sim.ai')
|
||||
})
|
||||
})
|
||||
|
||||
describe('validateWorkflow', () => {
|
||||
it('should return false when workflow status request fails', async () => {
|
||||
const fetch = await import('node-fetch')
|
||||
vi.mocked(fetch.default).mockRejectedValue(new Error('Network error'))
|
||||
|
||||
const result = await client.validateWorkflow('test-workflow-id')
|
||||
expect(result).toBe(false)
|
||||
})
|
||||
|
||||
it('should return true when workflow is deployed', async () => {
|
||||
const fetch = await import('node-fetch')
|
||||
const mockResponse = {
|
||||
ok: true,
|
||||
json: vi.fn().mockResolvedValue({
|
||||
isDeployed: true,
|
||||
deployedAt: '2023-01-01T00:00:00Z',
|
||||
needsRedeployment: false,
|
||||
}),
|
||||
}
|
||||
vi.mocked(fetch.default).mockResolvedValue(mockResponse as any)
|
||||
|
||||
const result = await client.validateWorkflow('test-workflow-id')
|
||||
expect(result).toBe(true)
|
||||
})
|
||||
|
||||
it('should return false when workflow is not deployed', async () => {
|
||||
const fetch = await import('node-fetch')
|
||||
const mockResponse = {
|
||||
ok: true,
|
||||
json: vi.fn().mockResolvedValue({
|
||||
isDeployed: false,
|
||||
deployedAt: null,
|
||||
needsRedeployment: true,
|
||||
}),
|
||||
}
|
||||
vi.mocked(fetch.default).mockResolvedValue(mockResponse as any)
|
||||
|
||||
const result = await client.validateWorkflow('test-workflow-id')
|
||||
expect(result).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('executeWorkflow - async execution', () => {
|
||||
it('should return AsyncExecutionResult when async is true', async () => {
|
||||
const fetch = await import('node-fetch')
|
||||
const mockResponse = {
|
||||
ok: true,
|
||||
status: 202,
|
||||
json: vi.fn().mockResolvedValue({
|
||||
success: true,
|
||||
jobId: 'job-123',
|
||||
statusUrl: 'https://test.sim.ai/api/jobs/job-123',
|
||||
message: 'Workflow execution queued',
|
||||
async: true,
|
||||
}),
|
||||
headers: {
|
||||
get: vi.fn().mockReturnValue(null),
|
||||
},
|
||||
}
|
||||
vi.mocked(fetch.default).mockResolvedValue(mockResponse as any)
|
||||
|
||||
const result = await client.executeWorkflow(
|
||||
'workflow-id',
|
||||
{ message: 'Hello' },
|
||||
{ async: true }
|
||||
)
|
||||
|
||||
expect(result).toHaveProperty('jobId', 'job-123')
|
||||
expect(result).toHaveProperty('statusUrl', 'https://test.sim.ai/api/jobs/job-123')
|
||||
expect(result).toHaveProperty('async', true)
|
||||
|
||||
// Verify headers were set correctly
|
||||
const calls = vi.mocked(fetch.default).mock.calls
|
||||
expect(calls[0][1]?.headers).toMatchObject({
|
||||
'X-Execution-Mode': 'async',
|
||||
})
|
||||
})
|
||||
|
||||
it('should return WorkflowExecutionResult when async is false', async () => {
|
||||
const fetch = await import('node-fetch')
|
||||
const mockResponse = {
|
||||
ok: true,
|
||||
status: 200,
|
||||
json: vi.fn().mockResolvedValue({
|
||||
success: true,
|
||||
output: { result: 'completed' },
|
||||
logs: [],
|
||||
}),
|
||||
headers: {
|
||||
get: vi.fn().mockReturnValue(null),
|
||||
},
|
||||
}
|
||||
vi.mocked(fetch.default).mockResolvedValue(mockResponse as any)
|
||||
|
||||
const result = await client.executeWorkflow(
|
||||
'workflow-id',
|
||||
{ message: 'Hello' },
|
||||
{ async: false }
|
||||
)
|
||||
|
||||
expect(result).toHaveProperty('success', true)
|
||||
expect(result).toHaveProperty('output')
|
||||
expect(result).not.toHaveProperty('jobId')
|
||||
})
|
||||
|
||||
it('should not set X-Execution-Mode header when async is undefined', 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', { message: 'Hello' })
|
||||
|
||||
const calls = vi.mocked(fetch.default).mock.calls
|
||||
expect(calls[0][1]?.headers).not.toHaveProperty('X-Execution-Mode')
|
||||
})
|
||||
})
|
||||
|
||||
describe('getJobStatus', () => {
|
||||
it('should fetch job status with correct endpoint', async () => {
|
||||
const fetch = await import('node-fetch')
|
||||
const mockResponse = {
|
||||
ok: true,
|
||||
json: vi.fn().mockResolvedValue({
|
||||
success: true,
|
||||
taskId: 'task-123',
|
||||
status: 'completed',
|
||||
metadata: {
|
||||
startedAt: '2024-01-01T00:00:00Z',
|
||||
completedAt: '2024-01-01T00:01:00Z',
|
||||
duration: 60000,
|
||||
},
|
||||
output: { result: 'done' },
|
||||
}),
|
||||
headers: {
|
||||
get: vi.fn().mockReturnValue(null),
|
||||
},
|
||||
}
|
||||
vi.mocked(fetch.default).mockResolvedValue(mockResponse as any)
|
||||
|
||||
const result = await client.getJobStatus('task-123')
|
||||
|
||||
expect(result).toHaveProperty('taskId', 'task-123')
|
||||
expect(result).toHaveProperty('status', 'completed')
|
||||
expect(result).toHaveProperty('output')
|
||||
|
||||
// Verify correct endpoint was called
|
||||
const calls = vi.mocked(fetch.default).mock.calls
|
||||
expect(calls[0][0]).toBe('https://test.sim.ai/api/jobs/task-123')
|
||||
})
|
||||
|
||||
it('should handle job not found error', async () => {
|
||||
const fetch = await import('node-fetch')
|
||||
const mockResponse = {
|
||||
ok: false,
|
||||
status: 404,
|
||||
statusText: 'Not Found',
|
||||
json: vi.fn().mockResolvedValue({
|
||||
error: 'Job not found',
|
||||
code: 'JOB_NOT_FOUND',
|
||||
}),
|
||||
headers: {
|
||||
get: vi.fn().mockReturnValue(null),
|
||||
},
|
||||
}
|
||||
vi.mocked(fetch.default).mockResolvedValue(mockResponse as any)
|
||||
|
||||
await expect(client.getJobStatus('invalid-task')).rejects.toThrow(SimStudioError)
|
||||
await expect(client.getJobStatus('invalid-task')).rejects.toThrow('Job not found')
|
||||
})
|
||||
})
|
||||
|
||||
describe('executeWithRetry', () => {
|
||||
it('should succeed on first attempt when no rate limit', async () => {
|
||||
const fetch = await import('node-fetch')
|
||||
const mockResponse = {
|
||||
ok: true,
|
||||
status: 200,
|
||||
json: vi.fn().mockResolvedValue({
|
||||
success: true,
|
||||
output: { result: 'success' },
|
||||
}),
|
||||
headers: {
|
||||
get: vi.fn().mockReturnValue(null),
|
||||
},
|
||||
}
|
||||
vi.mocked(fetch.default).mockResolvedValue(mockResponse as any)
|
||||
|
||||
const result = await client.executeWithRetry('workflow-id', { message: 'test' })
|
||||
|
||||
expect(result).toHaveProperty('success', true)
|
||||
expect(vi.mocked(fetch.default)).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('should retry on rate limit error', async () => {
|
||||
const fetch = await import('node-fetch')
|
||||
|
||||
// First call returns 429, second call succeeds
|
||||
const rateLimitResponse = {
|
||||
ok: false,
|
||||
status: 429,
|
||||
statusText: 'Too Many Requests',
|
||||
json: vi.fn().mockResolvedValue({
|
||||
error: 'Rate limit exceeded',
|
||||
code: 'RATE_LIMIT_EXCEEDED',
|
||||
}),
|
||||
headers: {
|
||||
get: vi.fn((header: string) => {
|
||||
if (header === 'retry-after') return '1'
|
||||
if (header === 'x-ratelimit-limit') return '100'
|
||||
if (header === 'x-ratelimit-remaining') return '0'
|
||||
if (header === 'x-ratelimit-reset') return String(Math.floor(Date.now() / 1000) + 60)
|
||||
return null
|
||||
}),
|
||||
},
|
||||
}
|
||||
|
||||
const successResponse = {
|
||||
ok: true,
|
||||
status: 200,
|
||||
json: vi.fn().mockResolvedValue({
|
||||
success: true,
|
||||
output: { result: 'success' },
|
||||
}),
|
||||
headers: {
|
||||
get: vi.fn().mockReturnValue(null),
|
||||
},
|
||||
}
|
||||
|
||||
vi.mocked(fetch.default)
|
||||
.mockResolvedValueOnce(rateLimitResponse as any)
|
||||
.mockResolvedValueOnce(successResponse as any)
|
||||
|
||||
const result = await client.executeWithRetry(
|
||||
'workflow-id',
|
||||
{ message: 'test' },
|
||||
{},
|
||||
{ maxRetries: 3, initialDelay: 10 }
|
||||
)
|
||||
|
||||
expect(result).toHaveProperty('success', true)
|
||||
expect(vi.mocked(fetch.default)).toHaveBeenCalledTimes(2)
|
||||
})
|
||||
|
||||
it('should throw after max retries exceeded', async () => {
|
||||
const fetch = await import('node-fetch')
|
||||
const mockResponse = {
|
||||
ok: false,
|
||||
status: 429,
|
||||
statusText: 'Too Many Requests',
|
||||
json: vi.fn().mockResolvedValue({
|
||||
error: 'Rate limit exceeded',
|
||||
code: 'RATE_LIMIT_EXCEEDED',
|
||||
}),
|
||||
headers: {
|
||||
get: vi.fn((header: string) => {
|
||||
if (header === 'retry-after') return '1'
|
||||
return null
|
||||
}),
|
||||
},
|
||||
}
|
||||
|
||||
vi.mocked(fetch.default).mockResolvedValue(mockResponse as any)
|
||||
|
||||
await expect(
|
||||
client.executeWithRetry(
|
||||
'workflow-id',
|
||||
{ message: 'test' },
|
||||
{},
|
||||
{ maxRetries: 2, initialDelay: 10 }
|
||||
)
|
||||
).rejects.toThrow('Rate limit exceeded')
|
||||
|
||||
expect(vi.mocked(fetch.default)).toHaveBeenCalledTimes(3) // Initial + 2 retries
|
||||
})
|
||||
|
||||
it('should not retry on non-rate-limit errors', async () => {
|
||||
const fetch = await import('node-fetch')
|
||||
const mockResponse = {
|
||||
ok: false,
|
||||
status: 500,
|
||||
statusText: 'Internal Server Error',
|
||||
json: vi.fn().mockResolvedValue({
|
||||
error: 'Server error',
|
||||
code: 'INTERNAL_ERROR',
|
||||
}),
|
||||
headers: {
|
||||
get: vi.fn().mockReturnValue(null),
|
||||
},
|
||||
}
|
||||
|
||||
vi.mocked(fetch.default).mockResolvedValue(mockResponse as any)
|
||||
|
||||
await expect(client.executeWithRetry('workflow-id', { message: 'test' })).rejects.toThrow(
|
||||
'Server error'
|
||||
)
|
||||
|
||||
expect(vi.mocked(fetch.default)).toHaveBeenCalledTimes(1) // No retries
|
||||
})
|
||||
})
|
||||
|
||||
describe('getRateLimitInfo', () => {
|
||||
it('should return null when no rate limit info available', () => {
|
||||
const info = client.getRateLimitInfo()
|
||||
expect(info).toBeNull()
|
||||
})
|
||||
|
||||
it('should return rate limit info after API call', async () => {
|
||||
const fetch = await import('node-fetch')
|
||||
const mockResponse = {
|
||||
ok: true,
|
||||
status: 200,
|
||||
json: vi.fn().mockResolvedValue({ success: true, output: {} }),
|
||||
headers: {
|
||||
get: vi.fn((header: string) => {
|
||||
if (header === 'x-ratelimit-limit') return '100'
|
||||
if (header === 'x-ratelimit-remaining') return '95'
|
||||
if (header === 'x-ratelimit-reset') return '1704067200'
|
||||
return null
|
||||
}),
|
||||
},
|
||||
}
|
||||
|
||||
vi.mocked(fetch.default).mockResolvedValue(mockResponse as any)
|
||||
|
||||
await client.executeWorkflow('workflow-id', {})
|
||||
|
||||
const info = client.getRateLimitInfo()
|
||||
expect(info).not.toBeNull()
|
||||
expect(info?.limit).toBe(100)
|
||||
expect(info?.remaining).toBe(95)
|
||||
expect(info?.reset).toBe(1704067200)
|
||||
})
|
||||
})
|
||||
|
||||
describe('getUsageLimits', () => {
|
||||
it('should fetch usage limits with correct structure', async () => {
|
||||
const fetch = await import('node-fetch')
|
||||
const mockResponse = {
|
||||
ok: true,
|
||||
json: vi.fn().mockResolvedValue({
|
||||
success: true,
|
||||
rateLimit: {
|
||||
sync: {
|
||||
isLimited: false,
|
||||
requestsPerMinute: 100,
|
||||
maxBurst: 200,
|
||||
remaining: 95,
|
||||
resetAt: '2024-01-01T01:00:00Z',
|
||||
},
|
||||
async: {
|
||||
isLimited: false,
|
||||
requestsPerMinute: 50,
|
||||
maxBurst: 100,
|
||||
remaining: 48,
|
||||
resetAt: '2024-01-01T01:00:00Z',
|
||||
},
|
||||
authType: 'api',
|
||||
},
|
||||
usage: {
|
||||
currentPeriodCost: 1.23,
|
||||
limit: 100.0,
|
||||
plan: 'pro',
|
||||
},
|
||||
storage: {
|
||||
usedBytes: 1024,
|
||||
limitBytes: 10240,
|
||||
percentUsed: 10,
|
||||
},
|
||||
}),
|
||||
headers: {
|
||||
get: vi.fn().mockReturnValue(null),
|
||||
},
|
||||
}
|
||||
|
||||
vi.mocked(fetch.default).mockResolvedValue(mockResponse as any)
|
||||
|
||||
const result = await client.getUsageLimits()
|
||||
|
||||
expect(result.success).toBe(true)
|
||||
expect(result.rateLimit.sync.requestsPerMinute).toBe(100)
|
||||
expect(result.rateLimit.sync.maxBurst).toBe(200)
|
||||
expect(result.rateLimit.async.requestsPerMinute).toBe(50)
|
||||
expect(result.usage.currentPeriodCost).toBe(1.23)
|
||||
expect(result.usage.plan).toBe('pro')
|
||||
expect(result.storage.usedBytes).toBe(1024)
|
||||
expect(result.storage.percentUsed).toBe(10)
|
||||
|
||||
// Verify correct endpoint was called
|
||||
const calls = vi.mocked(fetch.default).mock.calls
|
||||
expect(calls[0][0]).toBe('https://test.sim.ai/api/users/me/usage-limits')
|
||||
})
|
||||
|
||||
it('should handle unauthorized error', async () => {
|
||||
const fetch = await import('node-fetch')
|
||||
const mockResponse = {
|
||||
ok: false,
|
||||
status: 401,
|
||||
statusText: 'Unauthorized',
|
||||
json: vi.fn().mockResolvedValue({
|
||||
error: 'Invalid API key',
|
||||
code: 'UNAUTHORIZED',
|
||||
}),
|
||||
headers: {
|
||||
get: vi.fn().mockReturnValue(null),
|
||||
},
|
||||
}
|
||||
|
||||
vi.mocked(fetch.default).mockResolvedValue(mockResponse as any)
|
||||
|
||||
await expect(client.getUsageLimits()).rejects.toThrow(SimStudioError)
|
||||
await expect(client.getUsageLimits()).rejects.toThrow('Invalid API key')
|
||||
})
|
||||
})
|
||||
|
||||
describe('executeWorkflow - streaming with selectedOutputs', () => {
|
||||
it('should include stream and selectedOutputs in request 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',
|
||||
{ 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)
|
||||
|
||||
expect(requestBody).toHaveProperty('message', 'test')
|
||||
expect(requestBody).toHaveProperty('stream', true)
|
||||
expect(requestBody).toHaveProperty('selectedOutputs')
|
||||
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', () => {
|
||||
it('should create error with message', () => {
|
||||
const error = new SimStudioError('Test error')
|
||||
expect(error.message).toBe('Test error')
|
||||
expect(error.name).toBe('SimStudioError')
|
||||
})
|
||||
|
||||
it('should create error with code and status', () => {
|
||||
const error = new SimStudioError('Test error', 'TEST_CODE', 400)
|
||||
expect(error.message).toBe('Test error')
|
||||
expect(error.code).toBe('TEST_CODE')
|
||||
expect(error.status).toBe(400)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,519 @@
|
||||
import fetch from 'node-fetch'
|
||||
|
||||
export interface SimStudioConfig {
|
||||
apiKey: string
|
||||
baseUrl?: string
|
||||
}
|
||||
|
||||
export interface LargeValueRef {
|
||||
__simLargeValueRef: true
|
||||
version: 1
|
||||
id: string
|
||||
kind: 'array' | 'object' | 'string' | 'json'
|
||||
size: number
|
||||
/** Opaque execution-scoped server storage key. This is not a download URL. */
|
||||
key?: string
|
||||
executionId?: string
|
||||
preview?: unknown
|
||||
}
|
||||
|
||||
export interface WorkflowExecutionResult {
|
||||
success: boolean
|
||||
output?: any
|
||||
error?: string
|
||||
logs?: any[]
|
||||
metadata?: {
|
||||
duration?: number
|
||||
executionId?: string
|
||||
[key: string]: any
|
||||
}
|
||||
traceSpans?: any[]
|
||||
totalDuration?: number
|
||||
}
|
||||
|
||||
export interface WorkflowStatus {
|
||||
isDeployed: boolean
|
||||
isPublished?: boolean
|
||||
deployedAt?: string
|
||||
needsRedeployment: boolean
|
||||
}
|
||||
|
||||
export interface ExecutionOptions {
|
||||
timeout?: number
|
||||
stream?: boolean
|
||||
selectedOutputs?: string[]
|
||||
async?: boolean
|
||||
}
|
||||
|
||||
export interface AsyncExecutionResult {
|
||||
success: boolean
|
||||
jobId: string
|
||||
statusUrl: string
|
||||
executionId?: string
|
||||
message: string
|
||||
async: true
|
||||
}
|
||||
|
||||
export interface JobStatusResult {
|
||||
taskId: string
|
||||
status: string
|
||||
metadata?: Record<string, unknown>
|
||||
output?: unknown
|
||||
error?: string
|
||||
}
|
||||
|
||||
export interface RateLimitInfo {
|
||||
limit: number
|
||||
remaining: number
|
||||
reset: number
|
||||
retryAfter?: number
|
||||
}
|
||||
|
||||
export interface RetryOptions {
|
||||
maxRetries?: number
|
||||
initialDelay?: number
|
||||
maxDelay?: number
|
||||
backoffMultiplier?: number
|
||||
}
|
||||
|
||||
export interface UsageLimits {
|
||||
success: boolean
|
||||
rateLimit: {
|
||||
sync: {
|
||||
isLimited: boolean
|
||||
requestsPerMinute: number
|
||||
maxBurst: number
|
||||
remaining: number
|
||||
resetAt: string
|
||||
}
|
||||
async: {
|
||||
isLimited: boolean
|
||||
requestsPerMinute: number
|
||||
maxBurst: number
|
||||
remaining: number
|
||||
resetAt: string
|
||||
}
|
||||
authType: string
|
||||
}
|
||||
usage: {
|
||||
currentPeriodCost: number
|
||||
limit: number
|
||||
plan: string
|
||||
}
|
||||
storage: {
|
||||
usedBytes: number
|
||||
limitBytes: number
|
||||
percentUsed: number
|
||||
}
|
||||
}
|
||||
|
||||
export class SimStudioError extends Error {
|
||||
public code?: string
|
||||
public status?: number
|
||||
|
||||
constructor(message: string, code?: string, status?: number) {
|
||||
super(message)
|
||||
this.name = 'SimStudioError'
|
||||
this.code = code
|
||||
this.status = status
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove trailing slashes from a URL
|
||||
* Uses string operations instead of regex to prevent ReDoS attacks
|
||||
* @param url - The URL to normalize
|
||||
* @returns URL without trailing slashes
|
||||
*/
|
||||
function normalizeBaseUrl(url: string): string {
|
||||
let normalized = url
|
||||
while (normalized.endsWith('/')) {
|
||||
normalized = normalized.slice(0, -1)
|
||||
}
|
||||
return normalized
|
||||
}
|
||||
|
||||
export class SimStudioClient {
|
||||
private apiKey: string
|
||||
private baseUrl: string
|
||||
private rateLimitInfo: RateLimitInfo | null = null
|
||||
|
||||
constructor(config: SimStudioConfig) {
|
||||
this.apiKey = config.apiKey
|
||||
this.baseUrl = normalizeBaseUrl(config.baseUrl || 'https://sim.ai')
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert File objects in input to API format (base64)
|
||||
* Recursively processes nested objects and arrays
|
||||
*/
|
||||
private async convertFilesToBase64(
|
||||
value: any,
|
||||
visited: WeakSet<object> = new WeakSet()
|
||||
): Promise<any> {
|
||||
if (typeof File !== 'undefined' && value instanceof File) {
|
||||
const arrayBuffer = await value.arrayBuffer()
|
||||
const buffer = Buffer.from(arrayBuffer)
|
||||
const base64 = buffer.toString('base64')
|
||||
|
||||
return {
|
||||
type: 'file',
|
||||
data: `data:${value.type || 'application/octet-stream'};base64,${base64}`,
|
||||
name: value.name,
|
||||
mime: value.type || 'application/octet-stream',
|
||||
}
|
||||
}
|
||||
|
||||
if (Array.isArray(value)) {
|
||||
if (visited.has(value)) {
|
||||
return '[Circular]'
|
||||
}
|
||||
visited.add(value)
|
||||
const result = await Promise.all(
|
||||
value.map((item) => this.convertFilesToBase64(item, visited))
|
||||
)
|
||||
visited.delete(value)
|
||||
return result
|
||||
}
|
||||
|
||||
if (value !== null && typeof value === 'object') {
|
||||
if (visited.has(value)) {
|
||||
return '[Circular]'
|
||||
}
|
||||
visited.add(value)
|
||||
const converted: any = {}
|
||||
for (const [key, val] of Object.entries(value)) {
|
||||
converted[key] = await this.convertFilesToBase64(val, visited)
|
||||
}
|
||||
visited.delete(value)
|
||||
return converted
|
||||
}
|
||||
|
||||
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 { timeout = 30000, stream, selectedOutputs, async } = options
|
||||
|
||||
try {
|
||||
const timeoutPromise = new Promise<never>((_, reject) => {
|
||||
setTimeout(() => reject(new Error('TIMEOUT')), timeout)
|
||||
})
|
||||
|
||||
const headers: Record<string, string> = {
|
||||
'Content-Type': 'application/json',
|
||||
'X-API-Key': this.apiKey,
|
||||
}
|
||||
if (async) {
|
||||
headers['X-Execution-Mode'] = 'async'
|
||||
}
|
||||
|
||||
let jsonBody: any = {}
|
||||
if (input !== undefined && input !== null) {
|
||||
if (typeof input === 'object' && input !== null && !Array.isArray(input)) {
|
||||
jsonBody = { ...input }
|
||||
} else {
|
||||
jsonBody = { input }
|
||||
}
|
||||
}
|
||||
|
||||
jsonBody = await this.convertFilesToBase64(jsonBody)
|
||||
|
||||
if (stream !== undefined) {
|
||||
jsonBody.stream = stream
|
||||
}
|
||||
if (selectedOutputs !== undefined) {
|
||||
jsonBody.selectedOutputs = selectedOutputs
|
||||
}
|
||||
|
||||
const fetchPromise = fetch(url, {
|
||||
method: 'POST',
|
||||
headers,
|
||||
body: JSON.stringify(jsonBody),
|
||||
})
|
||||
|
||||
const response = await Promise.race([fetchPromise, timeoutPromise])
|
||||
|
||||
this.updateRateLimitInfo(response)
|
||||
|
||||
if (response.status === 429) {
|
||||
const retryAfter = this.rateLimitInfo?.retryAfter || 1000
|
||||
throw new SimStudioError(
|
||||
`Rate limit exceeded. Retry after ${retryAfter}ms`,
|
||||
'RATE_LIMIT_EXCEEDED',
|
||||
429
|
||||
)
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = (await response.json().catch(() => ({}))) as unknown as any
|
||||
throw new SimStudioError(
|
||||
errorData.error || `HTTP ${response.status}: ${response.statusText}`,
|
||||
errorData.code,
|
||||
response.status
|
||||
)
|
||||
}
|
||||
|
||||
const result = await response.json()
|
||||
return result as WorkflowExecutionResult | AsyncExecutionResult
|
||||
} catch (error: any) {
|
||||
if (error instanceof SimStudioError) {
|
||||
throw error
|
||||
}
|
||||
|
||||
if (error.message === 'TIMEOUT') {
|
||||
throw new SimStudioError(`Workflow execution timed out after ${timeout}ms`, 'TIMEOUT')
|
||||
}
|
||||
|
||||
throw new SimStudioError(error?.message || 'Failed to execute workflow', 'EXECUTION_ERROR')
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the status of a workflow (deployment status, etc.)
|
||||
*/
|
||||
async getWorkflowStatus(workflowId: string): Promise<WorkflowStatus> {
|
||||
const url = `${this.baseUrl}/api/workflows/${workflowId}/status`
|
||||
|
||||
try {
|
||||
const response = await fetch(url, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'X-API-Key': this.apiKey,
|
||||
},
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = (await response.json().catch(() => ({}))) as unknown as any
|
||||
throw new SimStudioError(
|
||||
errorData.error || `HTTP ${response.status}: ${response.statusText}`,
|
||||
errorData.code,
|
||||
response.status
|
||||
)
|
||||
}
|
||||
|
||||
const result = await response.json()
|
||||
return result as WorkflowStatus
|
||||
} catch (error: any) {
|
||||
if (error instanceof SimStudioError) {
|
||||
throw error
|
||||
}
|
||||
|
||||
throw new SimStudioError(error?.message || 'Failed to get workflow status', 'STATUS_ERROR')
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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> {
|
||||
const syncOptions = { ...options, async: false }
|
||||
return this.executeWorkflow(workflowId, input, syncOptions) as Promise<WorkflowExecutionResult>
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate that a workflow is ready for execution
|
||||
*/
|
||||
async validateWorkflow(workflowId: string): Promise<boolean> {
|
||||
try {
|
||||
const status = await this.getWorkflowStatus(workflowId)
|
||||
return status.isDeployed
|
||||
} catch (error) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a new API key
|
||||
*/
|
||||
setApiKey(apiKey: string): void {
|
||||
this.apiKey = apiKey
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a new base URL
|
||||
*/
|
||||
setBaseUrl(baseUrl: string): void {
|
||||
this.baseUrl = normalizeBaseUrl(baseUrl)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the status of an async job
|
||||
* @param taskId The job ID returned from async execution
|
||||
*/
|
||||
async getJobStatus(taskId: string): Promise<JobStatusResult> {
|
||||
const url = `${this.baseUrl}/api/jobs/${taskId}`
|
||||
|
||||
try {
|
||||
const response = await fetch(url, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'X-API-Key': this.apiKey,
|
||||
},
|
||||
})
|
||||
|
||||
this.updateRateLimitInfo(response)
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = (await response.json().catch(() => ({}))) as unknown as any
|
||||
throw new SimStudioError(
|
||||
errorData.error || `HTTP ${response.status}: ${response.statusText}`,
|
||||
errorData.code,
|
||||
response.status
|
||||
)
|
||||
}
|
||||
|
||||
const result = await response.json()
|
||||
return result as JobStatusResult
|
||||
} catch (error: any) {
|
||||
if (error instanceof SimStudioError) {
|
||||
throw error
|
||||
}
|
||||
|
||||
throw new SimStudioError(error?.message || 'Failed to get job status', 'STATUS_ERROR')
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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> {
|
||||
const {
|
||||
maxRetries = 3,
|
||||
initialDelay = 1000,
|
||||
maxDelay = 30000,
|
||||
backoffMultiplier = 2,
|
||||
} = retryOptions
|
||||
|
||||
let lastError: SimStudioError | null = null
|
||||
let delay = initialDelay
|
||||
|
||||
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
||||
try {
|
||||
return await this.executeWorkflow(workflowId, input, options)
|
||||
} catch (error: any) {
|
||||
if (!(error instanceof SimStudioError) || error.code !== 'RATE_LIMIT_EXCEEDED') {
|
||||
throw error
|
||||
}
|
||||
|
||||
lastError = error
|
||||
|
||||
if (attempt === maxRetries) {
|
||||
break
|
||||
}
|
||||
|
||||
const waitTime =
|
||||
error.status === 429 && this.rateLimitInfo?.retryAfter
|
||||
? this.rateLimitInfo.retryAfter
|
||||
: Math.min(delay, maxDelay)
|
||||
|
||||
// standalone package — cannot depend on @sim/utils
|
||||
const jitter = waitTime * (0.75 + Math.random() * 0.5)
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, jitter))
|
||||
|
||||
delay *= backoffMultiplier
|
||||
}
|
||||
}
|
||||
|
||||
throw lastError || new SimStudioError('Max retries exceeded', 'MAX_RETRIES_EXCEEDED')
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current rate limit information
|
||||
*/
|
||||
getRateLimitInfo(): RateLimitInfo | null {
|
||||
return this.rateLimitInfo
|
||||
}
|
||||
|
||||
/**
|
||||
* Update rate limit info from response headers
|
||||
* @private
|
||||
*/
|
||||
private updateRateLimitInfo(response: any): void {
|
||||
const limit = response.headers.get('x-ratelimit-limit')
|
||||
const remaining = response.headers.get('x-ratelimit-remaining')
|
||||
const reset = response.headers.get('x-ratelimit-reset')
|
||||
const retryAfter = response.headers.get('retry-after')
|
||||
|
||||
const resetTime = reset
|
||||
? /^\d+$/.test(reset)
|
||||
? Number.parseInt(reset, 10)
|
||||
: Date.parse(reset)
|
||||
: Number.NaN
|
||||
|
||||
if (limit || remaining || reset) {
|
||||
this.rateLimitInfo = {
|
||||
limit: limit ? Number.parseInt(limit, 10) : 0,
|
||||
remaining: remaining ? Number.parseInt(remaining, 10) : 0,
|
||||
reset: Number.isNaN(resetTime) ? 0 : resetTime,
|
||||
retryAfter: retryAfter ? Number.parseInt(retryAfter, 10) * 1000 : undefined,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current usage limits and quota information
|
||||
*/
|
||||
async getUsageLimits(): Promise<UsageLimits> {
|
||||
const url = `${this.baseUrl}/api/users/me/usage-limits`
|
||||
|
||||
try {
|
||||
const response = await fetch(url, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'X-API-Key': this.apiKey,
|
||||
},
|
||||
})
|
||||
|
||||
this.updateRateLimitInfo(response)
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = (await response.json().catch(() => ({}))) as unknown as any
|
||||
throw new SimStudioError(
|
||||
errorData.error || `HTTP ${response.status}: ${response.statusText}`,
|
||||
errorData.code,
|
||||
response.status
|
||||
)
|
||||
}
|
||||
|
||||
const result = await response.json()
|
||||
return result as UsageLimits
|
||||
} catch (error: any) {
|
||||
if (error instanceof SimStudioError) {
|
||||
throw error
|
||||
}
|
||||
|
||||
throw new SimStudioError(error?.message || 'Failed to get usage limits', 'USAGE_ERROR')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export { SimStudioClient as default }
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"extends": "@sim/tsconfig/library-build.json",
|
||||
"compilerOptions": {
|
||||
"target": "ES2020",
|
||||
"module": "nodenext",
|
||||
"lib": ["ES2020"],
|
||||
"moduleResolution": "nodenext",
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./src"
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["node_modules", "dist", "**/*.test.ts"]
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
/// <reference types="vitest" />
|
||||
import { defineConfig } from 'vitest/config'
|
||||
|
||||
export default defineConfig({
|
||||
test: {
|
||||
globals: true,
|
||||
environment: 'node',
|
||||
include: ['src/**/*.test.{ts,tsx}', 'tests/**/*.test.{ts,tsx}'],
|
||||
exclude: ['**/node_modules/**', '**/dist/**'],
|
||||
},
|
||||
resolve: {
|
||||
conditions: ['node', 'default'],
|
||||
},
|
||||
})
|
||||
Reference in New Issue
Block a user