dsh-opencode-go 0.1.0

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.
@@ -0,0 +1,52 @@
1
+ /**
2
+ * Configuration schema for the OpenCode Go adapter plugin. The section is
3
+ * installed under the `llm-opencode-go` settings namespace: a cordis.yml
4
+ * entry supplies the composition base and the settings document overrides it
5
+ * field by field, hot-reloaded without a restart. Self-contained constraints
6
+ * (URL shape, numeric bounds) fail at load for the composition layer and
7
+ * refuse the write for the settings layer.
8
+ *
9
+ * @module dsh-llm-opencode-go/config
10
+ */
11
+ import z from '@deepseek-ai/schemastery';
12
+ /** Environment variable resolving the OpenCode API key. */
13
+ export declare const DEFAULT_API_KEY_ENV = "OPENCODE_API_KEY";
14
+ /** Default interval between live catalog re-resolutions. */
15
+ export declare const DEFAULT_REFRESH_MINUTES = 60;
16
+ /** Default maximum idle interval while a stream read is outstanding. */
17
+ export declare const DEFAULT_STREAM_IDLE_TIMEOUT_MS = 300000;
18
+ /** Runtime configuration for one plugin mount. */
19
+ export interface OpencodeGoConfig {
20
+ /**
21
+ * Whether this adapter serves its route at all. False withdraws the
22
+ * `opencode-go` route and its models from every picker without unloading the
23
+ * plugin, so the settings page that owns this switch stays reachable to turn
24
+ * it back on. Independent of the credential: a key present while this is
25
+ * false registers nothing.
26
+ */
27
+ enabled: boolean;
28
+ /** Credential reference: the environment variable the key resolves from. */
29
+ apiKeyEnv: string;
30
+ /** The gateway endpoint; also the base of the live model listing. */
31
+ baseURL: string;
32
+ /** How long one live catalog resolution stays authoritative, in minutes. */
33
+ refreshMinutes: number;
34
+ /** Largest idle gap between stream events before the request fails. */
35
+ streamIdleTimeoutMs: number;
36
+ /** Request-level bound on base64-encoded image payload, in bytes. */
37
+ maxRequestImageBytes: number;
38
+ /** Total-pixel budget for one request image. */
39
+ requestImagePixelBudget: number;
40
+ /** Raw encoded-byte target for one request image before base64 expansion. */
41
+ requestImageMaxBytes: number;
42
+ }
43
+ /** Runtime schema for {@link OpencodeGoConfig}. */
44
+ export declare const Config: z<OpencodeGoConfig>;
45
+ /**
46
+ * Accept only an http(s) base without a query or fragment. Runs at load for
47
+ * the composition layer and as the settings section's write validator, so a
48
+ * bad URL fails where it is written, never at first request.
49
+ * @param raw - the configured base URL.
50
+ * @returns the normalized base URL without trailing slashes.
51
+ */
52
+ export declare function assertBaseURL(raw: string): string;
@@ -0,0 +1,4 @@
1
+ /** Default image budgets, overridable through the adapter configuration. */
2
+ export declare const DEFAULT_MAX_REQUEST_IMAGE_BYTES: number;
3
+ export declare const DEFAULT_REQUEST_IMAGE_PIXEL_BUDGET: number;
4
+ export declare const DEFAULT_REQUEST_IMAGE_MAX_BYTES: number;
@@ -0,0 +1,49 @@
1
+ /**
2
+ * Harness request-history conversion into pi-ai's Context vocabulary.
3
+ *
4
+ * @module dsh-opencode-go/conversion/context
5
+ */
6
+ import type { GenerateOptions, ImageAttachmentAccessResolver } from '@deepseek-ai/dsh-llm';
7
+ import type { AttachmentStore } from '@deepseek-ai/dsh-attachment';
8
+ import type { Context as PiContext } from '@earendil-works/pi-ai';
9
+ /** Inputs that bind deterministic request images to one current tool execution world. */
10
+ export interface PiImageRequestContext {
11
+ /** Durable provider that resolves request-image bytes and provider-owned host objects. */
12
+ attachments: AttachmentStore;
13
+ /** Resolve current tool access separately from deterministic request-image versions. */
14
+ resolveImageAccess: ImageAttachmentAccessResolver;
15
+ /** Request-level bound on the base64-encoded payload of retained images; omission leaves the bound unchecked. */
16
+ maxRequestImageBytes?: number;
17
+ /** Route pixel and raw encoded-byte budgets. */
18
+ requestImagePolicy?: PiImageRequestBudget;
19
+ }
20
+ /** Per-route budgets from which each request image's target is derived. */
21
+ export interface PiImageRequestBudget {
22
+ /** Total-pixel budget; larger sources are downscaled proportionally. */
23
+ maxPixels: number;
24
+ /** Encoded-byte target for one request image. */
25
+ maxBytes: number;
26
+ }
27
+ /**
28
+ * Convert text-only harness history to a synchronous pi-ai Context. Tool
29
+ * result names are recovered from preceding assistant tool calls.
30
+ * @param options - the harness request; `options.system`, else a leading `system` message, maps to pi-ai's single `systemPrompt` slot.
31
+ * @param images - absent; selects the synchronous conversion.
32
+ * @param onReplayDegrade - forwarded to {@link toPiAssistant} for each assistant message.
33
+ * @returns the pi-ai context; `tools` is omitted when the request declares none.
34
+ * @throws {LlmError} `UNSUPPORTED_CONTENT` for images in any history role, including a leading system message.
35
+ */
36
+ export declare function toPiContext(options: GenerateOptions, images?: undefined, onReplayDegrade?: (reason: string) => void): PiContext;
37
+ /**
38
+ * Convert harness history to a pi-ai Context while resolving durable images.
39
+ * Tool result names are recovered from preceding assistant tool calls. Image
40
+ * occurrences the surface marks offloaded become text placeholders; when the
41
+ * retained occurrences' exact base64 payload still exceeds
42
+ * `maxRequestImageBytes`, the call fails with `IMAGE_OFFLOAD_REQUIRED` naming
43
+ * how many more oldest occurrences must be offloaded.
44
+ * @param options - the harness request; `options.system`, else a leading `system` message, maps to pi-ai's single `systemPrompt` slot.
45
+ * @param images - attachment provider, current path resolver, and request limits.
46
+ * @param onReplayDegrade - forwarded to {@link toPiAssistant} for each assistant message.
47
+ * @returns the asynchronously resolved pi-ai context.
48
+ */
49
+ export declare function toPiContext(options: GenerateOptions, images: PiImageRequestContext, onReplayDegrade?: (reason: string) => void): Promise<PiContext>;
@@ -0,0 +1,5 @@
1
+ /** History, image, and stream conversion shared by this plugin's protocol adapters. */
2
+ export { toPiContext } from './context.ts';
3
+ export type { PiImageRequestContext } from './context.ts';
4
+ export { toStreamChunks } from './stream.ts';
5
+ export { DEFAULT_MAX_REQUEST_IMAGE_BYTES, DEFAULT_REQUEST_IMAGE_MAX_BYTES, DEFAULT_REQUEST_IMAGE_PIXEL_BUDGET, } from './config.ts';
@@ -0,0 +1,62 @@
1
+ /**
2
+ * Durable pi-ai replay metadata and assistant-history reconstruction.
3
+ *
4
+ * Harness content remains the durable source for text and tool calls. This
5
+ * module stores only the provider-native metadata needed to reconstruct a
6
+ * pi-ai assistant message on a later request.
7
+ *
8
+ * @module dsh-opencode-go/conversion/replay
9
+ */
10
+ import type { Message, ReplayEnvelope } from '@deepseek-ai/dsh-llm';
11
+ import type { Api, AssistantMessage } from '@earendil-works/pi-ai';
12
+ /** Per-block half of the pi-ai replay envelope, one entry per content block. */
13
+ export type PiAiReplayBlock = {
14
+ type: 'text';
15
+ textSignature?: string;
16
+ } | {
17
+ type: 'reasoning';
18
+ thinkingSignature?: string;
19
+ redacted?: boolean;
20
+ } | {
21
+ type: 'tool-call';
22
+ thoughtSignature?: string;
23
+ };
24
+ /** Versioned response-level half of the pi-ai replay envelope. */
25
+ export interface PiAiReplayResponse {
26
+ kind: 'pi-ai';
27
+ version: 2;
28
+ api: Api;
29
+ provider: string;
30
+ /** Requested model identity, matching the durable assistant source. */
31
+ model: string;
32
+ /** Provider-reported model; only Anthropic replays it as the native model (reported in `message.model`, not `message.responseModel`). */
33
+ responseModel?: string;
34
+ responseId?: string;
35
+ /** Provider-native effort for historical replay; absence is preserved. */
36
+ providerThinkingLevel?: string;
37
+ stopReason: AssistantMessage['stopReason'];
38
+ }
39
+ /**
40
+ * Project a successful pi-ai response into the minimal durable replay state.
41
+ * The per-block half is index-aligned with the streamed blocks (pi-ai content
42
+ * order), so `BlockAssembler` prunes an entry with its block whenever assembly
43
+ * removes one.
44
+ * @param message - completed native pi-ai assistant response.
45
+ * @param requestedModel - request identity stored in the assistant source; defaults to the native model.
46
+ * @returns the versioned lossless-JSON replay projection.
47
+ */
48
+ export declare function toPiReplayState(message: AssistantMessage, requestedModel?: string): ReplayEnvelope;
49
+ /**
50
+ * Convert one durable Harness assistant message into pi-ai history.
51
+ *
52
+ * Durable content is the authoritative record; replay metadata only restores
53
+ * native fidelity (ids, signatures). A replay state this build cannot use —
54
+ * another adapter's kind, another version, a malformed value, or metadata that
55
+ * no longer matches the content — therefore degrades the one message to
56
+ * provider-neutral history instead of failing the request.
57
+ * @param message - assistant content with required source and optional adapter-owned replay metadata.
58
+ * @param onDegrade - called with the diagnostic reason when an unusable replay
59
+ * state falls back to provider-neutral conversion.
60
+ * @returns a native pi-ai assistant message reconstructed from durable content.
61
+ */
62
+ export declare function toPiAssistant(message: Message, onDegrade?: (reason: string) => void): AssistantMessage;
@@ -0,0 +1,42 @@
1
+ /**
2
+ * pi-ai assistant event translation into the Harness streaming protocol.
3
+ *
4
+ * pi-ai tool-call arguments are parsed objects while the Harness keeps their
5
+ * raw JSON representation. pi-ai also reports failures as terminal stream
6
+ * events, which this module maps into Harness finish chunks.
7
+ *
8
+ * @module dsh-opencode-go/conversion/stream
9
+ */
10
+ import type { FinishReason, StreamChunk, TokenUsage } from '@deepseek-ai/dsh-llm';
11
+ import type { AssistantMessage, AssistantMessageEvent, Usage as PiUsage } from '@earendil-works/pi-ai';
12
+ /**
13
+ * Map pi-ai usage (reasoning folded into output by pi-ai).
14
+ * @param usage - cumulative usage from the terminal pi-ai event.
15
+ * @returns harness counts with pi-ai's exact total; cache fields appear only
16
+ * when non-zero (pi-ai reports zeros, not absence).
17
+ */
18
+ export declare function mapUsage(usage: PiUsage): TokenUsage;
19
+ /**
20
+ * Map a terminal pi-ai event to the harness finish reason.
21
+ * @param message - the assistant message carried by the `done` or `error` event.
22
+ * @param contextWindow - resolved catalog capacity for usage-based overflow detection.
23
+ * @returns the mapped harness reason. Recognized error text, `stop` usage above
24
+ * `contextWindow`, and zero-output `length` usage that fills the window map
25
+ * to `CONTEXT_WINDOW_EXCEEDED`; a `stop` with no content blocks maps to an
26
+ * `EMPTY_RESPONSE` error, while terminal `pending` and `deferred` states map
27
+ * to non-retryable `PI_AI_ERROR` failures.
28
+ */
29
+ export declare function mapStopReason(message: AssistantMessage, contextWindow?: number): FinishReason;
30
+ /**
31
+ * Translate the pi-ai event stream into StreamChunks. pi-ai never throws
32
+ * mid-stream — failures arrive as `error` events, which become error/aborted
33
+ * `finish` chunks (the harness protocol's other error-delivery style).
34
+ * @param events - one assistant turn's pi-ai event stream.
35
+ * @param contextWindow - resolved catalog capacity for usage-based overflow detection.
36
+ * @param callerSignal - caller cancellation state; an aborted caller makes any
37
+ * in-band terminal error an aborted finish.
38
+ * @param requestedModel - request model identity recorded for durable replay.
39
+ * @returns the harness chunks, ending with `usage` then `finish`; throws
40
+ * `LlmError` (`STREAM_CLOSED`) if the source ends without a terminal event.
41
+ */
42
+ export declare function toStreamChunks(events: AsyncIterable<AssistantMessageEvent>, contextWindow?: number, callerSignal?: AbortSignal, requestedModel?: string): AsyncGenerator<StreamChunk>;
@@ -0,0 +1,51 @@
1
+ /**
2
+ * Dedicated OpenCode Go adapter plugin. Registers one `opencode-go` route
3
+ * whose catalog is the curated table intersected with the gateway's live model
4
+ * listing, and installs the `llm-opencode-go` settings section: the Web UI
5
+ * renders it as its own settings page where the API key and every knob are
6
+ * edited, and a change reaches the next request without a restart. The plugin
7
+ * exists because the gateway has wire requirements a generic pi-ai route
8
+ * cannot express: a mandatory per-conversation `x-opencode-session` routing
9
+ * header and a model list that rotates faster than any shipped catalog.
10
+ *
11
+ * Configuration layers like every settings-backed plugin: a `cordis.yml`
12
+ * entry supplies the composition base and the settings document overrides it
13
+ * field by field.
14
+ *
15
+ * ```yaml
16
+ * - id: llm-opencode-go
17
+ * name: 'dsh-opencode-go'
18
+ * config:
19
+ * enabled: true # false withdraws the route; the plugin stays mounted
20
+ * apiKeyEnv: OPENCODE_API_KEY # default
21
+ * baseURL: https://opencode.ai/zen/go/v1 # default
22
+ * refreshMinutes: 60 # live catalog re-resolution interval
23
+ * ```
24
+ *
25
+ * The credential resolves per request through the credentials seam, falling
26
+ * back to the process environment — the same reference semantics the generic
27
+ * pi-ai adapter uses. The route registers atomically: if another adapter
28
+ * family already owns `opencode-go` (a profile in `llm-pi-ai`, for example),
29
+ * the refusal is logged with the reason and everything else this plugin does
30
+ * still works.
31
+ *
32
+ * @module dsh-opencode-go
33
+ */
34
+ import type { Context } from '@deepseek-ai/cordis';
35
+ import type { OpencodeGoConfig } from './config.ts';
36
+ export { OpencodeGoAdapter } from './adapter.ts';
37
+ export type { OpencodeGoAdapterOptions, OpencodeGoImageAccess } from './adapter.ts';
38
+ export { DEFAULT_BASE_URL, DISPLAY_NAME, PROVIDER_ID, OpencodeGoCatalog, discoverCatalogModels, readLiveModelIds, } from './catalog.ts';
39
+ export { Config, assertBaseURL } from './config.ts';
40
+ export type { OpencodeGoConfig } from './config.ts';
41
+ export declare const name = "llm-opencode-go";
42
+ export declare const inject: string[];
43
+ /** Settings namespace this plugin installs and the Web page edits. */
44
+ export declare const NS = "llm-opencode-go";
45
+ /**
46
+ * Register the route, its discovery, the settings section, and their
47
+ * teardown for one mount. Configuration starts as the cordis.yml entry and is
48
+ * replaced by the settings section's resolved value once the settings
49
+ * provider attaches; the adapter re-reads it at every operation.
50
+ */
51
+ export declare function apply(ctx: Context, raw?: OpencodeGoConfig): void;
package/package.json ADDED
@@ -0,0 +1,174 @@
1
+ {
2
+ "name": "dsh-opencode-go",
3
+ "version": "0.1.0",
4
+ "description": "OpenCode Go model provider and settings UI for DeepSeek Harness",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "engines": {
8
+ "node": "^22.19.0 || >=24.0.0"
9
+ },
10
+ "main": "./lib/index.js",
11
+ "types": "./lib/types/index.d.ts",
12
+ "exports": {
13
+ ".": {
14
+ "types": "./lib/types/index.d.ts",
15
+ "default": "./lib/index.js"
16
+ },
17
+ "./client": {
18
+ "types": "./lib/types/client/index.d.ts",
19
+ "default": "./lib/client.js"
20
+ },
21
+ "./package.json": "./package.json"
22
+ },
23
+ "files": [
24
+ "lib",
25
+ "cordis.patch.yml",
26
+ "THIRD_PARTY_NOTICES.md",
27
+ "docs",
28
+ "examples"
29
+ ],
30
+ "dsh": {
31
+ "bundle": {
32
+ "patch": "./cordis.patch.yml"
33
+ },
34
+ "client": {
35
+ "inject": [
36
+ "@deepseek-ai/dsh-client-locale",
37
+ "@deepseek-ai/dsh-client-ui-settings",
38
+ "@deepseek-ai/dsh-api-remotes"
39
+ ],
40
+ "platform": "web"
41
+ }
42
+ },
43
+ "scripts": {
44
+ "build": "node scripts/build.mjs",
45
+ "typecheck": "tsc -p tsconfig.host.json --noEmit && tsc -p tsconfig.client.json --noEmit",
46
+ "test": "vitest run",
47
+ "prepack": "npm run build",
48
+ "pretest": "npm run build",
49
+ "verify:installed": "node --expose-internals scripts/installed-smoke.mjs",
50
+ "verify:headless": "node scripts/headless-smoke.mjs"
51
+ },
52
+ "peerDependencies": {
53
+ "@deepseek-ai/cordis": "4.0.2",
54
+ "@deepseek-ai/dsh-attachment": "0.1.6-alpha.1",
55
+ "@deepseek-ai/dsh-credentials": "0.1.6-alpha.1",
56
+ "@deepseek-ai/dsh-fs": "0.1.6-alpha.1",
57
+ "@deepseek-ai/dsh-launch-environment": "0.1.6-alpha.1",
58
+ "@deepseek-ai/dsh-llm": "0.1.6-alpha.1",
59
+ "@deepseek-ai/dsh-settings": "0.1.6-alpha.1",
60
+ "@deepseek-ai/dsh-timeout": "0.1.6-alpha.1",
61
+ "@deepseek-ai/dsh-api-remotes": "0.1.6-alpha.1",
62
+ "@deepseek-ai/dsh-api-gateway": "0.1.6-alpha.1",
63
+ "@deepseek-ai/dsh-api-settings-controller": "0.1.6-alpha.1",
64
+ "@deepseek-ai/dsh-client-store": "0.1.6-alpha.1",
65
+ "@deepseek-ai/dsh-client-ui-slots": "0.1.6-alpha.1",
66
+ "@deepseek-ai/dsh-client-ui-settings": "0.1.6-alpha.1",
67
+ "@deepseek-ai/dsh-client-ui-renderer": "0.1.6-alpha.1",
68
+ "@deepseek-ai/dsh-client-locale": "0.1.6-alpha.1",
69
+ "@deepseek-ai/dsh-typert-protocol": "0.1.6-alpha.1"
70
+ },
71
+ "dependencies": {
72
+ "@deepseek-ai/dsh-brand": "0.1.6-alpha.1",
73
+ "@deepseek-ai/schemastery": "3.18.2",
74
+ "@earendil-works/pi-ai": "0.85.1"
75
+ },
76
+ "devDependencies": {
77
+ "@deepseek-ai/cordis": "4.0.2",
78
+ "@deepseek-ai/cordis-plugin-include": "1.0.7",
79
+ "@deepseek-ai/cordis-plugin-loader": "1.0.3",
80
+ "@deepseek-ai/dsh-api-gateway": "0.1.6-alpha.1",
81
+ "@deepseek-ai/dsh-api-remotes": "0.1.6-alpha.1",
82
+ "@deepseek-ai/dsh-api-settings-controller": "0.1.6-alpha.1",
83
+ "@deepseek-ai/dsh-attachment": "0.1.6-alpha.1",
84
+ "@deepseek-ai/dsh-brand": "0.1.6-alpha.1",
85
+ "@deepseek-ai/dsh-client-locale": "0.1.6-alpha.1",
86
+ "@deepseek-ai/dsh-client-store": "0.1.6-alpha.1",
87
+ "@deepseek-ai/dsh-client-ui-primitives": "0.1.6-alpha.1",
88
+ "@deepseek-ai/dsh-client-ui-renderer": "0.1.6-alpha.1",
89
+ "@deepseek-ai/dsh-client-ui-settings": "0.1.6-alpha.1",
90
+ "@deepseek-ai/dsh-client-ui-slots": "0.1.6-alpha.1",
91
+ "@deepseek-ai/dsh-credentials": "0.1.6-alpha.1",
92
+ "@deepseek-ai/dsh-credentials-local": "0.1.6-alpha.1",
93
+ "@deepseek-ai/dsh-fs": "0.1.6-alpha.1",
94
+ "@deepseek-ai/dsh-launch-environment": "0.1.6-alpha.1",
95
+ "@deepseek-ai/dsh-llm": "0.1.6-alpha.1",
96
+ "@deepseek-ai/dsh-settings": "0.1.6-alpha.1",
97
+ "@deepseek-ai/dsh-settings-file": "0.1.6-alpha.1",
98
+ "@deepseek-ai/dsh-timeout": "0.1.6-alpha.1",
99
+ "@deepseek-ai/dsh-typert-protocol": "0.1.6-alpha.1",
100
+ "@testing-library/react": "^16.3.2",
101
+ "@types/node": "^22.20.0",
102
+ "@types/react": "~18.3.1",
103
+ "esbuild": "^0.25.0",
104
+ "jsdom": "29.1.1",
105
+ "lightningcss": "^1.32.0",
106
+ "react": "^18.2.0",
107
+ "react-dom": "^18.2.0",
108
+ "typescript": "6.0.3",
109
+ "vitest": "^4.1.8",
110
+ "@types/react-dom": "~18.3.0",
111
+ "clsx": "^2.0.0",
112
+ "diff": "^9.0.0",
113
+ "anser": "^2.3.5",
114
+ "katex": "^0.16.47",
115
+ "micromark-core-commonmark": "^2.0.3",
116
+ "micromark-util-character": "^2.1.1",
117
+ "micromark-util-classify-character": "^2.0.1",
118
+ "micromark-util-symbol": "^2.0.1",
119
+ "micromark-util-types": "^2.0.2",
120
+ "shiki": "^4.3.1",
121
+ "@shikijs/langs": "^4.3.1",
122
+ "@types/mdast": "^4.0.4",
123
+ "micromark-factory-space": "^2.0.1",
124
+ "micromark-extension-math": "^3.1.0",
125
+ "mdast-util-from-markdown": "^2.0.3",
126
+ "mdast-util-gfm": "^3.1.0",
127
+ "mdast-util-math": "^3.0.0",
128
+ "micromark-extension-gfm": "^3.0.0",
129
+ "micromark-util-sanitize-uri": "^2.0.1",
130
+ "zustand": "~4.4.7",
131
+ "immer": "^10.1.1"
132
+ },
133
+ "peerDependenciesMeta": {
134
+ "@deepseek-ai/dsh-api-remotes": {
135
+ "optional": true
136
+ },
137
+ "@deepseek-ai/dsh-api-gateway": {
138
+ "optional": true
139
+ },
140
+ "@deepseek-ai/dsh-api-settings-controller": {
141
+ "optional": true
142
+ },
143
+ "@deepseek-ai/dsh-client-store": {
144
+ "optional": true
145
+ },
146
+ "@deepseek-ai/dsh-client-ui-slots": {
147
+ "optional": true
148
+ },
149
+ "@deepseek-ai/dsh-client-ui-settings": {
150
+ "optional": true
151
+ },
152
+ "@deepseek-ai/dsh-client-ui-renderer": {
153
+ "optional": true
154
+ },
155
+ "@deepseek-ai/dsh-client-locale": {
156
+ "optional": true
157
+ },
158
+ "@deepseek-ai/dsh-typert-protocol": {
159
+ "optional": true
160
+ }
161
+ },
162
+ "publishConfig": {
163
+ "access": "public",
164
+ "registry": "https://registry.npmjs.org/"
165
+ },
166
+ "keywords": [
167
+ "deepseek-harness",
168
+ "dsh",
169
+ "opencode",
170
+ "opencode-go",
171
+ "llm",
172
+ "cordis"
173
+ ]
174
+ }