# vision-control

- id: vision-control
- version: 0.1.4
- developer: DeepSeek Harness
- packageName: vision-control
- loaderId: vision-control
- sha256: 097cb0b4c5e448440278f0a3147a130f67fdc409b1bdedd3078dab27247cef8a
- byteSize: 23861
- publishedAt: 2026-08-25T10:53:52.446Z
- updatedAt: 2026-09-21T17:16:41.042Z
- releasedAt: 2026-09-21T17:16:41.042Z
- html: https://addon.dshdo.com/plugins/vision-control
- json: https://addon.dshdo.com/v1/plugins/vision-control
- markdown: https://addon.dshdo.com/plugins/vision-control.md
- tarball: https://addon.dshdo.com/v1/plugins/vision-control/versions/0.1.4/tarball

## Install

```
curl -fsSL https://addon.dshdo.com/install-plugin-market.sh | sh
# then install catalog id vision-control in /plugin-market/ and enable it
```

## Description

管理 llm-pi-ai 按模型的图片视觉开关；只写自有来源标记的配置，不覆盖用户声明。

## Changelog

(none)

## Versions

- 0.1.4 (2026-09-21T17:16:41.042Z)
- 0.1.3 (2026-09-08T11:20:12.321Z): 0.1.3: packaging-only release. The 0.1.2 archive unintentionally carried an AppleDouble sidecar (._cordis.patch.yml) from the platform tar tool
- 0.1.2 (2026-09-08T09:41:39.736Z): 0.1.2: apply is now a non-constructible arrow so Cordis fiber runner collects and awaits its disposer — unload waits for an in-flight settings.replace and writes nothing after dispose. Real-Cordis/Loader fixture regression test added.
- 0.1.1 (2026-09-08T06:04:22.207Z): 0.1.1: ownership/provenance rewrite — write only what this plugin provably owns (marker-gated), verify catalog-route disables against the authoritative llm adapter, async dispose awaiting in-flight reconcile; lost-adapter re-arm + repeated-dispose fixed.
- 0.1.0 (2026-08-25T10:53:52.446Z): 首个版本：默认全开 llm-pi-ai 各路由图片输入（defaultInput），支持按 provider/模型手动关闭（disabled），双向收敛与幂等。

## README

# vision-control

Out-of-tree Cordis plugin that manages the `llm-pi-ai` image-vision switch per
model (no main-program changes, no UI — a pure configuration plane).

Version: 0.1.3

## Purpose

Writes to the `llm-pi-ai` user section through `dsh-settings`, doing two things:

- **Vision on by default**: when a route declares no `defaultInput` in either
  the user section or the composition `base` layer, write route-level
  `defaultInput: [text, image]`. A declared `defaultInput` (even `[text]`) is
  never overwritten.
- **Vision off per model**: for models listed in `disabled`, write `input:
  [text]` to switch vision off; when a model leaves `disabled`, undo only the
  entry this plugin wrote itself.

## Ownership and undo rules (since 0.1.1)

The plugin writes only fields it "owns", and records every write with a
deterministic provenance marker:

- Disabling a model on a **hand-declared route** (a route whose config carries a
  `models` list) writes `input: [text]` only when the model entry declares **no
  `input`**, together with the provenance marker
  `_visionControl: { input: true }`.
- Disabling a model on a **catalog route** (no `models` list; served from the
  installed pi-ai catalog) writes
  `modelOverrides.<id>: { input: [text], _visionControl: { input: true } }`
  only when the authoritative host model catalog does describe that model on
  that route.
- Writing route-level `defaultInput: [text, image]` also records the marker
  `_visionControl: { defaultInput: true }` on the route object.
- When a disable is lifted or `defaultImage` is set to `false`, **only marker
  verified writes are removed**. Any unmarked `input` / `defaultInput` —
  including a user-authored `input: [text]` — is treated as user-owned and is
  never automatically deleted or rewritten.
- If the user later edits a value under a marker (an `input` or a route
  `defaultInput`), the plugin releases ownership of that entry (removes the
  marker, keeps the user's value) and will not auto-undo it.
- A route that exists only in the resolved layer (never user-declared) is never
  materialized into the user document.
- Idempotent: repeated loads and repeated reconciles have no side effects;
  config validation failures error at load, never silently skipped.

### Catalog-route disable verification

Before disabling a model on a catalog route, the plugin verifies membership
through the runtime `llm` service (`ctx.get('llm')`): the adapter that service
materializes for that route is exactly the set of models for which `llm-pi-ai`
would accept a `modelOverrides` entry. When membership **cannot be verified**
(no `llm` service, no adapter for the route, or a catalog error), the disable
is **explicitly refused and logged** — never silently treated as applied, which
would leave a model looking vision-off while still on. Adapter registration
often trails this plugin's mount or a settings edit, so the plugin listens for
`llm/adapters-updated`: a refused catalog disable retries automatically once
the adapter is ready instead of staying unapplied.

### 0.1.3 packaging-only release note

0.1.3 is a packaging-only release: identical plugin code and behavior to
0.1.2. The 0.1.2 archive unintentionally carried an AppleDouble sidecar
(`._cordis.patch.yml`) created by the platform tar tool from a local extended
attribute; 0.1.3 is built with the maintained node-tar writer (`portable`),
which never stores macOS filesystem metadata, so the archive contains exactly
the intended payload. The 0.1.2 release and its public archive remain
immutable history.

### 0.1.2 fix-release note

0.1.2 fixes a real-framework disposal bug: `apply` was a constructible
`function`, so Cordis' fiber runner `new`ed it and discarded the async disposer
it returns — the "dispose awaits an in-flight `settings.replace`" guarantee
silently never registered when the plugin was unloaded by the actual Loader.
`apply` is now an arrow function (`export const apply = …`), which Cordis calls
directly and whose disposer it collects and awaits. Behavior is otherwise
identical to 0.1.1; the 0.1.1 ownership/undo rules and the migration notes
below all still apply. The regression is covered by a real-Cordis/Loader
fixture test that holds a `settings.replace` in flight and proves the fiber
dispose waits for it, with nothing written after disposal.

### 0.1.0 → 0.1.1 migration notes

0.1.0's automatic sync **deleted** user-authored `input: [text]` /
`modelOverrides` and wrote its values **without provenance markers**
(including route-level `defaultInput`). After upgrading, which values were
originally user-authored cannot be recovered: every unmarked existing value is
treated as "user-owned" and is never auto-removed by this plugin (setting
`defaultImage` to `false` will not undo a route `defaultInput` that 0.1.0
wrote). If 0.1.0 removed an explicit declaration of yours, restore it by hand
(for example, rewrite a model's `input` as `['text']`).

## Configuration

In the plugin entry's `config` (a manual `cordis.patch.yml` or your config
source):

```yaml
- insert:
    - id: vision-control
      name: vision-control
      config:
        defaultImage: true                      # vision on by default (defaults to true)
        disabled:                               # per-model vision off (optional)
          seawork: [ gpt-5.6-terra ]
          grok: [ grok-imagine ]
```

- `defaultImage`: boolean, defaults to `true`. When `false`, route-level
  defaults are not written, and **undoes** route `defaultInput` values this
  plugin wrote earlier that still carry the marker (the schema's `['text']`
  floor resumes); user-declared or 0.1.0-era unmarked values are unaffected.
- `disabled`: an array of model ids (applies to every route), or a
  provider-keyed map; `*` matches any route:

  ```yaml
  config:
    disabled:
      seawork: [ gpt-5.6-terra ]
      '*': [ shared-model ]
  ```

## Behavior boundaries

- Writes only the `llm-pi-ai` namespace's user configuration section; does not
  read or write other namespaces and does not touch the main program.
- Disable writes act only on **hand-declared-route model entries** or
  **catalog-route `modelOverrides`**, never writing `modelOverrides` for a
  model also listed in a `models` array (`llm-pi-ai` rejects that combination).
- `llm-deepseek` runs through its own adapter and is outside this plugin's
  scope.
- This plugin is not a vision-content gate; it only keeps `llm-pi-ai`'s model
  capability declarations (`input` / `defaultInput`) consistent with your
  configuration intent.

