Skip to main content

TypeScript SDK Reference

The native TypeScript SDK for the Ferro Labs AI Gateway. A single OpenAI-compatible client routes chat, embeddings, and image requests across 30 providers, with first-class types, streaming, and an admin API.

  • Package: @ferro-labs-ai/sdk (v0.2.0)
  • Runtime: Node.js 18+ (uses the global fetch)
  • Modules: ESM and CommonJS, fully typed

Installationโ€‹

npm install @ferro-labs-ai/sdk

Authenticationโ€‹

The client reads credentials from environment variables or constructor options:

export FERRO_API_KEY=sk-ferro-...
export FERRO_BASE_URL=http://localhost:8080 # default

Or pass them directly:

import { FerroClient } from "@ferro-labs-ai/sdk";

const client = new FerroClient({
apiKey: "sk-ferro-...",
baseUrl: "https://gateway.example.com",
});
tip

If apiKey is omitted, the client checks FERRO_API_KEY and then falls back to OPENAI_API_KEY, so existing OpenAI key setups work out of the box. When no key is found, the constructor throws FerroAuthError.

FerroClientโ€‹

import { FerroClient } from "@ferro-labs-ai/sdk";

const client = new FerroClient();

Constructor optionsโ€‹

new FerroClient(options?: FerroClientOptions)

OptionTypeDefaultDescription
apiKeystringโ€”API key. Falls back to FERRO_API_KEY, then OPENAI_API_KEY
baseUrlstring"http://localhost:8080"Gateway URL. Falls back to FERRO_BASE_URL. Trailing slashes are trimmed
timeoutnumber120000Request timeout in milliseconds
maxRetriesnumber2Retries for connection errors and timeouts (must be a non-negative integer)
defaultHeadersRecord<string, string>โ€”Extra headers merged into every request
fetchtypeof fetchglobalThis.fetchCustom fetch implementation
logLevel"debug" | "info" | "warn" | "error" | "none""none"SDK debug logging. Also set via FERRO_LOG_LEVEL

The client exposes the resource groups client.chat.completions, client.embeddings, client.images, client.models, and client.admin, plus a read-only client.version string.

client.chat.completions.create()โ€‹

Create a chat completion. The method is overloaded: pass stream: true to receive a Stream<ChatCompletionChunk>; otherwise it resolves to a ChatCompletion.

const response = await client.chat.completions.create({
model: "gpt-4o",
messages: [{ role: "user", content: "Hello from Ferro Labs!" }],
});

console.log(response.choices[0].message.content);
console.log(`Provider: ${response.provider}`);
console.log(`Cost: $${response.usage?.cost_usd}`);
console.log(`Trace ID: ${response.trace_id}`);

The response carries Ferro-specific fields โ€” provider, trace_id, latency_ms, and usage.cost_usd / usage.cache_hit โ€” alongside the standard OpenAI response shape.

Parameters (ChatCompletionCreateParams)โ€‹

ParameterTypeRequiredDescription
modelstringYesModel name (e.g. "gpt-4o", "claude-3-5-sonnet-20241022")
messagesChatMessageParam[]YesConversation messages
streambooleanNoIf true, returns Stream<ChatCompletionChunk>
temperaturenumberNoSampling temperature
max_tokensnumberNoMaximum tokens in the response
max_completion_tokensnumberNoMaximum completion tokens
top_pnumberNoNucleus sampling parameter
nnumberNoNumber of completions
seednumberNoDeterministic sampling seed
frequency_penaltynumberNoFrequency penalty
presence_penaltynumberNoPresence penalty
stopstring | string[]NoStop sequences
toolsTool[]NoTool/function definitions
tool_choicestring | ToolChoiceNoTool selection strategy
response_formatResponseFormatNotext, json_object, or json_schema
logprobsbooleanNoReturn log probabilities
top_logprobsnumberNoNumber of top log probabilities
logit_biasRecord<string, number>NoToken bias map
userstringNoEnd-user identifier
template_idstringNoFerro-specific (Managed only): server-side prompt template ID
template_variablesRecord<string, unknown>NoFerro-specific (Managed only): template variables
route_tagstringNoFerro-specific: override routing strategy

Returnsโ€‹

  • Non-streaming: Promise<ChatCompletion>
  • Streaming: Promise<Stream<ChatCompletionChunk>>

Streamingโ€‹

Stream<T> implements AsyncIterable<T>, so you consume it with for await. Call .abort() to cancel the underlying request.

const stream = await client.chat.completions.create({
model: "claude-3-5-sonnet-20241022",
messages: [{ role: "user", content: "Write a haiku about AI gateways" }],
stream: true,
});

for await (const chunk of stream) {
process.stdout.write(chunk.choices[0]?.delta.content ?? "");
}
// Cancel an in-flight stream
const stream = await client.chat.completions.create({
model: "gpt-4o",
messages: [{ role: "user", content: "Tell me a long story" }],
stream: true,
});

setTimeout(() => stream.abort(), 1000);

Tool callingโ€‹

const response = await client.chat.completions.create({
model: "gpt-4o",
messages: [{ role: "user", content: "What is the weather in Tokyo?" }],
tools: [
{
type: "function",
function: {
name: "get_weather",
description: "Get the current weather for a city",
parameters: {
type: "object",
properties: { city: { type: "string" } },
required: ["city"],
},
},
},
],
tool_choice: "auto",
});

const toolCalls = response.choices[0].message.tool_calls;

Ferro-specific routingโ€‹

const response = await client.chat.completions.create({
model: "gpt-4o",
messages: [{ role: "user", content: "Summarize this document" }],
template_id: "summarizer-v2", // server-side template (Managed only)
template_variables: { tone: "formal" }, // template variables (Managed only)
route_tag: "premium", // override routing strategy
});
note

template_id / template_variables require server-side prompt templates, a Ferro Labs Managed feature. They are ignored by the open-source gateway.

Response typesโ€‹

interface ChatCompletion {
id: string;
object: string;
created: number;
model: string;
choices: Choice[];
usage: Usage | null;
trace_id?: string; // Ferro-specific
provider?: string; // Ferro-specific
latency_ms?: number; // Ferro-specific
}

interface Choice {
index: number;
message: ChatMessage;
finish_reason: string;
}

interface ChatMessage {
role: string;
content: string | null;
tool_calls?: ToolCall[];
}

interface Usage {
prompt_tokens: number;
completion_tokens: number;
total_tokens: number;
cost_usd?: number; // Ferro-specific
cache_hit?: boolean; // Ferro-specific
provider?: string; // Ferro-specific
}

For streaming, each chunk is a ChatCompletionChunk whose choices[i].delta is a StreamDelta (role?, content?, tool_calls?).

client.embeddings.create()โ€‹

Generate embeddings for one or more inputs.

const response = await client.embeddings.create({
model: "text-embedding-3-small",
input: ["Ferro routes LLM requests", "across many providers"],
});

for (const item of response.data) {
console.log(`Dimension: ${item.embedding.length}`);
}

Parameters (EmbeddingCreateParams)โ€‹

ParameterTypeRequiredDescription
modelstringYesEmbedding model name
inputstring | string[]YesText to embed (single string or batch)
encoding_format"float" | "base64"NoOutput encoding format
dimensionsnumberNoDesired embedding dimensions (model-dependent)
userstringNoEnd-user identifier

Returns: Promise<EmbeddingResponse> โ€” { object, data: EmbeddingData[], model, usage }, where each EmbeddingData is { index, embedding: number[], object }.

client.images.generate()โ€‹

Generate images from a text prompt.

const response = await client.images.generate({
model: "dall-e-3",
prompt: "A futuristic AI gateway routing requests across the cosmos",
size: "1024x1024",
});

console.log(response.data[0].url);

Parameters (ImageGenerateParams)โ€‹

ParameterTypeRequiredDescription
modelstringYesImage generation model name
promptstringYesDescription of the desired image
nnumberNoNumber of images to generate
sizestringNoImage size (e.g. "1024x1024")
quality"standard" | "hd"NoImage quality
response_format"url" | "b64_json"NoResponse format
stylestringNoImage style
userstringNoEnd-user identifier

Returns: Promise<ImageResponse> โ€” { created, data: ImageData[] }, where each ImageData is { url?, b64_json?, revised_prompt? }.

client.modelsโ€‹

Discover the models available on the gateway.

// List models from a specific provider
const models = await client.models.list({ provider: "anthropic" });
for (const m of models) {
console.log(`${m.id} โ€” ${m.context_window} tokens`);
}

// Retrieve a single model
const model = await client.models.retrieve("gpt-4o");

// Search the catalog
const results = await client.models.search("embedding");
MethodSignatureReturns
listlist(params?: ModelListParams)Promise<ModelInfo[]>
retrieveretrieve(modelId: string)Promise<ModelInfo>
searchsearch(query: string)Promise<ModelInfo[]>

ModelListParams accepts optional provider and capability filters. ModelInfo exposes id, object, provider, context_window, max_output_tokens, capabilities, status, and per-token pricing fields.

client.adminโ€‹

Administrative API for managing keys, configuration, logs, providers, and plugins.

caution

Admin endpoints require an API key with admin-level scopes.

admin.keysโ€‹

// Create a key โ€” the plaintext `key` is only returned on creation
const created = await client.admin.keys.create({
name: "production-backend",
scopes: ["chat", "embeddings"],
expires_at: "2026-12-31T23:59:59Z",
});
console.log(created.key);

const keys = await client.admin.keys.list();
const key = await client.admin.keys.retrieve("key_abc123");
await client.admin.keys.update("key_abc123", { name: "renamed-key", active: false });
const rotated = await client.admin.keys.rotate("key_abc123");
await client.admin.keys.revoke("key_abc123");
await client.admin.keys.delete("key_abc123");
const usage = await client.admin.keys.usage({ limit: 10, sort: "usage", active: true });
MethodSignatureReturns
listlist()Promise<APIKey[]>
retrieveretrieve(id: string)Promise<APIKey>
createcreate(params: KeyCreateParams)Promise<CreatedAPIKey>
updateupdate(id: string, params: KeyUpdateParams)Promise<APIKey>
deletedelete(id: string)Promise<void>
revokerevoke(id: string)Promise<void>
rotaterotate(id: string)Promise<CreatedAPIKey>
usageusage(params?: KeyUsageParams)Promise<Record<string, unknown>>

admin.configโ€‹

const config = await client.admin.config.get();
await client.admin.config.update({ routing: { strategy: "latency" } });
const history = await client.admin.config.history();
await client.admin.config.rollback(3);
MethodSignatureReturns
getget()Promise<GatewayConfig>
createcreate(config: Record<string, unknown>)Promise<Record<string, unknown>>
updateupdate(config: Record<string, unknown>)Promise<Record<string, unknown>>
deletedelete()Promise<Record<string, unknown>>
historyhistory()Promise<ConfigHistoryEntry[]>
rollbackrollback(version: number)Promise<Record<string, unknown>>

GatewayConfig is { strategy, targets, plugins, aliases, raw }.

admin.logsโ€‹

const logs = await client.admin.logs.list({ limit: 50, provider: "openai", model: "gpt-4o" });
const stats = await client.admin.logs.stats({ since: "2026-01-01T00:00:00Z" });
await client.admin.logs.delete({ before: "2025-01-01T00:00:00Z" });
MethodSignatureReturns
listlist(params?: LogListParams)Promise<Record<string, unknown>>
statsstats(params?: LogStatsParams)Promise<Record<string, unknown>>
deletedelete(params?: LogDeleteParams)Promise<Record<string, unknown>>

admin.providers and admin.pluginsโ€‹

const providers = await client.admin.providers.list();
const plugins = await client.admin.plugins.list();

Both return Promise<Record<string, unknown>[]>.

Convenience methodsโ€‹

const dashboard = await client.admin.dashboard(); // gateway health + usage summary
const health = await client.admin.health(); // gateway health status

Both return Promise<Record<string, unknown>>.

Error handlingโ€‹

The SDK raises typed errors for every failure mode. HTTP errors inherit from FerroAPIError, which exposes status, code, and requestId. All SDK errors inherit from the base FerroError.

FerroError                        // Base โ€” catch-all for all SDK errors
โ”œโ”€โ”€ FerroAPIError // Any non-2xx HTTP response (status, code, requestId)
โ”‚ โ”œโ”€โ”€ FerroAuthError // 401 Unauthorized
โ”‚ โ”œโ”€โ”€ FerroRateLimitError // 429 Too Many Requests
โ”‚ โ”œโ”€โ”€ FerroNotFoundError // 404 Not Found
โ”‚ โ””โ”€โ”€ FerroServerError // 5xx Server Error
โ”œโ”€โ”€ FerroConnectionError // Network / timeout (retried automatically)
โ””โ”€โ”€ FerroStreamError // SSE parse failure during streaming
ErrorHTTP statusDescription
FerroAuthError401Invalid or missing API key
FerroNotFoundError404Model or endpoint not found
FerroRateLimitError429Rate limit or quota exceeded
FerroServerError5xxGateway or upstream provider error
FerroAPIErrornon-2xxBase class for all HTTP errors
FerroConnectionErrorโ€”Cannot reach the gateway (network error, timeout)
FerroStreamErrorโ€”Error while consuming a streaming response
FerroErrorโ€”Base class for all SDK errors
import {
FerroClient,
FerroAuthError,
FerroRateLimitError,
FerroNotFoundError,
FerroServerError,
FerroConnectionError,
FerroStreamError,
FerroAPIError,
FerroError,
} from "@ferro-labs-ai/sdk";

const client = new FerroClient();

try {
const response = await client.chat.completions.create({
model: "gpt-4o",
messages: [{ role: "user", content: "Hello" }],
});
console.log(response.choices[0].message.content);
} catch (err) {
if (err instanceof FerroAuthError) {
console.error("Invalid or missing API key");
} else if (err instanceof FerroRateLimitError) {
console.error("Rate limited โ€” back off and retry");
} else if (err instanceof FerroNotFoundError) {
console.error("Model or endpoint not found");
} else if (err instanceof FerroServerError) {
console.error(`Server error (${err.status}): ${err.message}`);
} else if (err instanceof FerroConnectionError) {
console.error("Network error โ€” retries exhausted");
} else if (err instanceof FerroStreamError) {
console.error("Failed to parse streaming response");
} else if (err instanceof FerroAPIError) {
console.error(`API error ${err.status} (request ${err.requestId}): ${err.message}`);
} else if (err instanceof FerroError) {
console.error(`Unexpected SDK error: ${err.message}`);
} else {
throw err;
}
}
warning

Only connection errors and timeouts are retried (up to maxRetries). HTTP 4xx and 5xx responses are never retried โ€” the gateway already handles provider-level retries, so the SDK treats its response as final.

LangChain integrationโ€‹

The package ships an optional LangChain chat model under the @ferro-labs-ai/sdk/langchain subpath (requires @langchain/core as a peer dependency):

import { FerroChatModel } from "@ferro-labs-ai/sdk/langchain";

const model = new FerroChatModel({
model: "gpt-4o",
apiKey: "sk-ferro-...",
baseUrl: "https://gateway.example.com",
});

const result = await model.invoke("Hello from LangChain!");

Next stepsโ€‹