English · 简体中文

![dsh-agnes-image banner](docs/banner.svg) # dsh-agnes-image

license node runtime deps dsh profiles stars

**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