Skip to content
dsh.fish
Bundle

@dopilot/dsh-plugin-image-gen

OpenAI-compatible image generation tool for DeepSeek Harness

Source
tuofangzhe
License
MIT
Updated
Updated 6 days ago

Readme

# `@dopilot/dsh-plugin-image-gen`

DeepSeek Harness plugin that exposes an OpenAI-compatible `generate_image` tool. It sends one synchronous `POST <baseURL>/images/generations` request, writes every returned image to the current session workspace, and—when available—stores an attachment reference so an image-capable conversation model can see the result.

The package is ESM-only and requires Node.js 22.19 or newer. It intentionally has no default export; its Cordis entry points are the named exports `name`, `inject`, `Config`, and `apply`.

## Installation

Install the public bundle into the profile you use:

```sh
dsh plugin --profile web add @dopilot/dsh-plugin-image-gen
```

The package inserts a dormant `dopilot-image-gen` row into that profile. Configure it by patching the same id in the profile's `cordis.patch.yml`; `config` is replaced whole rather than deep-merged, so restate every key you need. Remove both the dependency and bundle layer with `dsh plugin --profile web remove @dopilot/dsh-plugin-image-gen`.

For source development, use `npm ci`, `npm run typecheck`, `npm test`, and `npm run build`. The DSH rc.2 subpackages are published under prerelease versions whose npm `latest` tags may still point at older builds. This repository pins the tested development versions and declares exact `0.1.1-rc.2` DSH peers.

## Compatibility

| Plugin | Supported DSH |
|---|---|
| `0.1.x` | `0.1.1-rc.2` |

## Configuration

In the target profile's `cordis.patch.yml`, override the bundle row by id:

```yaml
- id: dopilot-image-gen
  config:
    providers:
      openai:
        baseURL: https://api.openai.com/v1
        apiKeyEnv: OPENAI_IMAGE_API_KEY
        defaultModel: gpt-image-1
        models: [gpt-image-1]
        responseFormat: png
        timeoutMs: 120000
      gateway:
        baseURL: https://images.example.com/openai/v1
        apiKeyEnv: CUSTOM_IMAGE_API_KEY
        defaultModel: organization/image-model
        responseFormat: webp
        timeoutMs: 180000
    defaultProvider: openai
    maxImagesPerCall: 4
    outputDir: generated-images
    maxResponseBytes: 134217728
    maxImageBytes: 20971520
    maxTotalImageBytes: 83886080
    maxErrorChars: 500
    toolTimeoutGraceMs: 30000
```

`apiKeyEnv` is a credential reference name, not a secret value. It is resolved through the DSH credentials service for every call and is never cached. Omit it for a gateway that needs no Authorization header.

| Field | Meaning and default |
|---|---|
| `providers` | Optional route map. Missing or empty keeps the plugin dormant and registers no tool. |
| `providers.*.baseURL` | Absolute HTTP(S) URL without credentials, query, or fragment. Trailing slashes are removed. |
| `providers.*.apiKeyEnv` | Optional credential reference using POSIX shell-identifier syntax. |
| `providers.*.defaultModel` | Required non-empty default image model. |
| `providers.*.models` | Optional model allowlist; omitted or empty means unrestricted. |
| `providers.*.responseFormat` | Actual returned encoding: `png` (default), `jpeg`, or `webp`. The plugin does not transcode provider output. |
| `providers.*.timeoutMs` | Provider request deadline; default `120000`. |
| `defaultProvider` | Required with multiple providers; inferred with exactly one. |
| `maxImagesPerCall` | `1`–`10`; default `4`. |
| `outputDir` | Safe normalized relative path; default `generated-images`. |
| `maxResponseBytes` | Streaming HTTP body limit; default 128 MiB. |
| `maxImageBytes` | Per decoded image limit; default 20 MiB. |
| `maxTotalImageBytes` | Per-call decoded-image limit; default 80 MiB. |
| `maxErrorChars` | Provider/attachment error excerpt, 64–4000 Unicode code points; default 500. |
| `toolTimeoutGraceMs` | Cooperative tool deadline margin above the slowest provider; default 30000 ms. |

Missing or empty `providers` is an intentional dormant state: DSH starts normally, the plugin logs how to configure the profile, and `generate_image` is not registered. Once `providers` is non-empty, invalid values fail during plugin loading. The combined slowest provider timeout and tool grace may not exceed `2_147_483_647` ms. Byte limits must satisfy `maxImageBytes <= maxTotalImageBytes <= maxResponseBytes`.

## Tool contract

`generate_image` accepts:

- `prompt` (required string): the generation instructions.
- `output_paths` (required string array): one path per requested image; its length is the API `n` value.
- `provider`, `model`, and `size` (optional strings): route/model overrides and a size matching `\d{3,4}x\d{3,4}`.

Every path must already be normalized with `/`, be unique, and name a direct child of the configured `outputDir`. Filenames use ASCII letters, digits, `_`, or `-`, followed by `.png`, `.jpg`/`.jpeg`, or `.webp` as required by the provider's `responseFormat`. Absolute paths, backslashes, dot segments, nested arbitrary directories, Windows device names, and existing files are rejected. Files are created with exclusive `wx` semantics and are never overwritten.

Example model call:

```json
{
  "prompt": "A red circle centered on a clean white background",
  "provider": "openai",
  "size": "1024x1024",
  "output_paths": ["generated-images/red-circle-1.png"]
}
```

A successful canonical result records the provider/model, the original on-disk encoding, byte count and dimensions, plus any attachment facts returned by the attachment service. Attachment metadata is a separate fact set because the service may normalize the stored copy. The text result always reports the on-disk facts and paths. An image block is added only when the current conversation route explicitly advertises image input and a complete attachment reference exists.

## Local smoke test

From a source checkout:

```sh
cd /path/to/dsh-plugin-image-gen
npm ci
npm run build
cd ~/dsh/deepseek-harness-master
pnpm dsh plugin --profile web add /path/to/dsh-plugin-image-gen
export OPENAI_IMAGE_API_KEY='<real key>'
export IMAGE_API_MODEL='gpt-image-1'
pnpm dsh web --patch /path/to/dsh-plugin-image-gen/test/smoke.overlay.yml
```

For an installed DSH, use the same sequence with `dsh` in place of `pnpm dsh`. The `plugin ... add` step is required; `--patch` alone neither installs the package nor activates its bundle. Installation must not emit the “bundle-less dependency” warning. Before configuring providers, the profile must still boot with the plugin dormant and no `generate_image` schema.

In the Web UI, select a local workspace, create a session, and ask:

> Generate one image of a red circle on white background. Save it as generated-images/red-circle-smoke-1.png.

Verify all of the following:

- `generate_image` appears in the tool list, and the pending call displays the planned path.
- The successful call creates the exact PNG in the session workspace. The final “产出文件” row lists the same path and opens it.
- The generic tool card does not show an inline thumbnail; that is intentionally deferred.
- Reusing the same `output_paths` fails and leaves the original bytes unchanged.
- A text-only conversation model completes without `UNSUPPORTED_CONTENT`; it receives text but no image block.
- With default base composition, Node filesystem output alone does not trigger an approval dialog. A custom `tools/pre-execute` policy may still return `ask`; follow that deployment policy and record the difference.
- If practical, generate a 2048×2048 image. The rc.2 defaults do not reject it merely for being 2048 px. Any byte/pixel/deployment/storage rejection must appear in the result warning while the original file still lands on disk.

Before setting a real key, run an independent failure smoke. The result must name `OPENAI_IMAGE_API_KEY` as unconfigured and must not reveal any credential value. This does not replace the successful-key smoke.

## Model Experience

### Tool schema and result content

#### What the model sees

The complete tool name, description, parameter schema, configured `outputDir`, default provider, and each provider's declared format enter the model's system prompt. A successful result always includes a text envelope listing the durable workspace paths and original file facts. Image blocks appear only when the current conversation provider/model route explicitly supports image input and attachment storage succeeded; otherwise the model receives an explanatory warning and disk paths only.

#### Token effect

The tool schema consumes system-prompt tokens whenever the plugin is active. Each call adds a compact text summary to the conversation. Provider base64, API keys, prompts, and binary bytes are never copied into the canonical value or text result. An eligible image block contributes the image representation selected by the active LLM adapter.

#### KV Cache effect

The schema is deterministic for a fixed plugin configuration. Changing provider routes, formats, `outputDir`, or the default provider and then reloading/HMR-registering the plugin changes model-visible schema text, so the affected system-prompt prefix and its KV cache can no longer be reused. Ordinary calls do not mutate the schema.

## Known Limitations and Deferred Work

- Only synchronous OpenAI-compatible `POST /images/generations` with `b64_json` is supported. There are no video, asynchronous polling/job, image edit, or variation APIs.
- There is no settings-page UI card and no chat-card thumbnail. A future browser client plugin would need to render a keyed tool view and load the attachment through the host RPC.
- DoPilot payload integration is intentionally deferred; this package and smoke overlay are standalone development artifacts.
- Attachment storage is optional and has independent deployment limits. The rc.2 attachment-local defaults admit source images up to 20 MiB, 8192 px per dimension, 64 million pixels, 20 images and 200 MiB per batch; saved copies may be normalized to at most 2048 px and 4 MiB. A valid 2048×2048 source is not rejected merely for that dimension. Attachment rejection degrades to disk-only output.
- Attachment metadata describes the normalized stored copy, so its encoding, bytes, width, and height may differ from the original disk file. The optional `name` and, after scaling, `originalDimensions` are persisted and reconstructed exactly from the returned reference.
- `attachments.saveImages()` has no `AbortSignal`. Cancellation and the cooperative `TOOL_TIMEOUT` can only be observed after that operation settles, so attachment storage can delay timeout convergence.
- Binary files are written through Node's local filesystem because rc.2 `dsh-fs` exposes no binary writer. This supports only a trusted local host, not remote or sandbox filesystem abstractions. Parent `lstat` checks plus exclusive creation reduce accidents but do not eliminate malicious local-process TOCTOU races.
- Direct Node filesystem writes bypass `ctx.fs`, so `fs/write-intent` approvals and `fs/observed` policy telemetry cannot see them. Default base composition does not ask solely because of these writes, while a custom `tools/pre-execute` policy can still require approval for the tool call.
- A provider's configured `responseFormat` must match its actual returned bytes and the requested path extension. The plugin rejects mismatches and does not transcode.

Install

dsh plugin --profile web add github:tuofangzhe/dsh-plugin-image-gen

Profile: web

  • This package builds from source on install. pnpm will ask you to allow its build script — that is permission to run the package’s code on your machine, outside the agent sandbox. Only allow sources you trust.
  • This source has no pinned commit, so a later push upstream changes what installs. Prefer pinning a commit.
Source