@adelie/agent-development 0.0.0-stage → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +201 -0
- package/icon.svg +6 -0
- package/package.json +18 -3
- package/plugin.json +8 -0
- package/skills/penguin-config/SKILL.md +72 -0
- package/skills/penguin-orchestration/SKILL.md +162 -0
- package/skills/penguin-sdk/SKILL.md +536 -0
- package/skills/penguin-sdk/reference/agent-api.md +159 -0
- package/skills/unified-llm-api/SKILL.md +198 -0
- package/README.md +0 -3
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
# The API path: a program that calls an Adelie Agent
|
|
2
|
+
|
|
3
|
+
Read this before you write a program that calls an Adelie Agent over the Agent API. The rules in SKILL.md, *The API path*, hold throughout. The user documentation has every detail: the [Agent API guide](https://lmliheng.github.io/Adelie/docs/agent-api) and the [AMSP protocol](https://lmliheng.github.io/Adelie/docs/amsp).
|
|
4
|
+
|
|
5
|
+
## 1. Hand the opening to the user
|
|
6
|
+
|
|
7
|
+
Check first. This command only reads:
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
penguin agent api status --agent-id <agentId> --json
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
It prints one line of JSON: `agent` (the Agent ID), `baseUrl`, `api` (`enabled`, `open`, `approvalMode`, `keys`) and `serverEnabled`. A key's secret is never in it.
|
|
14
|
+
|
|
15
|
+
When `api.enabled` is false, or `api.keys` is empty and `api.open` is false, the program cannot run yet. Give the user these steps with the Agent's id filled in. Only the Project's owner can take them.
|
|
16
|
+
|
|
17
|
+
1. Open the Agent's settings and select the **API** tab (`/agents/<agentId>?tab=api`).
|
|
18
|
+
2. Turn on **Enable API access**.
|
|
19
|
+
3. Choose the **Approval mode for API conversations** (see the table below).
|
|
20
|
+
4. Under **Keys**, select **New key**, name it after the program, and copy the key. It is shown once.
|
|
21
|
+
5. Give the key to the program as the environment variable `ADELIE_AGENT_KEY`.
|
|
22
|
+
|
|
23
|
+
The same steps in the user's own terminal, never in yours:
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
penguin auth login
|
|
27
|
+
penguin agent api enable --agent-id <agentId> --approve <mode>
|
|
28
|
+
export ADELIE_AGENT_KEY="$(penguin agent api keys create --agent-id <agentId> --name <program>)"
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
`keys create` prints the key alone on stdout, so the variable takes it without showing it. These commands change the Agent's exposure, so they take the user's sign-in: `penguin auth login` once, or `penguin auth token` on the server's machine. From your shell the server refuses them with `403 human_required`. Never sign in or mint a session to get past that.
|
|
32
|
+
|
|
33
|
+
When `serverEnabled` is false, the server's administrator has turned the Agent API off, and every request is refused with `403`. Only an administrator turns it back on, in **Settings › Server › Agent API**.
|
|
34
|
+
|
|
35
|
+
Suggest the approval mode that fits the program, and say why in one line:
|
|
36
|
+
|
|
37
|
+
| The program | Mode | What happens |
|
|
38
|
+
| --- | --- | --- |
|
|
39
|
+
| Has no one to ask: a batch job, a backend | `allow-all` (the default) | Every tool call runs, inside the sandbox |
|
|
40
|
+
| May let the agent read but nothing more | `read-only` | Read-only tools run; every other call waits for the program's answer |
|
|
41
|
+
| Shows every tool call to its own user | `always-ask` | Every call waits for the program's answer |
|
|
42
|
+
| Must never run a tool | `deny-all` | Every call is denied; the agent answers without tools |
|
|
43
|
+
|
|
44
|
+
A call that waits reaches the program as an `approval.requested` event, and the run waits, with no time limit, for an answer, an abort or a disconnect. The `@adelie/amsp` client answers it with `onApproval`, or with `deny` when there is none. The mode is read when an API conversation is created; a change on the tab applies to conversations created after it.
|
|
45
|
+
|
|
46
|
+
Leave **Allow keyless access** off unless the user asks for it. With it on, anything that can reach the server can run the Agent on the Project's models.
|
|
47
|
+
|
|
48
|
+
To run the program yourself, you need the key in your environment. Ask the user to add it to your key vault as `ADELIE_AGENT_KEY`: the gear icon on your card on the Agents page, then the key vault tab. Vault values reach your shell on the next task. Until then, the program is unverified.
|
|
49
|
+
|
|
50
|
+
## 2. The connection values
|
|
51
|
+
|
|
52
|
+
| Value | Where it comes from | In the program |
|
|
53
|
+
| --- | --- | --- |
|
|
54
|
+
| Base URL | **Connection** on the API tab, or `baseUrl` from `status` | A constant, which `ADELIE_AGENT_URL` can override |
|
|
55
|
+
| Agent ID | **Connection** on the API tab, or `agent` from `status`: `<projectId>/<agentId>` | A constant |
|
|
56
|
+
| Key | Created by the owner, shown once | `process.env.ADELIE_AGENT_KEY`, and nowhere else |
|
|
57
|
+
|
|
58
|
+
- A local server's Base URL is `http://localhost:7364/api/amsp/v1`; `status` prints the server's real port. Use `localhost`, never `127.0.0.1`: a local server serves previews on `127.0.0.1` and answers every API request there with `401`.
|
|
59
|
+
- Do not give the program's settings the names the harness sets inside a session: `ADELIE_API_URL`, `ADELIE_API_TOKEN`, `ADELIE_PROJECT_ID`, `ADELIE_AGENT_ID`, `ADELIE_SESSION_ID`. They name the session's own server and Agent, and they override a vault entry of the same name.
|
|
60
|
+
|
|
61
|
+
## 3. TypeScript: `@adelie/amsp`
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
npm install @adelie/amsp
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
The client has no dependencies and runs in Node 24 and in current browsers. If your registry does not have the package, call the API over HTTP as in section 4.
|
|
68
|
+
|
|
69
|
+
```ts
|
|
70
|
+
import { AgentClient, parseArguments } from "@adelie/amsp";
|
|
71
|
+
|
|
72
|
+
const apiKey = process.env.ADELIE_AGENT_KEY;
|
|
73
|
+
if (!apiKey) throw new Error("Set ADELIE_AGENT_KEY to the key from the agent's API tab.");
|
|
74
|
+
|
|
75
|
+
const client = new AgentClient({
|
|
76
|
+
baseUrl: process.env.ADELIE_AGENT_URL ?? "http://localhost:7364/api/amsp/v1",
|
|
77
|
+
agent: "demo/coder", // <projectId>/<agentId>
|
|
78
|
+
apiKey, // the client reads no environment variable itself
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
// One question, the whole answer.
|
|
82
|
+
const first = await client.ask({ input: "Summarize README.md" });
|
|
83
|
+
if (first.status !== "completed") throw new Error(`${first.status}: ${first.error?.message ?? ""}`);
|
|
84
|
+
console.log(first.text);
|
|
85
|
+
|
|
86
|
+
// The same conversation, streamed: pass its Session id back.
|
|
87
|
+
const run = client.run({
|
|
88
|
+
sessionId: first.sessionId,
|
|
89
|
+
input: "Now list its headings",
|
|
90
|
+
onApproval: (request) => {
|
|
91
|
+
const args = parseArguments(request.tool_call);
|
|
92
|
+
return request.tool_call.name === "exec_command" && args?.command === "ls" ? "allow" : "deny";
|
|
93
|
+
},
|
|
94
|
+
});
|
|
95
|
+
for await (const event of run) {
|
|
96
|
+
// An event without `origin` is the main conversation's; a subagent's events carry one.
|
|
97
|
+
if (event.type === "text.delta" && !event.origin) process.stdout.write(event.text);
|
|
98
|
+
}
|
|
99
|
+
const second = await run.result();
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
- `ask()` runs to the end and resolves with the result: `status`, `error`, `sessionId`, `text` (the main conversation's answer), `items`, `usage`, `sessionUsage` and `requests`. `run()` starts the run and returns it at once: iterate it once for the events, await `run.session` for the Session id, and `run.result()` for the end.
|
|
103
|
+
- **Conversations.** Keep one `sessionId` per conversation of the program, such as one per end user or chat thread, and pass it back to continue. Omit it to start a new conversation. Only the Agent's own API conversations can be continued.
|
|
104
|
+
- **Approvals.** `onApproval` is called only when the approval mode makes a call wait. Without it, or when it throws, the client answers `deny`. A person can answer the same request in the Web App; the first answer counts.
|
|
105
|
+
- **Aborting.** Abort the `signal` passed to `run()` or `ask()` to drop the connection: the server aborts the run. `run.abort()` asks the server to abort and still delivers the final `run.done`, with `status: "aborted"`.
|
|
106
|
+
- **Failures.** A refusal before the stream starts rejects with `AmspHttpError` (`status`, `code`, `message`; see section 5). After that, `result()` resolves for every run: check `status` (`completed`, `aborted`, `retryable` or `fatal`) and `error`. A stream that breaks before `run.done` rejects with `AmspStreamError`.
|
|
107
|
+
- **Unknown events** are passed through. Switch on `event.type` with a default branch.
|
|
108
|
+
- **A web app** keeps the key on its own server. The page posts to the app's server, which runs the Agent and relays the `text.delta` events to the page over SSE, aborting through `signal` when the page disconnects: the shape of the RAG recipe's `server.ts`. A key inside a page is readable by everyone who loads the page.
|
|
109
|
+
|
|
110
|
+
## 4. Any language: HTTP and curl
|
|
111
|
+
|
|
112
|
+
```bash
|
|
113
|
+
BASE="${ADELIE_AGENT_URL:-http://localhost:7364/api/amsp/v1}"
|
|
114
|
+
|
|
115
|
+
# Start a conversation. The answer streams back as it is produced.
|
|
116
|
+
curl -N "$BASE/agents/demo/coder/runs" \
|
|
117
|
+
-H "Authorization: Bearer $ADELIE_AGENT_KEY" -H "Content-Type: application/json" \
|
|
118
|
+
-d '{"input":"Summarize README.md"}'
|
|
119
|
+
|
|
120
|
+
# Continue it with the session_id that the first event, run.started, carried.
|
|
121
|
+
curl -N "$BASE/agents/demo/coder/runs" \
|
|
122
|
+
-H "Authorization: Bearer $ADELIE_AGENT_KEY" -H "Content-Type: application/json" \
|
|
123
|
+
-d '{"session_id":"<session_id>","input":"Now list its headings"}'
|
|
124
|
+
|
|
125
|
+
# Answer an approval.requested event while its run is still streaming.
|
|
126
|
+
curl -X POST "$BASE/sessions/<session_id>/approvals/<tool_call_id>" \
|
|
127
|
+
-H "Authorization: Bearer $ADELIE_AGENT_KEY" -H "Content-Type: application/json" \
|
|
128
|
+
-d '{"decision":"allow"}'
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
To read the stream in your own code:
|
|
132
|
+
|
|
133
|
+
- Each event is one `data: <JSON>` line followed by a blank line. After the last event comes `data: [DONE]`. A line that starts with `:` is a keep-alive comment, sent after 15 seconds of silence: skip it.
|
|
134
|
+
- The first event is `run.started`, with the `session_id`. The last is `run.done`, with `status`, `error` and `usage`. The answer is the assistant `text.done` events without an `origin`, or their `text.delta` fragments as they arrive.
|
|
135
|
+
- Skip every event `type` you do not know.
|
|
136
|
+
- `POST <Base URL>/sessions/<session_id>/abort` aborts a run and keeps its stream open until `run.done`.
|
|
137
|
+
|
|
138
|
+
## 5. Errors
|
|
139
|
+
|
|
140
|
+
A request refused before the stream gets an HTTP status and `{"error": {"code": "…", "message": "…"}}`. Once the stream has started, `run.done` carries the error instead.
|
|
141
|
+
|
|
142
|
+
| Status | `code` | Usually | What to do |
|
|
143
|
+
| --- | --- | --- | --- |
|
|
144
|
+
| 400 | `bad_request` | A malformed body | The message names the field |
|
|
145
|
+
| 401 | `unauthorized` | No key, a wrong or deleted key, or `127.0.0.1` in the Base URL | Check `ADELIE_AGENT_KEY` and that the Base URL uses `localhost` |
|
|
146
|
+
| 403 | `agent_api_disabled` | The administrator turned the Agent API off | Ask the user to have an administrator turn it on |
|
|
147
|
+
| 404 | `agent_not_found` | API access is off, the Agent ID is wrong, or the key belongs to another Agent | Check with `penguin agent api status`, then ask the owner |
|
|
148
|
+
| 404 | `session_not_found` | The `session_id` is not one of this Agent's API conversations | Start a new conversation |
|
|
149
|
+
| 409 | `task_in_progress`, `compacting` | The conversation is still busy | Wait for its `run.done`: one run per conversation at a time |
|
|
150
|
+
| 429 | `too_many_runs` | The Agent already has 4 API runs going | Retry after `Retry-After` seconds |
|
|
151
|
+
|
|
152
|
+
## 6. Verify before you hand over
|
|
153
|
+
|
|
154
|
+
1. `penguin agent api status --agent-id <agentId> --json` shows `api.enabled` true and at least one key.
|
|
155
|
+
2. The key is in your environment: `test -n "$ADELIE_AGENT_KEY" && echo set`. Never print the value.
|
|
156
|
+
3. Run the program once with a real input. The run ends with `status` `completed`, and the answer appears.
|
|
157
|
+
4. Tell the user where to find the conversation: the Agent's **Background** folder in the sidebar, marked with the API icon.
|
|
158
|
+
|
|
159
|
+
When step 1 or 2 is not done, finish the program anyway, report it as **unverified**, and end your reply with the owner's steps from section 1. Report the files with relative paths, how to run the program, the Agent ID it calls, the approval mode it expects, and your assumptions.
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: unified-llm-api
|
|
3
|
+
description: Call model APIs through @prismshadow/agenthub — streaming text generation, image generation, speech synthesis, embeddings and the supported-model registry with one client.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Unified LLM API (AgentHub)
|
|
7
|
+
|
|
8
|
+
`@prismshadow/agenthub` is a unified TypeScript client for model APIs: streaming text, image generation, speech synthesis and embeddings behind one entry point.
|
|
9
|
+
|
|
10
|
+
```bash
|
|
11
|
+
npm install @prismshadow/agenthub
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
The main entry point is `AutoLLMClient`:
|
|
15
|
+
|
|
16
|
+
```ts
|
|
17
|
+
import { AutoLLMClient } from "@prismshadow/agenthub";
|
|
18
|
+
|
|
19
|
+
const client = new AutoLLMClient({ model: "<model_id>", apiKey: "<key>", baseUrl: "<url>", clientType: "<type>" });
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
`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`.
|
|
23
|
+
|
|
24
|
+
## Before you start
|
|
25
|
+
|
|
26
|
+
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.
|
|
27
|
+
|
|
28
|
+
**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 `~/.adelie`. Model ids can come from the penguin CLI catalog and the id table below.
|
|
29
|
+
|
|
30
|
+
Check for a usable API key before writing code — the client needs one for whichever provider you target:
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
env | grep -oE "(DEEPSEEK|OPENAI|ANTHROPIC|GEMINI|ZAI|MOONSHOT|MINIMAX)_API_KEY" || echo none
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
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 `~/.adelie` or any other `.adelie` 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.
|
|
37
|
+
|
|
38
|
+
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.
|
|
39
|
+
|
|
40
|
+
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 `~/.adelie/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 `~/.adelie` directory — that config belongs to the person running Penguin, not to your script.
|
|
41
|
+
|
|
42
|
+
## Model IDs
|
|
43
|
+
|
|
44
|
+
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.
|
|
45
|
+
|
|
46
|
+
| 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` | 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-v4p1-flash`, `accounts/fireworks/models/deepseek-v4-pro-0813`, `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` |
|
|
73
|
+
|
|
74
|
+
The image endpoint dropped its preview suffix: `gemini-3.1-flash-image-preview` is deprecated, use `gemini-3.1-flash-image`.
|
|
75
|
+
|
|
76
|
+
`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`.
|
|
77
|
+
|
|
78
|
+
Gateway model lists can be queried online:
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
curl https://openrouter.ai/api/v1/models
|
|
82
|
+
curl --request GET --url https://api.siliconflow.cn/v1/models --header 'Authorization: Bearer <token>'
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
## Supported-model registry
|
|
86
|
+
|
|
87
|
+
`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:
|
|
88
|
+
|
|
89
|
+
```ts
|
|
90
|
+
import { listSupportedModels } from "@prismshadow/agenthub";
|
|
91
|
+
|
|
92
|
+
for (const m of listSupportedModels()) {
|
|
93
|
+
console.log(m.model, m.base_url, m.client, m.context_window, m.pricing?.prompt_tokens);
|
|
94
|
+
}
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
- 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.
|
|
100
|
+
|
|
101
|
+
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.
|
|
102
|
+
|
|
103
|
+
## Routing and credentials
|
|
104
|
+
|
|
105
|
+
- 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`.
|
|
114
|
+
|
|
115
|
+
## Streaming text
|
|
116
|
+
|
|
117
|
+
```ts
|
|
118
|
+
for 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
|
+
```
|
|
127
|
+
|
|
128
|
+
- 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 })`.
|
|
131
|
+
|
|
132
|
+
## Config parameters the model may reject
|
|
133
|
+
|
|
134
|
+
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:
|
|
135
|
+
|
|
136
|
+
```ts
|
|
137
|
+
import { UnsupportedParameterError } from "@prismshadow/agenthub";
|
|
138
|
+
|
|
139
|
+
try {
|
|
140
|
+
// ...
|
|
141
|
+
} catch (err) {
|
|
142
|
+
if (err instanceof UnsupportedParameterError) console.error(err.parameter, err.message);
|
|
143
|
+
}
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
- `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.
|
|
151
|
+
|
|
152
|
+
Leave a parameter unset and the protocol default applies, which is the portable choice when a script must run against several families.
|
|
153
|
+
|
|
154
|
+
## Image generation
|
|
155
|
+
|
|
156
|
+
Use a Gemini image model (see Model IDs) and set `config.image_config` (optional `aspect_ratio`, and `image_size` of `"1K"` | `"2K"`):
|
|
157
|
+
|
|
158
|
+
```ts
|
|
159
|
+
import fs from "node:fs";
|
|
160
|
+
|
|
161
|
+
const client = new AutoLLMClient({ model: "gemini-3.1-flash-image" });
|
|
162
|
+
for 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
|
+
```
|
|
171
|
+
|
|
172
|
+
Images arrive as `inline_data` content items (`data` is a Buffer, with `mime_type`).
|
|
173
|
+
|
|
174
|
+
## Speech synthesis
|
|
175
|
+
|
|
176
|
+
Use a Gemini TTS model (`gemini-3.1-flash-tts-preview`) and set `config.tts_config`:
|
|
177
|
+
|
|
178
|
+
```ts
|
|
179
|
+
config: { tts_config: [{ voice: "Kore" }] }
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
- 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`.
|
|
184
|
+
|
|
185
|
+
## Embeddings
|
|
186
|
+
|
|
187
|
+
Two routes:
|
|
188
|
+
|
|
189
|
+
- 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.
|
|
191
|
+
|
|
192
|
+
Optional `config.embedding_config`:
|
|
193
|
+
|
|
194
|
+
```ts
|
|
195
|
+
config: { embedding_config: { dimensions: 768 } }
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
The output arrives as `embedding` content items (`embedding` is a number array).
|
package/README.md
DELETED