Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Bootstrap a modular AllToken agent — chat, async image+video, model routing, OpenAI-compatible SDK. Works inside Hermes, OpenClaw, Claude Code, Codex CLI, OpenCode, or any runtime that loads SKILL.md.
.claude/skills/alltoken-ai-alltoken/SKILL.md| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-01 | ✗→✓ | ▲ Improved | 223% | 0% |
| case-02 | ✗→✓ | ▲ Improved | 313% | 0% |
| case-03 | ✗→✓ | ▲ Improved | 569% | 0% |
| case-04 | ✗→✓ | ▲ Improved | 860% | 0% |
| case-05 | ✗→✓ | ▲ Improved | 532% | 0% |
This skill helps you create a modular AI agent powered by AllToken — a unified, OpenAI-compatible API with access to leading language, image, and video models behind one endpoint, plus automatic provider fallbacks and cost-effective routing.
Designed to be invoked from Hermes, OpenClaw, Claude Code, Codex CLI, or any other agent runtime that consumes skills.
┌─────────────────────────────────────────────────────┐
│ Your Application (TS/Py) │
├─────────────────────────────────────────────────────┤
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ Ink TUI │ │ HTTP API │ │ Hermes / │ │
│ │ │ │ │ │ OpenClaw │ │
│ └──────┬──────┘ └──────┬──────┘ └──────┬──────┘ │
│ │ │ │ │
│ └────────────────┼────────────────┘ │
│ ▼ │
│ ┌───────────────────────┐ │
│ │ Agent Core │ │
│ │ (hooks & lifecycle) │ │
│ └───────────┬───────────┘ │
│ ▼ │
│ ┌───────────────────────┐ │
│ │ AllToken REST API │ │
│ │ api.alltoken.ai/v1 │ │
│ └───────────────────────┘ │
└─────────────────────────────────────────────────────┘> Security: never commit your API key. Use ALLTOKEN_API_KEY from the environment.
https://api.alltoken.ai/v1Authorization: Bearer $ALLTOKEN_API_KEYbase_url/baseURL.POST /chat/completions — chat (streaming, tool calls, thinking, web search)GET /models — OpenAI-compatible model listPOST /images/generations/async + GET /images/generations/{id} — async image generationPOST /videos/generations + GET /videos/generations/{id} — async video generationGET /api-account/models / /{model_path} / /filters — full catalog with pricing and capabilities (public, no auth required)GET /api-account/providers (+ /{id}/stats) — providers, health, throughput (public)GET /api-account/rankings/all — leaderboards, benchmarks, speed rankings (public)GET /api-account/health/{routes,summary} — route health & availability (public)GET /api-account/user/{api-keys,usage,billing,balance} — web-session token only, not callable with your Bearer API key (you'll get 401 auth_error / invalid_token). Manage these in Settings → API Keys / Billing on https://alltoken.ai.bashmkdir my-alltoken-agent && cd my-alltoken-agent npm init -y npm pkg set type="module"
bashnpm install openai zod eventemitter3 npm install ink react # optional: TUI only npm install -D typescript @types/react tsx
tsconfig.jsonjson{ "compilerOptions": { "target": "ES2022", "module": "NodeNext", "moduleResolution": "NodeNext", "jsx": "react-jsx", "strict": true, "esModuleInterop": true, "skipLibCheck": true, "outDir": "dist" }, "include": ["src"] }
package.jsonjson{ "scripts": { "start": "tsx src/cli.tsx", "start:headless": "tsx src/headless.ts", "dev": "tsx watch src/cli.tsx" } }
bashsrc/ ├── client.ts # AllToken client (OpenAI SDK with overridden baseURL) ├── agent.ts # Standalone agent core with hooks ├── tools.ts # Function-calling tool definitions ├── media.ts # Async image + video helpers (poll loop) ├── cli.tsx # Optional Ink TUI └── headless.ts # Headless / scriptable example
Create src/client.ts. AllToken is OpenAI-compatible; we just override the base URL.
typescriptimport OpenAI from 'openai'; export function createAllTokenClient(apiKey = process.env.ALLTOKEN_API_KEY): OpenAI { if (!apiKey) throw new Error('ALLTOKEN_API_KEY is not set'); return new OpenAI({ apiKey, baseURL: 'https://api.alltoken.ai/v1', }); }
Create src/agent.ts — the standalone agent. It streams via OpenAI's SSE protocol and emits typed events for any UI to consume.
typescriptimport OpenAI from 'openai'; import type { ChatCompletionMessageParam, ChatCompletionTool, ChatCompletionToolMessageParam, } from 'openai/resources/chat/completions'; import { EventEmitter } from 'eventemitter3'; import { createAllTokenClient } from './client.js'; export interface Message { role: 'user' | 'assistant' | 'system' | 'tool'; content: string; tool_call_id?: string; name?: string; } export interface AgentEvents { 'message:user': (message: Message) => void; 'message:assistant': (message: Message) => void; 'stream:start': () => void; 'stream:delta': (delta: string, accumulated: string) => void; 'stream:end': (fullText: string) => void; 'tool:call': (name: string, args: unknown, callId: string) => void; 'tool:result': (name: string, result: unknown, callId: string) => void; 'thinking:start': () => void; 'thinking:end': () => void; 'error': (error: Error) => void; } export interface ToolHandler { definition: ChatCompletionTool; execute: (args: any) => Promise<unknown> | unknown; } export interface AgentConfig { apiKey?: string; model?: string; // e.g. 'minimax-m2.7', 'gpt-5.4' instructions?: string; tools?: ToolHandler[]; maxSteps?: number; // tool-loop step limit temperature?: number; enableSearch?: boolean; // AllToken-specific web-search toggle } export class Agent extends EventEmitter<AgentEvents> { private client: OpenAI; private messages: ChatCompletionMessageParam[] = []; private cfg: Required<Omit<AgentConfig, 'apiKey'>>; private toolMap: Map<string, ToolHandler>; constructor(config: AgentConfig = {}) { super(); this.client = createAllTokenClient(config.apiKey); this.cfg = { model: config.model ?? 'minimax-m2.7', instructions: config.instructions ?? 'You are a helpful assistant.', tools: config.tools ?? [], maxSteps: config.maxSteps ?? 5, temperature: config.temperature ?? 0.7, enableSearch: config.enableSearch ?? false, }; this.toolMap = new Map(this.cfg.tools.map((t) => [t.definition.function.name, t])); if (this.cfg.instructions) { this.messages.push({ role: 'system', content: this.cfg.instructions }); } } getMessages(): ChatCompletionMessageParam[] { return [...this.messages]; } clearHistory(): void { this.messages = this.cfg.instructions ? [{ role: 'system', content: this.cfg.instructions }] : []; } setInstructions(text: string): void { this.cfg.instructions = text; if (this.messages[0]?.role === 'system') this.messages[0] = { role: 'system', content: text }; else this.messages.unshift({ role: 'system', content: text }); } addTool(t: ToolHandler): void { this.cfg.tools.push(t); this.toolMap.set(t.definition.function.name, t); } /** Send a user message, run the tool-loop, stream tokens. Returns the final assistant text. */ async send(content: string): Promise<string> { this.messages.push({ role: 'user', content }); this.emit('message:user', { role: 'user', content }); this.emit('thinking:start'); let finalText = ''; try { for (let step = 0; step < this.cfg.maxSteps; step++) { const stream = await this.client.chat.completions.create({ model: this.cfg.model, messages: this.messages, temperature: this.cfg.temperature, tools: this.cfg.tools.length ? this.cfg.tools.map((t) => t.definition) : undefined, stream: true, // AllToken extension: opt-in web search (model-dependent) ...(this.cfg.enableSearch ? ({ enable_search: true } as any) : {}), }); this.emit('stream:start'); let text = ''; const toolCalls: Record<number, { id?: string; name?: string; args: string }> = {}; let finishReason: string | undefined; for await (const chunk of stream) { const choice = chunk.choices[0]; if (!choice) continue; const delta: any = choice.delta; if (delta?.content) { text += delta.content; this.emit('stream:delta', delta.content, text); } if (delta?.tool_calls) { for (const tc of delta.tool_calls) { const slot = toolCalls[tc.index] ?? (toolCalls[tc.index] = { args: '' }); if (tc.id) slot.id = tc.id; if (tc.function?.name) slot.name = tc.function.name; if (tc.function?.arguments) slot.args += tc.function.arguments; } } if (choice.finish_reason) finishReason = choice.finish_reason; } this.emit('stream:end', text); // Persist the assistant turn (with tool_calls if any) const calls = Object.values(toolCalls).filter((c) => c.id && c.name); if (calls.length) { this.messages.push({ role: 'assistant', content: text || null, tool_calls: calls.map((c) => ({ id: c.id!, type: 'function', function: { name: c.name!, arguments: c.args || '{}' }, })), } as any); // Execute tools and append results for (const c of calls) { const handler = this.toolMap.get(c.name!); const parsed = safeJson(c.args); this.emit('tool:call', c.name!, parsed, c.id!); const result = handler ? await handler.execute(parsed) : { error: `unknown tool: ${c.name}` }; this.emit('tool:result', c.name!, result, c.id!); const toolMsg: ChatCompletionToolMessageParam = { role: 'tool', tool_call_id: c.id!, content: typeof result === 'string' ? result : JSON.stringify(result), }; this.messages.push(toolMsg); } continue; // next loop step } // Terminal: regular completion this.messages.push({ role: 'assistant', content: text }); this.emit('message:assistant', { role: 'assistant', content: text }); finalText = text; break; } return finalText; } catch (err) { const error = err instanceof Error ? err : new Error(String(err)); this.emit('error', error); throw error; } finally { this.emit('thinking:end'); } } /** Non-streaming convenience method. */ async sendSync(content: string): Promise<string> { this.messages.push({ role: 'user', content }); this.emit('message:user', { role: 'user', content }); const res = await this.client.chat.completions.create({ model: this.cfg.model, messages: this.messages, temperature: this.cfg.temperature, }); const text = res.choices[0]?.message?.content ?? ''; this.messages.push({ role: 'assistant', content: text }); this.emit('message:assistant', { role: 'assistant', content: text }); return text; } } function safeJson(s: string): unknown { try { return JSON.parse(s || '{}'); } catch { return { _raw: s }; } } export function createAgent(config: AgentConfig = {}): Agent { return new Agent(config); }
Create src/tools.ts:
typescriptimport type { ToolHandler } from './agent.js'; export const timeTool: ToolHandler = { definition: { type: 'function', function: { name: 'get_current_time', description: 'Get the current date and time', parameters: { type: 'object', properties: { timezone: { type: 'string', description: 'IANA timezone, e.g. "UTC", "America/New_York"' }, }, }, }, }, execute: ({ timezone }: { timezone?: string }) => ({ time: new Date().toLocaleString('en-US', { timeZone: timezone || 'UTC' }), timezone: timezone || 'UTC', }), }; export const calculatorTool: ToolHandler = { definition: { type: 'function', function: { name: 'calculate', description: 'Evaluate a basic math expression', parameters: { type: 'object', properties: { expression: { type: 'string' } }, required: ['expression'], }, }, }, execute: ({ expression }: { expression: string }) => { // Safe arithmetic evaluator — shunting-yard + RPN, no eval/Function. const tokens = expression.match(/\d+(?:\.\d+)?|[+\-*/()]/g) ?? []; const prec: Record<string, number> = { '+': 1, '-': 1, '*': 2, '/': 2 }; const out: string[] = []; const ops: string[] = []; for (const t of tokens) { if (/^\d/.test(t)) { out.push(t); } else if (t === '(') { ops.push(t); } else if (t === ')') { while (ops.length && ops[ops.length - 1] !== '(') out.push(ops.pop()!); ops.pop(); } else { while ( ops.length && ops[ops.length - 1] !== '(' && (prec[ops[ops.length - 1]] ?? 0) >= prec[t] ) { out.push(ops.pop()!); } ops.push(t); } } while (ops.length) out.push(ops.pop()!); const stack: number[] = []; for (const t of out) { if (/^\d/.test(t)) { stack.push(parseFloat(t)); } else { const b = stack.pop()!; const a = stack.pop()!; stack.push(t === '+' ? a + b : t === '-' ? a - b : t === '*' ? a * b : a / b); } } return { expression, result: stack[0] }; }, }; export const defaultTools = [timeTool, calculatorTool];
AllToken's image and video endpoints are asynchronous: create a task, poll until completed, then read the result. Create src/media.ts:
typescriptimport { createAllTokenClient } from './client.js'; const BASE = 'https://api.alltoken.ai/v1'; async function authedFetch(path: string, init: RequestInit = {}) { const apiKey = process.env.ALLTOKEN_API_KEY; if (!apiKey) throw new Error('ALLTOKEN_API_KEY is not set'); const res = await fetch(`${BASE}${path}`, { ...init, headers: { 'Authorization': `Bearer ${apiKey}`, 'Content-Type': 'application/json', ...(init.headers ?? {}), }, }); if (!res.ok) { const body = await res.text(); throw new Error(`AllToken ${res.status}: ${body}`); } return res.json(); } // ── Images ──────────────────────────────────────────────────────────────── // Result is delivered ONCE: persist `b64_json` immediately. Tasks expire in 30 min. export interface ImageRequest { model?: 'gpt-image-2' | string; // discover via GET /images/models prompt: string; size?: '1024x1024' | '1536x1024' | '1024x1536' | 'auto'; quality?: 'low' | 'medium' | 'high' | 'auto'; output_format?: 'png' | 'jpeg' | 'webp'; background?: 'auto' | 'opaque'; moderation?: 'auto' | 'low'; } export interface ImageResult { id: string; status: 'queued' | 'processing' | 'completed' | 'failed' | 'cancelled'; data?: Array<{ b64_json: string; revised_prompt?: string }>; error?: unknown; } export async function generateImage(req: ImageRequest, opts: { pollMs?: number } = {}): Promise<ImageResult> { const created = await authedFetch('/images/generations/async', { method: 'POST', body: JSON.stringify({ model: 'gpt-image-2', ...req }), // Recommended: deduplicate retries with an Idempotency-Key headers: { 'Idempotency-Key': crypto.randomUUID() }, }); const id = created.id as string; const intervalMs = opts.pollMs ?? 2000; while (true) { const status = await authedFetch(`/images/generations/${id}`); if (status.status === 'completed' || status.status === 'failed' || status.status === 'cancelled') { return status; } await new Promise((r) => setTimeout(r, intervalMs)); } } // ── Videos ──────────────────────────────────────────────────────────────── export interface VideoRequest { model: 'seedance-1.5-pro' | 'seedance-2.0' | string; prompt: string; duration?: number; // seconds; -1 = model decides ratio?: '16:9' | '9:16' | '4:3' | '3:4' | '21:9' | '1:1' | 'adaptive'; resolution?: '480p' | '720p' | '1080p'; generate_audio?: boolean; seed?: number; watermark?: boolean; callback_url?: string; // Image-to-video: pass `content` with image_url + role: 'first_frame' content?: Array<{ type: 'image_url' | 'video_url' | 'audio_url' | 'draft_task'; image_url?: { url: string }; video_url?: { url: string }; audio_url?: { url: string }; role?: 'first_frame' | 'last_frame' | 'reference_image' | 'reference_video' | 'reference_audio'; }>; } export async function generateVideo(req: VideoRequest, opts: { pollMs?: number } = {}) { const created = await authedFetch('/videos/generations', { method: 'POST', body: JSON.stringify(req), }); const id = created.id as string; const intervalMs = opts.pollMs ?? 3000; while (true) { const status = await authedFetch(`/videos/generations/${id}`); if (['completed', 'failed', 'cancelled', 'expired'].includes(status.status)) return status; await new Promise((r) => setTimeout(r, intervalMs)); } } export async function cancelVideo(id: string) { return authedFetch(`/videos/generations/${id}/cancel`, { method: 'POST' }); }
Persist b64_json to disk in one shot — re-polling a delivered image returns 410 image_already_retrieved and the result is gone. The 410 envelope:
json{"error":{"code":"image_already_retrieved","message":"Image data was already retrieved; please submit a new generation request","request_id":"...","type":"invalid_request_error"}}
Observed latencies (use these to size your retry budget, not as SLAs):
gpt-image-2 1024×1024 quality=low: ~15–25 s end-to-end (verified 20.6 s on a real submit).quality=high or 1536×1024: 30–60 s per docs.seedance-1.5-pro 5 s @ 480 p: 30–120 s typical; 1080 p can take 3–5 min.Submit-response fields (the full shape, not just id):
json{"id":"igen_d3b8...","status":"queued","model":"gpt-image-2","created_at":"2026-05-12T13:46:09Z"}
After status==completed, the GET adds: data: [{b64_json}], usage: {input_tokens, output_tokens, total_tokens, input_tokens_details}, size, quality, output_format, completed_at, expires_at. Note: revised_prompt is not present in current responses despite appearing in the docs example — treat it as optional.
Create src/headless.ts:
typescriptimport { createAgent } from './agent.js'; import { defaultTools } from './tools.js'; import { generateImage } from './media.js'; import { writeFile } from 'node:fs/promises'; async function main() { const agent = createAgent({ model: 'minimax-m2.7', instructions: 'You are a helpful assistant with tools.', tools: defaultTools, enableSearch: false, }); agent.on('thinking:start', () => console.log('\n🤔 Thinking...')); agent.on('tool:call', (name, args) => console.log(`🔧 ${name}`, args)); agent.on('stream:delta', (delta) => process.stdout.write(delta)); agent.on('stream:end', () => console.log()); agent.on('error', (e) => console.error('❌', e.message)); // Chat await agent.send('What time is it in Tokyo?'); // Image (async) const img = await generateImage({ prompt: 'A clean studio product photo of a glass teapot on a walnut table', size: '1024x1024', quality: 'high', }); if (img.status === 'completed' && img.data?.[0]?.b64_json) { await writeFile('teapot.png', Buffer.from(img.data[0].b64_json, 'base64')); console.log('\n💾 Saved teapot.png'); } } main().catch(console.error);
Run: ALLTOKEN_API_KEY=sk-... npm run start:headless
Create src/cli.tsx for a terminal chat UI. Subscribe to stream:delta and tool:call events from the agent and render them. The agent core is UI-agnostic — the same instance can power Hermes, OpenClaw, Discord, or HTTP.
tsximport React, { useState, useEffect, useCallback } from 'react'; import { render, Box, Text, useInput, useApp } from 'ink'; import { createAgent, type Message } from './agent.js'; import { defaultTools } from './tools.js'; const agent = createAgent({ model: 'minimax-m2.7', instructions: 'You are a concise assistant.', tools: defaultTools, }); function App() { const { exit } = useApp(); const [messages, setMessages] = useState<Message[]>([]); const [input, setInput] = useState(''); const [streaming, setStreaming] = useState(''); const [loading, setLoading] = useState(false); useInput((ch, key) => { if (key.escape) exit(); if (loading) return; if (key.return) { const text = input.trim(); if (!text) return; setInput(''); setMessages((m) => [...m, { role: 'user', content: text }]); agent.send(text); } else if (key.backspace || key.delete) setInput((v) => v.slice(0, -1)); else if (ch && !key.ctrl && !key.meta) setInput((v) => v + ch); }); useEffect(() => { const onStart = () => { setLoading(true); setStreaming(''); }; const onDelta = (_d: string, acc: string) => setStreaming(acc); const onAssistant = (m: Message) => { setMessages((prev) => [...prev, m]); setStreaming(''); setLoading(false); }; agent.on('thinking:start', onStart); agent.on('stream:delta', onDelta); agent.on('message:assistant', onAssistant); return () => { agent.off('thinking:start', onStart); agent.off('stream:delta', onDelta); agent.off('message:assistant', onAssistant); }; }, []); return ( <Box flexDirection="column" padding={1}> <Text bold color="magenta">🤖 AllToken Agent</Text> {messages.map((m, i) => ( <Box key={i} flexDirection="column" marginTop={1}> <Text bold color={m.role === 'user' ? 'cyan' : 'green'}> {m.role === 'user' ? '▶ You' : '◀ Assistant'} </Text> <Text wrap="wrap">{m.content}</Text> </Box> ))} {streaming && ( <Box flexDirection="column" marginTop={1}> <Text bold color="green">◀ Assistant</Text> <Text wrap="wrap">{streaming}<Text color="gray">▌</Text></Text> </Box> )} <Box borderStyle="single" borderColor="gray" marginTop={1} paddingX={1}> <Text color="yellow">{'> '}</Text> <Text>{input}</Text> <Text color="gray">{loading ? ' ···' : '█'}</Text> </Box> </Box> ); } render(<App />);
For Python users — including those embedding the agent inside Hermes or OpenClaw Python tools:
pythonimport os from openai import OpenAI client = OpenAI( api_key=os.environ["ALLTOKEN_API_KEY"], base_url="https://api.alltoken.ai/v1", ) # Streaming chat stream = client.chat.completions.create( model="minimax-m2.7", messages=[{"role": "user", "content": "Explain SSE in one sentence."}], stream=True, ) for chunk in stream: if chunk.choices[0].delta.content: print(chunk.choices[0].delta.content, end="", flush=True) print()
Async image (poll loop):
pythonimport os, time, base64, uuid, requests BASE = "https://api.alltoken.ai/v1" H = {"Authorization": f"Bearer {os.environ['ALLTOKEN_API_KEY']}", "Content-Type": "application/json"} task = requests.post( f"{BASE}/images/generations/async", headers={**H, "Idempotency-Key": str(uuid.uuid4())}, json={"model": "gpt-image-2", "prompt": "A cat astronaut, studio light", "size": "1024x1024", "quality": "high"}, ).json() while True: res = requests.get(f"{BASE}/images/generations/{task['id']}", headers=H).json() if res["status"] in ("completed", "failed", "cancelled"): break time.sleep(2) if res["status"] == "completed": with open("cat.png", "wb") as f: f.write(base64.b64decode(res["data"][0]["b64_json"]))
Both Hermes and OpenClaw load skills from SKILL.md files and can run TypeScript or Python tools at the agent boundary. There are two integration patterns:
Point your host agent's HTTP client at AllToken. In OpenClaw / Hermes config, set:
yamlprovider: base_url: https://api.alltoken.ai/v1 api_key: ${ALLTOKEN_API_KEY} model: minimax-m2.7
No code changes needed — the OpenAI-compatible endpoint accepts the same requests.
Drop the agent.ts / media.ts modules into the host agent's tools directory and expose them as callable tools (chat, generate_image, generate_video). The host agent (running on any model) then delegates multimodal work to AllToken on demand.
typescript// host-agent-tool.ts import { createAgent } from './agent.js'; import { generateImage, generateVideo } from './media.js'; const alltoken = createAgent({ model: 'minimax-m2.7' }); export const tools = { alltoken_chat: (input: { prompt: string }) => alltoken.sendSync(input.prompt), alltoken_image: (input: { prompt: string; size?: string }) => generateImage(input as any), alltoken_video: (input: { prompt: string; duration?: number }) => generateVideo({ model: 'seedance-1.5-pro', ...input }), };
Verified-working model IDs as of 2026-05-12 (use these for quick starts; re-confirm via GET /v1/models before production):
| Use case | IDs | |----------|-----| | Chat — cheap / fast | gpt-5.4-nano, gpt-5.4-mini, claude-haiku-4-5, gemini-3-flash-preview, glm-4.7-flash, qwen3.6-flash, deepseek-v4-flash, minimax-m2.5-highspeed | | Chat — flagship | gpt-5.4, gpt-5.4-pro, gpt-5.5, claude-opus-4-7, claude-sonnet-4-6, gemini-3.1-pro-preview, glm-5.1, deepseek-v4-pro, qwen3.6-max-preview, kimi-k2.6, minimax-m2.7 | | Chat — code | gpt-5.3-codex, qwen3-coder-next | | Image | gpt-image-2 | | Video — text/image to video | seedance-1.5-pro, seedance-2.0, happyhorse-1.0-t2v, happyhorse-1.0-i2v | | Video — editing / reference | happyhorse-1.0-video-edit, happyhorse-1.0-r2v |
Available chat models on a fresh key: 38 as of this writing. Image: 1. Video: 7.
Do not hardcode model IDs in production — the catalog evolves. Use the live endpoints:
typescript// OpenAI-compatible list (good for SDK clients) const list = await fetch('https://api.alltoken.ai/v1/models', { headers: { Authorization: `Bearer ${process.env.ALLTOKEN_API_KEY}` }, }).then((r) => r.json()); // Rich catalog with pricing, capabilities, tags (used by the website) const catalog = await fetch('https://api.alltoken.ai/api-account/models').then((r) => r.json()); // Single model detail page const detail = await fetch('https://api.alltoken.ai/api-account/models/gpt-5.4').then((r) => r.json());
Pair with the Rankings API (GET /api-account/rankings/all) for live leaderboards by usage, benchmarks, throughput, and category leaders — useful for --auto model selection.
AllToken handles provider routing internally. Two knobs:
routing_mode (code or manual), allowed_models, and a default_models priority list on each API key: POST /api-account/user/api-keys PUT /api-account/user/api-keys/{key_id}/default-models
model ID in the request body to bypass routing for that call.When a provider returns 502/503, AllToken may automatically fall back to the next provider for the model.
enable_search)Pass enable_search: true on a chat completion to opt into AllToken's unified web-search backend. Support is per-provider, not per-request shape — same flag, different effective behavior across model families. Live probe on 2026-05-12 (asking "current Bitcoin price"):
| Family | Outcome | Notes | |---|---|---| | DeepSeek (deepseek-v3.2, deepseek-v4-pro) | ✅ Searches | Returns fresh prices with timestamps | | Qwen (qwen3.6-flash, qwen3.6-max-preview) | ✅ Searches | Same fresh data via the unified backend | | Claude (claude-opus-4-7, claude-sonnet-4-6) | ❌ Silently ignores | Model responds "I don't have web search" | | GLM (glm-5, glm-5.1) | ❌ Silently ignores | Same as Claude | | Kimi (kimi-k2.6) | ❌ Silently ignores | | | Minimax (minimax-m2.7) | ❌ Silently ignores | | | Gemini (gemini-3.1-pro-preview) | ⚠️ Empty / refusal | Inconsistent — re-test before relying | | OpenAI (gpt-5.4, gpt-5.4-nano, gpt-5.5) | 🔴 HTTP 503 all_providers_failed | Upstream rejects the flag |
Recommendation: when you need search, default to a DeepSeek or Qwen model. If you're on a different family, fall back to a function-calling pattern (model emits a tool call → your tool hits a search API → you re-invoke). The enable_search matrix above is empirical and provider-side support may change — re-test for critical paths.
Note: AllToken does not include search-result citations in the response annotations[] field today, so detecting "did search fire" requires latency heuristics (typically +6 – 15 s vs no-search baseline) or content sniffing for fresh facts.
rpm_limit, tpm_limit, monthly_quota, credit_limit when creating the key.400 invalid params · 401 bad key · 402 insufficient balance · 403 forbidden · 404 not found · 429 rate limited (respect Retry-After) · 5xx upstream — already retried server-side when safe.json { "error": { "code": "invalid_api_key", "message": "Invalid or revoked API key", "param": null, "type": "auth_error", "request_id": "d81itf8gdg1fp5ko4bjg" } } Note: code is a string slug (e.g. "invalid_api_key", "image_already_retrieved", "all_providers_failed"), not the numeric HTTP status. type groups errors (auth_error, invalid_request_error, api_error, …). Include request_id when filing support tickets.
Python error-dispatch helper:
pythonimport json, time, urllib.request, urllib.error def call(req): try: return urllib.request.urlopen(req, timeout=60) except urllib.error.HTTPError as e: body = e.read() try: err = json.loads(body).get("error", {}) except Exception: err = {} retry_after = e.headers.get("Retry-After") # integer seconds (AllToken format) if e.code == 429 and retry_after: time.sleep(int(retry_after)) return call(req) # one retry if e.code == 401: raise RuntimeError(f"auth: {err.get('code')} — rotate API key") if e.code == 402: raise RuntimeError(f"top up credits: {err.get('message')}") if e.code == 410 and err.get("code") == "image_already_retrieved": raise RuntimeError("re-submit; image was already delivered") if 500 <= e.code < 600 and err.get("code") == "all_providers_failed": raise RuntimeError("upstream — try fallback model or retry with jitter") raise RuntimeError(f"{e.code} {err.get('type')}/{err.get('code')}: {err.get('message')} [req={err.get('request_id')}]")
Retry-After is sent as integer seconds. Always combine an explicit retry-after read with exponential backoff (+ jitter) as the fallback when the header is missing.
GET /api-account/health/summary (returns {"data": {...}} envelope) and /health/routes show live availability, p50/p95 latency, and incident routes — wire this into your runbook.Per-request cost: every chat response includes a usage block:
json"usage": { "prompt_tokens": 13, "completion_tokens": 4, "total_tokens": 17, "prompt_tokens_details": { "cached_tokens": 0, "cache_creation_input_tokens": 0, "audio_tokens": 0 }, "completion_tokens_details": { "reasoning_tokens": 0, "audio_tokens": 0, "accepted_prediction_tokens": 0, "rejected_prediction_tokens": 0 } }
Capture usage from a streaming response: pass stream_options: {"include_usage": true}. The final data: chunk before data: [DONE] will have choices: [] and the populated usage. Without this option, usage is null on every streamed chunk.
pythonstream = client.chat.completions.create( model="gpt-5.4-nano", messages=[...], stream=True, stream_options={"include_usage": True}, ) usage = None for chunk in stream: if chunk.choices and chunk.choices[0].delta.content: print(chunk.choices[0].delta.content, end="", flush=True) if chunk.usage is not None: usage = chunk.usage # only present on the terminal chunk
Per-request cost telemetry (vendor extension): AllToken also emits one extra SSE comment line after data: [DONE] with a fiat-priced breakdown:
: {"cost":"0.0000188000","input_price":"0.0002000000","output_price":"0.0012500000","prompt_tokens":19,"completion_tokens":12}Standard OpenAI SDKs drop comment lines (lines beginning with :), so this is invisible when using openai. To capture it, parse the raw SSE stream yourself and do not stop on [DONE]:
python# stdlib-only — captures both usage (from data: chunks) AND cost comment (post-DONE) import urllib.request, json req = urllib.request.Request(URL, data=BODY, method="POST", headers=H) r = urllib.request.urlopen(req) saw_done = False for raw in iter(r.readline, b""): line = raw.decode().rstrip("\n") if line.startswith(":"): cost = json.loads(line[1:]) # {"cost": "...", ...} elif line.startswith("data: "): data = line[6:] if data == "[DONE]": saw_done = True; continue # ... parse chunk
Other useful response metadata: chat responses also carry top-level service_tier (e.g. "default") and x-gateway-request-id (use this when filing support tickets).
Account-wide totals: /api-account/user/{balance,billing,usage,billing/orders,...} exist but are not callable with the API key — they need the web-session token. Check balance and history in Settings → Billing on https://alltoken.ai, or top up via the same dashboard.
typescriptconst agent = createAgent({ model: 'minimax-m2.7' }); agent.on('message:user', (m) => db.insert('user', m.content)); agent.on('message:assistant', (m) => db.insert('assistant', m.content)); agent.on('tool:call', (name, args) => analytics.track('tool', { name, args })); agent.on('error', (err) => sentry.capture(err));
typescriptimport express from 'express'; import { createAgent, type Agent } from './agent.js'; const app = express(); app.use(express.json()); const sessions = new Map<string, Agent>(); app.post('/chat', async (req, res) => { const { sessionId, message } = req.body; let agent = sessions.get(sessionId); if (!agent) { agent = createAgent(); sessions.set(sessionId, agent); } res.json({ response: await agent.sendSync(message), history: agent.getMessages() }); }); app.listen(3000);
createAgent(config)| Option | Type | Default | Description | |----------------|---------------|-------------------------------|------------------------------------------| | apiKey | string | process.env.ALLTOKEN_API_KEY| AllToken API key | | model | string | 'minimax-m2.7' | Model ID (see model discovery) | | instructions | string | 'You are a helpful assistant.' | System prompt | | tools | ToolHandler[]| [] | Function-calling tools | | maxSteps | number | 5 | Max tool-loop iterations | | temperature | number | 0.7 | Sampling temperature 0–2 | | enableSearch | boolean | false | AllToken enable_search extension |
| Method | Returns | Description | |---------------------|-------------------|-----------------------------------| | send(content) | Promise<string> | Streaming send + tool loop | | sendSync(content) | Promise<string> | Non-streaming send | | getMessages() | Message[] | Full conversation | | clearHistory() | void | Reset (keeps system prompt) | | setInstructions() | void | Update system prompt | | addTool(tool) | void | Register tool at runtime |
| Event | Payload | Notes | |--------------------|-------------------------------|------------------------------------| | message:user | Message | | | message:assistant| Message | Final turn (post tool loop) | | stream:start | — | | | stream:delta | (delta, accumulated) | OpenAI-style token chunks | | stream:end | fullText | | | tool:call | (name, args, callId) | | | tool:result | (name, result, callId) | | | thinking:start | — | | | thinking:end | — | | | error | Error | |
Core API
Guides (one topic per page)
Live endpoints (callable now)
GET https://api.alltoken.ai/v1/models (Bearer)GET https://api.alltoken.ai/api-account/modelsGET https://api.alltoken.ai/api-account/health/summaryAccount management: Settings → API Keys / Billing on https://alltoken.ai (web session required; not callable with your Bearer API key)
SDKs
| Case | Status | Duration (ms) | Turns | Tokens | Tool calls | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Without | With | Δ | Without | With | Δ | Without | With | Δ | Without | With | Δ | ||
case-01 | fail→pass | 29,817 | 15,666 | -47% | 1 | 1 | 0% | 4,889 | 15,769 | +223% | 0 | 0 | — |
case-02 | fail→pass | 16,926 | 14,504 | -14% | 1 | 1 | 0% | 3,727 | 15,381 | +313% | 0 | 0 | — |
case-03 | fail→pass | 11,688 | 13,756 | +18% | 1 | 1 | 0% | 2,281 | 15,251 | +569% | 0 | 0 | — |
case-04 | fail→pass | 7,087 | 4,044 | -43% | 1 | 1 | 0% | 1,347 | 12,935 | +860% | 0 | 0 | — |
case-05 | fail→pass | 13,580 | 3,505 | -74% | 1 | 1 | 0% | 2,034 | 12,852 | +532% | 0 | 0 | — |
case-06 | fail→pass | 13,107 | 4,874 | -63% | 1 | 1 | 0% | 1,623 | 13,116 | +708% | 0 | 0 | — |
case-24 | fail→pass | 10,174 | 4,107 | -60% | 1 | 1 | 0% | 1,695 | 12,936 | +663% | 0 | 0 | — |
case-07 | fail→pass | 14,916 | 3,265 | -78% | 1 | 1 | 0% | 2,554 | 12,805 | +401% | 0 | 0 | — |
case-08 | pass→pass | 8,618 | 4,581 | -47% | 1 | 1 | 0% | 1,625 | 12,875 | +692% | 0 | 0 | — |
case-09 | fail→pass | 9,820 | 3,175 | -68% | 1 | 1 | 0% | 1,723 | 12,571 | +630% | 0 | 0 | — |
case-10 | pass→pass | 7,520 | 3,498 | -53% | 1 | 1 | 0% | 921 | 12,847 | +1295% | 0 | 0 | — |
case-11 | fail→pass | 10,401 | 4,530 | -56% | 1 | 1 | 0% | 1,919 | 12,911 | +573% | 0 | 0 | — |
case-12 | fail→pass | 9,238 | 2,808 | -70% | 1 | 1 | 0% | 1,521 | 12,691 | +734% | 0 | 0 | — |
case-13 | pass→pass | 4,426 | 1,827 | -59% | 1 | 1 | 0% | 772 | 12,436 | +1511% | 0 | 0 | — |
case-14 | pass→pass | 8,469 | 2,357 | -72% | 1 | 1 | 0% | 1,463 | 12,594 | +761% | 0 | 0 | — |
case-15 | pass→pass | 5,593 | 3,537 | -37% | 1 | 1 | 0% | 911 | 12,811 | +1306% | 0 | 0 | — |
case-16 | fail→pass | 7,927 | 3,393 | -57% | 1 | 1 | 0% | 1,245 | 12,870 | +934% | 0 | 0 | — |
case-17 | fail→pass | 9,513 | 4,154 | -56% | 1 | 1 | 0% | 1,735 | 12,931 | +645% | 0 | 0 | — |
case-18 | fail→pass | 6,500 | 2,519 | -61% | 1 | 1 | 0% | 1,052 | 12,561 | +1094% | 0 | 0 | — |
case-19 | pass→pass | 4,744 | 3,110 | -34% | 1 | 1 | 0% | 855 | 12,748 | +1391% | 0 | 0 | — |
case-20 | fail→pass | 7,867 | 2,067 | -74% | 1 | 1 | 0% | 1,333 | 12,577 | +844% | 0 | 0 | — |
case-21 | fail→pass | 7,789 | 2,772 | -64% | 1 | 1 | 0% | 1,172 | 12,660 | +980% | 0 | 0 | — |
case-22 | fail→pass | 8,710 | 8,559 | -2% | 1 | 1 | 0% | 1,351 | 13,922 | +930% | 0 | 0 | — |
case-23 | fail→fail | 6,695 | 6,172 | -8% | 1 | 1 | 0% | 1,287 | 13,365 | +938% | 0 | 0 | — |
DecimalAI ran this skill against gemini-3.6-flash twice over the same eval suite — once with the skill loaded and once without — and compared the two runs case by case. 24 cases were attempted. The headline lift of +71 percentage points is the difference between those two pass rates over the 24 comparable cases.
Without the skill loaded, the model failed this case. With it loaded, the same prompt on the same model passed. This is one improved case from the latest verified run; every case, including any that regressed, is in the table above.
Other measured skills in the registry, with their headline benchmark lift.