@metrio-ai/client
v1.6.4
Published
The official Metrio AI SDK for JavaScript and TypeScript
Readme
@metrio-ai/client
The official JavaScript/TypeScript SDK for MetrioAI — native fetch (no HTTP dependencies), automatic retries, and full type definitions. The metrio CLI now ships separately as @metrio-ai/cli. If you installed the old CLI globally, run npm rm -g @metrio-ai/client before npm i -g @metrio-ai/cli.
Installation
npm install @metrio-ai/client # or: yarn add / pnpm add @metrio-ai/clientQuick Start
import { MetrioAI } from '@metrio-ai/client';
// CommonJS: const { MetrioAI } = require('@metrio-ai/client');
const client = new MetrioAI({
apiKey: 'your-api-key', // or set METRIOAI_API_KEY in the environment
// Optional:
baseUrl: 'https://api.metrio.ai', // default (or METRIOAI_BASE_URL)
maxRetries: 3, // retry attempts for failed requests
retryDelay: 500 // base delay between retries (ms)
});
const response = await client.chatCompletion({
promptId: 1,
messages: [
{ role: 'system', content: { type: 'text', text: 'You are a helpful assistant.' } },
{ role: 'user', content: { type: 'text', text: 'What is the capital of France?' } }
]
});
console.log(response.response);Providers and Models
const providers = await client.providers();
console.log(providers.providers); // ['openai', 'anthropic', 'gemini', 'xai', ...]
const models = await client.models('openai');
console.log(models.models); // ['gpt-3.5-turbo', 'gpt-4', ...]Chat Completion
const response = await client.chatCompletion({
promptId: 1,
messages: messages,
// Optional parameters
variables: [{ name: 'customVar', value: 'customValue' }],
tag: 'custom-tag', // Version tag to pin (defaults to active version)
timezone: 'Asia/Taipei', // IANA timezone — adds local time + offset + weekday context
tags: ['test'] // Request-level tags recorded on the run (see "Request Tags")
});
console.log(response.response); // The AI-generated response
console.log(response.inputTokens); // Number of input tokens
console.log(response.outputTokens); // Number of output tokens
console.log(response.cacheReadTokens); // Tokens read from prompt cache (if applicable)
console.log(response.cacheCreationTokens); // Tokens written to prompt cache (if applicable)
console.log(response.elapsedTime); // Time taken in milliseconds
console.log(response.elapsedTimeFormatted); // Human-readable elapsed time (e.g., "1.5s")
console.log(response.logId); // Log ID for conversation history
console.log(response.taskId); // Task ID for agent orchestration trackingNote:
chatCompletionno longer requiresprojectId— your project is determined by the API key. The field is still accepted for backward compatibility but is silently ignored.
Request Tags
Both chatCompletion and workflowCompletion accept an optional tags: string[]. Tags are recorded on the run so logs can later be filtered by tag — for example, tag automated traffic with ['test'] to keep it out of your real-usage metrics.
Validation (enforced client-side before the request is sent, mirroring the API):
- At most 5 tags per request.
- Each tag is 1–32 characters matching
^[a-z0-9][a-z0-9_-]{0,31}$. - An empty array is treated as no tags and the field is omitted from the request.
An invalid tag throws a MetrioApiError and no request is sent.
Streaming Responses
const completion = await client.chatCompletion({
promptId: 1,
messages: messages,
stream: true
}, (chunk) => {
console.log('Received chunk:', chunk.chunk); // called for each chunk
});
console.log('Final response:', completion.response); // complete response when doneThe SDK handles both SSE-formatted chunks (parsed as JSON with full metadata) and plain text lines, and combines all chunks into the final response.
MCP Tools Requiring Approval
For MCP (Model Context Protocol) tools that need human approval before execution, list them in toolsRequiringApproval. When approval is needed the response includes a logId; make a follow-up call with the user's approval message and that logId, and MetrioAI retrieves the stored conversation history and continues execution.
// Step 1: Initial request with tools requiring approval
const initialResponse = await client.chatCompletion({
promptId: 1,
messages: [{ role: 'user', content: { type: 'text', text: 'Send a message to the team on Slack about the deployment' } }],
toolsRequiringApproval: ['send-slack-message', 'post-to-channel']
});
// Step 2: After the user approves, continue with the logId
const approvalResponse = await client.chatCompletion({
promptId: 1,
messages: [{ role: 'user', content: { type: 'text', text: 'Approved. Please proceed.' } }],
logId: initialResponse.logId
});Multimodal Input
Messages support text, images, PDFs, and binary content:
const completion = await client.chatCompletion({
promptId: 1,
messages: [
{
role: 'user',
content: {
type: 'image', // or 'pdf' with mime 'application/pdf'
mime: 'image/jpeg',
data: 'base64encodedimagedata...'
}
}
]
});Workflow Completion
Run a workflow by its slug. This is parallel to chatCompletion — same base URL, same auth, same messages shape — but it executes a multi-node workflow and returns a structured reply plus a per-node trace. It is non-streaming.
const result = await client.workflowCompletion({
workflowId: 'groceriesfriends-order', // required — the workflow slug
messages: [
{ role: 'user', content: { type: 'text', text: '我要訂 5 包麵粉' } }
],
// Optional
version: 3, // pin a workflow version; omit to run the active version
channel: { User: { name: '王小明', customer_id: '44785' } }, // surfaced as $channel
variables: { region: 'TW' }, // surfaced as $variables
timezone: 'Asia/Taipei',
tags: ['test'], // see "Request Tags"
// trace: 'full', // per-node detail level ('path' | 'full')
idempotencyKey: 'line-msg-123', // e.g. the platform message ID; a retry with the same key
// (within 24h) replays the first result instead of re-running
});
// Prefer the structured `reply` object over re-parsing `response`:
console.log(result.reply.responseType); // 'text' | 'struct' | 'none'
console.log(result.reply.content); // text content (responseType === 'text')
console.log(result.reply.structContent); // Unified Block Message (responseType === 'struct')
console.log(result.version); // the workflow version actually executed
console.log(result.cost); // total cost of the run
console.log(result.llmCalls); // number of LLM calls
console.log(result.elapsedTime); // elapsed time in ms
console.log(result.nodeTrace); // per-node observability trace
console.log(result.logId); // workflow_runs.id (-1 if persistence failed)
console.log(result.taskId);
console.log(result.terminalNodeId); // the node id the run ended on
console.log(result.tags); // request tags ∪ node run_tags actually recorded on the run
console.log(result.state); // whatever the workflow's own "state" nodes wroteNon-2xx responses throw a MetrioApiError with the HTTP status preserved, so callers can distinguish a missing workflow (404) from other request errors. A disabled workflow is not an error at all — the request succeeds (200) with skipped: true and a benign reply of responseType: "none" (its reason names the disabled workflow); no DAG runs and no workflow_runs row is recorded:
try {
const result = await client.workflowCompletion({ workflowId: 'maybe-disabled', messages });
if (result.skipped) {
// workflow exists but is disabled — 200 response, reply.responseType === 'none'
}
} catch (err) {
if (err instanceof MetrioApiError && err.statusCode === 404) {
// workflow doesn't exist in this project
}
}Agents
Run Metrio agents with client.agents.runs. Ids are numbers; previousRunId, maxTurns, tags, variables and timezone are optional.
const messages: MessageV2[] = [{ role: 'user', content: { type: 'text', text: 'Summarize today' } }];
// Sync (default): waits for the run to finish
const run = await client.agents.runs.create(7, { messages });
console.log(run.status, run.output);
// Async: returns the queued run; poll with get
const queued = await client.agents.runs.create(7, { messages, mode: 'async' });
let current = queued;
while (current.status === 'queued' || current.status === 'running') {
await new Promise((r) => setTimeout(r, 1000));
current = await client.agents.runs.get(7, queued.runId);
}
// get also lists every sub-agent run below it (summary only; get a child's output by its own ids)
for (const child of current.children ?? []) {
console.log(child.agentId, child.runId, child.status, child.cost);
}
// Streaming: ends when the server closes the stream
for await (const ev of client.agents.runs.createStream(7, { messages })) {
if (ev.event === 'text_delta') process.stdout.write(ev.data.text);
else if (ev.event === 'final') console.log('\n', ev.data.output);
else if (ev.event === 'error') console.error(ev.data.error);
}When a run stops at interrupted, approve or reject its pending_approvals (each decision needs the entry's fingerprint):
const decisions = (run.pending_approvals ?? []).map((p): AgentApprovalDecision => ({
call_id: p.call_id,
fingerprint: p.fingerprint,
decision: 'approve', // or 'reject', with an optional `reason`
}));
const resumed = await client.agents.runs.approve(7, run.runId, { decisions });
// or stream the resumed run: client.agents.runs.approveStream(7, run.runId, { decisions })Resume a dropped stream with after set to the id of the last event you received:
let lastId;
try {
for await (const ev of client.agents.runs.stream(7, run.runId)) {
lastId = ev.id ?? lastId;
}
} catch {
for await (const ev of client.agents.runs.stream(7, run.runId, { after: lastId })) {
// continues after lastId (omit `after` if no event arrived yet)
}
}The SDK never retries run creation (create, createStream) or the approve methods. Pass idempotencyKey and retry yourself: a completed run replays, an in-flight one returns 409, and a failed run releases the key so a retry runs the agent again. client.agents.runs.cancel(agentId, runId) cancels a run (a retried cancel may get 409 if the first attempt succeeded; check status in error.responseData).
Evaluate a Prompt
Test a prompt with specific model settings:
const evalResponse = await client.evaluate({
projectId: 'test-project',
promptId: 1,
modelProvider: 'openai',
modelName: 'gpt-3.5-turbo',
modelSettings: {
temperature: 0.7,
maxTokens: 1000,
topP: 0.9,
topK: 40,
frequencyPenalty: 0,
presencePenalty: 0
},
messages: [
{ role: 'user', content: { type: 'text', text: 'What is the capital of France?' } }
],
outputFormat: 'json' // optional
});Error Handling
API failures throw a MetrioApiError with detailed error information:
import { MetrioAI, MetrioApiError } from '@metrio-ai/client';
try {
const response = await client.chatCompletion({ promptId: 1, messages });
} catch (error) {
if (error instanceof MetrioApiError) {
console.error('API Error:', error.message);
console.error('Status Code:', error.statusCode);
console.error('Response Data:', error.responseData);
console.error('Request Data:', error.requestData);
}
}Retryable failures (network errors, timeouts, 5xx server errors) are retried automatically; tune with the maxRetries (default: 3) and retryDelay (default: 500 ms) client options.
TypeScript Support
The SDK is fully written in TypeScript and exports types for every parameter and response:
| Type | Description |
|------|-------------|
| MetrioAIOptions | Configuration options for the client |
| RunParams | Parameters for chat completion requests |
| EvalParams | Parameters for evaluation requests |
| RunResponse | Response from chat completion or evaluation |
| StreamChunk | A chunk from a streaming response |
| Message | A single message in a conversation |
| MessageContent | Content of a message (text or file) |
| ProvidersResponse | Response from the providers endpoint |
| ModelsResponse | Response from the models endpoint |
| ModelSettings | Model configuration settings |
| Variable | Variable for template substitution |
import { MetrioAI, RunParams, RunResponse } from '@metrio-ai/client';
const params: RunParams = {
promptId: 1,
messages: [{ role: 'user', content: { type: 'text', text: 'Hello' } }],
timezone: 'Asia/Taipei'
};
const response: RunResponse = await client.chatCompletion(params);Requirements
- Node.js 18.0.0 or higher (for the native
fetchAPI)
License
This SDK is licensed under the BSD 3-Clause License.
Support
For questions or issues, please contact [email protected].
