English · 简体中文

# dsh-agnes-image
**One tool, three modes.** A [DeepSeek Harness](https://www.npmjs.com/package/@deepseek-ai/dsh) plugin that gives your agent [Agnes Image 2.5 Flash](https://www.agnes-ai.com/zh-Hans/docs/agnes-image-25-flash) (`agnes-image-2.5-flash`): **text-to-image**, **image-to-image**, and **multi-image composition** — with the result saved as a real PNG inside the session workspace, ready to show, present, or re-inspect with `read_image`.
Underneath the one-tool surface is the part most image plugins skip: **the network engineering that makes a lossy endpoint behave like a reliable one.** Everything in [Engineering deep-dive](#engineering-deep-dive) was measured, not guessed.
## Why this plugin
| What you get | What it changes |
| --- | --- |
| **Three modes, one schema** | `image` omitted → text-to-image; one reference → image-to-image; several → multi-image composition. The model never has to pick a tool. |
| **Files, not URLs** | Bytes are requested inline (base64) and written into the session workspace. The agent gets a path it can render, diff, or feed back in as a reference. |
| **Deterministic connect path** | Pinned IPv4 on this plugin's sockets only — same request lands in **~9s instead of timing out at 20s+** (measured, see below). |
| **Loss-tolerant by design** | Bounded retry on connect-level failures only; HTTP statuses and aborts are never retried. |
| **Hot-rotating credentials** | API key resolved per call from 4 sources — rotate the key and the very next call picks it up, no restart. |
| **Zero runtime dependencies** | Only `node:https` + the harness SDK. Nothing else to install, nothing else to break. |
| **Readable failures** | Every error — including a message-less `AggregateError` — is normalized into one actionable line naming the attempt count. |
## Install
Requires Node.js **22+** and a DeepSeek Harness profile (`web`, `desktop`, or `headless`).
```sh
# from GitHub
dsh plugin --profile web add github:Lijianan-123/dsh-agnes-image
# or from a local checkout
git clone https://github.com/Lijianan-123/dsh-agnes-image.git
dsh plugin --profile web add ./dsh-agnes-image
```
Restart the harness after installing. Then set the API key (any one of):
```sh
# 1. environment variable
export AGNES_API_KEY=sk-...
# 2. harness credential store: ~/.dsh/.credentials.yaml → refs.AGONES_API_KEY
# 3. key file: ~/.dsh/agnes-image.json → {"apiKey": "sk-..."}
```
## Use it
Once installed, just ask in natural language:
> Generate a 16:9 hero image for my landing page: a glass-and-steel medical device on a dark background, soft rim light, product photography style.
> Take this UI screenshot and this logo, compose them into a clean App Store preview card.
| Parameter | Required | Description |
| --- | --- | --- |
| `prompt` | yes | What to draw or how to edit. Recommended structure: subject + scene + style + lighting + composition + quality. |
| `size` | | Resolution tier `1K` / `2K` / `3K` / `4K` (default `2K`). |
| `ratio` | | Aspect ratio `1:1` `3:4` `4:3` `16:9` `9:16` `2:3` `3:2` `21:9` (default `1:1`). |
| `image` | | Reference images (public https URL or `data:` URI). One = image-to-image, several = composition. Omit for text-to-image. |
| `model` | | Model id (default `agnes-image-2.5-flash`). |
| `output_path` | | Destination, relative to the session workspace. Default `agnes-images/-.png`. |
Measured output dimensions (match the official docs):
| ratio | 1K | 2K |
| --- | --- | --- |
| `1:1` | 1024×1024 | 2048×2048 |
| `16:9` | 1312×736 | 2624×1472 |
## Configuration
All keys are optional, applied over these defaults by the Loader:
```jsonc
{
"apiKey": "", // inline key; wins over every other source
"credentialRef": "AGONES_API_KEY",
"endpoint": "https://apihub.agnes-ai.com/v1/images/generations",
"model": "agnes-image-2.5-flash",
"defaultSize": "2K",
"defaultRatio": "1:1",
"outputDir": "", // empty → /agnes-images
"timeoutMs": 300000,
"attempts": 3 // total attempts on connect-level failures
}
```
**API key resolution order** (re-resolved on every call, never cached): plugin config → harness credential store → environment (`AGONES_API_KEY` / `AGNES_API_KEY`) → key file. Key rotation reaches the very next call with no restart.
## Engineering deep-dive
Four field-tested details are baked into ~470 lines of dependency-free code. Each one cost a real debugging session — the measurements below are why this plugin is reliable where a naive `fetch` wrapper is not.
### 1. Pin IPv4 and disable happy-eyeballs — on our sockets only
The endpoint resolves to Cloudflare (`104.18.18.62` / `104.18.19.62`) **and advertises unroutable AAAA records**. Node's default `autoSelectFamily` runs a happy-eyeballs race: it burns the IPv6 attempts first and ends in a **20s+ all-address `ETIMEDOUT`**, surfaced as an `AggregateError` with an *empty message* — the model literally sees a bare `Error:`.
The fix is `family: 4` + `autoSelectFamily: false`, collapsing the connect to **one deterministic IPv4 path without touching process-level DNS**:
| Approach | Result |
| --- | --- |
| Default (`autoSelectFamily` + AAAA) | Intermittent 20s+ all-address `ETIMEDOUT`, empty `AggregateError` |
| Custom `lookup` pinning family 4 | **Still reaches IPv6** (not honored on the multi-address path) — unreliable |
| `family: 4` + `autoSelectFamily: false` | **Stable success in ~9s** |
> The middle row is the trap: pinning `family: 4` through a custom `lookup` still produced IPv6 addresses in the error — it is not applied on the `autoSelectFamily` path. Don't conclude from a single success.
### 2. The route is genuinely lossy — retry, but only connect-level failures
An identical request can time out on every address one minute and answer normally the next. The plugin retries with bounded backoff (600/1200 ms, 3 attempts by default) and **only when no HTTP response was ever received**. `AbortError` and any HTTP status are never retried; every thrown failure is normalized into one line that names how many attempts were actually made.
### 3. Fetch the image in one hop, not two (a deliberate trade-off)
The API returns an image **URL** by default, requiring a second network round-trip to a different host (`platform-outputs.agnes-ai.space`). In testing, generations *succeeded* and then **died on the download hop**, wasting the generation.
So the plugin asks for the bytes **inline in the generation response**: `return_base64: true` for text-to-image, `extra_body.response_format: "b64_json"` for reference-image work. One request, one host, no download to strand. The URL path remains only as a fallback.
| Fetch strategy | Measured |
| --- | --- |
| URL + download (two hops) | 58.3s — and the download was aborted once mid-test |
| **Inline base64 (one hop)** | **16.6s** |
### 4. `response_format` must never sit at the top level
The API requires it inside `extra_body` (same for `extra_body.image` on reference work). A top-level `response_format` — or `tags: ["img2img"]` for image-to-image — is silently wrong. The payload is constructed per mode accordingly.
## Verify
An offline end-to-end smoke test imports the plugin, captures the tool through a fake cordis context, executes a real generation, and checks the bytes on disk:
```sh
node scripts/smoke.mjs
# registered tool: agnes_image
# elapsed: 16600 ms
# PASS: byte count matches disk
```
## Known boundary: router-standard preset gating
The `router-standard` preset uses `tools.restrict` for progressive disclosure (whitelist = stage tools ∪ `META_TOOLS` ∩ `GLOBAL_SAFE`). `agnes_image` is not in that list, so under that preset it is **hidden in stages 0–2 and unlocked at stage 3** (verification/delivery). Built-in presets (`standard` / `minimal` / `ptc` / `cordis`) and no-preset sessions are unaffected.
## Compatibility
- `@deepseek-ai/dsh-tools` `>=0.1.5-rc.1 <0.2.0-0 || >=0.2.0-rc.1 <0.3.0-0`, `@deepseek-ai/schemastery` `^3.18.2` — all declared as ranges, no hard-pinned harness version.
- Node.js 22+; built-in modules only beyond the harness SDK.
## Like it?
If this plugin saved you a debugging session — or the deep-dive taught you something — a ⭐ is much appreciated. Issues and PRs welcome.
## License
[MIT](LICENSE) © 2026 Jianan Li