Unified LLM API (AgentHub)
@prismshadow/agenthub is a unified TypeScript client for model APIs: streaming text, image generation, speech synthesis and embeddings behind one entry point.
npm install @prismshadow/agenthub
The main entry point is AutoLLMClient:
import { AutoLLMClient } from "@prismshadow/agenthub";
const client = new AutoLLMClient({ model: "<model_id>", apiKey: "<key>", baseUrl: "<url>", clientType: "<type>" });
apiKey, baseUrl and clientType are optional (see routing below). The package also exports listSupportedModels (the model registry) and the error classes AgentHubError, UnsupportedParameterError, EmptyResponseError and ToolCallArgumentParseError.
Before you start
If the user's message only invokes this skill (e.g. "use agenthub-models skill") without a concrete task, ask the user what they want to build. Do not write code until the requirement is clear.
Important prerequisite — set the key up first, then develop. When the script is an AI app you are building for the user, have them add the model API key in this agent's key vault (gear icon on its card, Agents page → settings → key vault tab) before you start, so the credential is in your shell environment. If the app stores its own model config, keep its Penguin data root inside the CWD workspace (--root ./penguin_data), never ~/.penguin. Model ids can come from the penguin CLI catalog and the id table below.
Check for a usable API key before writing code — the client needs one for whichever provider you target:
env | grep -oE "(DEEPSEEK|OPENAI|ANTHROPIC|GEMINI|ZAI|MOONSHOT|MINIMAX)_API_KEY" || echo none
Vault keys also appear in your Vault Keys section. Only two sources count as a usable key: a vault-injected environment variable (the check above), or — when the app stores its own model config — a key already configured in the app's own data root (penguin config model list --root <data_dir>). Keys living in the global ~/.penguin or any other .penguin directory do not count — a bare penguin config model list (no --root) reads the global store, because the CLI defaults to the global root unless --root is given, so a key showing up there proves nothing for your script and must never be used or copied.
If neither counted source yields a usable key, stop immediately and ask the user to configure one — do not write code, and do not keep calling tools to retry: ask them to add one in the agent's key vault (gear icon on the agent's card, Agents page → settings → key vault tab); vault values reach your shell environment on the next task. Re-checking the environment or the vault in a loop just wastes turns — one clear check, then hand back to the user.
Keep model API keys project-local: for an app that stores its own model config, write the key into the project under the working directory with the penguin CLI, always passing --root <data_dir> for a directory inside the current working directory (penguin config model add --root ./penguin_data --provider <group> --model-id <id> --api-key <key>) — without --root it writes to the global ~/.penguin/data instead. --provider is required alongside --model-id: a model entry is the (provider, model_id) pair and the group is never inferred (use custom for an endpoint outside the built-in groups). Otherwise rely on vault-injected environment variables. Never read, copy or fall back to model keys stored in the user's global ~/.penguin directory — that config belongs to the person running Penguin, not to your script.
Model IDs
Use exact model ids. If an id is not in the table below and the user has not given one, ask the user to confirm the exact id before writing code.
| Family |
Official IDs |
Gateway variants |
| Gemini 3.8 |
gemini-3.8-flash |
OpenRouter google/gemini-3.8-flash |
| Gemini 3.7 |
gemini-3.7-flash |
OpenRouter google/gemini-3.7-flash |
| Gemini 3.6 |
gemini-3.6-flash, gemini-3.5-flash-lite |
— |
| Gemini 3 |
gemini-3.1-pro-preview, gemini-3.5-flash, gemini-3.1-flash-lite |
— |
| Gemini 3 image |
gemini-3.1-flash-image, gemini-3-pro-image-preview |
— |
| Gemini 3 TTS |
gemini-3.1-flash-tts-preview |
— |
| Gemini embedding |
gemini-embedding-2 |
— |
| Claude 5 |
claude-fable-5, claude-opus-5, claude-sonnet-5 |
OpenRouter anthropic/claude-fable-5, anthropic/claude-opus-5, anthropic/claude-sonnet-5 |
| Claude 4 |
claude-sonnet-4-6, claude-opus-4-7, claude-opus-4-8 |
OpenRouter anthropic/claude-opus-4.8, anthropic/claude-opus-4.7 |
| GPT-6 |
gpt-6-astra |
OpenRouter openai/gpt-6-astra |
| GPT-5.6 |
gpt-5.6 (routes to sol), gpt-5.6-terra, gpt-5.6-luna |
OpenRouter openai/gpt-5.6-sol, openai/gpt-5.6-terra, openai/gpt-5.6-luna |
| GPT-5.5 / 5.4 |
gpt-5.5, gpt-5.4, gpt-5.4-mini, gpt-5.4-nano |
OpenRouter openai/gpt-5.5, openai/gpt-5.5-pro, openai/gpt-5.4, openai/gpt-5.4-mini, openai/gpt-5.4-nano, openai/gpt-5.4-pro |
| OpenAI embedding |
text-embedding-3-small, text-embedding-3-large |
— |
| MiniMax M3 |
MiniMax-M3 |
OpenRouter minimax/minimax-m3 |
| Kimi K3 |
kimi-k3 |
OpenRouter moonshotai/kimi-k3 |
| Kimi K2.7 Code |
— |
SiliconFlow moonshotai/Kimi-K2.7-Code; Fireworks AI accounts/fireworks/models/kimi-k2p7-code |
| Kimi K2.6 |
kimi-k2.6 |
OpenRouter moonshotai/kimi-k2.6; SiliconFlow Pro/moonshotai/Kimi-K2.6 |
| DeepSeek V4 |
deepseek-flash, deepseek-v4-pro, deepseek-v4-flash, deepseek-v4-flash-vision-exp |
OpenRouter deepseek/deepseek-v4.1-flash, deepseek/deepseek-v4-pro-0813, deepseek/deepseek-v4-pro, deepseek/deepseek-v4-flash, deepseek/deepseek-v4-flash-0731, deepseek/deepseek-v4-flash-vision-exp; Fireworks AI accounts/fireworks/models/deepseek-v4-flash-0731; SiliconFlow deepseek-ai/DeepSeek-V4-Pro, deepseek-ai/DeepSeek-V4-Flash |
| GLM 5.3 |
glm-5.3, glm-5.3-flash |
OpenRouter z-ai/glm-5.3, z-ai/glm-5.3-flash |
| GLM 5.2 |
glm-5.2 |
OpenRouter z-ai/glm-5.2; SiliconFlow zai-org/GLM-5.2 |
| GLM 5.1 |
glm-5.1 |
— |
| Qwen 3.8 Max |
— |
OpenRouter qwen/qwen3.8-max |
| Qwen 3.8 Flash |
— |
Qwen DashScope qwen3.8-flash |
| Qwen 3.6 |
— |
OpenRouter qwen/qwen3.6-35b-a3b; SiliconFlow Qwen/Qwen3.6-35B-A3B |
| Inkling |
— |
OpenRouter thinkingmachines/inkling; Fireworks AI accounts/fireworks/models/inkling |
The image endpoint dropped its preview suffix: gemini-3.1-flash-image-preview is deprecated, use gemini-3.1-flash-image.
glm-5.3-flash is the one GLM model that reads images (@prismshadow/agenthub >= 0.4.8). The GLM client sends an image_url item as an image_url part, in a prompt and in a tool result alike, and both an HTTP(S) URL and a base64 data URL pass through unchanged. The version match is case-insensitive, so the gateway spellings z-ai/glm-5.3-flash and zai-org/GLM-5.3-Flash are recognised too. Every other GLM id refuses an image rather than dropping it — GLM <id> does not support image inputs. in a prompt, GLM <id> does not support images in tool results. in a tool result — and that includes glm-5v-turbo.
Gateway model lists can be queried online:
curl https://openrouter.ai/api/v1/models
curl --request GET --url https://api.siliconflow.cn/v1/models --header 'Authorization: Bearer <token>'
Supported-model registry
listSupportedModels(currency?) returns the models AgentHub itself knows how to route, so ids, endpoints, modalities, context windows and prices can be read from the package instead of being hardcoded:
import { listSupportedModels } from "@prismshadow/agenthub";
for (const m of listSupportedModels()) {
console.log(m.model, m.base_url, m.client, m.context_window, m.pricing?.prompt_tokens);
}
- Each
SupportedModel is { model, base_url, client, input_modalities, output_modalities, context_window?, pricing? }. The (model, base_url, client) triple maps straight onto the constructor: new AutoLLMClient({ model, baseUrl: base_url, clientType: client }).
- Modalities are
"Text" | "Image" | "Video" | "Audio" | "Embed". Coverage includes the official vendor endpoints plus the OpenRouter and SiliconFlow gateways; context_window and pricing are omitted where the platform publishes no authoritative value (image and TTS models, for instance).
pricing is per million tokens, keyed by the same usage buckets as usage_metadata: prompt_tokens (non-cached input), thoughts_tokens / response_tokens (both the output price) and optional cached_tokens (cache-hit price). Values are stored in USD; pass listSupportedModels("CNY") to convert at 7 CNY/USD.
The registry is the curated current line-up, so prefer it when picking a model or estimating cost. It is not the routing table, and it lags in both directions: older ids in the table above (gpt-5.4, claude-opus-4-7, gemini-3.1-pro-preview, gemini-3.1-flash-lite) still route fine without appearing in it, and a newly launched id can route before the registry carries it. For an id the registry omits, take the context window and price from the vendor's own page.
Routing and credentials
- Without
clientType, the client auto-routes by model id substring, in this order: minimax-m3 (exact), gemini-3* / gemini-embedding, claude 4-6/4-7/4-8/-5, gpt-5.4/gpt-5.5/gpt-5.6/gpt-6, glm-5 (whole series, 5.3 included), kimi-k3/kimi-k2.5/kimi-k2.6, deepseek-v4, ant-messages, openai-responses, openai+embedding (embeddings), openai (chat). Ids matching none of these throw. Most gateway variants in the table above hit the same substrings, so they route to the right family — just set baseUrl to the gateway endpoint.
- Three generic protocol clients cover everything else (all take
baseUrl + apiKey):
clientType: "openai-chat" — any OpenAI Chat Completions compatible endpoint (gateway models, Qwen via OpenRouter/SiliconFlow or DashScope https://dashscope.aliyuncs.com/compatible-mode/v1, local vLLM, …). Renamed from openai in AgentHub 0.4.2; the bare openai string still routes as a deprecated alias.
clientType: "openai-responses" — OpenAI Responses-compatible endpoints (OpenAI, OpenRouter, DeepSeek, Z.AI, MiniMax all serve one).
clientType: "ant-messages" — Anthropic Messages-compatible endpoints (Anthropic, OpenRouter https://openrouter.ai/api, DeepSeek https://api.deepseek.com/anthropic, Z.AI, MiniMax).
- Exception: an id served by an OpenAI-compatible gateway that still matches a first-party substring (e.g. OpenRouter's
google/gemini-3.7-flash, anthropic/claude-sonnet-5 or openai/gpt-5.6-sol on the /api/v1 endpoint) would auto-route to the vendor protocol client — and a dotted id like anthropic/claude-opus-4.8 matches nothing and throws. Always pass an explicit clientType for gateway ids; never rely on the id. Routing reads clientType (or the model id) as a plain lowercased string and never looks at baseUrl, so the vendor prefix gives no protection.
- OpenRouter serves both protocols at
https://openrouter.ai/api/v1, for every upstream it resells and not just its openai/* ids, so any id it serves works with clientType: "openai-responses" as well as "openai-chat"; use Responses when you want reasoning items round-tripped.
- The first-party
deepseek-v4 client posts to {baseUrl}/responses (AgentHub 0.4.6 moved it off Chat Completions). A self-hosted endpoint serving a deepseek-v4* id over Chat Completions must therefore pass clientType: "openai-chat" explicitly rather than rely on id routing.
- API key: constructor parameter first, then the provider environment variable —
DEEPSEEK_API_KEY, ANTHROPIC_API_KEY (also for ant-messages), OPENAI_API_KEY (also for openai-chat/openai-responses), GEMINI_API_KEY, ZAI_API_KEY, MOONSHOT_API_KEY, MINIMAX_API_KEY. Base URLs read the same names with _BASE_URL.
Streaming text
for await (const event of client.streamingResponseStateful({
message: { role: "user", content_items: [{ type: "text", text: "Hello" }] },
config: {},
})) {
for (const item of event.content_items) {
if (item.type === "text") process.stdout.write(item.text);
}
}
- Each
event is a UniEvent: event_type is start | delta | stop, and content_items carry the increments.
config accepts max_tokens, temperature, system_prompt, thinking_level (the ThinkingLevel enum, NONE to MAX), tool_choice, prompt_caching, fast_mode and tools.
streamingResponseStateful keeps conversation history inside the client; manage it with getHistory() / setHistory(history) / clearHistory(). The stateless variant is streamingResponse({ messages, config }).
Config parameters the model may reject
A config value the target client cannot honour throws UnsupportedParameterError (an AgentHubError carrying client and parameter) while building the request, before anything reaches the network:
import { UnsupportedParameterError } from "@prismshadow/agenthub";
try {
// ...
} catch (err) {
if (err instanceof UnsupportedParameterError) console.error(err.parameter, err.message);
}
thinking_level never throws: every client maps each level onto the closest one the model supports, and MAX (the tier above XHIGH, added in AgentHub 0.4.4) degrades silently wherever the vendor has no such tier — Gemini and MiniMax M3 stop at high. Kimi K3 reasons unconditionally, so NONE degrades to its lowest effort rather than disabling thinking; GLM-5.2 sends reasoning_effort alongside its thinking block and only NONE disables it. GLM-5.3 thinks unconditionally (NONE degrades to the light low effort) and clamps reasoning_effort to low/high/max; gemini-3.7-* clamps to low/medium/high (NONE degrades to low). DeepSeek V4 accepts low/high/max and maps medium and xhigh onto high server-side, so since 0.4.4 LOW sends low (it sent high) and XHIGH sends high (it sent max).
temperature is rejected outright by Gemini 3.6/3.7 — those generations deprecated the sampling parameters, so the client refuses them instead of sending a value the API ignores. GPT-5.5/5.6, the whole Claude 4.6+ family (4.6 included since AgentHub 0.4.2), DeepSeek V4, Kimi K2.6 and Kimi K3 accept only the protocol default 1.0 and reject any other value. Gemini 3, GLM and the generic protocol clients (openai-chat / openai-responses / ant-messages) pass it through.
tool_choice: "auto" is safe everywhere. Claude accepts a single forced tool name; DeepSeek V4 and Kimi K2.6 allow "auto" / "none"; Kimi K3 adds "required" but refuses a specific tool (K2.x also rejects "required"); GLM only accepts "auto".
prompt_caching: every client accepts PromptCaching.ENABLE and rejects the other values — caching is on by default and Kimi K3 caches context automatically.
fast_mode (UniConfig, AgentHub 0.4.2): fast processing at premium pricing. OpenAI-protocol clients (openai-chat, openai-responses, gpt-6, minimax-m3) map it to service_tier: "priority"; Anthropic-protocol clients (ant-messages, claude-5) map it to speed: "fast" with a beta header (an Anthropic research preview limited to Claude Opus 5 / Opus 4.8 — organizations without access get a 429). Clients without a fast tier (Gemini, GLM, Kimi, DeepSeek, embeddings, Claude 4.6 models) raise UnsupportedParameterError; DeepSeek and Z.AI's OpenAI-compatible endpoints simply ignore the tier.
Leave a parameter unset and the protocol default applies, which is the portable choice when a script must run against several families.
Image generation
Use a Gemini image model (see Model IDs) and set config.image_config (optional aspect_ratio, and image_size of "1K" | "2K"):
import fs from "node:fs";
const client = new AutoLLMClient({ model: "gemini-3.1-flash-image" });
for await (const event of client.streamingResponseStateful({
message: { role: "user", content_items: [{ type: "text", text: "A penguin on a glacier" }] },
config: { image_config: { aspect_ratio: "16:9", image_size: "2K" } },
})) {
for (const item of event.content_items) {
if (item.type === "inline_data") fs.writeFileSync("image.png", item.data);
}
}
Images arrive as inline_data content items (data is a Buffer, with mime_type).
Speech synthesis
Use a Gemini TTS model (gemini-3.1-flash-tts-preview) and set config.tts_config:
config: { tts_config: [{ voice: "Kore" }] }
- One entry → single voice; two entries → multi-speaker, and each entry must also set
speaker.
- The
inline_data output is raw PCM (24kHz 16-bit mono) — wrap it in a WAV header yourself before saving as .wav.
Embeddings
Two routes:
- Gemini: a model whose id contains
gemini-embedding auto-routes (gemini-embedding-2).
- Any OpenAI-compatible embeddings endpoint: pass
clientType: "openai-embedding" (plus baseUrl and apiKey as needed) — ids like text-embedding-3-small / text-embedding-3-large match no auto-route substring and would throw without it.
Optional config.embedding_config:
config: { embedding_config: { dimensions: 768 } }
The output arrives as embedding content items (embedding is a number array).
1---2name: unified-llm-api3description: Call model APIs through @prismshadow/agenthub — streaming text generation, image generation, speech synthesis, embeddings and the supported-model registry with one client.4---56# Unified LLM API (AgentHub)78`@prismshadow/agenthub` is a unified TypeScript client for model APIs: streaming text, image generation, speech synthesis and embeddings behind one entry point.910```bash11npm install @prismshadow/agenthub12```1314The main entry point is `AutoLLMClient`:1516```ts17import { AutoLLMClient } from "@prismshadow/agenthub";1819const client = new AutoLLMClient({ model: "<model_id>", apiKey: "<key>", baseUrl: "<url>", clientType: "<type>" });20```2122`apiKey`, `baseUrl` and `clientType` are optional (see routing below). The package also exports `listSupportedModels` (the model registry) and the error classes `AgentHubError`, `UnsupportedParameterError`, `EmptyResponseError` and `ToolCallArgumentParseError`.2324## Before you start2526If the user's message only invokes this skill (e.g. "use agenthub-models skill") without a concrete task, ask the user what they want to build. Do not write code until the requirement is clear.2728**Important prerequisite — set the key up first, then develop.** When the script is an AI app you are building for the user, have them add the model API key in **this agent's key vault** (gear icon on its card, Agents page → settings → key vault tab) *before* you start, so the credential is in your shell environment. If the app stores its own model config, keep its Penguin data root **inside the CWD workspace** (`--root ./penguin_data`), never `~/.penguin`. Model ids can come from the penguin CLI catalog and the id table below.2930Check for a usable API key before writing code — the client needs one for whichever provider you target:3132```bash33env | grep -oE "(DEEPSEEK|OPENAI|ANTHROPIC|GEMINI|ZAI|MOONSHOT|MINIMAX)_API_KEY" || echo none34```3536Vault keys also appear in your Vault Keys section. **Only two sources count as a usable key**: a vault-injected environment variable (the check above), or — when the app stores its own model config — a key already configured in the app's own data root (`penguin config model list --root <data_dir>`). Keys living in the global `~/.penguin` or any other `.penguin` directory do **not** count — a bare `penguin config model list` (no `--root`) reads the global store, because the CLI defaults to the global root unless `--root` is given, so a key showing up there proves nothing for your script and must never be used or copied.3738If neither counted source yields a usable key, **stop immediately and ask the user to configure one — do not write code, and do not keep calling tools to retry**: ask them to add one in the agent's **key vault** (gear icon on the agent's card, Agents page → settings → key vault tab); vault values reach your shell environment on the next task. Re-checking the environment or the vault in a loop just wastes turns — one clear check, then hand back to the user.3940Keep model API keys **project-local**: for an app that stores its own model config, write the key into the project under the working directory with the penguin CLI, **always passing `--root <data_dir>` for a directory inside the current working directory** (`penguin config model add --root ./penguin_data --provider <group> --model-id <id> --api-key <key>`) — without `--root` it writes to the global `~/.penguin/data` instead. `--provider` is required alongside `--model-id`: a model entry is the `(provider, model_id)` pair and the group is never inferred (use `custom` for an endpoint outside the built-in groups). Otherwise rely on vault-injected environment variables. Never read, copy or fall back to model keys stored in the user's global `~/.penguin` directory — that config belongs to the person running Penguin, not to your script.4142## Model IDs4344Use exact model ids. If an id is not in the table below and the user has not given one, ask the user to confirm the exact id before writing code.4546| Family | Official IDs | Gateway variants |47| ---------------- | --------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- |48| Gemini 3.8 | `gemini-3.8-flash` | OpenRouter `google/gemini-3.8-flash` |49| Gemini 3.7 | `gemini-3.7-flash` | OpenRouter `google/gemini-3.7-flash` |50| Gemini 3.6 | `gemini-3.6-flash`, `gemini-3.5-flash-lite` | — |51| Gemini 3 | `gemini-3.1-pro-preview`, `gemini-3.5-flash`, `gemini-3.1-flash-lite` | — |52| Gemini 3 image | `gemini-3.1-flash-image`, `gemini-3-pro-image-preview` | — |53| Gemini 3 TTS | `gemini-3.1-flash-tts-preview` | — |54| Gemini embedding | `gemini-embedding-2` | — |55| Claude 5 | `claude-fable-5`, `claude-opus-5`, `claude-sonnet-5` | OpenRouter `anthropic/claude-fable-5`, `anthropic/claude-opus-5`, `anthropic/claude-sonnet-5` |56| Claude 4 | `claude-sonnet-4-6`, `claude-opus-4-7`, `claude-opus-4-8` | OpenRouter `anthropic/claude-opus-4.8`, `anthropic/claude-opus-4.7` |57| GPT-6 | `gpt-6-astra` | OpenRouter `openai/gpt-6-astra` |58| GPT-5.6 | `gpt-5.6` (routes to sol), `gpt-5.6-terra`, `gpt-5.6-luna` | OpenRouter `openai/gpt-5.6-sol`, `openai/gpt-5.6-terra`, `openai/gpt-5.6-luna` |59| GPT-5.5 / 5.4 | `gpt-5.5`, `gpt-5.4`, `gpt-5.4-mini`, `gpt-5.4-nano` | OpenRouter `openai/gpt-5.5`, `openai/gpt-5.5-pro`, `openai/gpt-5.4`, `openai/gpt-5.4-mini`, `openai/gpt-5.4-nano`, `openai/gpt-5.4-pro` |60| OpenAI embedding | `text-embedding-3-small`, `text-embedding-3-large` | — |61| MiniMax M3 | `MiniMax-M3` | OpenRouter `minimax/minimax-m3` |62| Kimi K3 | `kimi-k3` | OpenRouter `moonshotai/kimi-k3` |63| Kimi K2.7 Code | — | SiliconFlow `moonshotai/Kimi-K2.7-Code`; Fireworks AI `accounts/fireworks/models/kimi-k2p7-code` |64| Kimi K2.6 | `kimi-k2.6` | OpenRouter `moonshotai/kimi-k2.6`; SiliconFlow `Pro/moonshotai/Kimi-K2.6` |65| DeepSeek V4 | `deepseek-flash`, `deepseek-v4-pro`, `deepseek-v4-flash`, `deepseek-v4-flash-vision-exp` | OpenRouter `deepseek/deepseek-v4.1-flash`, `deepseek/deepseek-v4-pro-0813`, `deepseek/deepseek-v4-pro`, `deepseek/deepseek-v4-flash`, `deepseek/deepseek-v4-flash-0731`, `deepseek/deepseek-v4-flash-vision-exp`; Fireworks AI `accounts/fireworks/models/deepseek-v4-flash-0731`; SiliconFlow `deepseek-ai/DeepSeek-V4-Pro`, `deepseek-ai/DeepSeek-V4-Flash` |66| GLM 5.3 | `glm-5.3`, `glm-5.3-flash` | OpenRouter `z-ai/glm-5.3`, `z-ai/glm-5.3-flash` |67| GLM 5.2 | `glm-5.2` | OpenRouter `z-ai/glm-5.2`; SiliconFlow `zai-org/GLM-5.2` |68| GLM 5.1 | `glm-5.1` | — |69| Qwen 3.8 Max | — | OpenRouter `qwen/qwen3.8-max` |70| Qwen 3.8 Flash | — | Qwen DashScope `qwen3.8-flash` |71| Qwen 3.6 | — | OpenRouter `qwen/qwen3.6-35b-a3b`; SiliconFlow `Qwen/Qwen3.6-35B-A3B` |72| Inkling | — | OpenRouter `thinkingmachines/inkling`; Fireworks AI `accounts/fireworks/models/inkling` |7374The image endpoint dropped its preview suffix: `gemini-3.1-flash-image-preview` is deprecated, use `gemini-3.1-flash-image`.7576`glm-5.3-flash` is the one GLM model that reads images (`@prismshadow/agenthub` >= 0.4.8). The GLM client sends an `image_url` item as an `image_url` part, in a prompt and in a tool result alike, and both an HTTP(S) URL and a base64 data URL pass through unchanged. The version match is case-insensitive, so the gateway spellings `z-ai/glm-5.3-flash` and `zai-org/GLM-5.3-Flash` are recognised too. Every other GLM id refuses an image rather than dropping it — `GLM <id> does not support image inputs.` in a prompt, `GLM <id> does not support images in tool results.` in a tool result — and that includes `glm-5v-turbo`.7778Gateway model lists can be queried online:7980```bash81curl https://openrouter.ai/api/v1/models82curl --request GET --url https://api.siliconflow.cn/v1/models --header 'Authorization: Bearer <token>'83```8485## Supported-model registry8687`listSupportedModels(currency?)` returns the models AgentHub itself knows how to route, so ids, endpoints, modalities, context windows and prices can be read from the package instead of being hardcoded:8889```ts90import { listSupportedModels } from "@prismshadow/agenthub";9192for (const m of listSupportedModels()) {93 console.log(m.model, m.base_url, m.client, m.context_window, m.pricing?.prompt_tokens);94}95```9697- Each `SupportedModel` is `{ model, base_url, client, input_modalities, output_modalities, context_window?, pricing? }`. The `(model, base_url, client)` triple maps straight onto the constructor: `new AutoLLMClient({ model, baseUrl: base_url, clientType: client })`.98- Modalities are `"Text" | "Image" | "Video" | "Audio" | "Embed"`. Coverage includes the official vendor endpoints plus the OpenRouter and SiliconFlow gateways; `context_window` and `pricing` are omitted where the platform publishes no authoritative value (image and TTS models, for instance).99- `pricing` is per million tokens, keyed by the same usage buckets as `usage_metadata`: `prompt_tokens` (non-cached input), `thoughts_tokens` / `response_tokens` (both the output price) and optional `cached_tokens` (cache-hit price). Values are stored in USD; pass `listSupportedModels("CNY")` to convert at 7 CNY/USD.100101The registry is the curated current line-up, so prefer it when picking a model or estimating cost. It is not the routing table, and it lags in both directions: older ids in the table above (`gpt-5.4`, `claude-opus-4-7`, `gemini-3.1-pro-preview`, `gemini-3.1-flash-lite`) still route fine without appearing in it, and a newly launched id can route before the registry carries it. For an id the registry omits, take the context window and price from the vendor's own page.102103## Routing and credentials104105- Without `clientType`, the client auto-routes by model id substring, in this order: `minimax-m3` (exact), `gemini-3*` / `gemini-embedding`, `claude` 4-6/4-7/4-8/-5, `gpt-5.4`/`gpt-5.5`/`gpt-5.6`/`gpt-6`, `glm-5` (whole series, 5.3 included), `kimi-k3`/`kimi-k2.5`/`kimi-k2.6`, `deepseek-v4`, `ant-messages`, `openai-responses`, `openai`+`embedding` (embeddings), `openai` (chat). Ids matching none of these throw. Most gateway variants in the table above hit the same substrings, so they route to the right family — just set `baseUrl` to the gateway endpoint.106- Three generic protocol clients cover everything else (all take `baseUrl` + `apiKey`):107 - `clientType: "openai-chat"` — any OpenAI Chat Completions compatible endpoint (gateway models, Qwen via OpenRouter/SiliconFlow or DashScope `https://dashscope.aliyuncs.com/compatible-mode/v1`, local vLLM, …). Renamed from `openai` in AgentHub 0.4.2; the bare `openai` string still routes as a deprecated alias.108 - `clientType: "openai-responses"` — OpenAI Responses-compatible endpoints (OpenAI, OpenRouter, DeepSeek, Z.AI, MiniMax all serve one).109 - `clientType: "ant-messages"` — Anthropic Messages-compatible endpoints (Anthropic, OpenRouter `https://openrouter.ai/api`, DeepSeek `https://api.deepseek.com/anthropic`, Z.AI, MiniMax).110- Exception: an id served by an OpenAI-compatible gateway that still matches a first-party substring (e.g. OpenRouter's `google/gemini-3.7-flash`, `anthropic/claude-sonnet-5` or `openai/gpt-5.6-sol` on the `/api/v1` endpoint) would auto-route to the vendor protocol client — and a dotted id like `anthropic/claude-opus-4.8` matches nothing and throws. Always pass an explicit `clientType` for gateway ids; never rely on the id. Routing reads `clientType` (or the model id) as a plain lowercased string and never looks at `baseUrl`, so the vendor prefix gives no protection.111- OpenRouter serves both protocols at `https://openrouter.ai/api/v1`, for every upstream it resells and not just its `openai/*` ids, so any id it serves works with `clientType: "openai-responses"` as well as `"openai-chat"`; use Responses when you want reasoning items round-tripped.112- The first-party `deepseek-v4` client posts to `{baseUrl}/responses` (AgentHub 0.4.6 moved it off Chat Completions). A self-hosted endpoint serving a `deepseek-v4*` id over Chat Completions must therefore pass `clientType: "openai-chat"` explicitly rather than rely on id routing.113- API key: constructor parameter first, then the provider environment variable — `DEEPSEEK_API_KEY`, `ANTHROPIC_API_KEY` (also for `ant-messages`), `OPENAI_API_KEY` (also for `openai-chat`/`openai-responses`), `GEMINI_API_KEY`, `ZAI_API_KEY`, `MOONSHOT_API_KEY`, `MINIMAX_API_KEY`. Base URLs read the same names with `_BASE_URL`.114115## Streaming text116117```ts118for await (const event of client.streamingResponseStateful({119 message: { role: "user", content_items: [{ type: "text", text: "Hello" }] },120 config: {},121})) {122 for (const item of event.content_items) {123 if (item.type === "text") process.stdout.write(item.text);124 }125}126```127128- Each `event` is a `UniEvent`: `event_type` is `start` | `delta` | `stop`, and `content_items` carry the increments.129- `config` accepts `max_tokens`, `temperature`, `system_prompt`, `thinking_level` (the `ThinkingLevel` enum, `NONE` to `MAX`), `tool_choice`, `prompt_caching`, `fast_mode` and `tools`.130- `streamingResponseStateful` keeps conversation history inside the client; manage it with `getHistory()` / `setHistory(history)` / `clearHistory()`. The stateless variant is `streamingResponse({ messages, config })`.131132## Config parameters the model may reject133134A config value the target client cannot honour throws `UnsupportedParameterError` (an `AgentHubError` carrying `client` and `parameter`) while building the request, before anything reaches the network:135136```ts137import { UnsupportedParameterError } from "@prismshadow/agenthub";138139try {140 // ...141} catch (err) {142 if (err instanceof UnsupportedParameterError) console.error(err.parameter, err.message);143}144```145146- `thinking_level` never throws: every client maps each level onto the closest one the model supports, and `MAX` (the tier above `XHIGH`, added in AgentHub 0.4.4) degrades silently wherever the vendor has no such tier — Gemini and MiniMax M3 stop at `high`. Kimi K3 reasons unconditionally, so `NONE` degrades to its lowest effort rather than disabling thinking; GLM-5.2 sends `reasoning_effort` alongside its `thinking` block and only `NONE` disables it. GLM-5.3 thinks unconditionally (`NONE` degrades to the light `low` effort) and clamps `reasoning_effort` to `low`/`high`/`max`; `gemini-3.7-*` clamps to `low`/`medium`/`high` (`NONE` degrades to `low`). DeepSeek V4 accepts `low`/`high`/`max` and maps `medium` and `xhigh` onto `high` server-side, so since 0.4.4 `LOW` sends `low` (it sent `high`) and `XHIGH` sends `high` (it sent `max`).147- `temperature` is rejected outright by Gemini 3.6/3.7 — those generations deprecated the sampling parameters, so the client refuses them instead of sending a value the API ignores. GPT-5.5/5.6, the whole Claude 4.6+ family (4.6 included since AgentHub 0.4.2), DeepSeek V4, Kimi K2.6 and Kimi K3 accept only the protocol default `1.0` and reject any other value. Gemini 3, GLM and the generic protocol clients (`openai-chat` / `openai-responses` / `ant-messages`) pass it through.148- `tool_choice`: `"auto"` is safe everywhere. Claude accepts a single forced tool name; DeepSeek V4 and Kimi K2.6 allow `"auto"` / `"none"`; Kimi K3 adds `"required"` but refuses a specific tool (K2.x also rejects `"required"`); GLM only accepts `"auto"`.149- `prompt_caching`: every client accepts `PromptCaching.ENABLE` and rejects the other values — caching is on by default and Kimi K3 caches context automatically.150- `fast_mode` (`UniConfig`, AgentHub 0.4.2): fast processing at premium pricing. OpenAI-protocol clients (`openai-chat`, `openai-responses`, `gpt-6`, `minimax-m3`) map it to `service_tier: "priority"`; Anthropic-protocol clients (`ant-messages`, `claude-5`) map it to `speed: "fast"` with a beta header (an Anthropic research preview limited to Claude Opus 5 / Opus 4.8 — organizations without access get a 429). Clients without a fast tier (Gemini, GLM, Kimi, DeepSeek, embeddings, Claude 4.6 models) raise `UnsupportedParameterError`; DeepSeek and Z.AI's OpenAI-compatible endpoints simply ignore the tier.151152Leave a parameter unset and the protocol default applies, which is the portable choice when a script must run against several families.153154## Image generation155156Use a Gemini image model (see Model IDs) and set `config.image_config` (optional `aspect_ratio`, and `image_size` of `"1K"` | `"2K"`):157158```ts159import fs from "node:fs";160161const client = new AutoLLMClient({ model: "gemini-3.1-flash-image" });162for await (const event of client.streamingResponseStateful({163 message: { role: "user", content_items: [{ type: "text", text: "A penguin on a glacier" }] },164 config: { image_config: { aspect_ratio: "16:9", image_size: "2K" } },165})) {166 for (const item of event.content_items) {167 if (item.type === "inline_data") fs.writeFileSync("image.png", item.data);168 }169}170```171172Images arrive as `inline_data` content items (`data` is a Buffer, with `mime_type`).173174## Speech synthesis175176Use a Gemini TTS model (`gemini-3.1-flash-tts-preview`) and set `config.tts_config`:177178```ts179config: { tts_config: [{ voice: "Kore" }] }180```181182- One entry → single voice; two entries → multi-speaker, and each entry must also set `speaker`.183- The `inline_data` output is raw PCM (24kHz 16-bit mono) — wrap it in a WAV header yourself before saving as `.wav`.184185## Embeddings186187Two routes:188189- Gemini: a model whose id contains `gemini-embedding` auto-routes (`gemini-embedding-2`).190- Any OpenAI-compatible embeddings endpoint: pass `clientType: "openai-embedding"` (plus `baseUrl` and `apiKey` as needed) — ids like `text-embedding-3-small` / `text-embedding-3-large` match no auto-route substring and would throw without it.191192Optional `config.embedding_config`:193194```ts195config: { embedding_config: { dimensions: 768 } }196```197198The output arrives as `embedding` content items (`embedding` is a number array).