feat(assistants): make deepseek-harness selectable (#639) - #653
feat(assistants): make deepseek-harness selectable (#639)#653umi-appcoder[bot] wants to merge 1 commit into
Conversation
|
Note on the empty checks list. CI is configured as In the meantime everything CI runs was run locally on this exact branch: |
0e86e20 to
f643a3a
Compare
Fifth slice of #639. This is the one that turns the entry on: the dashboard picker, the Hypervisor, the Builds tab and the model/effort switchers all become aware of it. Gating is on BOTH `shutil.which('dsh')` AND DEEPSEEK_API_KEY. Binary presence alone is the right signal only for the OAuth CLIs (agy, codex); `dsh` authenticates with an API key, so listing it without one would offer an entry whose every turn fails with "Authentication Fails". An image predating the install simply doesn't list it, and the existing `opencode-deepseek` entry is untouched — one key now enables two different agents over the same API, which is the whole point. Builds run the ACP bridge's serve mode rather than a REPL, because `dsh` ships none we can use. `auto_approve` is deliberately a NO-OP here and that is a real behaviour difference: ACP permission requests are JSON-RPC calls that must be answered inside the turn, and a tmux pane cannot put that question to a user, so the bridge always approves on both surfaces. Called out in the code and given its own section in docs/llm-setup.md rather than left to be discovered. Effort needed a third delivery shape. The existing table knows an env var and a `-c key=value` pair; the bridge takes a plain `--effort value` flag, and the harness's stops (off/low/high/max) don't line up with the canonical five. So _EFFORT_DELIVERY grows `flag` plus an optional `vocab` translation, and _DSH_EFFORT_VOCAB mirrors the adapter's table under the same lockstep-test discipline _EFFORT_CAP already has. Nothing clamps: the harness really does expose `max`. Models are the harness's own ids, read off a live `session/new`'s advertised option; the bridge resolves a bare id against that option so no layer above it handles the encoded ["provider","model"] pair form. The default is the harness's own, so listing the assistant changes no behaviour. The experimental vision model it also offers is left out of the default list — KC_DSH_MODELS curates. Chart: assistant.deepseekHarness.{model,models,effort} → KC_DSH_*. No key of its own; it reuses assistant.deepseek.apiKey. Docs: a DeepSeek Harness section in llm-setup.md covering the two-signal gate, an explicit table of how it differs from opencode-deepseek, the Node 22 and ~/.dsh requirements, why it goes through ACP rather than --profile headless, and the approval-prompt difference. The "three assistants" heading was already counting wrong before this change and is now just "the assistants". 30 new tests: every gating combination (binary+key, binary only, key only, neither) including that an older image leaves the other entries byte-identical; both DeepSeek paths coexisting; the model list and its env override; resolve_model defending the boundary against a free-form webhook caller; the cap/vocab/delivery lockstep with hypervisor_session; the effort translation; the argv the Build tab launches, including that a hostile KC_DSH_MODEL survives shlex round-tripping as exactly one argument; and that the new flag delivery shape leaves claude and codex alone. 5 chart tests for the env wiring. Refs #639
f643a3a to
6f8c777
Compare
|
One thing worth knowing about the gate, found while checking this over. Not a defect in this PR, but it affects how discoverable the new entry is.
This is pre-existing and shared by every key-gated entry — Flagging it because the failure mode is quiet and specific to a key-gated assistant: "I set my DeepSeek key and the harness never showed up." |
Fifth of six PRs for #639. Stacked on #652 — base is
kc/639-dsh-serve.This is the one that turns the entry on. After this,
deepseek-harnessappears in the picker, the Hypervisor routes to the ACP adapter, the Builds tab launches the bridge, and both switchers (model, effort) are populated.Gating — the acceptance criterion that matters most
Listed only when both
shutil.which('dsh')andDEEPSEEK_API_KEYare present.Binary presence alone is the right signal only for the OAuth CLIs (
agy,codex).dshauthenticates with an API key, so listing it without one offers an entry whose every turn fails withAuthentication Fails— the exact error captured from the real harness. An image predating the install simply doesn't list it, and the existingopencode-deepseekentry is untouched: one key now enables two genuinely different agents over the same API, which is the point of the issue.Tested across all four combinations, plus an assertion that an older image leaves the other entries byte-identical.
Builds, and one honest behaviour difference
Builds run the bridge's serve mode (#652) rather than a REPL, because
dshships none we can use.auto_approveis deliberately a no-op, and that differs from every other assistant. Forclaude/antethe Build tab leaves the CLI's approval prompts on and only the Hypervisor passes a skip flag. ACP permission requests are JSON-RPC calls that must be answered inside the turn, and a tmux pane has no way to put that question to a user and get an answer back — an unanswered request stalls the turn forever. The bridge therefore always approves, on both surfaces. The pod is the sandbox.I've put this in the code comment and given it its own section in
docs/llm-setup.mdrather than leaving it to be discovered.Effort needed a third delivery shape
_EFFORT_DELIVERYknew two shapes: an env var (claude, kc-harness) and a-c key=valuepair (codex). The bridge takes a plain--effort valueflag, and the harness's stops (off/low/high/max) don't line up with the canonical five. So the table growsflagplus an optionalvocabtranslation:_DSH_EFFORT_VOCABmirrorsDeepseekHarnessAdapter._EFFORT_NATIVEunder the same lockstep-test discipline_EFFORT_CAPalready has — I kept them as mirrored tables rather than importing across modules, because thehypervisor_sessionimport inserver.pyis deliberately guarded and aNoneon a broken install would blow up at class-definition time.Nothing clamps: the harness really does expose
max. Canonicalmediumrounds up to the harness'shigh(its default and documented balance point) rather than down tolow.Models
The harness's own ids, read off a live
session/new's advertisedmodeloption. The bridge resolves a bare id against that option, so no layer above it handles the encoded["provider","model"]pair form. The default is the harness's own (deepseek-v4-flash), so listing the assistant changes no behaviour. The experimental vision model it also offers is left out of the default list —KC_DSH_MODELScurates.Chart + docs
assistant.deepseekHarness.{model,models,effort}→KC_DSH_*. No key of its own — it reusesassistant.deepseek.apiKey.docs/llm-setup.mdgets a DeepSeek Harness section: the two-signal gate, an explicit table of how it differs fromopencode-deepseek, the Node 22 and~/.dshrequirements, why it goes through ACP rather than--profile headless, and the approval-prompt difference. (The "three assistants" heading was already counting wrong before this change — four rows under it — and adding a row would have made it worse, so it is now just "the assistants". I left the doc's other staleness alone.)Verification
resolve_modeldefending the boundary against a free-form webhook caller; the cap/vocab/delivery lockstep withhypervisor_session; the full effort translation; the argv the Build tab launches, including that a hostileKC_DSH_MODELsurvivesshlexround-tripping as exactly one argument; and that the new flag shape leavesclaudeandcodexdelivery alone.KC_DEEPSEEK_MODEL.helm lintclean;helm unittest194/194.boards_*import errors that reproduce on unmodifiedmainin this environment.Still not verified: anything needing a real
DEEPSEEK_API_KEY. The picker/Builds/Hypervisor wiring is unit-tested, but a completed turn against live DeepSeek has not been observed from this workspace.Last one
_HEADLESS_CAPABLE, assistant list, MCP tool enum),mcp_registry_PROVIDERS, and the skills provider.Refs #639
🤖 Generated with Claude Code