li-sky/dsh-aiservice-deeplink ↗★ 0
dsh-aiservice-deeplink
AI Service Deeplink (aiservice://) import and export for DSH model configuration
安装
此插件尚未提供可验证的 bundle,或兼容性检查未通过。请先阅读仓库说明。 阅读完整 README ↗
说明文档
阅读完整 README ↗dsh-aiservice-deeplink
AI Service Deeplink (aiservice://) import and export for DSH model
configuration. Implements the transport and configuration layers of
the protocol draft v1
against the composition's llm-pi-ai provider routes.
A single aiservice://model, aiservice://provider, or aiservice://bundle
link carries an endpoint, a wire protocol, a credential, model capabilities,
and inference and transport policy. This package turns such a link into DSH
provider routes, and turns the configured routes back into a link.
Surfaces
| Surface | What it does |
|---|---|
| Settings → Models panel | Paste a link, preview what it will configure, import it, or build a share link from the current routes |
aiservice_deeplink Tool | The same three operations (preview, import, export) for an agent |
POST /aiservice/preview | Transport + schema validation and a target summary, with no side effects |
POST /aiservice/import | Validates, discovers models, then writes the routes and credentials |
POST /aiservice/export | Builds a link from any configured route, including the built-in DeepSeek one |
GET /aiservice/routes | Every shareable route, from both namespaces |
GET /aiservice/status | Whether the namespaces exist and are writable |
Layout
| File | Role |
|---|---|
lib/protocol.js | Transport layer (§2), integrity (§9), and schema validation (§3–§6, §8); strict JSON reader, Base64URL codec, merge precedence |
lib/mapping.js | llm.model / llm.provider / llm.bundle ⇄ llm-pi-ai profiles and the native llm-deepseek route |
lib/index.js | Host plugin: Tool, HTTP routes, settings and credential writes |
lib/client.js | Browser plugin: the Settings → Models panel (__ModuleLoader__ bundle, hand-written, no build step) |
test/ | 78 node --test cases, including the protocol repository's own published examples |
Where it is mounted
~/.dsh/profiles/web/cordis.patch.yml inserts one row pointing at
lib/index.js by absolute path. The package is deliberately
dependency-free: the row is not part of the profile's pnpm project, so it
cannot import @deepseek-ai/*, and every host service is reached through the
declared inject list or through ctx.get at request time.
Mapping
Two adapters own routes in this composition, and import and export treat them differently.
| Deeplink | DSH llm-pi-ai |
|---|---|
endpoint.base_url | providers..baseURL |
endpoint.api_format | providers..api (openai_chat_completions → openai-completions, …) |
endpoint.provider / provider.id | the route key (slugified, deduplicated against unrelated routes) |
endpoint.credential.api_key | a credential record named _API_KEY; the profile keeps apiKeyEnv |
transport.mode / timeout_seconds / cache_retention / retry | transport / timeoutMs / cacheRetention / retryPolicy |
transport.extra_headers | headers (a name the credential also provides is an error, per §3.2) |
capabilities.* | models[].contextWindow / maxTokens / input |
capabilities.api_features + compatibility | compat, where public fields win over the compatibility override (§5) |
inference.reasoning | models[].reasoningEfforts (false for mode: disabled), plus a route-wide reasoning only when every model can take it |
autodiscover | one llm.discoverModels call after validation, before persistence (§4.2.1) |
required_features | provider.autodiscover, transport.websocket, transport.websocket-cached are served; anything else is refused |
Export reads both namespaces, so the built-in DeepSeek route shares like any
other. llm-pi-ai is a registry keyed by route; llm-deepseek owns exactly one
fixed route (deepseek-official) with a flat section, and is projected onto the
same shape — endpoint, credential reference, thinking policy, and the resolved
catalog, which the adapter supplies as schema defaults. The exported
endpoint.provider is deepseek, a catalog identifier a receiver can infer
defaults from, rather than the harness's own route name.
Two limits are worth knowing:
- The adapter falls back to
DEEPSEEK_BASE_URLfrom the trusted launch environment when nobaseURLis configured, and that value is not readable from a plugin. An explicitly configured endpoint travels; otherwise the link carries DeepSeek's public endpoint. - The adapter's own
reasoningEffortdefault is applied per request and never appears in the resolved settings, so an unconfigured effort is omitted rather than guessed. An explicit one travels.
Reasoning is the one field that can break a whole route
providers..reasoning is a hard default applied to every model on
the route, and a request naming a level a model does not support is refused
with UNSUPPORTED_REASONING_EFFORT. A hand-declared model declares no
reasoning at all, so it supports only off. Hoisting one model's effort onto
the route therefore makes every other model unusable — which is what an
unconditional mapping from inference.reasoning.effort does.
So the effort is handled per model:
mode: disabled→reasoningEfforts: false, the harness's own way of declaring a non-reasoning model.- An explicit effort the payload also equips for the wire (it names
compatibility.thinking_formatorsupports_reasoning_effort) →reasoningEfforts: { off: null, : }on that model, so the level exists and the session picker offers it. - An explicit effort with nothing saying how the level reaches the wire → the model is left alone and the loss is reported. Declaring the level anyway would advertise a control that sends nothing, which is the same fabrication as inventing a wire spelling.
- A route-wide
reasoningdefault is set only when every model on the route can take the same level. Note this is about what each model supports, not about the models agreeing: provider defaults make every model resolve to the same effort, and that is exactly the case that would otherwise force a level no model was equipped to send.
Import always writes llm-pi-ai. The native DeepSeek route is a
single-vendor adapter, not a place to point at an arbitrary endpoint, and route
names are unique across the whole llm registry — so an imported link never
claims a name another adapter owns, even when its endpoint matches.
Everything the mapping cannot carry (transport.user_agent,
inference.temperature, inference.reasoning.history and budget_tokens,
retry.max_retry_after_seconds, a completions-only switch on an Anthropic
route) is reported in the preview's notes rather than dropped silently. On
export the same applies to a thinking level above every level the protocol
names: the harness's max is shared as xhigh and the substitution is
reported beside the link.
Guarantees
- Parsing never has side effects. No network, no writes; discovery and
credential storage happen only inside
import. - Failure is atomic. The whole change goes through one
settings.update('llm-pi-ai', …)call, which validates before persisting, so a rejected import leaves the previous configuration untouched (§7). - Discovery failure does not erase configuration. A provider that declares models keeps them when its endpoint is unreachable (§4.2.1).
- Credentials stay out of logs and errors. Responses and diagnostics carry error codes and paths, never the URI or a secret (§6, §7).
- Sharing includes credentials by default, and the panel offers the
credential-free mode, which writes
credential.type = "prompt"instead (§6.1). - An import never replaces a credential that is already there. The preview
reports whether the reference is new, already stored, or supplied by the
launching environment, and replacing a stored value takes an explicit
overwriteCredentials. - An import writes only the routes it names. Nothing else is restated, so a route the user edited concurrently, and the stored formatting of every other entry, both survive.
Credentials
The harness owns two disjoint key spaces, and an imported API key belongs to the first:
| Space | Address | Stored as | Used by |
|---|---|---|---|
| reference | ` | ||
| _API_KEY`, a POSIX shell identifier | .credentials.yaml refs: | a route's apiKeyEnv, resolved once per request | |
| record | /, lowercase hyphenated | .credentials.yaml records: | a plugin's own sign-in flow (e.g. llm-pi-ai/) |
A pi-ai route names a reference, so that is what this plugin writes: the
same _API_KEY derivation the Models page uses, through
the same credentials.set. References are layered against the environment, and
the inherited process environment outranks the managed file — so a write the
launching shell would shadow is refused by the provider, and this plugin checks
credentials.describe first and reports it instead of failing mid-import.
Because a reference is a shared name rather than a private slot for one link,
the derived name is never written over a value that is already there. A link
naming anthropic would otherwise replace whatever key the user had stored for
that provider.
Tests
node --test 'test/*.test.mjs'
test/protocol.test.mjs reproduces the protocol repository's published
model.uri, provider.uri, and bundle.uri byte for byte and checks the
rejections the spec requires. Point AISERVICE_EXAMPLES at another checkout of
that repository to run against a different revision.
Changing it
The profile mounts the host half by absolute path, and Node caches an ES module
by URL: edits to lib/*.js reach the running process only after dsh web
restarts. The client bundle additionally needs a page refresh.