@metrio-ai/client
v1.5.2
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, full type definitions, and a metrio 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. From the CLI, pass --request-tag <tag> (repeatable).
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'); see CLI's --trace
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
}
}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
});CLI
Installing the package also provides a metrio command — handy for testing prompts and scripting:
metrio [options]— chat completion (the default command)metrio workflow run— see CLI: workflow runmetrio workflow run-batch— see CLI: workflow run-batchmetrio prompt run-batch— see CLI: prompt run-batchmetrio prompt create-version— see CLI: prompt create-version
# Global install exposes `metrio` on the PATH
npm install -g @metrio-ai/client
# Or run via npx without installing
npx @metrio-ai/client --prompt-id 7 --messages messages.jsonCredentials are read from a .env file in the current directory (or any file passed with --env), so you can keep one env file per project and run the same messages file against each:
METRIOAI_API_KEY=your-project-api-key
METRIO_PROMPT_ID=7
# Optional overrides
# METRIOAI_BASE_URL=https://api.metrio.aimetrio --env project-a.env --messages messages.json
metrio --env project-b.env --messages messages.jsonThe API key is read from METRIOAI_API_KEY, METRIO_API_KEY, or METRIO_PROJECT_API_KEY; the prompt ID from METRIO_PROMPT_ID or METRIOAI_PROMPT_ID. Command-line flags (--api-key, --prompt-id) always take precedence over the env file.
Messages file (messages.json) — a JSON array of messages. Each entry has a role, optional content text, and an optional file path (resolved relative to the messages file):
[
{ "role": "system", "content": "You are a helpful assistant." },
{ "role": "user", "content": "Describe this image", "file": "./cat.png" },
{ "role": "user", "file": "./report.pdf" }
]A message with both content and file is automatically split into two messages (text first, then the file), since the API allows a single content block per message. File type (image / pdf / binary) and MIME type are inferred from the file extension and the file is base64-encoded automatically.
metrio --messages messages.json # prompt id + key from .env
metrio -p 7 -m messages.json # full JSON response
metrio -p 7 -m messages.json --text # response text only
metrio -p 7 -m messages.json --stream # stream tokens to stderr
metrio -e project-a.env -m messages.json # pick a specific env file
metrio -p 7 -m messages.json -s "You are concise." # prepend a system message
metrio -p 7 -m messages.json --tag staging --var name=Alice --timezone Asia/Taipei
metrio -p 7 -m messages.json --request-tag test --request-tag canary-01 # tag the run
metrio --help # full option listBy default the full RunResponse JSON (response text, token counts, timing, logId, etc.) is printed to stdout. Use --text to print only the response text for piping.
| Option | Description |
| --- | --- |
| -m, --messages <file> | Path to the messages JSON file (required) |
| -p, --prompt-id <id> | Prompt ID to run (or set METRIO_PROMPT_ID in the env file) |
| -s, --system <text> | System message prepended as the first message |
| -e, --env <file> | Env file to load (default: .env in the current directory) |
| -t, --tag <tag> | Version tag to pin (defaults to the active version) |
| --var <name=value> | Template variable (repeatable) |
| --request-tag <tag> | Request-level tag recorded on the run (repeatable; see Request Tags) |
| --timezone <tz> | IANA timezone, e.g. Asia/Taipei |
| --text | Print only the response text |
| --stream | Stream tokens to stderr while running |
| --api-key <key> | Override the API key from the env file |
| --base-url <url> | Override the base API URL |
CLI: workflow run
metrio workflow run --workflow-id groceriesfriends-order --message "我要訂 5 包麵粉"
metrio workflow run -w groceriesfriends-order --message "我要訂 5 包麵粉" \
--channel '{"User":{"name":"王小明","customer_id":"44785"}}' \
--timezone Asia/Taipei
metrio workflow run -w my-flow --messages-file conversation.json --json
metrio workflow run -w my-flow --message "試跑草稿" --version 4 # pin a specific version
metrio workflow run -w my-flow --message "冒煙測試" --request-tag test # tag the run
metrio workflow --helpBy default a human-readable reply is printed to stdout — the text content, or the pretty-printed structContent for struct replies — followed by a one-line run summary (llmCalls, elapsedTime, taskId) on stderr. Use --json for the full response. A non-2xx response prints the error to stderr and exits with a non-zero status.
| Option | Description |
| --- | --- |
| -w, --workflow-id <slug> | Workflow slug to run (required) |
| --message <text> | A single user text message |
| --messages-file <path> | A full messages JSON file (same format as chat); use instead of --message |
| --version <n> | Pin a workflow version (positive integer); omit to run the active version |
| --channel <json> | Channel JSON, e.g. '{"User":{"name":"...","customer_id":"..."}}' |
| --variables <json> | Variables JSON, surfaced as $variables |
| --timezone <tz> | IANA timezone, e.g. Asia/Taipei |
| --request-tag <tag> | Request-level tag recorded on the run (repeatable; see Request Tags) |
| -e, --env <file> | Env file to load (default: .env in the current directory) |
| --api-key <key> | Override the API key from the env file |
| --base-url <url> | Override the base API URL |
| --json | Print the full response JSON instead of just the reply |
| --trace <mode> | Node trace detail with --json (path, summary, full; default: summary) |
--trace: node trace detail
--trace requires --json (the human-readable output prints only the reply, so there is no trace to shape). It controls how much of the per-node trace comes back in the JSON:
path— routing only: each node'snode_id,type,went_to, andelapsed_ms(pluserrorwhen a node failed) innodeTrace. Nostate.summary— the default. Same per-node fields aspath, plusstate. Bare--jsonbehaves exactly like--trace=summary.full— adds per-nodeinput/outputtonodeTracefor debugging (each capped by the engine at 16000 characters).
With --json, path (the node id sequence), terminalNodeId, and tags are always present — including on silent runs — so a single call is enough to verify a route even when the workflow produced no visible reply. Here is the full summary-mode shape, for a silent run that still recorded routing, tokens, cost, and a state payload:
metrio workflow run -w groceriesfriends-order --message "我要訂三鳥" --json{
"response": "…",
"reply": { "action": "respond", "status": "completed", "responseType": "none", "reason": "silent" },
"version": 87,
"inputTokens": 412,
"outputTokens": 0,
"cost": 0.0021,
"llmCalls": 1,
"elapsedTime": 843,
"nodeTrace": [
{ "node_id": "parse_related", "type": "llm", "elapsed_ms": 620, "went_to": "t_build_text" },
{ "node_id": "t_build_text", "type": "transform", "elapsed_ms": 4, "went_to": "end_silent" },
{ "node_id": "end_silent", "type": "end", "elapsed_ms": 1 }
],
"logId": 55021,
"taskId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"terminalNodeId": "end_silent",
"tags": ["silent"],
"state": { "order_summary": { "items": [{ "identity": "4133", "quantity": 1 }] } },
"path": ["parse_related", "t_build_text", "end_silent"]
}(response is the same reply as a raw JSON string, kept only for backward compatibility.)
state holds whatever the workflow's own state nodes wrote — the SDK/CLI does not define or interpret its keys, so a workflow publishes its own run summary by writing one there. That's what makes the example above assertable even though the reply itself is silent.
CLI: workflow run-batch
Run several cases against one workflow in a single process. Over a shell loop calling metrio workflow run per case you get: process start-up and env loading paid once, cases run in parallel, and one JSONL stream in input order to diff across runs.
metrio workflow run-batch -w groceriesfriends-order --cases cases.jsonl
metrio workflow run-batch -w my-flow --cases cases.jsonl --concurrency 8 --out results.jsonl
metrio workflow run-batch -w my-flow --cases cases.jsonl --version 4 --request-tag test
metrio workflow run-batch -w my-flow --cases cases.jsonl --trace path # routing only
cat cases.jsonl | metrio workflow run-batch -w my-flow --cases - # read cases from stdin
metrio workflow run-batch --helpCases file (cases.jsonl) — one JSON object per line; blank lines and // comment lines are ignored:
{"id":"c1","message":"我要訂三鳥"}
{"id":"c2","messages":[{"role":"user","content":"這張看得懂嗎","file":"receipt.png"}]}
{"id":"c3","message":"換一個人問","channel":{"User":{"name":"王小明"}},"variables":{"k":"v"},"version":3}A case takes either message (a single user text) or messages (the same format as the chat command's messages file, including file attachments — paths resolve relative to the cases file, or the current directory when reading from stdin). A case without an id is named by its line number, e.g. case-3. A case's channel and variables merge key by key over the global --channel and --variables flags with the case winning; its version replaces the global --version for that case only.
Results are JSONL in input order, one line per case. A successful row carries the case id, ok, and then exactly the fields workflow run --json prints — including path, terminalNodeId, tags, and state — so route assertions read the same in a batch as they do for a single run:
{"id":"c1","ok":true,"path":["parse_related","t_build_text","end_silent"],"terminalNodeId":"end_silent","tags":["silent"],"state":{…},"reply":{…},"elapsedTime":843,"taskId":"…"}
{"id":"c2","ok":false,"error":{"message":"…","status":404}}--trace works the same as on workflow run (path, summary, full; default summary) and applies to every row — no --json flag is needed since batch output is always JSON. For diffing runs against each other, --trace path is the clean choice: elapsedTime, cost, and taskId differ on every run, so a routing-only row is the one that only changes when behaviour does.
Progress goes to stderr as each case finishes ([2/3] c2 ok 843ms terminal=reply, in completion order) followed by a 3 cases: 2 ok, 1 failed tally, so stdout carries nothing but the result rows. Those rows are written once the whole batch has finished rather than streamed per case — watch stderr for live progress, and note that piping stdout into jq produces nothing until the batch is done.
A failing case does not stop the batch — every other case still runs and is reported. The exit code is 1 if any case failed, so batches are scriptable (metrio workflow run-batch ... || alert-on-failures).
| Option | Description |
| --- | --- |
| -w, --workflow-id <slug> | Workflow slug to run (required) |
| --cases <file> | JSONL cases file; - reads stdin (required) |
| --concurrency <n> | Cases to run at once (default: 4) |
| --version <n> | Pin a workflow version for every case (a case may override) |
| --channel <json> | Channel JSON applied to every case (a case's own keys win) |
| --variables <json> | Variables JSON applied to every case (a case's own keys win) |
| --timezone <tz> | IANA timezone, e.g. Asia/Taipei |
| --request-tag <tag> | Request-level tag recorded on every run (repeatable; see Request Tags) |
| --trace <mode> | Node trace detail per case (path, summary, full; default: summary) |
| -o, --out <file> | Write result JSONL here (default: stdout) |
| -e, --env <file> | Env file to load (default: .env in the current directory) |
| --api-key <key> | Override the API key from the env file |
| --base-url <url> | Override the base API URL |
CLI: prompt run-batch
Run several prompt cases in one process. The win over a shell loop calling metrio per case is paying process start-up and env loading once; results are written as JSONL in input order so they can be diffed across runs.
metrio prompt run-batch -p 7 --cases cases.jsonl
metrio prompt run-batch -p 7 --cases cases.jsonl --concurrency 8 --out results.jsonl
metrio prompt run-batch -p 7 --cases cases.jsonl --tag staging --var region=TW
cat cases.jsonl | metrio prompt run-batch -p 7 --cases - # read cases from stdin
metrio prompt --helpCases file (cases.jsonl) — one JSON object per line; blank lines and // comment lines are ignored:
{"id":"c1","messages":[{"role":"user","content":"我要訂三鳥"}],"system":"…","variables":{"k":"v"},"tag":"v3"}
{"id":"c2","messages":[{"role":"user","content":"..."}]}messages uses the same format as the chat command's messages file, including file attachments — attachment paths resolve relative to the cases file (or the current directory when reading from stdin). A case without an id is named by its line number, e.g. case-3. A case's own tag and variables override the global --tag and --var flags for that case only.
Results are JSONL in input order, one line per case:
{"id":"c1","ok":true,"response":"…","inputTokens":120,"outputTokens":45,"elapsedTime":1240,"taskId":"…"}
{"id":"c2","ok":false,"error":{"message":"…","status":429}}Progress goes to stderr as each case finishes ([2/3] c2 ok 1240ms, in completion order) followed by a 3 cases: 2 ok, 1 failed tally, so stdout carries nothing but the result rows. Those rows are written once the whole batch has finished rather than streamed per case — watch stderr for live progress, and note that piping stdout into jq produces nothing until the batch is done.
A failing case does not stop the batch — every other case still runs and is reported. The exit code is 1 if any case failed, so batches are scriptable (metrio prompt run-batch ... || alert-on-failures).
| Option | Description |
| --- | --- |
| -p, --prompt-id <id> | Prompt ID to run (or set METRIO_PROMPT_ID in the env file) |
| --cases <file> | JSONL cases file; - reads stdin (required) |
| --concurrency <n> | Cases to run at once (default: 4) |
| -t, --tag <tag> | Version tag applied to every case (a case may override) |
| --var <name=value> | Global template variable (repeatable; a case may override) |
| --request-tag <tag> | Request-level tag recorded on every run (repeatable; see Request Tags) |
| --timezone <tz> | IANA timezone, e.g. Asia/Taipei |
| -o, --out <file> | Write result JSONL here (default: stdout) |
| -e, --env <file> | Env file to load (default: .env in the current directory) |
| --api-key <key> | Override the API key from the env file |
| --base-url <url> | Override the base API URL |
CLI: prompt create-version
Create a new version of a prompt from a local file (prompt-as-code):
metrio prompt create-version -p 42 -f prompts/support-bot.md --description "tighten tone"The entire file content becomes the version's system prompt. Auth uses the same project METRIOAI_API_KEY as every other command; the prompt must belong to that key's project. The new version is not promoted unless you pass --set-as-head, so pushing from CI never changes what runs in production. Use --json to get the created version's id/serial for scripting, and --model-tier (or --model-provider + --model-name) to set the model.
| Option | Description |
| --- | --- |
| -p, --prompt-id <id> | Prompt ID to create a version for (required) |
| -f, --file <path> | Path to the file containing the system prompt (required) |
| --description <text> | Description for this version |
| --model-tier <tier> | Model tier to use (e.g., standard, extended) |
| --model-provider <provider> | AI provider (e.g., openai, anthropic) |
| --model-name <name> | Specific model name (e.g., gpt-4-turbo) |
| --set-as-head | Promote this version to be the active version (default: false) |
| --json | Print the full response JSON with version id/serial |
| --output-format <fmt> | Output format: "text" or "json" |
| -e, --env <file> | Env file to load (default: .env in the current directory) |
| --api-key <key> | Override the API key from the env file |
| --base-url <url> | Override the base API URL |
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].
