{"_id":"@creait/dsh-think-level","name":"@creait/dsh-think-level","dist-tags":{"latest":"0.1.0"},"versions":{"0.1.0":{"name":"@creait/dsh-think-level","description":"Thinking level for DeepSeek Harness: a per provider/model default reasoning effort that also reaches subagents, plus one composer pill to change it for the session.","version":"0.1.0","type":"module","main":"lib/index.js","exports":{".":{"default":"./lib/index.js"},"./client":"./client/client.cjs","./package.json":"./package.json"},"dsh":{"bundle":{"patch":"./cordis.patch.yml"},"client":{"inject":["@deepseek-ai/dsh-client-runtime","@deepseek-ai/dsh-client-locale","@deepseek-ai/dsh-client-ui-conversation","@deepseek-ai/dsh-client-ui-model-selection"],"platform":"web"}},"license":"MIT","engines":{"node":">=20"},"scripts":{"test":"node --test test/*.test.js"},"dependencies":{"@deepseek-ai/dsh-settings":"^0.1.0-rc.6","@deepseek-ai/schemastery":"^3.18.1"},"peerDependencies":{"@deepseek-ai/cordis":"^4.0.1","@deepseek-ai/dsh-llm":"^0.1.0-rc.6"},"keywords":["dsh","deepseek-harness","cordis","plugin","reasoning","reasoning-effort","thinking","model-defaults"],"repository":{"type":"git","url":"git+https://github.com/CREAIT-nl/dsh-plugins.git","directory":"think-level"},"homepage":"https://github.com/CREAIT-nl/dsh-plugins/tree/main/think-level#readme","bugs":{"url":"https://github.com/CREAIT-nl/dsh-plugins/issues"},"author":{"name":"Francesco G","email":"francesco@creait.nl"},"gitHead":"488ad11d2a6582854dc98b84feacf32e0a2d2337","_id":"@creait/dsh-think-level@0.1.0","_nodeVersion":"25.8.1","_npmVersion":"11.11.0","dist":{"integrity":"sha512-9aWA/Gz6R2uviHMwJC506C5CC6+ELk2GeFA/YGtJ+Pu09/3PVRd2gFnyL4eViPV8IcF59X91T/pRF/HZ3EFieQ==","shasum":"0f7cd90ddf12e0f5677a9e75eeeb43db14e2d59e","tarball":"https://registry.npmjs.org/@creait/dsh-think-level/-/dsh-think-level-0.1.0.tgz","fileCount":10,"unpackedSize":107290,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEQCIEhcgN/eIY/mEOydU7Hhr6TvmFw5vtucMMEI4EcW7l75AiA5m42cIF1epYM+lpjcDVleZKltjybn8/ev/yxmbvHS+g=="}]},"_npmUser":{"name":"creait","email":"francesco@creait.nl"},"directories":{},"maintainers":[{"name":"creait","email":"francesco@creait.nl"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/dsh-think-level_0.1.0_1787577866744_0.949430668247589"},"_hasShrinkwrap":false}},"time":{"created":"2026-08-24T13:24:26.558Z","0.1.0":"2026-08-24T13:24:26.894Z","modified":"2026-08-24T13:24:27.080Z"},"maintainers":[{"name":"creait","email":"francesco@creait.nl"}],"description":"Thinking level for DeepSeek Harness: a per provider/model default reasoning effort that also reaches subagents, plus one composer pill to change it for the session.","homepage":"https://github.com/CREAIT-nl/dsh-plugins/tree/main/think-level#readme","keywords":["dsh","deepseek-harness","cordis","plugin","reasoning","reasoning-effort","thinking","model-defaults"],"repository":{"type":"git","url":"git+https://github.com/CREAIT-nl/dsh-plugins.git","directory":"think-level"},"author":{"name":"Francesco G","email":"francesco@creait.nl"},"bugs":{"url":"https://github.com/CREAIT-nl/dsh-plugins/issues"},"license":"MIT","readme":"# @creait/dsh-think-level\n\nA thinking level per provider **and** model for DeepSeek Harness, plus one\ncontrol to change it for the session you are in.\n\n## Why this exists\n\ndsh already lets you set reasoning effort — there is a dropdown next to the\nmodel name. What it does not have is an *answer*: the dropdown is per session,\nremembered nowhere, so \"this model should think hard\" is re-typed in every new\nconversation and forgotten in every old one. Subagents never get asked at all —\nthey carry no model selection, so they run at whatever the provider defaults to,\nwhich is usually not what you would have picked for the work they do.\n\nEffort is a property of the model and the deployment, not of the conversation.\nSo this plugin stores it as one:\n\n```\nsession selection  >  this plugin's table  >  the adapter's own default\n```\n\nA model with no row is untouched — the provider default stands exactly as it did\nbefore the plugin was installed, and removing a row is how you go back to it.\n\n## What you get\n\n**A `Thinking Level` page in Settings** — the table. Pick a provider, a model\nand one of the levels that model actually publishes, and it applies from the\nnext message, in every session, including subagents.\n\n**A pill in the composer** — the level for *this* session, sitting immediately\nleft of the model select at the trailing end of the tool row. A thinking level\nis a property of the model, so it belongs next to the model, not among the\naccess-mode and plan controls at the leading end where it reads as one of the\nmode's own switches. It is the harness's own menu — same trigger, same popup,\nopening upward like the others in that row — and it shows `Auto` when nothing is\npinned, labelled with the level that will actually be used, so \"auto\" is never a\nmystery.\n\nIf the model publishes no levels the pill stays put rather than vanishing — a\ncontrol that comes and goes with the model reads as a broken plugin rather than\nas a model with nothing to offer. For a route you declared by hand it also\noffers to fix that in one click; see\n[A model with no levels](#a-model-with-no-levels).\n\nThe pill writes through the session's own model-selection channel — the same one\nthe model picker uses — so the `/model` popup shows the same value instead of\ndisagreeing with it. There is no second, plugin-owned notion of \"the session's\nlevel\" to drift out of sync.\n\n## Install\n\n```bash\ndsh plugin --profile web add @creait/dsh-think-level\n```\n\nRestart `dsh web` afterwards: the boot manifest is built at startup.\n\n## How it works\n\nOne listener, on `agent/request` — the waterfall the agent loop dispatches once\nper step to replace the frozen call configuration, and the last seam before\n`llm.prepareCall` materialises adapter defaults. At that point\n`reasoningEffort === undefined` genuinely means nobody chose, rather than\n\"nobody has chosen yet\".\n\nThe listener is **prepended**, so it runs outermost and its rewrite lands after\neverything registered later — including the harness's own step-by-step\nre-derivation of the session's effort. Running last is what lets it see the\nfinal answer; filling only a hole is what keeps your own choice winning.\n\nIt is registered **untagged**, which the scope carrier admits for every agent.\nThat is how subagents are covered with no second code path: they reach the same\nwaterfall with nothing in the effort field and take the table's answer.\n\nBefore applying a row it resolves the model's published levels and drops the row\nif the level is not among them. An unsupported id makes `prepareCall` throw\n`UNSUPPORTED_REASONING_EFFORT` and kills the turn, so a stale row — the model was\nreconfigured, the adapter changed its levels — has to cost the request nothing.\nModel metadata is resolved once per model and re-resolved when the table is\nedited.\n\n### Who else writes the effort field\n\nFilling only an empty field is what keeps your own choice winning — but three\nother things were writing that field, and each one would have shadowed the table\nforever by never leaving it empty.\n\n**The model pickers.** Both shipped pickers write the model's **adapter default**\neffort into the session selection whenever you change model. The pill therefore\nre-applies the configured default on a **model change**, and only on a model\nchange: editing the level itself never triggers it, and the first render of a\nsession never overwrites a level that is already set, so a level you picked by\nhand survives both a model switch and a page reload.\n\n**Any third-party picker.** A community model picker may ship an effort dropdown\nof its own and pin `defaultEffort` the same way on every model change. Two\ncontrols over one session field is confusing enough by itself, but the pinning is\nthe real problem: a field that is always set leaves the table no hole to fill. If\nyours has one, cut it — exactly one effort control should exist.\n\n**The harness itself**, which is the one you cannot see. `selectModel` resolves\nyour switch through `llm.resolveCallConfig`, which materialises the adapter's own\ndefault effort, and then saves that whole resolved selection into\n`agent-default-model` — the settings section every *new* session inherits from.\nSo picking a model, from any picker, silently pins that model's adapter default\nglobally, and sessions opened afterwards arrive with the field already set.\n\nNobody types that value and nobody sees it, so this plugin unsets it: it watches\nthe `agent-default-model` namespace and removes `reasoningEffort` whenever it\nreappears, leaving the provider and model alone. What you give up is the global\n\"remember my last effort across sessions\" behaviour — which is exactly the tier\nthis plugin replaces with a per-model table, and one global value shadowing a\nper-model table would make the table decorative. Uninstall the plugin and the\nharness goes back to pinning.\n\n### What sticks, and for how long\n\nA session that has already sent a message derives its selection from its own\nlogged request header, so the level it used is the level it keeps — a later edit\nto the table moves *new* sessions, not that one. Change it there and then with\nthe pill.\n\n## Configuration\n\nStored under the `dsh-think-level` settings namespace:\n\n```yaml\ndsh-think-level:\n  defaults:\n    - provider: local-gpu\n      model: deepseek-v4-flash\n      effort: high\n    - provider: openrouter\n      model: gpt-5.2\n      effort: low\n```\n\n`effort` is an adapter-owned id — `off` / `low` / `high` / `max` on the DeepSeek\nadapter, whatever the adapter publishes elsewhere. It is not a number and not a\nscale this plugin invents; the ids come from the model's own `reasoning.efforts`,\nwhich is what both the Settings page and the pill list.\n\n### A model with no levels\n\nA model offers levels only if its adapter says so, and for a route you declared\nby hand under `llm-pi-ai` the adapter says nothing: no `reasoningEfforts` on the\nentry means `reasoning: false`, so the pill has nothing to pick and this table\nhas nothing to store. The old answer was \"go and edit `settings.yaml`\", which is\nnot an answer.\n\n**So the plugin declares them for you.** The pill's menu offers *Enable thinking\nlevels*, and the Settings page carries the same button under the table. Either\none writes the standard five for that model:\n\n```yaml\nllm-pi-ai:\n  providers:\n    local-gpu:\n      models:\n        - id: deepseek-v4-flash\n          reasoningEfforts:\n            off: none\n            low: low\n            medium: medium\n            high: high\n            max: max\n```\n\nThe key is the level id — `off`, `minimal`, `low`, `medium`, `high`, `xhigh`,\n`max` — and the value is the spelling to send on the wire. `off` maps to `none`\nrather than to `off` because that is what \"do not think\" is called on the wire.\nWrite `false` instead of a dict for a model that cannot think at all, and omit\nthe field to keep whatever the installed catalog already says.\n\n`reasoningEfforts` is not adapter-internal state: it is a field of the\n`llm-pi-ai` *settings namespace*, and pi-ai re-reads that namespace on every\nwrite. So the levels are live on the next request — no restart, no reload, and\nthe pill relabels itself where it stands. The Settings row turns into *Remove\nlevels* afterwards, which puts the model back exactly as it was, emptied\ncontainers and all.\n\nTwo shapes exist and which one you get is pi-ai's call, not this plugin's: a\nroute with a `models:` list spells every model out and carries the field on the\nentry, while a route the installed catalog describes carries it in\n`modelOverrides[<model>]`. The declaration goes wherever the route keeps it. A\nroute pi-ai does not serve at all — the built-in DeepSeek adapter, anything else\nregistered — owns its own capability metadata, and there the button is not\noffered.\n\nNothing here can break a route. pi-ai validates the write with its own\n`assertServiceable`, so a map it cannot serve is refused *at the write*, naming\nthe route and the model, rather than stored and quietly disabling the provider.\nAnd a level the deployment does not understand is a no-op, not a failure —\nremove it again whenever.\n\nThe Settings page and the pill read and write this over four loopback-only\nroutes (`/api/dsh-think-level/config`, `/catalog`, `/efforts`, `/levels`) rather\nthan the settings RPC, because that wire only exposes namespaces on a hard-coded\nallowlist a plugin cannot widen.\n\n## Development\n\n```bash\npnpm install\nnpm test\n```\n\nTests cover the listener's restraint (fills only an empty field, drops an\nunpublished level, never throws), the loopback guard on the routes, the two\ndeclaration shapes and what a withdrawal cleans up, and the client bundle\nrendered against a miniature React — including the model-change re-apply, which\nis the part that has to be exactly right in both directions.\n\n## License\n\nMIT\n","readmeFilename":"README.md","_rev":"1-6e3c138f6e57a6a07c8ee1a27564485a"}