dsh-local-ai 0.1.3 → 0.1.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -5,6 +5,24 @@ All notable changes to this project are documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [0.1.5] - 2026-08-23
9
+
10
+ ### Added
11
+
12
+ - Endpoint liveness (M3): `scripts/check-endpoints.mjs` probes the configured Ollama HTTP endpoint (`/api/version`; 2xx = alive, any transport error or non-2xx = fail) with `OLLAMA_HOST` / `CHECK_ENDPOINTS` / `TIMEOUT_MS` overrides, and `.github/workflows/check-endpoints.yml` runs it monthly and on demand against a throwaway local Ollama; `test/check-endpoints.spec.ts` covers the endpoint-resolution, verdict, error-classification, and timeout helpers plus a plain-Node syntax check.
13
+
14
+ ## [0.1.4] - 2026-08-22
15
+
16
+ ### Added
17
+
18
+ - **Vision support (issue #5)** — models whose `/api/show` capabilities include `vision` now declare `inputModalities: ['text', 'image']` and carry base64 image payloads on user messages (resolved through the optional `attachments` service); the `vision` config knob (default `true`) keeps the route text-only on opt-out, and text-only models still reject image content loudly (`UNSUPPORTED_CONTENT`).
19
+
20
+
21
+ ### Changed
22
+
23
+ - Upgraded every `@deepseek-ai/dsh-*` dev dependency from `0.1.0-rc.8` to `0.1.1-rc.2` for DeepSeek Harness `0.1.1-rc.2` compatibility. Peer ranges stay `>=0.1.0-rc.8 <0.2.0`: no adapter, routing, tool, or command code uses an rc2-only API.
24
+ - Repinned `minimumReleaseAgeExclude` to the whole `@deepseek-ai/*` scope and synchronized the `0.1.1-rc.2` baseline across the five-language READMEs, AGENTS.md, THIRD_PARTY_NOTICES.md, the CI workflow name, and the compat workflow.
25
+
8
26
  ## [0.1.3] - 2026-08-21
9
27
 
10
28
  ### Changed
package/README.es.md CHANGED
@@ -24,7 +24,7 @@
24
24
 
25
25
  | Superficie | Estado |
26
26
  |---|---|
27
- | Harness | DeepSeek Harness `0.1.0-rc.8` |
27
+ | Harness | DeepSeek Harness `0.1.1-rc.2` |
28
28
  | Node | `^22.19.0 \|\| >=24.0.0` |
29
29
  | Backend | [Ollama](https://ollama.com) (API HTTP local + sonda CLI) |
30
30
  | Modelo | Ruta solo texto (`inputModalities: ['text']`); se admiten llamadas y resultados de herramientas |
@@ -102,6 +102,7 @@ Todos los ajustes son campos `Config` de Schemastery (modificables desde cordis.
102
102
  | `defaultContextWindow` | `8192` | Capacidad de contexto cuando un modelo no tiene valor exacto |
103
103
  | `maxTokens` | `4096` | Límite de salida por solicitud cuando un modelo no tiene valor exacto |
104
104
  | `temperature` | *(none)* | Temperatura de muestreo por defecto (0..2); omitir deja el valor del proveedor |
105
+ | `vision` | `true` | Declara y serializa el soporte de imágenes cuando el modelo informa vision; `false` mantiene la ruta solo texto |
105
106
  | `models` | `[]` | Mapeos de nombre visible → modelo Ollama |
106
107
  | `models[].name` | *(required)* | Nombre de modelo visible en el harness (`GenerateOptions.model`) |
107
108
  | `models[].model` | `= name` | Id del modelo Ollama |
@@ -143,22 +144,23 @@ Todos los ajustes son campos `Config` de Schemastery (modificables desde cordis.
143
144
 
144
145
  ## Known limitations
145
146
 
146
- - **Solo rc.8** — desarrollado y probado contra `@deepseek-ai/dsh@0.1.0-rc.8`; se espera que baselines más nuevos funcionen, pero los verifica el workflow mensual de compat.
147
- - **Ruta solo texto** — el contenido de imagen se rechaza (`UNSUPPORTED_CONTENT`); los modelos locales multimodales aún no están conectados.
147
+ - **Solo rc.2** — desarrollado y probado contra `@deepseek-ai/dsh@0.1.1-rc.2`; se espera que baselines más nuevos funcionen, pero los verifica el workflow mensual de compat.
148
+ - **Vision cuando el modelo la informa** — los modelos cuyas capacidades de `/api/show` incluyen `vision` declaran `inputModalities: ["text","image"]` y llevan cargas de imagen base64 en los mensajes de usuario (exclusión con `vision: false`); los modelos solo texto siguen rechazando el contenido de imagen (`UNSUPPORTED_CONTENT`).
148
149
  - **Respaldo a mitad de flujo** — una vez que una ruta local empezó a producir contenido, un fallo posterior se reenvía (no se retira); solo un fallo antes del primer token respalda a la nube.
149
150
 
150
151
  ## Development
151
152
 
152
153
  ```sh
153
154
  pnpm install # node ^22.19 || >=24
154
- pnpm run typecheck # tsc: src + tests contra los tipos publicados 0.1.0-rc.8
155
- pnpm run typecheck:ci # tsc estricto contra los tipos publicados rc.8 (skipLibCheck off)
155
+ pnpm run typecheck # tsc: src + tests contra los tipos publicados 0.1.1-rc.2
156
+ pnpm run typecheck:ci # tsc estricto contra los tipos publicados rc.2 (skipLibCheck off)
156
157
  pnpm test # vitest: costuras reales Context/LlmRuntime/ToolRuntime/CommandRuntime/subprocess
157
158
  pnpm run test:coverage # puerta de cobertura (90/80/90/90)
158
159
  pnpm run build # bundle tsdown + declaraciones tsc (lib/)
159
160
  pnpm run verify:self-contained # las especificaciones de dependencias resuelven desde el registry
160
161
  pnpm run verify:artifacts # cara ESM construida + bundle patch presentes
161
162
  node scripts/check-readme-sync.mjs # puerta de sincronización de README en cinco idiomas
163
+ node scripts/check-endpoints.mjs # sonda de actividad M3 (Ollama /api/version)
162
164
  pnpm pack # el tarball publicado
163
165
  ```
164
166
 
package/README.hi.md CHANGED
@@ -24,7 +24,7 @@
24
24
 
25
25
  | सतह | स्थिति |
26
26
  |---|---|
27
- | Harness | DeepSeek Harness `0.1.0-rc.8` |
27
+ | Harness | DeepSeek Harness `0.1.1-rc.2` |
28
28
  | Node | `^22.19.0 \|\| >=24.0.0` |
29
29
  | Backend | [Ollama](https://ollama.com) (स्थानीय HTTP API + CLI जाँच) |
30
30
  | Model | केवल-पाठ रूट (`inputModalities: ['text']`); टूल कॉल व परिणाम समर्थित हैं |
@@ -102,6 +102,7 @@ dsh --profile web --dump-config | grep -A2 'id: dsh-local-ai'
102
102
  | `defaultContextWindow` | `8192` | जब मॉडल का कोई सटीक मान न हो तो संदर्भ क्षमता |
103
103
  | `maxTokens` | `4096` | जब मॉडल का कोई सटीक मान न हो तो प्रति-अनुरोध आउटपुट सीमा |
104
104
  | `temperature` | *(none)* | डिफ़ॉल्ट सैंपलिंग तापमान (0..2); छोड़ने पर प्रदाता डिफ़ॉल्ट रहता है |
105
+ | `vision` | `true` | मॉडल द्वारा vision रिपोर्ट करने पर छवि समर्थन घोषित व सीरियलाइज़ करता है; `false` रूट को केवल-टेक्स्ट रखता है |
105
106
  | `models` | `[]` | Harness-दृश्य नाम → Ollama मॉडल मैपिंग |
106
107
  | `models[].name` | *(required)* | Harness-दृश्य मॉडल नाम (`GenerateOptions.model`) |
107
108
  | `models[].model` | `= name` | Ollama मॉडल id |
@@ -143,22 +144,23 @@ dsh --profile web --dump-config | grep -A2 'id: dsh-local-ai'
143
144
 
144
145
  ## Known limitations
145
146
 
146
- - **केवल rc.8** — `@deepseek-ai/dsh@0.1.0-rc.8` के विरुद्ध विकसित व परीक्षित; नए harness बेसलाइन काम करने की अपेक्षा है, पर मासिक compat workflow उन्हें सत्यापित करता है।
147
- - **केवल-पाठ रूट** — छवि सामग्री अस्वीकृत होती है (`UNSUPPORTED_CONTENT`); मल्टीमॉडल स्थानीय मॉडल अभी जुड़े नहीं हैं।
147
+ - **केवल rc.2** — `@deepseek-ai/dsh@0.1.1-rc.2` के विरुद्ध विकसित व परीक्षित; नए harness बेसलाइन काम करने की अपेक्षा है, पर मासिक compat workflow उन्हें सत्यापित करता है।
148
+ - **मॉडल द्वारा vision रिपोर्ट करने पर विज़न** — जिन मॉडलों की `/api/show` capabilities में `vision` है, वे `inputModalities: ["text","image"]` घोषित करते हैं और उपयोगकर्ता संदेशों पर base64 छवि पेलोड ले जाते हैं (`vision: false` से बंद करें); केवल-टेक्स्ट मॉडल अब भी छवि सामग्री अस्वीकार करते हैं (`UNSUPPORTED_CONTENT`)।
148
149
  - **मध्य-स्ट्रीम वापसी** — एक बार स्थानीय रूट सामग्री बनाना शुरू कर दे, तो बाद की विफलता आगे भेजी जाती है (वापस नहीं ली जाती); केवल पहले टोकन से पहले की विफलता क्लाउड पर लौटती है।
149
150
 
150
151
  ## Development
151
152
 
152
153
  ```sh
153
154
  pnpm install # node ^22.19 || >=24
154
- pnpm run typecheck # tsc: src + परीक्षण, प्रकाशित 0.1.0-rc.8 प्रकारों के विरुद्ध
155
- pnpm run typecheck:ci # सख्त tsc, प्रकाशित rc.8 प्रकारों के विरुद्ध (skipLibCheck बंद)
155
+ pnpm run typecheck # tsc: src + परीक्षण, प्रकाशित 0.1.1-rc.2 प्रकारों के विरुद्ध
156
+ pnpm run typecheck:ci # सख्त tsc, प्रकाशित rc.2 प्रकारों के विरुद्ध (skipLibCheck बंद)
156
157
  pnpm test # vitest: वास्तविक Context/LlmRuntime/ToolRuntime/CommandRuntime/subprocess सीम
157
158
  pnpm run test:coverage # कवरेज द्वार (90/80/90/90)
158
159
  pnpm run build # tsdown बंडल + tsc घोषणाएँ (lib/)
159
160
  pnpm run verify:self-contained # निर्भरता विनिर्देश registry से हल होते हैं
160
161
  pnpm run verify:artifacts # निर्मित ESM फलक + बंडल पैच मौजूद
161
162
  node scripts/check-readme-sync.mjs # पाँच-भाषा README समन्वय द्वार
163
+ node scripts/check-endpoints.mjs # M3 एंडपॉइंट-लाइवनेस प्रोब (Ollama /api/version)
162
164
  pnpm pack # प्रकाशित tarball
163
165
  ```
164
166
 
package/README.md CHANGED
@@ -1,6 +1,7 @@
1
1
  <div align="center">
2
2
 
3
3
  # 🤖 dsh-local-ai
4
+ [![Gitee](https://img.shields.io/badge/Gitee-mirror-c71d23?logo=gitee)](https://gitee.com/perrylink/dsh-local-ai)
4
5
 
5
6
  **Local-model (Ollama) integration for DeepSeek Harness.**
6
7
 
@@ -24,7 +25,7 @@
24
25
 
25
26
  | Surface | Status |
26
27
  |---|---|
27
- | Harness | DeepSeek Harness `0.1.0-rc.8` |
28
+ | Harness | DeepSeek Harness `0.1.1-rc.2` |
28
29
  | Node | `^22.19.0 \|\| >=24.0.0` |
29
30
  | Backend | [Ollama](https://ollama.com) (local HTTP API + CLI probe) |
30
31
  | Model | Text-only route (`inputModalities: ['text']`); tool calls and tool results are supported |
@@ -102,6 +103,7 @@ All tunables are Schemastery `Config` fields (changeable from cordis.yml). An id
102
103
  | `defaultContextWindow` | `8192` | Context capacity used when a model has no exact value |
103
104
  | `maxTokens` | `4096` | Per-request output cap used when a model has no exact value |
104
105
  | `temperature` | *(none)* | Default sampling temperature (0..2); omitted leaves the provider default |
106
+ | `vision` | `true` | Declare and serialize image support when the model reports vision; `false` keeps the route text-only |
105
107
  | `models` | `[]` | Harness-visible → Ollama model mappings |
106
108
  | `models[].name` | *(required)* | Harness-visible model name (`GenerateOptions.model`) |
107
109
  | `models[].model` | `= name` | Ollama model id |
@@ -143,22 +145,23 @@ All tunables are Schemastery `Config` fields (changeable from cordis.yml). An id
143
145
 
144
146
  ## Known limitations
145
147
 
146
- - **rc.8 only** — developed and tested against `@deepseek-ai/dsh@0.1.0-rc.8`; newer harness baselines are expected to work but are verified by the monthly compat workflow.
147
- - **Text-only route** — image content is rejected (`UNSUPPORTED_CONTENT`); multimodal local models are not wired up yet.
148
+ - **rc.2 only** — developed and tested against `@deepseek-ai/dsh@0.1.1-rc.2`; newer harness baselines are expected to work but are verified by the monthly compat workflow.
149
+ - **Vision when the model reports it** — models whose `/api/show` capabilities include `vision` declare `inputModalities: ["text","image"]` and carry base64 image payloads on user messages (opt out with `vision: false`); text-only models still reject image content (`UNSUPPORTED_CONTENT`).
148
150
  - **Mid-stream fallback** — once a local route has started producing content, a later failure is forwarded (not retracted); only a failure before the first token falls back to the cloud.
149
151
 
150
152
  ## Development
151
153
 
152
154
  ```sh
153
155
  pnpm install # node ^22.19 || >=24
154
- pnpm run typecheck # tsc: src + tests against the published 0.1.0-rc.8 types
155
- pnpm run typecheck:ci # strict tsc against published rc.8 types (skipLibCheck off)
156
+ pnpm run typecheck # tsc: src + tests against the published 0.1.1-rc.2 types
157
+ pnpm run typecheck:ci # strict tsc against published rc.2 types (skipLibCheck off)
156
158
  pnpm test # vitest: real Context/LlmRuntime/ToolRuntime/CommandRuntime/subprocess seams
157
159
  pnpm run test:coverage # coverage gate (90/80/90/90)
158
160
  pnpm run build # tsdown bundle + tsc declarations (lib/)
159
161
  pnpm run verify:self-contained # dependency specs resolve from the registry
160
162
  pnpm run verify:artifacts # built ESM face + bundle patch present
161
163
  node scripts/check-readme-sync.mjs # five-language README sync gate
164
+ node scripts/check-endpoints.mjs # M3 endpoint-liveness probe (Ollama /api/version)
162
165
  pnpm pack # the published tarball
163
166
  ```
164
167
 
package/README.pt.md CHANGED
@@ -24,7 +24,7 @@
24
24
 
25
25
  | Superfície | Status |
26
26
  |---|---|
27
- | Harness | DeepSeek Harness `0.1.0-rc.8` |
27
+ | Harness | DeepSeek Harness `0.1.1-rc.2` |
28
28
  | Node | `^22.19.0 \|\| >=24.0.0` |
29
29
  | Backend | [Ollama](https://ollama.com) (API HTTP local + sonda CLI) |
30
30
  | Modelo | Rota somente texto (`inputModalities: ['text']`); chamadas e resultados de ferramentas são suportados |
@@ -102,6 +102,7 @@ Todos os ajustes são campos `Config` de Schemastery (modificáveis pelo cordis.
102
102
  | `defaultContextWindow` | `8192` | Capacidade de contexto quando um modelo não tem valor exato |
103
103
  | `maxTokens` | `4096` | Limite de saída por solicitação quando um modelo não tem valor exato |
104
104
  | `temperature` | *(none)* | Temperatura de amostragem padrão (0..2); omitir mantém o padrão do provedor |
105
+ | `vision` | `true` | Declara e serializa o suporte a imagens quando o modelo informa vision; `false` mantém a rota somente texto |
105
106
  | `models` | `[]` | Mapeamentos nome visível → modelo Ollama |
106
107
  | `models[].name` | *(required)* | Nome de modelo visível no harness (`GenerateOptions.model`) |
107
108
  | `models[].model` | `= name` | Id do modelo Ollama |
@@ -143,22 +144,23 @@ Todos os ajustes são campos `Config` de Schemastery (modificáveis pelo cordis.
143
144
 
144
145
  ## Known limitations
145
146
 
146
- - **Somente rc.8** — desenvolvido e testado contra `@deepseek-ai/dsh@0.1.0-rc.8`; espera-se que baselines mais novos funcionem, mas são verificados pelo workflow mensal de compat.
147
- - **Rota somente texto** — conteúdo de imagem é rejeitado (`UNSUPPORTED_CONTENT`); modelos locais multimodais ainda não estão conectados.
147
+ - **Somente rc.2** — desenvolvido e testado contra `@deepseek-ai/dsh@0.1.1-rc.2`; espera-se que baselines mais novos funcionem, mas são verificados pelo workflow mensal de compat.
148
+ - **Vision quando o modelo a informa** — modelos cujas capacidades de `/api/show` incluem `vision` declaram `inputModalities: ["text","image"]` e carregam payloads de imagem base64 nas mensagens do usuário (desative com `vision: false`); modelos somente texto continuam rejeitando conteúdo de imagem (`UNSUPPORTED_CONTENT`).
148
149
  - **Fallback no meio do fluxo** — uma vez que uma rota local começou a produzir conteúdo, uma falha posterior é reencaminhada (não retirada); apenas uma falha antes do primeiro token faz fallback para a nuvem.
149
150
 
150
151
  ## Development
151
152
 
152
153
  ```sh
153
154
  pnpm install # node ^22.19 || >=24
154
- pnpm run typecheck # tsc: src + testes contra os tipos publicados 0.1.0-rc.8
155
- pnpm run typecheck:ci # tsc estrito contra os tipos publicados rc.8 (skipLibCheck off)
155
+ pnpm run typecheck # tsc: src + testes contra os tipos publicados 0.1.1-rc.2
156
+ pnpm run typecheck:ci # tsc estrito contra os tipos publicados rc.2 (skipLibCheck off)
156
157
  pnpm test # vitest: costuras reais Context/LlmRuntime/ToolRuntime/CommandRuntime/subprocess
157
158
  pnpm run test:coverage # porta de cobertura (90/80/90/90)
158
159
  pnpm run build # bundle tsdown + declarações tsc (lib/)
159
160
  pnpm run verify:self-contained # especificações de dependências resolvem do registry
160
161
  pnpm run verify:artifacts # face ESM construída + bundle patch presentes
161
162
  node scripts/check-readme-sync.mjs # porta de sincronização de README em cinco idiomas
163
+ node scripts/check-endpoints.mjs # sonda de atividade M3 (Ollama /api/version)
162
164
  pnpm pack # o tarball publicado
163
165
  ```
164
166
 
package/README.zh.md CHANGED
@@ -24,7 +24,7 @@
24
24
 
25
25
  | 项目 | 状态 |
26
26
  |---|---|
27
- | Harness | DeepSeek Harness `0.1.0-rc.8` |
27
+ | Harness | DeepSeek Harness `0.1.1-rc.2` |
28
28
  | Node | `^22.19.0 \|\| >=24.0.0` |
29
29
  | 后端 | [Ollama](https://ollama.com)(本地 HTTP API + CLI 探测) |
30
30
  | 模型 | 纯文本路由(`inputModalities: ['text']`);支持工具调用与工具结果 |
@@ -102,6 +102,7 @@ dsh --profile web --dump-config | grep -A2 'id: dsh-local-ai'
102
102
  | `defaultContextWindow` | `8192` | 模型无精确值时的上下文容量 |
103
103
  | `maxTokens` | `4096` | 模型无精确值时的单请求输出上限 |
104
104
  | `temperature` | *(none)* | 默认采样温度(0..2);省略则用提供方默认值 |
105
+ | `vision` | `true` | 模型报告 vision 能力时声明并序列化图片支持;`false` 保持纯文本路由 |
105
106
  | `models` | `[]` | Harness 可见名 → Ollama 模型映射 |
106
107
  | `models[].name` | *(required)* | Harness 可见模型名(`GenerateOptions.model`) |
107
108
  | `models[].model` | `= name` | Ollama 模型 id |
@@ -143,22 +144,23 @@ dsh --profile web --dump-config | grep -A2 'id: dsh-local-ai'
143
144
 
144
145
  ## Known limitations
145
146
 
146
- - **仅 rc.8** —— 针对 `@deepseek-ai/dsh@0.1.0-rc.8` 开发与测试;更新版本的 harness 基线预期可用,但由每月 compat workflow 验证。
147
- - **纯文本路由** —— 图片内容会被拒绝(`UNSUPPORTED_CONTENT`);多模态本地模型尚未接入。
147
+ - **仅 rc.2** —— 针对 `@deepseek-ai/dsh@0.1.1-rc.2` 开发与测试;更新版本的 harness 基线预期可用,但由每月 compat workflow 验证。
148
+ - **模型声明 vision 时启用视觉** — `/api/show` capabilities 含 `vision` 的模型声明 `inputModalities: ["text","image"]`,并在用户消息上携带 base64 图片载荷(可用 `vision: false` 退出);纯文本模型仍拒绝图片内容(`UNSUPPORTED_CONTENT`)。
148
149
  - **中途失败不回退** —— 本地路由一旦开始产出内容,之后的失败会透传(无法撤回);只有首个 token 前的失败才回退云端。
149
150
 
150
151
  ## Development
151
152
 
152
153
  ```sh
153
154
  pnpm install # node ^22.19 || >=24
154
- pnpm run typecheck # tsc:src + 测试,对发布版 0.1.0-rc.8 类型
155
- pnpm run typecheck:ci # 严格 tsc,对发布版 rc.8 类型(关闭 skipLibCheck)
155
+ pnpm run typecheck # tsc:src + 测试,对发布版 0.1.1-rc.2 类型
156
+ pnpm run typecheck:ci # 严格 tsc,对发布版 rc.2 类型(关闭 skipLibCheck)
156
157
  pnpm test # vitest:真实 Context/LlmRuntime/ToolRuntime/CommandRuntime/subprocess 机制
157
158
  pnpm run test:coverage # 覆盖率门禁(90/80/90/90)
158
159
  pnpm run build # tsdown 打包 + tsc 声明(lib/)
159
160
  pnpm run verify:self-contained # 依赖声明均来自 registry
160
161
  pnpm run verify:artifacts # 构建产物 ESM 面 + bundle patch 存在
161
162
  node scripts/check-readme-sync.mjs # 五语 README 同步门禁
163
+ node scripts/check-endpoints.mjs # M3 端点存活探测(Ollama /api/version)
162
164
  pnpm pack # 发布用 tarball
163
165
  ```
164
166
 
@@ -13,7 +13,7 @@ published tarball; these are install-time dependencies:
13
13
  | [typescript](https://github.com/microsoft/TypeScript) | `^5.9.0` | Apache-2.0 | Build-time declaration emission (`lib/types/`) |
14
14
  | [@deepseek-ai/cordis](https://www.npmjs.com/package/@deepseek-ai/cordis) | `^4.0.1` (peer) | See package | The plugin runtime |
15
15
  | [@deepseek-ai/schemastery](https://www.npmjs.com/package/@deepseek-ai/schemastery) | `^3.18.0` (peer) | See package | Configuration schema |
16
- | `@deepseek-ai/dsh-*` peers | `0.1.0-rc.8` (peer) | See packages | Official harness seams (`dsh-llm`, `dsh-tools`, `dsh-subprocess`, `dsh-commands`, `dsh-timeout`) |
16
+ | `@deepseek-ai/dsh-*` peers | `0.1.1-rc.2` (peer) | See packages | Official harness seams (`dsh-llm`, `dsh-tools`, `dsh-subprocess`, `dsh-commands`, `dsh-timeout`) |
17
17
 
18
18
  At runtime the plugin only talks to the Ollama endpoint you configure (over
19
19
  its HTTP API and, for the process-liveness probe, its CLI); it performs no
package/cordis.patch.yml CHANGED
@@ -21,6 +21,8 @@
21
21
  maxTokens: 4096
22
22
  # Default sampling temperature; omit to keep the provider default.
23
23
  # temperature: 0.7
24
+ # Declare image support when the model reports vision; false keeps text-only.
25
+ # vision: true
24
26
  # Harness-visible -> Ollama model mappings. Each entry maps a name you
25
27
  # select in the harness to an Ollama model id (defaults to the name).
26
28
  models: []
package/lib/index.js CHANGED
@@ -23,6 +23,7 @@ const Config = z.object({
23
23
  defaultContextWindow: z.number().default(8192),
24
24
  maxTokens: z.number().default(4096),
25
25
  temperature: z.number(),
26
+ vision: z.boolean().default(true),
26
27
  models: z.array(z.object({
27
28
  name: z.string().required(),
28
29
  model: z.string(),
@@ -84,6 +85,7 @@ function resolveConfig(config = {}) {
84
85
  assertPositiveInt("maxTokens", maxTokens);
85
86
  const temperature = config.temperature;
86
87
  if (temperature !== void 0) assertFiniteRange("temperature", temperature, 0, 2);
88
+ const vision = config.vision ?? true;
87
89
  const seenNames = /* @__PURE__ */ new Set();
88
90
  const models = (config.models ?? []).map((mapping, index) => {
89
91
  if (typeof mapping.name !== "string" || mapping.name.trim().length === 0) throw new TypeError(`models[${index}].name must be a non-empty string`);
@@ -127,6 +129,7 @@ function resolveConfig(config = {}) {
127
129
  defaultContextWindow,
128
130
  maxTokens,
129
131
  ...temperature === void 0 ? {} : { temperature },
132
+ vision,
130
133
  models,
131
134
  route
132
135
  };
@@ -242,6 +245,10 @@ function sanitizeText(value, maxChars = 4e3) {
242
245
  * `LlmError`. The fetch implementation is injectable for tests.
243
246
  * @module dsh-local-ai/ollama
244
247
  */
248
+ /** True when the reported model capabilities include vision. */
249
+ function hasVision(capabilities) {
250
+ return capabilities?.includes("vision") ?? false;
251
+ }
245
252
  /** Build an absolute API URL from a normalized base URL. */
246
253
  function endpointUrl(baseURL, path) {
247
254
  return `${baseURL}${path}`;
@@ -412,8 +419,10 @@ function contextLengthOf(show) {
412
419
  * vocabulary. User text is joined; assistant text becomes `content` and tool
413
420
  * calls become `tool_calls` (with arguments parsed from the raw JSON string to
414
421
  * the object Ollama expects); tool results become separate `tool` messages.
415
- * Core image blocks are rejected explicitly because this route is text-only;
416
- * unknown declaration-merged block types retain the documented extension
422
+ * Top-level user-message image blocks map onto `images` (base64, no data-URI
423
+ * prefix) when the request carries resolved payloads; images anywhere else —
424
+ * or on a text-only route — still fail loud with `UNSUPPORTED_CONTENT`.
425
+ * Unknown declaration-merged block types retain the documented extension
417
426
  * fallback (ignored for content, retained as text where text is expected).
418
427
  * @module dsh-local-ai/serialize
419
428
  */
@@ -421,9 +430,29 @@ function contextLengthOf(show) {
421
430
  function flattenText(blocks) {
422
431
  return blocks.filter((block) => block.type === "text").map((block) => block.text).join("");
423
432
  }
424
- /** Reject core image content before any text-flattening path can silently erase it. */
425
- function assertTextOnly(blocks) {
426
- if (contentHasImage(blocks)) throw new LlmError("The Ollama adapter does not support image content.", "UNSUPPORTED_CONTENT");
433
+ /**
434
+ * Reject image content the wire cannot carry. Top-level user-message images
435
+ * map onto `images` when the request provides resolved payloads; every other
436
+ * image (non-user roles, nested tool results, or a text-only route without
437
+ * payloads) still fails loud instead of silently dropping the image.
438
+ */
439
+ function assertImagesSupported(message, imagesByRef) {
440
+ for (const block of message.content) if (block.type === "tool-result" && contentHasImage(block.content)) throw new LlmError("The Ollama adapter does not support tool-result image content.", "UNSUPPORTED_CONTENT");
441
+ const hasTopLevelImage = message.content.some((block) => block.type === "image");
442
+ if (message.role !== "user" && hasTopLevelImage) throw new LlmError(`The Ollama adapter does not support image content on ${message.role} messages.`, "UNSUPPORTED_CONTENT");
443
+ if (hasTopLevelImage && imagesByRef === void 0) throw new LlmError("The Ollama adapter does not support image content for this model.", "UNSUPPORTED_CONTENT");
444
+ }
445
+ /** Collect the base64 payloads for one user message's top-level image blocks. */
446
+ function imagesOf(message, imagesByRef) {
447
+ if (imagesByRef === void 0) return [];
448
+ const images = [];
449
+ for (const block of message.content) {
450
+ if (block.type !== "image") continue;
451
+ const data = imagesByRef.get(String(block.attachment.attachmentId));
452
+ if (data === void 0) throw new LlmError("The Ollama adapter could not resolve an image payload.", "UNSUPPORTED_CONTENT");
453
+ images.push(data);
454
+ }
455
+ return images;
427
456
  }
428
457
  /**
429
458
  * Parse a tool-call argument string into the object Ollama expects. The raw
@@ -468,7 +497,7 @@ function toolNameOf(callId, namesByCallId) {
468
497
  * @param messages - the harness conversation, in order.
469
498
  * @returns the wire messages; order preserved, each tool result expanded into its own entry.
470
499
  */
471
- function serializeMessages(messages) {
500
+ function serializeMessages(messages, imagesByRef) {
472
501
  const namesByCallId = /* @__PURE__ */ new Map();
473
502
  for (const message of messages) {
474
503
  if (message.role !== "assistant") continue;
@@ -476,7 +505,8 @@ function serializeMessages(messages) {
476
505
  }
477
506
  const wire = [];
478
507
  for (const message of messages) {
479
- assertTextOnly(message.content);
508
+ assertImagesSupported(message, imagesByRef);
509
+ const images = imagesOf(message, imagesByRef);
480
510
  if (message.role === "system") {
481
511
  wire.push({
482
512
  role: "system",
@@ -490,9 +520,10 @@ function serializeMessages(messages) {
490
520
  }
491
521
  const toolResults = message.content.filter((block) => block.type === "tool-result");
492
522
  const text = flattenText(message.content);
493
- if (text.length > 0 || toolResults.length === 0) wire.push({
523
+ if (text.length > 0 || toolResults.length === 0 || images.length > 0) wire.push({
494
524
  role: "user",
495
- content: text
525
+ content: text,
526
+ ...images.length > 0 ? { images } : {}
496
527
  });
497
528
  for (const result of toolResults) {
498
529
  const name = toolNameOf(String(result.toolCallId), namesByCallId);
@@ -514,7 +545,7 @@ function serializeMessages(messages) {
514
545
  * @param resolved - the resolved plugin config.
515
546
  * @returns the `/api/chat` request body.
516
547
  */
517
- function serializeRequest(options, resolved) {
548
+ function serializeRequest(options, resolved, imagesByRef) {
518
549
  const mapping = resolved.models.find((entry) => entry.name === options.model);
519
550
  const model = mapping?.model ?? options.model;
520
551
  const messages = [];
@@ -522,7 +553,7 @@ function serializeRequest(options, resolved) {
522
553
  role: "system",
523
554
  content: options.system
524
555
  });
525
- messages.push(...serializeMessages(options.messages));
556
+ messages.push(...serializeMessages(options.messages, imagesByRef));
526
557
  const temperature = options.temperature ?? mapping?.temperature ?? resolved.temperature;
527
558
  const ollamaOptions = {};
528
559
  if (temperature !== void 0) ollamaOptions.temperature = temperature;
@@ -855,25 +886,35 @@ function _usingCtx() {
855
886
  * resolution (one `() => ResolvedConfig` thunk re-read per operation), so a
856
887
  * changed base URL, model mapping, or timeout reaches the next request without
857
888
  * re-registration, while an in-flight stream keeps the facts it started with.
858
- * The adapter is text-only (`inputModalities: ['text']`); tool calls and tool
859
- * results are translated by {@link serializeRequest}.
889
+ * Input modalities follow the model's reported `/api/show` capabilities
890
+ * (`vision` → `['text', 'image']`) unless the `vision` config knob opts out;
891
+ * image payloads resolve through the optional `attachments` service.
860
892
  * @module dsh-local-ai/adapter
861
893
  */
862
894
  /** Idle-timeout abort code stamped onto a stalled stream's timeout reason. */
863
895
  const STREAM_IDLE_TIMEOUT_CODE = "LLM_STREAM_IDLE_TIMEOUT";
896
+ /**
897
+ * Deterministic request-image policy: aspect-preserving projection at ≤ 4 MP
898
+ * and a 10 MiB encoded-byte cap per image (a protocol default, not a tunable —
899
+ * the attachment service owns admission limits).
900
+ */
901
+ const REQUEST_IMAGE_POLICY = {
902
+ maxPixels: 4194304,
903
+ maxBytes: 10485760
904
+ };
864
905
  /** The single provider route this adapter owns. */
865
906
  const OLLAMA_PROVIDER = "ollama";
866
907
  /** Reverse a model mapping: Ollama model id → harness-visible name. */
867
908
  function harnessNameOf(resolved, ollamaName) {
868
909
  return resolved.models.find((entry) => entry.model === ollamaName)?.name ?? ollamaName;
869
910
  }
870
- /** Advertise one configured or discovered local model as text-only. */
871
- function modelInfo(provider, id, name) {
911
+ /** Advertise one configured or discovered local model with its input modalities. */
912
+ function modelInfo(provider, id, name, vision) {
872
913
  return {
873
914
  provider,
874
915
  id,
875
916
  name,
876
- inputModalities: ["text"]
917
+ inputModalities: vision ? ["text", "image"] : ["text"]
877
918
  };
878
919
  }
879
920
  /**
@@ -890,27 +931,45 @@ var OllamaAdapter = class extends LlmAdapter {
890
931
  fetchImpl() {
891
932
  return this.options.fetchImpl ?? ((input, init) => globalThis.fetch(input, init));
892
933
  }
934
+ /** Probe one model's `/api/show` capabilities; failures degrade to text-only. */
935
+ async visionOf(resolved, name, signal) {
936
+ if (!resolved.vision) return false;
937
+ try {
938
+ return hasVision((await showModel(resolved.baseURL, name, this.fetchImpl(), signal)).capabilities);
939
+ } catch {
940
+ return false;
941
+ }
942
+ }
893
943
  providerInfo(provider) {
894
944
  return {
895
945
  id: provider,
896
946
  name: "Ollama (local)"
897
947
  };
898
948
  }
899
- listModels(provider) {
949
+ async listModels(provider) {
900
950
  const resolved = this.options.config();
901
- return listModels(resolved.baseURL, this.fetchImpl()).then((models) => models.map((model) => modelInfo(provider, harnessNameOf(resolved, model.name), harnessNameOf(resolved, model.name)))).catch(() => resolved.models.map((entry) => modelInfo(provider, entry.name, entry.name)));
951
+ const models = await listModels(resolved.baseURL, this.fetchImpl()).catch(() => []);
952
+ const list = models.length > 0 ? models : resolved.models.map((entry) => ({
953
+ name: entry.model,
954
+ model: entry.model,
955
+ size: 0,
956
+ digest: ""
957
+ }));
958
+ const probes = await Promise.all(list.map((model) => this.visionOf(resolved, model.name)));
959
+ return list.map((model, index) => modelInfo(provider, harnessNameOf(resolved, model.name), harnessNameOf(resolved, model.name), probes[index] === true));
902
960
  }
903
- resolveModel(provider, model, _signal) {
961
+ async resolveModel(provider, model, signal) {
904
962
  const resolved = this.options.config();
905
963
  const mapping = resolved.models.find((entry) => entry.name === model);
906
- return Promise.resolve({
964
+ const ollamaName = mapping?.model ?? model;
965
+ return {
907
966
  provider,
908
967
  id: model,
909
968
  name: model,
910
- inputModalities: ["text"],
969
+ inputModalities: await this.visionOf(resolved, ollamaName, signal) ? ["text", "image"] : ["text"],
911
970
  context: { contextWindow: mapping?.contextWindow ?? resolved.defaultContextWindow },
912
971
  defaultMaxTokens: mapping?.maxTokens ?? resolved.maxTokens
913
- });
972
+ };
914
973
  }
915
974
  async *stream(options) {
916
975
  try {
@@ -948,7 +1007,7 @@ var OllamaAdapter = class extends LlmAdapter {
948
1007
  }
949
1008
  }
950
1009
  async *request(options, resolved, signal) {
951
- const body = serializeRequest(options, resolved);
1010
+ const body = serializeRequest(options, resolved, await this.prepareImages(options, resolved, signal));
952
1011
  let response;
953
1012
  try {
954
1013
  response = await postStream(resolved.baseURL, "/api/chat", body, this.fetchImpl(), signal);
@@ -960,6 +1019,26 @@ var OllamaAdapter = class extends LlmAdapter {
960
1019
  if (!response.body) throw new LlmError("Ollama API returned no response body", "EMPTY_RESPONSE");
961
1020
  yield* translate(readNdjsonLines(response.body));
962
1021
  }
1022
+ /**
1023
+ * Resolve request-image payloads for one image-bearing request. A text-only
1024
+ * route (config opt-out), a missing attachment service, or an unresolvable
1025
+ * ref fails loud here — images are never silently dropped.
1026
+ * @returns a `attachmentId → base64` map, or `undefined` when the request has no images.
1027
+ */
1028
+ async prepareImages(options, resolved, signal) {
1029
+ if (!options.messages.some((message) => contentHasImage(message.content))) return void 0;
1030
+ if (!resolved.vision) throw new LlmError("The Ollama adapter does not support image content for this model (vision disabled).", "UNSUPPORTED_CONTENT");
1031
+ const attachments = this.options.resolveAttachments?.();
1032
+ if (attachments === void 0) throw new LlmError("Image content requires the attachments service; mount a profile with @deepseek-ai/dsh-attachment.", "UNSUPPORTED_CONTENT");
1033
+ const map = /* @__PURE__ */ new Map();
1034
+ for (const message of options.messages) for (const block of message.content) {
1035
+ if (block.type !== "image") continue;
1036
+ const ref = block.attachment;
1037
+ const requestImage = await attachments.readImageRequest(ref, REQUEST_IMAGE_POLICY, signal);
1038
+ map.set(String(ref.attachmentId), Buffer.from(requestImage.data).toString("base64"));
1039
+ }
1040
+ return map;
1041
+ }
963
1042
  };
964
1043
  //#endregion
965
1044
  //#region src/route.ts
@@ -1156,7 +1235,7 @@ async function checkHealth(baseURL, fetchImpl, subprocess, requestTimeoutMs, gra
1156
1235
  * The `dsh-local-ai` plugin version. The release script bumps this string
1157
1236
  * alongside `package.json`; `test/version.spec.ts` trips when the two drift.
1158
1237
  */
1159
- const VERSION = "0.1.3";
1238
+ const VERSION = "0.1.5";
1160
1239
  //#endregion
1161
1240
  //#region src/index.ts
1162
1241
  const name = "local-ai";
@@ -1232,7 +1311,8 @@ function apply(ctx, config = {}) {
1232
1311
  const fetchImpl = (input, init) => globalThis.fetch(input, init);
1233
1312
  const adapter = new OllamaAdapter({
1234
1313
  config: () => resolved,
1235
- fetchImpl
1314
+ fetchImpl,
1315
+ resolveAttachments: () => ctx.get("attachments")
1236
1316
  });
1237
1317
  ctx.llm.registerAdapter([OLLAMA_PROVIDER], adapter);
1238
1318
  ctx.on("llm/stream", (options, next) => {
@@ -4,12 +4,14 @@
4
4
  * resolution (one `() => ResolvedConfig` thunk re-read per operation), so a
5
5
  * changed base URL, model mapping, or timeout reaches the next request without
6
6
  * re-registration, while an in-flight stream keeps the facts it started with.
7
- * The adapter is text-only (`inputModalities: ['text']`); tool calls and tool
8
- * results are translated by {@link serializeRequest}.
7
+ * Input modalities follow the model's reported `/api/show` capabilities
8
+ * (`vision` → `['text', 'image']`) unless the `vision` config knob opts out;
9
+ * image payloads resolve through the optional `attachments` service.
9
10
  * @module dsh-local-ai/adapter
10
11
  */
11
12
  import { LlmAdapter } from '@deepseek-ai/dsh-llm';
12
13
  import type { GenerateOptions, LlmModelInfo, LlmProviderInfo, LlmResolvedModelInfo, StreamChunk } from '@deepseek-ai/dsh-llm';
14
+ import type { AttachmentStore } from '@deepseek-ai/dsh-attachment';
13
15
  import type { FetchLike } from './ollama.js';
14
16
  import type { ResolvedConfig } from './config.js';
15
17
  /** Constructor options for {@link OllamaAdapter}. */
@@ -18,6 +20,8 @@ export interface OllamaAdapterOptions {
18
20
  config: () => ResolvedConfig;
19
21
  /** Fetch implementation, injectable for tests; defaults to `globalThis.fetch`. */
20
22
  fetchImpl?: FetchLike;
23
+ /** Optional attachment service resolving durable image refs to request bytes. */
24
+ resolveAttachments?: () => AttachmentStore | undefined;
21
25
  }
22
26
  /** The single provider route this adapter owns. */
23
27
  export declare const OLLAMA_PROVIDER = "ollama";
@@ -30,10 +34,19 @@ export declare class OllamaAdapter extends LlmAdapter {
30
34
  private readonly options;
31
35
  constructor(options: OllamaAdapterOptions);
32
36
  private fetchImpl;
37
+ /** Probe one model's `/api/show` capabilities; failures degrade to text-only. */
38
+ private visionOf;
33
39
  providerInfo(provider: string): LlmProviderInfo;
34
40
  listModels(provider: string): Promise<readonly LlmModelInfo[]>;
35
- resolveModel(provider: string, model: string, _signal?: AbortSignal): Promise<LlmResolvedModelInfo>;
41
+ resolveModel(provider: string, model: string, signal?: AbortSignal): Promise<LlmResolvedModelInfo>;
36
42
  stream(options: GenerateOptions): AsyncIterable<StreamChunk>;
37
43
  private request;
44
+ /**
45
+ * Resolve request-image payloads for one image-bearing request. A text-only
46
+ * route (config opt-out), a missing attachment service, or an unresolvable
47
+ * ref fails loud here — images are never silently dropped.
48
+ * @returns a `attachmentId → base64` map, or `undefined` when the request has no images.
49
+ */
50
+ private prepareImages;
38
51
  }
39
52
  //# sourceMappingURL=adapter.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"adapter.d.ts","sourceRoot":"","sources":["../../src/adapter.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,EAAE,UAAU,EAAY,MAAM,sBAAsB,CAAA;AAC3D,OAAO,KAAK,EAAE,eAAe,EAAE,YAAY,EAAE,eAAe,EAAE,oBAAoB,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAA;AAI7H,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,aAAa,CAAA;AAG5C,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,aAAa,CAAA;AAKjD,qDAAqD;AACrD,MAAM,WAAW,oBAAoB;IACnC,2DAA2D;IAC3D,MAAM,EAAE,MAAM,cAAc,CAAA;IAC5B,kFAAkF;IAClF,SAAS,CAAC,EAAE,SAAS,CAAA;CACtB;AAED,mDAAmD;AACnD,eAAO,MAAM,eAAe,WAAW,CAAA;AAavC;;;;GAIG;AACH,qBAAa,aAAc,SAAQ,UAAU;IAC/B,OAAO,CAAC,QAAQ,CAAC,OAAO;gBAAP,OAAO,EAAE,oBAAoB;IAI1D,OAAO,CAAC,SAAS;IAIR,YAAY,CAAC,QAAQ,EAAE,MAAM,GAAG,eAAe;IAI/C,UAAU,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,SAAS,YAAY,EAAE,CAAC;IAO9D,YAAY,CACnB,QAAQ,EAAE,MAAM,EAChB,KAAK,EAAE,MAAM,EACb,OAAO,CAAC,EAAE,WAAW,GACpB,OAAO,CAAC,oBAAoB,CAAC;IAaxB,MAAM,CAAC,OAAO,EAAE,eAAe,GAAG,aAAa,CAAC,WAAW,CAAC;YA8CpD,OAAO;CA0BxB"}
1
+ {"version":3,"file":"adapter.d.ts","sourceRoot":"","sources":["../../src/adapter.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,EAAmB,UAAU,EAAY,MAAM,sBAAsB,CAAA;AAC5E,OAAO,KAAK,EAAE,eAAe,EAAE,YAAY,EAAE,eAAe,EAAE,oBAAoB,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAA;AAC7H,OAAO,KAAK,EAAE,eAAe,EAAsB,MAAM,6BAA6B,CAAA;AAItF,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,aAAa,CAAA;AAG5C,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,aAAa,CAAA;AAYjD,qDAAqD;AACrD,MAAM,WAAW,oBAAoB;IACnC,2DAA2D;IAC3D,MAAM,EAAE,MAAM,cAAc,CAAA;IAC5B,kFAAkF;IAClF,SAAS,CAAC,EAAE,SAAS,CAAA;IACrB,iFAAiF;IACjF,kBAAkB,CAAC,EAAE,MAAM,eAAe,GAAG,SAAS,CAAA;CACvD;AAED,mDAAmD;AACnD,eAAO,MAAM,eAAe,WAAW,CAAA;AAavC;;;;GAIG;AACH,qBAAa,aAAc,SAAQ,UAAU;IAC/B,OAAO,CAAC,QAAQ,CAAC,OAAO;gBAAP,OAAO,EAAE,oBAAoB;IAI1D,OAAO,CAAC,SAAS;IAIjB,iFAAiF;YACnE,QAAQ;IAUb,YAAY,CAAC,QAAQ,EAAE,MAAM,GAAG,eAAe;IAIzC,UAAU,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,SAAS,YAAY,EAAE,CAAC;IAa9D,YAAY,CACzB,QAAQ,EAAE,MAAM,EAChB,KAAK,EAAE,MAAM,EACb,MAAM,CAAC,EAAE,WAAW,GACnB,OAAO,CAAC,oBAAoB,CAAC;IAexB,MAAM,CAAC,OAAO,EAAE,eAAe,GAAG,aAAa,CAAC,WAAW,CAAC;YA8CpD,OAAO;IA4BvB;;;;;OAKG;YACW,aAAa;CAwB5B"}