Image Bridge

# Image Bridge (dsh-image-bridge) English | [中文](README.md) [![stars](https://img.shields.io/github/stars/kkj-luna/dsh-image-bridge?style=flat)](https://github.com/kkj-luna/dsh-image-bridge) [![dsh](https://img.shields.io/badge/DeepSeek%20Harness-plugin-4f8cff)](https://github.com/deepseek-ai) Give a [DeepSeek Harness](https://github.com/deepseek-ai) model that **cannot draw** access to an external image service: install one plugin and the model gains two tools — `image_generate` and `image_edit` — with each result shown in the conversation as a persistent image attachment and saved to `generated-images/` in the session workspace. ![Image Bridge settings page](docs/settings-en.png) ## Install Install the plugin (it is on npm — one command): ```sh dsh plugin --profile web add kkj-luna-dsh-image-bridge ``` Restart `dsh web` and open **Settings → Image Bridge**. You get a prebuilt install: no cloning, no local path to remember, and no source-build approval step. This requires a local `@deepseek-ai/dsh` (a working `dsh web`). If you do not have one: ```sh npm i -g @deepseek-ai/dsh ``` **Rather not type commands?** Let your agent do it — start a new conversation and send this: ```text Install this plugin: dsh plugin --profile web add kkj-luna-dsh-image-bridge Then restart dsh web and confirm that Settings → Image Bridge opens and both tools are registered. ``` The agent installs, restarts, and checks the result for you. **To track the latest commits** (when npm lags behind): ```sh dsh plugin --profile web add github:kkj-luna/dsh-image-bridge ``` **Working from a local checkout** (when you edit the code — installs a `link:` dependency, so a restart picks up your changes): ```sh dsh plugin --profile web add ``` > **A freshly published version may not install.** pnpm has a **24-hour minimum release age** by default (`minimumReleaseAge`, which stops a just-poisoned version from being pulled in immediately): within 24 hours of a release, the command above **silently falls back to the previous version** — it looks like it succeeded, but you did not get the newest one. > > To install a specific version right away, name it and the age gate is bypassed: > > ```sh > dsh plugin --profile web add kkj-luna-dsh-image-bridge@0.4.2 > ``` > > To check which version you actually got, look at the version in the settings-page badge, or run `dsh plugin --profile web list`. ### How to confirm it loaded After restarting `dsh web`, any one of the three (**the restart is required** — plugin code is not hot-reloaded): - **Settings → Image Bridge** shows the settings page; - `$DSH_HOME/dsh-image-bridge-startup.json` lists three `steps` (schema ready, both tools registered, test channel mounted) and `services.tools` is `true`; - The model can call both `image_generate` and `image_edit` in a session. ### Uninstall ```sh dsh plugin --profile web remove kkj-luna-dsh-image-bridge ``` You can also remove it under **Settings → Plugins**. | You may be wondering | Answer | | --- | --- | | Why does the package name carry `kkj-luna-`? | To stay unique on npm — the plain name `dsh-image-bridge` was already taken. The settings-page badge shows only the repository name | | Why not just publish to npm? | It is published. The `github:` form is only for tracking the latest commits | | Do I have to restart? | **Yes** — plugin code is not hot-reloaded | | Will upgrading lose my configuration? | No. The row id and settings namespace have always been `image-bridge-v4`; the package name changed twice but it is only an install identifier | | What if I want to change the code? | See "Developing this plugin" in the [full manual](docs/manual.en.md) — a developer path, not needed for normal use | ## Three steps to start 1. **Add a provider**: Settings → Image Bridge → "+ Add model provider", then fill in the endpoint (e.g. `https://api.openai.com/v1`) and the API key. 2. **Test first, then save**: click "Test connection", and once the model list appears click **Save** in the dialog. 3. **Pick models and draw**: back on the page, in the **Models** section choose one model each for generation and editing (each may have a fallback), click "Save model settings", then just ask — "draw me a Shiba Inu in an astronaut helmet". > **The step people trip on**: the dropdowns draw from the model list that has been **saved**. Test without saving, close the dialog, and those models never appear — the order is "Test connection / Fetch models" → **Save** → the dropdowns. ## What you get - **Drawing does not depend on the model** — vision or no vision, it works the same: the model calls the plugin, the plugin asks your external image service, and the image comes back as an attachment in the conversation - **Several providers side by side** — each with its own endpoint and API key, with a connection light right on the card; one "Test connection" also lists the models that provider offers - **Mix and match models** — generation and editing may use models from different vendors, each with a primary and a fallback; if the primary fails the fallback takes over and the reply says which one was used - **Only drawable models are listed** — your key may expose hundreds of models; by default only the ones recognised as image models show up, and one toggle reveals the rest - **Size is up to you** — eight common presets, or any size string you type - **Keys stay put** — API keys live in the DSH credential store (`$DSH_HOME/.credentials.yaml`, mode `0600`), never in the config file and never back to the browser - **Updates keep your config** — providers, models and size live in the profile, so updating the plugin leaves them alone ## Supported providers Any **OpenAI-compatible** endpoint works: OpenAI, Zhipu, Qwen, SiliconFlow and the various relay services. Generation posts to `POST {endpoint}/images/generations`; editing posts to `POST {endpoint}/images/edits` (multipart, with the reference image given as a session workspace path). All you need is an endpoint and an API key. ## Timeouts and fallback Image generation is synchronous, so tens of seconds is normal (42–61 s measured) — which is exactly why **only a failed connection switches to the fallback, never a slow drawing**: | Behaviour | Value | | --- | --- | | Connect timeout (a setting, in seconds) | **15** by default | | The fallback's connect limit | a fixed **30** seconds, not a setting | | Whole-call ceiling | `timeoutMs`, **180000** ms by default | - **The connect timeout only covers "did the request get out"**: if DNS / TCP / TLS / proxy has not put the request on the wire within 15 seconds, that provider counts as unreachable and the fallback model takes over. - **Once the request is sent it is no longer timed**: however long the provider takes is fine, bounded by `timeoutMs`. A wrong address therefore costs you 15 seconds at most, while a slow but healthy generation is never mistaken for a failure. - **Three cases switch to the fallback**: the provider cannot be reached (connect timeout or a plain network-layer failure), a non-2xx response (including a body that is not valid JSON), or a 2xx response from which no usable image can be parsed. - **Two cases do not**: you cancel it in the UI; or the image was produced and only the download failed — it is already billed, so it must not be generated again. - **When neither can be reached**, the error starts with **"模型连接失败" (model connection failed)** and lists both attempts' reasons; to check whether a provider is reachable at all, use the "Test connection" button in Settings. > Older versions switched the fallback on **response speed**; that setting was `fallbackAfterMs` ("主模型超时(毫秒)", 90000 by default). **It has been removed entirely** — a value left in an old config is treated as an unknown field and dropped, with no effect on runtime. ## FAQ Start with the **status light** and the failure reason on the card — most answers are already there. | Symptom | Cause | | --- | --- | | "No usable API key" | This card has no key typed and none stored. It is **not** a network error — no request was sent | | "Key rejected (401/403)" | The request went out; the provider refused the key | | "Fetch models" returns 404 | That provider has no `/models`; generation still works | | All four dropdowns are empty (disabled) | No model list has been saved yet: test the connection, then **Save**; or use "Enter manually…" | | There is no "Image Bridge" page in Settings | Check that the plugin is enabled and whether `client apply` is fatal in the client log; **changed code needs a DSH restart** (hot reload ignores `node_modules` by default) | ## Security and privacy - **Keys never land in the config**: API keys live in `$DSH_HOME/.credentials.yaml` (`0600`); the config file holds only the credential name. Keys are never sent back to the browser and never written to logs. - **No explicit key, no request**: when a request carries neither an explicit key nor a valid credential reference, the host sends **not a single byte** and never falls back to another provider's key; the endpoint does not fall back either. - **Image URLs from the provider must pass an SSRF gate**: a provider may answer with a URL for the client to download — and that address is **its choice**, so only `http`/`https` is allowed, and `localhost` / `.local` / loopback and private ranges are refused (`127.x`, `10.x`, `172.16-31.x`, `192.168.x`, `169.254.169.254`, `fe80:` / `fc` / `fd`, …). - **The endpoint you type yourself may be local**: a local image service is a legitimate setup, as long as the scheme is http/https. - **Both the response and the download are capped**: 32 MiB for the response body, 15 s / 20 MiB for a downloaded image, and the read can be cancelled. - **Deleting a provider does not delete its key**: deletion is a config operation. To remove the key, delete that line from `$DSH_HOME/.credentials.yaml` yourself. ## Development and verification ```sh node .dib-verify/verify-manifest.mjs # 47 manifest and cross-file consistency node .dib-verify/verify-client.mjs # 183 client registration and component rendering node .dib-verify/verify-host.mjs # 147 host end to end node .dib-verify/verify-security.mjs # 34 security boundaries (per-hop SSRF refusal, response caps) node .dib-verify/verify-resolve.mjs # 8 dependency resolution robustness node .dib-verify/verify-upgrade.mjs # 24 user config survives an upgrade ``` **443 assertions**, no network and no real API key needed. The cross-process end-to-end run additionally needs the local mock service: ```sh node .dib-verify/mock-image-api.mjs 8799 # in a second terminal node .dib-verify/verify-cross.mjs ``` Every path is derived at runtime — no machine paths are hardcoded — and a missing environment tells you which variable to set (`DSH_BIN` / `DSH_PROFILE_DIR` / `DIB_PLUGIN_DIR`). **Why `verify-upgrade` exists**: your provider config is **not inside the plugin** but on that profile row, so the plugin's own tests can never catch a rename, a row-id change or a changed default that loses config you already saved. This suite calls the patch-merge algorithm the Harness actually exports, composes the real layers, and asserts zero config drift. ## Layout **The repository root is the plugin package** (since v0.3.0 — that is what makes the one-command install work): ``` package.json cordis.patch.yml package manifest and mount declaration index.js client.js host half and client half locale/ icon.svg plugin-card strings and the icon README.md docs/manual.md this file (repo front page / npm page) and the full manual .dib-verify/ test and acceptance suites (travel with the repo, not the package) generated-images/ generated output (ignored, not committed) ``` `.npmignore` keeps `.dib-verify/`, the screenshots and local caches out of the package, so what lands on a user's machine is only what has to run. `dsh-image-bridge/`, `image-bridge/` and `plugins/dsh-image-bridge{,-live,-v3}/` are historical copies superseded by this package and not tracked — do not edit them. ## Known limitations - **Plugin code changes require restarting DSH**: Node caches loaded modules and hot reload ignores `node_modules` (config hot reload is a separate channel and unaffected) - **The sidebar icon is a workaround**: the shell hardcodes icons per section id, so third-party plugins always get a gear. This plugin claims its own row and swaps in its mark; it should revert to the official capability once `settings.section` supports an `icon` field - **Historical copies are not tracked**: `dsh-image-bridge/`, `image-bridge/` and `plugins/dsh-image-bridge{,-live,-v3}/` exist only on this machine ## Documentation | Document | Contents | | --- | --- | | [Full plugin manual](docs/manual.en.md) | configuration, behaviour details, troubleshooting, implementation notes | | [Changelog](CHANGELOG.md) | what changed in each version (currently **v0.4.3**) | | [Verification report](.dib-verify/verify-report.md) · [second round](.dib-verify/verify-report-2.md) | independent verification results, including what was not covered | | [Listing on the plugin marketplace](docs/marketplace.md) | want it to show up in the marketplace? entry file and PR steps | ## Feedback When you open an [issue](https://github.com/kkj-luna/dsh-image-bridge/issues), attach the paths of the three self-diagnostics — it makes triage much faster: | File | Contents | | --- | --- | | `$DSH_HOME/dsh-image-bridge-startup.json` | rewritten by the host on every activation; names the provider actually in use | | `$DSH_HOME/dsh-image-bridge-client.log` | browser-side log, **never containing keys** | | `$DSH_HOME/dsh-image-bridge-degraded.json` | appears only when dependency resolution fails | ## License [MIT](LICENSE)