通过 OpenRouter 统一 Image API 的文生图与图生图:以别名配置模型,参数按实时模型能力列表校验,支持参考图输入、设置卡片与对话内联展示结果。
安装
# GitHub 源码(首次需按提示配置 allowBuilds 构建授权后重试)
dsh plugin --profile web add github:Pappet/dsh-tool-imagegen
装任何插件都等于在你的机器上跑第三方代码,权限和你本人一样大——能读你的文件、用你的凭据、访问网络,工具审批管不到它。GitHub 来源的插件还会在安装时执行构建脚本——pnpm 默认拦截,所以安装可能停在 ERR_PNPM_GIT_DEP_PREPARE_NOT_ALLOWED 或 ERR_PNPM_IGNORED_BUILDS;dsh 会打印出需要添加的确切键名,把它加进该 profile 的 pnpm-workspace.yaml 的 allowBuilds 下,重跑一次即可装上。放行构建本身就是一次信任判断:请只安装可信来源,并尽量锁定 commit(github:owner/repo#sha)。
README
该插件的 README 只有英文版本。
Image generation for DeepSeek Harness
through OpenRouter's unified Image API (POST /api/v1/images — not the
OpenAI-compatible /images/generations).
One tool, generate_image: it takes a prompt, optionally a few reference
images, writes the result into the workspace and returns the paths and the
exact cost. Generated images appear inline in the chat.
- Models are aliases. The alias is the vocabulary the model uses and the allowlist — a model without an alias cannot be reached.
- Parameters are gated against the model's capability record
(
GET /api/v1/images/models), not hard-wired. A call asking for something a model cannot do fails with a message naming the parameter and the model. - Image-to-image via
input_references: workspace paths or URLs, up to what the model accepts (14 for Seedream, 16 for the GPT-Image family). - Editable at runtime. A card in the Plugins settings section edits the aliases and tunables; changes apply live, no restart.
Install
dsh plugin --profile web add dsh-tool-imagegen
That installs the package into the profile and lists it under
dsh.profile.bundles; the shipped bundle patch inserts the plugin row. Then
give it a key. apiKeyEnv names a credential reference, never the
secret itself:
export OPENROUTER_API_KEY=sk-or-...
or, preferred, put it in $DSH_HOME/.credentials.yaml under
refs.OPENROUTER_API_KEY. The credentials seam is tried first, the
same-named environment variable second — the same convention as llm-pi-ai
and dsh-github. The value never reaches config, logs, or model-visible text.
The plugin ships no model aliases; see Configuration.
Tool: generate_image
| Parameter | Type | Notes |
|---|---|---|
prompt |
string, required | What to depict. |
model |
string | Configured alias; defaults to defaultModel. |
resolution |
string | e.g. 1K | 2K | 4K — model dependent. |
aspect_ratio |
string | e.g. 1:1, 16:9 — model dependent. |
n |
integer | How many images (default 1, at most maxImagesPerCall). |
seed |
integer | When the model supports it. |
output_format |
string | e.g. png | jpeg — most models decide the encoding themselves and list no descriptor. |
input_references |
string[] | Reference images to edit or vary: workspace paths or http(s) URLs. |
output_path |
string | Target for the first image, absolute or workspace-relative. The extension follows the returned encoding. |
The return value carries model, alias, images[] (path, mediaType,
bytes), costUsd (exact, from the API's usage.cost), applied — the
parameters actually sent — and droppedDefaults. applied is what lets the
model see what was really used, e.g. after the gate dropped a config default,
and adjust the next attempt.
output_pathnames the file, but the model decides the encoding. A call asking forbild.pngfrom a model that emits JPEG getsbild.jpg: a file must not lie about its contents.
Capability gate
supported_parameters uses typed descriptors (enum, range, boolean); an
absent key means the parameter is unsupported. Resolution order per parameter:
call argument → alias defaults → omitted. Two error classes, deliberately
different:
| Origin | Parameter unsupported | Value outside the descriptor |
|---|---|---|
| Named in the call | Error naming parameter and model | Error listing allowed values |
Config defaults |
Silently dropped (reported in droppedDefaults) |
Error attributed to the config |
Defaults are wishes, not promises. An explicit call argument is the model asking for something specific, so it is never silently ignored.
The cache holds the /images/models listing for capabilityTtlMs (default
24 h) and is invalidated once on a 400, then re-gated and retried once — so a
stale record self-heals without an extra round-trip per image.
The record is not the last word: Seedream 4.5 lists
1Kas a validresolutionbut rejects it at call time ("requires at least 3,686,400 output pixels"). The gate forwards what the record allows; the API's own error text is carried through verbatim.
Image-to-image
{
"prompt": "Turn this into a soft watercolor painting, same composition.",
"input_references": ["bilder/vorlage.png", "https://example.com/style.jpg"]
}
Each value is either an http(s) URL, passed through untouched, or a path
(absolute or workspace-relative, resolved against the session cwd like
output_path). A path is read, capped, identified by its magic bytes and
inlined as a base64 data URL. The file name is never consulted: a name can
lie about the encoding — this plugin used to write JPEG payloads under a .png
name — and a mislabelled data URL fails at the provider with an error that
points nowhere near the cause.
applied.input_references echoes what the call named, never the payload.
The capability descriptor is a range over the count, not over a value:
"input_references": { "type": "range", "min": 0, "max": 14 }
so it gets its own gate, with the same two error classes as everything else. Files are read only after that gate passes: a call against a model without the capability never touches the disk.
Caps are maxReferenceBytes (per file) and maxReferenceTotalBytes (all
together), both measured on the bytes on disk; base64 adds roughly a third on
the wire.
Support varies by model. Seedream 4.5 / 5.0 accept up to 14 references but list no
output_format; the GPT-Image family accepts 16 and addsquality,backgroundandoutput_compression; the Recraft vector models emitsvgonly and some require at least one reference (min: 1).
Configuration
No aliases ship by default, so the plugin does nothing until you name at least
one. Put a config override into your profile's cordis.patch.yml:
- id: imagegen
config:
apiKeyEnv: OPENROUTER_API_KEY # credential ref / env var NAME, never the secret
baseURL: https://openrouter.ai/api/v1
outputDir: .dsh/images # workspace-relative
defaultModel: seedream
capabilityTtlMs: 86400000 # 24 h
maxImagesPerCall: 4 # guard against a hallucinated n
maxReferenceBytes: 8388608 # per reference image (8 MiB on disk)
maxReferenceTotalBytes: 33554432 # all references of one call (32 MiB)
showInChat: true
models:
seedream:
id: bytedance-seed/seedream-4.5
defaults: { resolution: "2K", aspect_ratio: "16:9" }
seedream-pro:
id: bytedance-seed/seedream-5-0-pro
seedream-lite:
id: bytedance-seed/seedream-5-0-lite
A config override replaces the row's config wholesale — it is never deep-merged — so every key that matters has to be repeated. Keys you omit fall back to their schema defaults, not to the bundle patch's values.
Slugs verified against
GET /api/v1/images/models: the Seedream 5.0 models areseedream-5-0-pro/seedream-5-0-lite(dashes, not dots), and 5.0-pro accepts onlyn ≤ 1and resolutions1K|2K. An alias whose slug is wrong surfaces as an HTTP error carrying the API's body text.
Settings card
The plugin registers the settings namespace dsh-tool-imagegen, and the
browser half contributes a card to the Plugins settings section: the alias
table (alias, slug, defaults as JSON) plus the scalar tunables. The two byte
caps are entered in MiB; the document keeps bytes.
Config and card are layered, not alternatives:
schema defaults → base (this plugin's cordis config) → user layer (the card)
so cordis.yml stays the deployment's stated intent, a card edit is an
override on top of it, and Reset falls back to exactly the configured value
rather than to a schema default nobody chose. Changes apply live.
The alias registry is a list in the settings layer even though the config uses a dict. That is not cosmetic: the layers merge plain objects recursively and replace arrays wholesale, so a dict in the user layer could never delete an alias the config declares — a removed row would silently re-inherit. As a list, what the card writes is the whole registry.
apiKeyEnv, baseURL and capabilityTtlMs stay config-only: they are
deployment decisions, and the capability cache is built from the latter two
once at apply time, so a live edit could not take effect.
Without a settings service (a headless deployment) the tool runs on the configured values — unconfigurable, but working.
In the chat
With showInChat (default true), every generated image is committed to the
durable attachment store, and execute defers one plugin-sourced message so
the model sees the picture too — useful for iteration; text-only adapters
substitute their text placeholder.
What the reader sees is the tool card: lib/client.js registers the keyed
tool.call.toolview for generate_image and renders the image inline, loaded
through session.readAttachment from the durable refs in the result meta. A
click opens the file through the Host opener.
The deferred message lands as a context-injection row rather than in the
history gallery — a message whose source is not user is classified as
injected context — so it declares the notice form and a one-line summary,
and the collapsed row reads Image created: /path/to/file.jpg.
Every step is contained: an attachment-store outage never fails an otherwise successful generation, and non-attachable media (SVG) are skipped.
All user-facing copy is English, with a Simplified Chinese dictionary beside it, registered through the harness locale service — the UI offers 中文 and English, so the cards follow whichever is selected.
Policy boundary
No permission logic lives in this plugin. Allow/deny/ask belongs in a
tools/pre-execute listener, a final deny in ctx.tools.guard(), and a cost
cap in a separate hook plugin. maxImagesPerCall is a sanity guard against a
hallucinated n, not a budget control.
Development
npm install # `prepare` builds; tests import the compiled output
npm run build # tsc → lib/
npm test # node:test, offline: the host half + the browser half in jsdom
npm run typecheck # tsc --noEmit
Tests never touch the network: fetch is faked, files land in a temp
directory, and the browser half is mounted in jsdom and clicked — expanded,
typed into, saved — because nothing else looks at that file (tsc ignores it
and node --check only proves it parses).
The @deepseek-ai/* packages are peer dependencies: the harness provides
them at runtime, and a second copy of an identity-sensitive contract would
shadow the host's.
For a live install, link the checkout into a dsh profile (dsh plugin add);
after npm run build, a profile restart reloads both halves.
applyWithDeps(ctx, config, { fetchImpl, workspaceRoot, attachments, settings })
is the injectable entry point used by the tests; apply(ctx, config) is what
the harness loads.
Layout
src/
index.ts # apply(), tool registration, presenters, the execute flow
config.ts # Schemastery schema for the cordis entry config
settings.ts # the settings namespace behind the configuration card
capabilities.ts # capability cache + the parameter gate
openrouter.ts # HTTP client: /images, /images/models (no DSH imports)
references.ts # reference images → data URLs (no DSH imports)
write.ts # base64 → file, naming, collisions (no DSH imports)
chat.ts # attachment store + the deferred message
key.ts # credential resolution
lib/client.js # browser half: tool card + settings card (hand-written, no build step)
test/
imagegen.test.mjs # host half
client.test.mjs # browser half, in jsdom
License
MIT — see LICENSE.
链接
同类插件
Tencent/WeKnora#dsh-weknora★ 21308
把 WeKnora 知识库接入 dsh 的四个只读工具:列出知识库、混合检索原文片段、按顺序还原单篇文档,以及直接取用 WeKnora 自己带引用的 RAG 或 ReAct agent 回答(含可续聊的 session id)。
superdesigndev/treg★ 1158
给 Agent 的工具目录:按「要做的事」检索约 2,600 个外部接口(SEO 与 SERP、外链、社交、人物与公司信息补全、广告库、抓取),查看参数与单次调用价格后直接调用,凭据由服务端注入。附带技能,MCP 行在未设置 TREG_TOKEN 前保持禁用。
anysearch-team/anysearch-dsh★ 399
基于 AnySearch 的实时网页与垂直搜索插件,为 DeepSeek Harness 提供搜索工具。
EthanYoQ/Invoice-Downloader#dsh-invoice-downloader★ 375
面向 DeepSeek Harness 的本地 IMAP 发票下载、OCR 识别、归档与 Excel 报销汇总。
omdsh-dev/dsh-data-agent★ 184
让 AI 帮你连数据库、写 SQL。
zhaoolee/notes#dsh-plugin★ 158
将 DSH 对话导出为锤子便签风格 PNG,或在配置的账号工作区中新建和更新 Markdown 便签。
社区评论
评论公开保存在 GitHub Discussions。加载评论会连接 GitHub 和 Giscus;发表内容需要 GitHub 账号。