dsh-any-deepseek 0.0.0-stage → 0.0.2

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Benard. Y
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,127 @@
1
- # Temporary Holding Version
1
+ # dsh-any-deepseek
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ One **DeepSeek V4.1 Flash** provider for [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness).
4
+
5
+ Instead of configuring every third-party DeepSeek relay as its own provider, install one
6
+ provider with **exactly one model**, paste a key per channel in the settings panel, and pick
7
+ which channel is live. Token usage is booked **per real upstream**, so you can see what each
8
+ relay actually cost you in tokens.
9
+
10
+ ```text
11
+ composer → any-deepseek / deepseek-v4.1-flash
12
+ │
13
+ ├─ channel selected in the panel (manual switch, v1)
14
+ ├─ key resolved from ctx.credentials per request
15
+ └─ forwarded as openai-completions to the channel's endpoint
16
+ ```
17
+
18
+ ## Why
19
+
20
+ - **One row in the model picker.** Your relay list stops being N provider groups.
21
+ - **Only a key is user input.** Endpoint, upstream model id, context window, output cap and
22
+ compatibility flags ship in the plugin's own preset table.
23
+ - **Per-channel accounting.** Input / cache-read / cache-write / output are recorded per
24
+ request and grouped by the channel that actually served it.
25
+ - **Keys never enter configuration.** They live in the harness credential store; the config
26
+ only carries the reference name, exactly like the official Models page.
27
+
28
+ ## Install
29
+
30
+ ```bash
31
+ dsh plugin --profile web add dsh-any-deepseek
32
+ ```
33
+
34
+ Then restart DSH and open **Settings → Any DeepSeek**.
35
+
36
+ > **Restart is required for a first install** — the profile's layer list changed, and the
37
+ > composing graph is built at boot. Editing an already-installed plugin's client half reloads
38
+ > in the browser without a restart; its node half does not.
39
+ >
40
+ > `dsh plugin` forwards its arguments to pnpm, so `add <spec>` accepts a registry name, a
41
+ > tarball path, or a local directory (which installs as a symlink).
42
+
43
+ ## Configuration
44
+
45
+ Everything lives under the `dsh-any-deepseek` settings namespace — that is the **profile entry
46
+ id** from `cordis.patch.yml`, and it is what the panel binds to:
47
+
48
+ ```yaml
49
+ dsh-any-deepseek:
50
+ current: opencode-go # the live channel (manual)
51
+ channels:
52
+ opencode-go:
53
+ apiKeyEnv: ANY_DEEPSEEK_OPENCODE_GO_KEY
54
+ enabled: true
55
+ commandcode:
56
+ apiKeyEnv: ANY_DEEPSEEK_COMMANDCODE_KEY
57
+ enabled: true
58
+ usage:
59
+ enabled: true
60
+ ```
61
+
62
+ `channels.<id>` may be either:
63
+
64
+ - **preset-backed** — `preset: opencode-go` plus optional overrides; or
65
+ - **fully declared** — `baseURL` + `model` are required; `api` defaults to
66
+ `openai-completions`, and `contextWindow` / `maxTokens` fall back to documented defaults
67
+ (the panel shows a warning next to the row when a default was applied rather than declared).
68
+
69
+ ### Channels vs. protocols
70
+
71
+ `openai-completions` is the default and the recommended protocol: it is what almost every
72
+ relay speaks, it exposes the largest compatibility surface (~20 switches), and its usage
73
+ normalization already reconciles the three cache-token spellings relays use
74
+ (`prompt_tokens_details.cached_tokens`, `prompt_cache_hit_tokens`, top-level `cached_tokens`).
75
+
76
+ Set `api` on one channel only when that relay speaks something else (`openai-responses`,
77
+ `anthropic-messages`). The picker never changes per channel behind your back.
78
+
79
+ ### Switching channels
80
+
81
+ v1 is deliberately manual. A channel that fails reports its error; it does not silently fail
82
+ over to another one. Switching invalidates the prompt cache for the running conversation, so
83
+ switch at a natural boundary (or start a new session).
84
+
85
+ ## Usage accounting
86
+
87
+ The ledger is append-only JSONL, split by month, under the harness home:
88
+
89
+ ```text
90
+ ~/.dsh/any-deepseek/usage-YYYY-MM.jsonl
91
+ ```
92
+
93
+ One line per request:
94
+
95
+ ```json
96
+ {"at":"2026-10-19T10:12:03+08:00","channel":"opencode-go","model":"deepseek-v4.1-flash","ok":true,"ms":4210,"firstTokenMs":810,"usage":{"input":812,"output":96,"cacheRead":40960,"cacheWrite":0,"reasoning":0,"total":41868}}
97
+ ```
98
+
99
+ `usage` is `null` when a relay does not report token counts — the panel shows `—`, never `0`,
100
+ because a zero would read as "this request was free".
101
+
102
+ **Counting convention.** `TokenUsage` counts are *disjoint*: `input` is uncached input only,
103
+ cache reads and writes are reported separately, and the three together are the billable input.
104
+ The stats table states this next to the numbers.
105
+
106
+ ## Development
107
+
108
+ ```bash
109
+ pnpm install
110
+ pnpm run typecheck # host types come from pinned devDependencies
111
+ pnpm run test # pure layers only; no harness needed
112
+ pnpm run build # lib/index.js + lib/client.js
113
+ ```
114
+
115
+ The pure layers (`presets`, `channels`, `route`, `usage-ledger`) do not import the harness and
116
+ are covered by the unit tests. The adapter and panel talk to the host through public seams
117
+ only.
118
+
119
+ ## Boundaries
120
+
121
+ - Does not modify the harness, and does not replace `deepseek-official` or `llm-pi-ai` routes.
122
+ - Exposes exactly one model id. Upstream model ids differ per relay and are mapped in presets.
123
+ - No cost estimation (no price table), no automatic failover (v1), no multi-model aggregation.
124
+
125
+ ## License
126
+
127
+ MIT
@@ -0,0 +1,8 @@
1
+ # Any-deepseek bundle layer (declared by package.json `dsh.bundle.patch`).
2
+ #
3
+ # The entry id becomes the settings namespace: the panel, the config document and every
4
+ # recorded reference are all keyed by `dsh-any-deepseek`, so renaming the id would move the
5
+ # user's stored configuration. Keep it stable.
6
+ - insert:
7
+ - id: dsh-any-deepseek
8
+ name: 'dsh-any-deepseek'
@@ -0,0 +1,126 @@
1
+ /**
2
+ * adapter.ts — the harness-side `LlmAdapter` that fans one model out to many relays.
3
+ *
4
+ * ## The shape of the problem
5
+ *
6
+ * The harness asks an adapter for exactly two kinds of thing: "what models do you serve" and
7
+ * "stream this request". Everything between — which relay is live, which key it uses, what it
8
+ * calls the model, how it spells its thinking parameters — is this file's business, and none
9
+ * of it is visible to the harness.
10
+ *
11
+ * ## Why a new pi-ai collection per call
12
+ *
13
+ * The pi-ai adapter makes the argument and it applies here unchanged: `streamSimple()` resolves
14
+ * the provider lazily, after the credential await, so a collection mutated between requests
15
+ * would let a call that started under one configuration finish under another. Building the
16
+ * collection per call makes a settings change reach the next request and never the current one.
17
+ * The cost is one small object graph per request, which is noise next to an HTTP call.
18
+ *
19
+ * ## Why the ledger is written here
20
+ *
21
+ * `usage` only exists at this boundary: the harness's token meter replays a session log to
22
+ * estimate context pressure, which is a different question from "what did this relay report for
23
+ * this request". The ledger write cannot fail the call — accounting is not allowed to be a
24
+ * single point of failure for model traffic.
25
+ */
26
+ import { LlmAdapter } from '@deepseek-ai/dsh-llm';
27
+ import type { GenerateOptions, LlmModelInfo, LlmProviderInfo, LlmResolvedModelInfo, PreparedAdapterCall, StreamChunk, TokenUsage } from '@deepseek-ai/dsh-llm';
28
+ import { credentialRef } from '@deepseek-ai/dsh-credentials';
29
+ import { type Api, type AssistantMessage, type AssistantMessageEvent, type Context as PiContext, type Model } from '@earendil-works/pi-ai';
30
+ import type { ResolvedChannel } from './channels.js';
31
+ import type { LedgerEntry } from './usage-ledger.js';
32
+ /** The single provider route this plugin owns. */
33
+ export declare const PROVIDER_ID = "any-deepseek";
34
+ /** Display name for the route in provider selectors. */
35
+ export declare const PROVIDER_NAME = "Any DeepSeek";
36
+ /** What the adapter needs from the plugin, so it can be tested without a harness context. */
37
+ export interface AdapterDeps {
38
+ /** Enabled, resolved channels in configuration order. */
39
+ channels: () => readonly ResolvedChannel[];
40
+ /** The configured channel id. */
41
+ current: () => string | undefined;
42
+ /** Resolve a credential reference to its value; undefined when unconfigured. */
43
+ resolveKey: (ref: string) => Promise<string | undefined>;
44
+ /** Record one finished request. Must never throw. */
45
+ record: (entry: LedgerEntry) => Promise<void>;
46
+ /** Clock, injectable so tests are deterministic. */
47
+ now: () => Date;
48
+ /** Monotonic-ish millisecond clock for durations. */
49
+ elapsed: () => number;
50
+ }
51
+ /** One finished call, as the adapter observed it. */
52
+ interface CallOutcome {
53
+ ok: boolean;
54
+ code?: string;
55
+ usage: TokenUsage | null;
56
+ }
57
+ /**
58
+ * Build the pi-ai model entry for one channel.
59
+ *
60
+ * The advertised id (`deepseek-v4.1-flash`) never reaches the wire here: each channel's own
61
+ * `model` does. That substitution is the entire reason this layer exists.
62
+ * @param channel - the resolved channel.
63
+ * @returns a pi-ai chat model describing that relay's view of the model.
64
+ */
65
+ export declare function buildPiModel(channel: ResolvedChannel): Model<Api>;
66
+ /**
67
+ * Translate one harness request into a pi-ai context.
68
+ *
69
+ * The harness already projects images and files before an adapter sees them, so this is a
70
+ * straightforward content mapping; anything the harness did not project is passed through.
71
+ * @param options - the harness request.
72
+ * @returns a pi-ai context plus the one-shot system prompt, when the caller set one.
73
+ */
74
+ export declare function toPiContext(options: GenerateOptions): PiContext;
75
+ /** Map pi-ai usage onto the harness's disjoint counters. */
76
+ export declare function toTokenUsage(usage: AssistantMessage['usage']): TokenUsage;
77
+ /**
78
+ * Map pi-ai events onto harness stream chunks.
79
+ *
80
+ * The two protocols agree on the important part (indexed blocks, deltas, one terminal event),
81
+ * so this is a rename table plus the terminal handling. Failures arrive as an `error` event
82
+ * rather than a throw — pi-ai never throws mid-stream — which is why the terminal event is the
83
+ * only place a failure code can be produced.
84
+ * @param events - the pi-ai event stream.
85
+ * @param onOutcome - called once with the terminal outcome, before the finish chunk.
86
+ * @returns harness chunks, ending with `finish`.
87
+ */
88
+ export declare function toStreamChunks(events: AsyncIterable<AssistantMessageEvent>, onOutcome: (outcome: CallOutcome) => void): AsyncGenerator<StreamChunk>;
89
+ /**
90
+ * Assign a provider-neutral failure code.
91
+ *
92
+ * The harness routes retries on these codes, so the mapping matters more than the message. A
93
+ * relay's own wording is preserved in the message; only the class is normalized here.
94
+ * @param message - the terminal pi-ai message.
95
+ * @returns a harness failure code.
96
+ */
97
+ export declare function classifyFailure(message: AssistantMessage): string;
98
+ /**
99
+ * The adapter. One instance serves the single provider route.
100
+ */
101
+ export declare class AnyDeepSeekAdapter extends LlmAdapter {
102
+ private readonly deps;
103
+ constructor(deps: AdapterDeps);
104
+ providerInfo(provider: string): LlmProviderInfo;
105
+ /**
106
+ * Advertise exactly one model.
107
+ *
108
+ * A relay that is missing a key still gets no catalog entry — the picker then hides the whole
109
+ * group, which is the harness's own convention for a dormant provider — but the settings page
110
+ * keeps its entry, so the user can go fix it.
111
+ */
112
+ listModels(_provider: string): Promise<LlmModelInfo[]>;
113
+ resolveModel(provider: string, model: string, _signal?: AbortSignal): Promise<LlmResolvedModelInfo>;
114
+ /**
115
+ * Bind the model metadata and the dispatch to one channel generation.
116
+ *
117
+ * The harness calls `prepareCall` before logging a request header and dispatches later; binding
118
+ * here is what stops a panel change from landing in the middle of a recorded step.
119
+ */
120
+ prepareCall(provider: string, model: string, signal?: AbortSignal): Promise<PreparedAdapterCall>;
121
+ stream(options: GenerateOptions): AsyncGenerator<StreamChunk>;
122
+ /** The channel the panel has selected, if any (used for model metadata). */
123
+ private selected;
124
+ }
125
+ /** Re-export the credential helper so callers build references the same way the adapter does. */
126
+ export { credentialRef };
@@ -0,0 +1,108 @@
1
+ /**
2
+ * channels.ts — the configuration entry → resolved channel profile layer.
3
+ *
4
+ * ## Two shapes, one resolution
5
+ *
6
+ * A channel entry is either preset-backed (`preset: opencode-go`, key only) or fully declared
7
+ * (every field present). Both resolve through {@link resolveChannel} to the same
8
+ * {@link ResolvedChannel}, so the adapter never branches on which shape the user chose.
9
+ *
10
+ * ## Why a missing field is an error rather than a default
11
+ *
12
+ * A partially declared custom channel is a configuration mistake, not a preference: guessing
13
+ * the endpoint would send the user's key somewhere they did not name. The only defaults
14
+ * applied are the ones the host itself documents (capacities), and they are applied *loudly*
15
+ * in the returned `warnings` so the panel can show them.
16
+ */
17
+ import { ADVERTISED_MODEL_ID, type ChannelApi, type ChannelModality } from './presets.js';
18
+ /** One channel as the configuration document stores it. */
19
+ export interface ChannelEntry {
20
+ /** Human label; falls back to the preset's or the id. */
21
+ label?: string;
22
+ /** Preset id this entry references. Mutually exclusive with a fully declared channel. */
23
+ preset?: string;
24
+ /** Wire protocol. Required for a channel that names no preset. */
25
+ api?: string;
26
+ /** Endpoint. Required for a channel that names no preset. */
27
+ baseURL?: string;
28
+ /** Upstream model id. Required for a channel that names no preset. */
29
+ model?: string;
30
+ /** Credential reference (environment-variable name). Required. */
31
+ apiKeyEnv?: string;
32
+ /** Whether the panel offers this channel. Absent means enabled. */
33
+ enabled?: boolean;
34
+ /** Overrides applied on top of a preset, or the whole declaration for a custom channel. */
35
+ contextWindow?: number;
36
+ maxTokens?: number;
37
+ input?: string[];
38
+ reasoningEfforts?: Record<string, string | null>;
39
+ compat?: Record<string, unknown>;
40
+ headers?: Record<string, string>;
41
+ }
42
+ /** A channel with every fact the adapter needs, and nothing optional left to guess. */
43
+ export interface ResolvedChannel {
44
+ /** Stable channel id (the configuration key). */
45
+ id: string;
46
+ label: string;
47
+ api: ChannelApi;
48
+ baseURL: string;
49
+ /** Upstream model id, as the relay spells it. */
50
+ model: string;
51
+ apiKeyEnv: string;
52
+ contextWindow: number;
53
+ maxTokens: number;
54
+ input: ChannelModality[];
55
+ reasoningEfforts?: Record<string, string | null>;
56
+ compat?: Record<string, unknown>;
57
+ headers?: Record<string, string>;
58
+ /** Facts the user should see because a default was applied rather than declared. */
59
+ warnings: string[];
60
+ }
61
+ /** Why one entry could not be resolved. */
62
+ export interface ChannelProblem {
63
+ id: string;
64
+ code: 'UNKNOWN_PRESET' | 'MISSING_API_KEY_REF' | 'MISSING_FIELD' | 'INVALID_API' | 'UNKNOWN_KEY';
65
+ detail: string;
66
+ }
67
+ /** Result of resolving a whole configuration's channel map. */
68
+ export interface ChannelResolution {
69
+ /** Channels that resolved, keyed by id, in configuration order. */
70
+ channels: Map<string, ResolvedChannel>;
71
+ /** Entries that could not resolve; each is reported rather than skipped silently. */
72
+ problems: ChannelProblem[];
73
+ }
74
+ /**
75
+ * Resolve one channel entry.
76
+ *
77
+ * Precedence is preset first, entry second: an entry that names a preset inherits everything
78
+ * and overrides only what it declares. A custom entry (no preset) must supply the endpoint
79
+ * facts itself.
80
+ * @param id - configuration key, used verbatim as the channel's identity.
81
+ * @param raw - the entry as stored.
82
+ * @returns the resolved channel, or the problem that prevented resolution.
83
+ */
84
+ export declare function resolveChannel(id: string, raw: unknown): {
85
+ ok: true;
86
+ channel: ResolvedChannel;
87
+ } | {
88
+ ok: false;
89
+ problem: ChannelProblem;
90
+ };
91
+ /**
92
+ * Resolve every channel in one configuration document.
93
+ *
94
+ * Problems are collected, never thrown: one broken channel must not make the rest of the
95
+ * panel unusable, and the panel needs the per-entry reason to show next to the row.
96
+ * @param entries - the configuration's `channels` map.
97
+ * @returns resolved channels plus every problem found.
98
+ */
99
+ export declare function resolveChannels(entries: unknown): ChannelResolution;
100
+ /**
101
+ * Channels that should appear and serve requests: resolved AND not explicitly disabled.
102
+ * @param resolution - output of {@link resolveChannels}.
103
+ * @param entries - the same configuration map, read again for the `enabled` flags.
104
+ * @returns enabled channels, configuration order preserved.
105
+ */
106
+ export declare function enabledChannels(resolution: ChannelResolution, entries: unknown): ResolvedChannel[];
107
+ /** The advertised model id, re-exported so callers need one import for routing facts. */
108
+ export { ADVERTISED_MODEL_ID };