@synova-cloud/sdk
v2.17.0
Published
Official Node.js SDK for Synova Cloud API
Downloads
1,865
Maintainers
Readme
Synova Cloud SDK for Node.js
Official Node.js SDK for the Synova Cloud API.
Installation
npm install @synova-cloud/sdk
# or
yarn add @synova-cloud/sdk
# or
pnpm add @synova-cloud/sdkQuick Start
import { SynovaCloudSdk } from '@synova-cloud/sdk';
const client = new SynovaCloudSdk('your-api-key');
// Execute a prompt
const response = await client.prompts.execute('prm_abc123', {
provider: 'openai',
model: 'gpt-4o',
variables: { name: 'World' },
});
console.log(response.content);Configuration
import { SynovaCloudSdk } from '@synova-cloud/sdk';
const client = new SynovaCloudSdk('your-api-key', {
baseUrl: 'https://api.synova.cloud', // Custom API URL
timeout: 30000, // Request timeout in ms (default: 30000)
debug: true, // Enable debug logging
retry: {
maxRetries: 3, // Max retry attempts (default: 3)
strategy: 'exponential', // 'exponential' or 'linear' (default: 'exponential')
initialDelayMs: 1000, // Initial retry delay (default: 1000)
maxDelayMs: 30000, // Max retry delay (default: 30000)
backoffMultiplier: 2, // Multiplier for exponential backoff (default: 2)
},
});API Reference
Prompts
Get Prompt
Retrieve a prompt template by ID.
// Get latest version (default)
const prompt = await client.prompts.get('prm_abc123');
// Get by tag
const prompt = await client.prompts.get('prm_abc123', { tag: 'production' });
// Get specific version
const prompt = await client.prompts.get('prm_abc123', { version: '1.2.0' });Execute Prompt
Execute a prompt with an LLM provider.
const response = await client.prompts.execute('prm_abc123', {
provider: 'openai', // Required: 'openai', 'anthropic', 'google', etc.
model: 'gpt-4o', // Required: model ID
variables: { // Optional: template variables
userMessage: 'Hello!',
},
});
console.log(response.content); // LLM response text
console.log(response.usage); // { inputTokens, outputTokens, totalTokens }Execute with Tag or Version
// Execute specific tag
const response = await client.prompts.execute('prm_abc123', {
provider: 'anthropic',
model: 'claude-sonnet-4-20250514',
tag: 'production',
variables: { topic: 'AI' },
});
// Execute specific version
const response = await client.prompts.execute('prm_abc123', {
provider: 'openai',
model: 'gpt-4o',
version: '1.2.0',
variables: { topic: 'AI' },
});With Model Parameters
const response = await client.prompts.execute('prm_abc123', {
provider: 'openai',
model: 'gpt-4o',
variables: { topic: 'TypeScript' },
parameters: {
temperature: 0.7,
maxTokens: 1000,
topP: 0.9,
},
});Response Caching
Cache exact-match LLM responses on the server to save money and latency on repeated calls. The server hashes the full request (provider + model + rendered messages + tools + responseSchema + parameters) and, if a matching non-expired entry exists for your organization, returns the cached response instantly without calling the LLM.
Useful for:
- Debounce / retry — user cancels and re-triggers the same action (e.g. "Improve Text") → second call is free.
- Batch jobs that re-run the same prompt with the same inputs.
- Cron / webhook replays where idempotency matters.
// Enable cache with a 1-hour TTL for this call
const response = await client.prompts.execute('prm_abc123', {
provider: 'anthropic',
model: 'claude-sonnet-4-20250514',
variables: { text: 'Make this more concise' },
cache: { ttl: 3600 }, // seconds
});Omit the cache option to disable caching for a call. Both execute() and stream() (and their tag/version variants) honor the flag. On a cache hit in streaming mode, the full response is emitted as a single text-delta chunk followed by finish — you get the result immediately instead of token-by-token.
What is never cached (regardless of cache.ttl):
- Multi-turn requests (non-empty
messageshistory) — each turn is unique. - Error responses — retries should re-hit the LLM.
- Image / file responses — URLs expire and binaries are too large to cache.
Cache scope and isolation:
- Entries are scoped per organization. No cross-tenant sharing.
- Different variables produce different rendered messages → different cache keys → no false hits.
- Publishing a new prompt version changes the template → new cache keys. Old entries expire naturally by TTL.
Observability:
Cached responses still appear in your traces with a cached: true metadata flag and usage: null (they don't count toward token/cost aggregates).
Provider-Side Prompt Caching
Different mechanism from cache above. Where cache short-circuits the LLM call entirely, providerCache keeps the LLM in the loop but tells the provider to cache the request prefix (system prompt + tool definitions) so repeat invocations pay ~10% of the base input price for the cached portion. The model still runs and generates fresh output every time.
Currently honored by Anthropic (prompt_caching: 'manual' capability). OpenAI caches automatically — the flag is a no-op there.
// Enable with default 5-minute TTL
const response = await client.prompts.execute('prm_abc123', {
provider: 'anthropic',
model: 'claude-opus-4-7',
variables: { question: 'Summarize the document' },
providerCache: true,
});
// Or explicit 1-hour TTL
const response = await client.prompts.execute('prm_abc123', {
provider: 'anthropic',
model: 'claude-opus-4-7',
variables: { question: 'Summarize the document' },
providerCache: { ttl: '1h' },
});TTL choice and pricing impact (Anthropic, relative to base input price):
| TTL | Write cost | Read cost | Pays off after |
|------|-----------:|----------:|---------------:|
| 5m (default) | ~1.25× | ~0.1× | 2 hits |
| 1h | ~2× | ~0.1× | ~3 hits |
Use 5m for chat/burst workloads, 1h when prefix is reused across longer gaps (cron-style invocations, scheduled jobs).
When it works (and when it doesn't):
- The cache is a prefix match. A single byte difference anywhere in the rendered prefix invalidates everything after it. Don't interpolate timestamps, UUIDs, or per-request IDs into the system prompt — keep dynamic content in user messages.
- Minimum cacheable prefix is model-dependent (~4096 tokens for Opus 4.7 / 4.6 / Haiku 4.5; ~2048 for Sonnet 4.6). Shorter prefixes silently won't cache.
- Never cached: multi-turn
messagescontent varying per request — but stabletoolsandsystem(from your prompt template) still cache.
Combining with exact-match cache: cache and providerCache are independent and can be used together. cache wins when it hits (LLM doesn't run); on a miss, providerCache discounts the LLM call.
Verifying: check executionUsage.cacheReadTokens in the response — non-zero means the provider cache was hit. cacheTtl on the same usage object reflects the TTL bucket the write was billed at.
With Your Own API Key
You can pass your own LLM provider API key directly in the request:
// OpenAI, Anthropic, Google, DeepSeek
const response = await client.prompts.execute('prm_abc123', {
provider: 'openai',
model: 'gpt-4o',
apiKey: 'sk-your-openai-key',
variables: { topic: 'TypeScript' },
});
// Azure OpenAI (requires endpoint)
const response = await client.prompts.execute('prm_abc123', {
provider: 'azure_openai',
model: 'my-gpt4-deployment',
apiKey: 'your-azure-key',
azureEndpoint: 'https://my-resource.openai.azure.com',
variables: { topic: 'TypeScript' },
});With Conversation History
const response = await client.prompts.execute('prm_chat456', {
provider: 'anthropic',
model: 'claude-sonnet-4-20250514',
variables: { topic: 'TypeScript' },
messages: [
{ role: 'user', content: 'What is TypeScript?' },
{ role: 'assistant', content: 'TypeScript is a typed superset of JavaScript...' },
{ role: 'user', content: 'How do I use generics?' },
],
});Tools (Function Calling)
Define tools that the LLM can call. If a tool has an execute handler, the SDK runs an agentic loop automatically — calling the LLM, executing tools, and continuing until the LLM returns a final response.
import { z } from 'zod';
const result = await client.prompts.execute('prm_agent123', {
provider: 'anthropic',
model: 'claude-sonnet-4-20250514',
variables: { question: 'What is the weather in Paris?' },
tools: {
getWeather: {
description: 'Get current weather for a city',
parameters: z.object({
city: z.string().describe('City name'),
}),
execute: async ({ city }) => {
// Your tool implementation
const weather = await fetchWeather(city);
return { temperature: weather.temp, condition: weather.condition };
},
},
},
maxToolRoundtrips: 25, // Default: 25, hard cap: 100
toolTimeoutMs: 60000, // Default: 60000 (60s) per tool call
});
console.log(result.content); // "The weather in Paris is 18°C and sunny."Loop limits:
maxToolRoundtrips— how many tool-call cycles the SDK will run before forcing the loop to end. Default 25 (suitable for most agentic workflows). Hard-capped at 100 to protect against runaway loops; values above are clamped silently. Drop to 5–10 for simple single-tool flows where you want tight latency bounds.toolTimeoutMs— per-tool execution timeout. Each tool call is timed independently, so a slow tool doesn't block siblings. Default 60s. Raise for tools that hit slow APIs / DBs / LLMs; lower for latency-critical UIs.
Tools without execute return tool_calls for manual handling:
const result = await client.prompts.execute('prm_abc123', {
provider: 'anthropic',
model: 'claude-sonnet-4-20250514',
tools: {
getWeather: {
description: 'Get weather',
parameters: z.object({ city: z.string() }),
// No execute — SDK returns tool_calls
},
},
});
if (result.type === 'tool_calls') {
for (const call of result.toolCalls) {
console.log(`Tool: ${call.name}, Args:`, call.arguments);
}
}Sending tool results back (manual mode)
To continue the conversation after handling tool_calls yourself, push the assistant's tool-call turn and one tool message per result into messages. One tool result per message — do not batch multiple results into a single tool message with a toolResults: [...] array (the server rejects that shape).
// 1st call returned result.toolCalls = [{ id: 'call_1', name: 'getWeather', arguments: {...} }]
const followUp = await client.prompts.execute('prm_abc123', {
provider: 'anthropic',
model: 'claude-sonnet-4-20250514',
tools: { getWeather: { description: 'Get weather', parameters: z.object({ city: z.string() }) } },
messages: [
// ...prior user/assistant turns...
{ role: 'assistant', toolCalls: result.toolCalls }, // assistant turn that requested the call
{ role: 'tool', toolCallId: 'call_1', content: '{"temp":18,"conditions":"sunny"}' }, // one per result
// if there were 2 calls, you'd have 2 separate { role: 'tool', toolCallId, content } messages
],
});Rules:
contentmust be a string. JSON-stringify objects yourself.toolCallIdmust match theidfrom the assistant'stoolCalls. The server validates this — mismatched/missing IDs reject the request.- The order is: assistant tool-call message → all matching
toolmessages → nextuser(or another assistant turn). Do not interleave.
The SDK's built-in agentic loop (execute() with execute handlers) builds this format for you; you only need this when handling tool calls manually.
Streaming
Stream responses in real-time using stream(). Returns an async generator of events:
const stream = client.prompts.stream('prm_abc123', {
provider: 'anthropic',
model: 'claude-sonnet-4-20250514',
variables: { topic: 'TypeScript' },
});
for await (const event of stream) {
switch (event.type) {
case 'text-delta':
process.stdout.write(event.textDelta);
break;
case 'reasoning-start':
// Model started a thinking block. Use to render a separator/header
// (e.g. an expandable "Thinking…" section in your UI).
break;
case 'reasoning-delta':
// Incremental thinking text. Append to your reasoning buffer.
break;
case 'reasoning-end':
// Thinking block finished. With adaptive thinking, more reasoning
// blocks can follow later in the same stream (interleaved with tool calls).
break;
case 'tool-call':
console.log(`Tool call: ${event.toolName}`, event.args);
break;
case 'finish':
console.log('Done. Usage:', event.usage);
break;
case 'error':
console.error('Error:', event.error);
break;
}
}Also available: streamByTag() and streamByVersion().
Stream events reference
The async generator yielded by stream() can emit any of the following events. All events share the shape ISynovaStreamEvent — fields not listed for an event type are undefined.
| Type | Fields | When it fires |
|---|---|---|
| text-delta | textDelta: string | Incremental visible-output token(s). Append to your answer buffer. |
| reasoning-start | — | Model opened a thinking block (Anthropic adaptive thinking). Render a separator/header. |
| reasoning-delta | reasoningDelta: string | Incremental reasoning text inside the current thinking block. |
| reasoning-end | — | Current thinking block closed. More may follow in the same stream (interleaved with tool calls). |
| tool-call | toolCallId, toolName, args | Model decided to call a tool. Always fires once per call, before the SDK invokes your execute handler (in agentic mode). Use this to commit the assistant's tool-call intent or render UI. |
| tool-call-delta | toolCallId, toolName, argsTextDelta | Incremental JSON of the tool arguments as the model streams them (only some providers/models emit this — most send a single consolidated tool-call). Safe to ignore unless you want a typing-style UX. |
| tool-result | toolCallId, result | Agentic mode only. Fires after your execute handler resolves, with the raw return value. Use to log tool outputs or render results in UI. |
| roundtrip-finish | roundtrip: number, finishReason?, usage? | Agentic mode only. Fires at the end of each non-final roundtrip — i.e. after all tool-call events of that round have been emitted, but before the SDK runs the execute handlers. This is the boundary at which the assistant turn is complete; commit your ASSISTANT row here. roundtrip is 0-based. |
| object-delta | objectDelta: unknown | Incremental partial object during structured-output streaming (responseSchema). |
| finish | finishReason?, usage? | The stream is done. Only fired once, at the very end. In agentic mode, this is the finish of the final roundtrip (intermediate roundtrips emit roundtrip-finish instead). usage here covers only the final roundtrip — sum roundtrip-finish.usage + finish.usage for total tokens. |
| error | error: ISynovaExecutionError | Server returned an execution error mid-stream. The generator terminates after this. |
Streaming agentic loop (tools with execute)
If any tool you pass to stream() has an execute handler, the SDK auto-runs the agentic loop while streaming — same behavior as execute(), just yielded incrementally. Event order in a single roundtrip with tools:
[ reasoning-start → reasoning-delta×N → reasoning-end ]?
[ text-delta×N ]?
tool-call × N ← one per tool the model called
roundtrip-finish ← assistant turn complete; SAFE TO COMMIT here
(SDK runs your execute() handlers in parallel)
tool-result × N ← one per handler that resolved
→ next roundtrip, or final `finish` if no more tool callsIn the final roundtrip (no tool calls, just an answer), you get text-delta/reasoning-* and then a single finish — no trailing roundtrip-finish.
const stream = client.prompts.stream('prm_abc123', {
provider: 'anthropic',
model: 'claude-sonnet-4-20250514',
tools: {
get_weather: {
description: 'Get weather for a city',
parameters: z.object({ city: z.string() }),
execute: async ({ city }) => ({ temp: 21, conditions: 'sunny' }),
},
},
});
for await (const event of stream) {
switch (event.type) {
case 'text-delta':
process.stdout.write(event.textDelta!);
break;
case 'tool-call':
console.log(`→ calling ${event.toolName}`, event.args);
break;
case 'roundtrip-finish':
// Assistant turn finished. Commit ASSISTANT row to your DB here —
// all tool-call events for this round have already been emitted.
console.log(`roundtrip ${event.roundtrip} done, usage:`, event.usage);
break;
case 'tool-result':
console.log(`← ${event.toolCallId} returned`, event.result);
break;
case 'finish':
console.log('All done. Final usage:', event.usage);
break;
}
}If no tools have execute (or you pass no tools), stream() runs in pass-through mode: events come straight from the server and you handle tool-call yourself.
Thinking / Reasoning
Surface the model's chain of thought to the user. Currently supported on Anthropic Opus 4.7, Opus 4.6, and Sonnet 4.6.
Two parameters control it (passed via parameters):
| Parameter | Type | What it does |
|---|---|---|
| adaptiveThinking | boolean | Lets the model decide when and how much to think. Required to get any reasoning output. |
| thinkingDisplay | 'summarized' \| 'omitted' | Controls whether reasoning text is returned. Required as 'summarized' on Opus 4.7 — its default is 'omitted' (silent change), so without it you'll get empty thinking blocks. Optional on Opus 4.6 / Sonnet 4.6 (default there is 'summarized'). |
Cost note: The model thinks the same amount regardless of thinkingDisplay. Reasoning tokens are charged as output tokens whether or not you receive the text — so showing thinking is essentially free.
Stream — incremental thinking:
const stream = client.prompts.stream('prm_abc123', {
provider: 'anthropic',
model: 'claude-opus-4-7',
variables: { question: 'Plan a refactor for this module' },
parameters: {
adaptiveThinking: true,
thinkingDisplay: 'summarized', // required on 4.7 to actually get text
effort: 'xhigh', // recommended on 4.7 for coding/agentic
},
});
let answer = '';
let reasoning = '';
for await (const event of stream) {
switch (event.type) {
case 'reasoning-start':
console.log('\n--- thinking ---');
break;
case 'reasoning-delta':
reasoning += event.reasoningDelta;
process.stdout.write(event.reasoningDelta!);
break;
case 'reasoning-end':
console.log('\n--- /thinking ---');
break;
case 'text-delta':
answer += event.textDelta;
process.stdout.write(event.textDelta!);
break;
case 'finish':
console.log('\nReasoning tokens:', event.usage?.reasoningTokens);
break;
}
}Adaptive thinking interleaves with tool calls. A single turn can emit:
reasoning-start → reasoning-delta×N → reasoning-end
→ tool-call → (your handler runs) →
reasoning-start → reasoning-delta×N → reasoning-end
→ text-delta×N → finishSo expect multiple reasoning blocks per stream — use reasoning-start/reasoning-end markers to render them as separate chunks in the UI.
Sync — full thinking on the response:
const response = await client.prompts.execute('prm_abc123', {
provider: 'anthropic',
model: 'claude-opus-4-7',
variables: { question: '...' },
parameters: { adaptiveThinking: true, thinkingDisplay: 'summarized' },
});
console.log('Answer: ', response.content);
console.log('Thinking: ', response.reasoning);
console.log('Reasoning tokens:', response.executionUsage?.reasoningTokens);Behavior matrix:
| Model | adaptiveThinking | thinkingDisplay | What you get |
|---|---|---|---|
| Opus 4.7 | true | omitted (default) | empty thinking blocks |
| Opus 4.7 | true | 'summarized' | summarized reasoning text |
| Opus 4.7 | false / unset | — | no thinking (model doesn't think) |
| Opus 4.6 / Sonnet 4.6 | true | unset / 'summarized' | summarized reasoning text |
| Opus 4.6 / Sonnet 4.6 | true | 'omitted' | empty thinking blocks |
| Other models | — | — | params silently ignored |
Image Generation
const response = await client.prompts.generateImage('prm_image123', {
provider: 'openai',
model: 'dall-e-3',
variables: { description: 'A sunset over mountains' },
});
if (response.type === 'image') {
for (const file of response.files) {
console.log('Generated image:', file.url);
console.log('MIME type:', file.mimeType);
}
}Structured Output (Zod, Yup, Joi)
You can pass a schema from your preferred validation library as responseSchema to get structured JSON responses from LLMs. The SDK automatically converts it to JSON Schema.
Zod (v3 and v4):
import { z } from 'zod'; // or 'zod/v4'
const schema = z.object({
title: z.string(),
keywords: z.array(z.string()),
priority: z.number(),
});
const response = await client.prompts.execute('prm_abc123', {
provider: 'openai',
model: 'gpt-4o',
responseSchema: schema,
});
console.log(response.object); // { title: '...', keywords: [...], priority: 1 }Yup:
import * as yup from 'yup';
const schema = yup.object({
title: yup.string().required(),
keywords: yup.array().of(yup.string()).required(),
priority: yup.number().required(),
});
const response = await client.prompts.execute('prm_abc123', {
provider: 'openai',
model: 'gpt-4o',
responseSchema: schema,
});Joi:
import Joi from 'joi';
const schema = Joi.object({
title: Joi.string().required(),
keywords: Joi.array().items(Joi.string()).required(),
priority: Joi.number().required(),
});
const response = await client.prompts.execute('prm_abc123', {
provider: 'openai',
model: 'gpt-4o',
responseSchema: schema,
});Raw JSON Schema:
const response = await client.prompts.execute('prm_abc123', {
provider: 'openai',
model: 'gpt-4o',
responseSchema: {
type: 'object',
properties: {
title: { type: 'string' },
keywords: { type: 'array', items: { type: 'string' } },
priority: { type: 'number' },
},
required: ['title', 'keywords', 'priority'],
},
});Observability
Track and group your LLM calls using traces and spans. Each execution creates a trace with a span. Pass the returned traceId in subsequent calls to add spans to the same trace.
Trace-Based Grouping
Use traceId to group related calls (e.g., a conversation) into a single trace:
// First message - creates new trace
const response1 = await client.prompts.execute('prm_abc123', {
provider: 'openai',
model: 'gpt-4o',
variables: { topic: 'TypeScript' },
});
console.log(response1.traceId); // trc_xxx
console.log(response1.spanId); // spn_xxx
// Follow-up - pass traceId to add span to the same trace
const response2 = await client.prompts.execute('prm_abc123', {
provider: 'openai',
model: 'gpt-4o',
traceId: response1.traceId,
messages: [
{ role: 'assistant', content: response1.content },
{ role: 'user', content: 'Tell me more' },
],
});
// response2.traceId === response1.traceId (same trace)
// response2.spanId !== response1.spanId (new span)Response Properties
Every execution returns observability IDs:
| Property | Type | Description |
|----------|------|-------------|
| spanDataId | string | Execution data ID (messages, response, usage) |
| traceId | string | Trace ID (groups related calls) |
| spanId | string | Span ID (this specific call) |
Custom Span Tracking
Track tool calls, retrieval operations, and custom logic as spans within a trace.
Manual approach:
// Create span
const span = await client.spans.create(traceId, {
type: 'tool',
toolName: 'fetch_weather',
toolArguments: { city: 'NYC' },
parentSpanId: generationSpanId,
});
// Execute
const weather = await fetchWeather('NYC');
// End span
await client.spans.end(span.id, {
status: 'completed',
toolResult: weather,
});Wrapper approach:
// wrapTool() - for tools
const weather = await client.spans.wrapTool(
{ traceId, toolName: 'fetch_weather', parentSpanId },
{ city: 'NYC' },
async (args) => fetchWeather(args.city),
);
// wrap() - for custom/retriever/embedding
const docs = await client.spans.wrap(
{ traceId, type: 'retriever', name: 'vector_search' },
{ query: 'how to...', topK: 5 },
async () => vectorDb.search(query),
);Wrappers automatically handle errors and set status: 'error' with message.
Span Types
| Type | Use Case |
|------|----------|
| generation | LLM calls (auto-created by execute()) |
| tool | Tool/function calls |
| retriever | RAG document retrieval |
| embedding | Embedding generation |
| custom | Any custom operation |
Viewing Traces
View your traces in the Synova Cloud Dashboard under the Observability section. Each trace shows:
- All spans (LLM calls) in the session
- Input/output for each span
- Token usage and latency
- Error details if any
Models
List All Models
const { providers } = await client.models.list();
for (const provider of providers) {
console.log(`${provider.displayName}:`);
for (const model of provider.models) {
console.log(` - ${model.displayName} (${model.id})`);
}
}Filter Models
// Filter by type
const textModels = await client.models.list({ type: 'text' });
const imageModels = await client.models.list({ type: 'image' });
// Filter by capability
const visionModels = await client.models.list({ capability: 'vision' });
// Filter by provider
const openaiModels = await client.models.list({ provider: 'openai' });Get Models by Provider
const models = await client.models.getByProvider('anthropic');
for (const model of models) {
console.log(`${model.displayName}: context=${model.limits.contextWindow}`);
}Get Specific Model
const model = await client.models.get('openai', 'gpt-4o');
console.log('Capabilities:', model.capabilities);
console.log('Context window:', model.limits.contextWindow);
console.log('Pricing:', model.pricing);Files
Upload Files
Upload files for use in prompt execution (e.g., images for vision models).
const result = await client.files.upload(
[file1, file2], // File[] or Blob[]
{ projectId: 'prj_abc123' }
);
for (const file of result.data) {
console.log(`Uploaded: ${file.originalName}`);
console.log(` ID: ${file.id}`);
console.log(` URL: ${file.url}`);
console.log(` Size: ${file.size} bytes`);
}Use Uploaded Files in Messages
// Upload an image
const uploadResult = await client.files.upload([imageFile], { projectId: 'prj_abc123' });
const fileId = uploadResult.data[0].id;
// Use in prompt execution with vision model
const response = await client.prompts.execute('prm_vision123', {
provider: 'openai',
model: 'gpt-4o',
messages: [
{
role: 'user',
content: 'What is in this image?',
files: [{ fileId }],
},
],
});
console.log(response.content);Error Handling
The SDK provides typed errors for different failure scenarios:
import {
SynovaCloudSdk,
ExecutionSynovaError,
AuthSynovaError,
NotFoundSynovaError,
RateLimitSynovaError,
ServerSynovaError,
TimeoutSynovaError,
NetworkSynovaError,
ApiSynovaError,
} from '@synova-cloud/sdk';
try {
const response = await client.prompts.execute('prm_abc123', {
provider: 'openai',
model: 'gpt-4o',
});
} catch (error) {
// LLM execution error (rate limit, invalid key, context too long, etc.)
if (error instanceof ExecutionSynovaError) {
console.error(`LLM error [${error.code}]: ${error.message}`);
console.error(`Provider: ${error.provider}`);
console.error(`Retryable: ${error.retryable}`);
if (error.retryAfterMs) {
console.error(`Retry after: ${error.retryAfterMs}ms`);
}
}
// API errors
if (error instanceof AuthSynovaError) {
console.error('Invalid API key');
} else if (error instanceof NotFoundSynovaError) {
console.error('Resource not found');
} else if (error instanceof RateLimitSynovaError) {
console.error(`Rate limited. Retry after: ${error.retryAfterMs}ms`);
} else if (error instanceof ServerSynovaError) {
console.error(`Server error: ${error.message}`);
} else if (error instanceof TimeoutSynovaError) {
console.error(`Request timed out after ${error.timeoutMs}ms`);
} else if (error instanceof NetworkSynovaError) {
console.error(`Network error: ${error.message}`);
} else if (error instanceof ApiSynovaError) {
console.error(`API error [${error.code}]: ${error.message}`);
console.error(`Request ID: ${error.requestId}`);
}
}Error Properties
All API errors (AuthSynovaError, NotFoundSynovaError, RateLimitSynovaError, ServerSynovaError, and ApiSynovaError) include:
| Property | Type | Description |
|----------|------|-------------|
| code | string | Error code (e.g., "common.validation") |
| httpCode | number | HTTP status code |
| message | string | Human-readable error message |
| requestId | string | Request ID for debugging |
| timestamp | string | When the error occurred |
| path | string? | API endpoint path |
| method | string? | HTTP method |
| details | unknown? | Additional details (e.g., validation errors) |
Validation Errors
When validation fails, details contains an array of field errors:
try {
await client.llmProviderKeys.create({ provider: 'invalid' as any, apiKey: '' });
} catch (error) {
if (error instanceof ApiSynovaError && error.code === 'common.validation') {
console.log('Validation errors:', error.details);
// [{ field: "provider", message: "must be a valid enum value", code: "FIELD_PROVIDER_INVALID" }]
}
}Retry Behavior
The SDK automatically retries requests on:
- Rate limit errors (429) - uses
Retry-Afterheader if available - Server errors (5xx)
- Network errors
- Timeout errors
Non-retryable errors (fail immediately):
- Authentication errors (401)
- Not found errors (404)
- Client errors (4xx)
Retry Strategies
Exponential Backoff (default):
delay = initialDelayMs * backoffMultiplier^(attempt-1)Example with defaults: 1000ms, 2000ms, 4000ms...
Linear:
delay = initialDelayMs * attemptExample with defaults: 1000ms, 2000ms, 3000ms...
Both strategies add ±10% jitter to prevent thundering herd.
Custom Logger
You can provide a custom logger that implements the ISynovaLogger interface:
import { SynovaCloudSdk, ISynovaLogger } from '@synova-cloud/sdk';
const customLogger: ISynovaLogger = {
debug: (message, ...args) => myLogger.debug(message, ...args),
info: (message, ...args) => myLogger.info(message, ...args),
warn: (message, ...args) => myLogger.warn(message, ...args),
error: (messageOrError, ...args) => {
if (messageOrError instanceof Error) {
myLogger.error(messageOrError.message, messageOrError, ...args);
} else {
myLogger.error(messageOrError, ...args);
}
},
};
const client = new SynovaCloudSdk('your-api-key', {
debug: true,
logger: customLogger,
});TypeScript
The SDK is written in TypeScript and provides full type definitions:
import type {
// Config
ISynovaConfig,
ISynovaRetryConfig,
TSynovaRetryStrategy,
ISynovaLogger,
// Prompts
ISynovaPrompt,
ISynovaPromptVariable,
ISynovaGetPromptOptions,
// Execution
ISynovaExecuteOptions, // includes traceId, tools, maxToolRoundtrips, cache
ISynovaExecuteCacheOption, // { ttl: number }
ISynovaExecuteResponse, // includes spanDataId, traceId, spanId
ISynovaTypedExecuteResponse, // typed structured output
ISynovaExecutionUsage,
ISynovaExecutionError,
// Messages
ISynovaMessage,
TSynovaMessageRole,
TSynovaResponseType,
// Tools
ISynovaTool,
ISynovaToolParameter,
TSynovaTools,
// Stream Events
ISynovaStreamEvent,
TSynovaStreamEventType,
// Spans
ISynovaSpan,
ISynovaSpanData,
ISynovaCreateSpanOptions,
ISynovaEndSpanOptions,
ISynovaWrapOptions,
ISynovaWrapToolOptions,
TSynovaSpanType,
TSynovaSpanStatus,
TSynovaSpanLevel,
// Files
ISynovaFileAttachment,
ISynovaFileThumbnails,
ISynovaUploadedFile,
ISynovaUploadResponse,
ISynovaUploadOptions,
// Models
ISynovaModel,
ISynovaModelCapabilities,
ISynovaModelLimits,
ISynovaModelPricing,
ISynovaProvider,
ISynovaModelsResponse,
ISynovaListModelsOptions,
TSynovaModelType,
// Schema
IJsonSchema,
TJsonSchemaType,
TJsonSchemaFormat,
} from '@synova-cloud/sdk';CommonJS
The SDK supports both ESM and CommonJS:
const { SynovaCloudSdk } = require('@synova-cloud/sdk');
const client = new SynovaCloudSdk('your-api-key');Requirements
- Node.js 18+ (uses native
fetch)
Optional Peer Dependencies
For structured output with responseSchema:
# Zod v4 (recommended, native JSON Schema support)
npm install zod
# Zod v3 (requires additional package)
npm install zod zod-to-json-schema
# Yup
npm install yup @sodaru/yup-to-json-schema
# Joi
npm install joi joi-to-jsonThese are all optional - install only what you need.
License
MIT
