@sigrun/provider-opencode 0.0.0-stage → 0.1.1

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/CHANGELOG.md ADDED
@@ -0,0 +1,52 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [0.1.1] - 2026-10-27
9
+ ### Changed
10
+ - Re-released through CI trusted publishing; this version carries SLSA
11
+ provenance. No functional change from 0.1.0.
12
+
13
+ ## [Unreleased]
14
+
15
+ ## [0.1.0] - 2026-10-18
16
+ ### Changed
17
+ - Renamed to `@sigrun/provider-opencode` for the 0.1.0 release (GitHub
18
+ repo `sigrun-ai/sigrun-provider-opencode`): published package metadata
19
+ (`publishConfig` access/provenance), host modules
20
+ (`@earendil-works/pi-ai`, `@earendil-works/pi-durable`) moved to
21
+ `peerDependencies` mirrored in `devDependencies` at exact `1.3.0`,
22
+ CHANGELOG.md joined the files whitelist, and a manifest hygiene check
23
+ wired into `npm test`. The contributor identity follows the package
24
+ slug: the factory result's `name` becomes `provider-opencode` and the
25
+ scoped settings file `settings/provider-opencode.json` (the loader
26
+ pre-keys the two-phase host from the package name; a stale declared
27
+ name would split the factory-time settings scope).
28
+ ### Added
29
+ - Deployment-configurable base URL: the (now async) factory reads
30
+ `baseUrl` from the plugin's scoped settings
31
+ (`settings/provider-opencode.json`) with the constant as fallback; a
32
+ malformed override fails the factory loudly instead of silently
33
+ falling back (mirrors sigrun-pareto; restart-required).
34
+ ### Added
35
+ - Sigrun provider plugin: OpenCode's paid Go tier
36
+ (`https://opencode.ai/zen/go/v1`) as one dynamic OpenAI-compatible
37
+ provider (`provider: "opencode"` stamped on every catalog entry so
38
+ persisted catalogs survive cold-boot restore). The `/zen/v1` free tier
39
+ is client-gated and out of scope.
40
+ - Provider-only contributor factory: `{ name: "provider-opencode",
41
+ providers: [{ provider, readiness }], health }` — no pi-durable
42
+ extension surface.
43
+ - Credential resolution through sigrun's auth.json bridge with
44
+ `OPENCODE_GO_API_KEY` as the ambient fallback (bridge credential
45
+ always wins).
46
+ - Three-state readiness tracker (`not_configured` / `degraded` /
47
+ `ready`) seeded from credential resolution and updated per refresh
48
+ outcome; `console.warn` on state transitions only. `health()` maps
49
+ `not_configured` to `"ok"` and configured-and-failing to
50
+ `"degraded"`.
51
+ - `fetchModels` throws on unconfigured and on failure — never returns
52
+ `[]` (an empty array would publish and wipe the persisted catalog).
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Michael MacLeod
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,79 @@
1
- # Temporary Holding Version
1
+ # @sigrun/provider-opencode
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
+ A [sigrun](https://github.com/sigrun-ai) provider plugin:
4
+ [OpenCode](https://opencode.ai)'s **paid Go tier**
5
+ (`https://opencode.ai/zen/go/v1`, OpenAI-compatible) as one dynamic chat
6
+ provider. The `/zen/v1` free tier is client-gated by design (FreeTierError
7
+ for non-CLI requests) and is out of scope.
8
+
9
+ ## What it contributes
10
+
11
+ A **provider-only contributor** — the default export is a factory
12
+ returning `{ name: "provider-opencode", providers: [{ provider, readiness }],
13
+ health }` with no pi-durable extension surface. Sigrun's loader merges
14
+ the provider into its `Models` store at boot.
15
+
16
+ - **Provider** `opencode` — dynamic catalog fetched from
17
+ `<base>/models` (OpenAI `[OI]` list shape; entries carry
18
+ `id`/`object`/`created`/`owned_by` and no capability metadata) over
19
+ `openai-completions`; boots with an empty baseline and publishes the
20
+ fetched catalog.
21
+ - **Readiness** (`not_configured` / `degraded` / `ready`, surfaced on
22
+ sigrun's `/v1/auth/check`) — seeded from credential resolution and
23
+ updated per refresh outcome; state transitions log via `console.warn`
24
+ (transitions only, never per failure). On the first configured boot
25
+ the provider announces `startup → ready` at credential *resolution* —
26
+ before any refresh has run (`ready` there means "credential resolved,
27
+ refresh not yet attempted", not "catalog verified").
28
+ - **Health** — `degraded` only when the provider is configured and its
29
+ last refresh failed; an unconfigured optional plugin reports `ok`.
30
+
31
+ ## Credentials
32
+
33
+ Resolved per request through sigrun's auth.json bridge, falling back to
34
+ the ambient environment:
35
+
36
+ 1. the bridged auth.json credential (`"default"` entry, then the
37
+ `"opencode"` entry), then
38
+ 2. `OPENCODE_GO_API_KEY`.
39
+
40
+ The plugin never reads auth.json or `process.env` itself. Rotating the
41
+ key applies on the next resolution — no restart needed (there are no
42
+ plugin settings; the endpoint is fixed).
43
+
44
+ ## Failure semantics
45
+
46
+ `fetchModels` **throws** when unconfigured or when the catalog fetch
47
+ fails — it never returns an empty array, which pi-ai would publish and
48
+ persist, wiping the catalog. A failed refresh retains the previous
49
+ catalog and records a per-provider error.
50
+
51
+ ## Install
52
+
53
+ ```jsonc
54
+ // settings.json (sigrun config dir) — the UNIFIED extensions list:
55
+ // install + load in one place; npm and git specifiers are the same list
56
+ // npm ("@sigrun/provider-opencode@^0.1.0"), git
57
+ // ("git:github.com/sigrun-ai/sigrun-provider-opencode", or any
58
+ // https git URL with an optional @ref), or the file: form below.
59
+ {
60
+ "extensions": ["file:/path/to/sigrun-opencode"]
61
+ }
62
+ ```
63
+
64
+ The committed `dist/` is the load target for git installs; pi-ai and
65
+ pi-durable are peerDependencies (sigrun supplies them at runtime);
66
+ they are mirrored in devDependencies for this repo's own build and tests.
67
+
68
+ ## Development
69
+
70
+ ```sh
71
+ npm install # also builds dist/ via prepare
72
+ npm test # vitest — hermetic fixture HTTP server, no network
73
+ npm run typecheck
74
+ npm run build
75
+ ```
76
+
77
+ ## License
78
+
79
+ MIT
@@ -0,0 +1,116 @@
1
+ /**
2
+ * @sigrun/provider-opencode — a sigrun provider plugin (provider plugins Task 8):
3
+ * OpenCode's PAID Go tier (`https://opencode.ai/zen/go/v1`) as one
4
+ * dynamic OpenAI-compatible provider. The `/zen/v1` free tier is
5
+ * client-gated by design (FreeTierError for non-CLI requests) and is out
6
+ * of scope.
7
+ *
8
+ * Structurally identical to sigrun-ollama (the Task 7 reference plugin):
9
+ * the default export is the sigrun extension-package factory returning a
10
+ * PROVIDER-ONLY contributor — `{ name, providers, health }` with no
11
+ * `extension` field — consumed structurally by sigrun's loader. The only
12
+ * intended deltas from the reference are the provider id, the base URL,
13
+ * and the ambient env key. Credentials resolve through sigrun's auth.json
14
+ * bridge (the stored credential arrives as `credential` on the auth
15
+ * resolver, ambient env as `ctx.env`) with `OPENCODE_GO_API_KEY` as the
16
+ * fallback; the plugin never reads auth.json or `process.env` itself.
17
+ *
18
+ * Runtime imports from pi-ai are deliberate (`createProvider` + the
19
+ * OpenAI-completions api); the package still declares pi-ai/pi-durable as
20
+ * devDependencies only, because sigrun (the host) ships them and
21
+ * `syncPackages` installs with `--omit=dev` — the provider-plugins spec's
22
+ * dependency posture.
23
+ */
24
+ import { type Provider } from "@earendil-works/pi-ai/models";
25
+ import type { Model } from "@earendil-works/pi-ai";
26
+ /** The provider id models are registered (and stamped) under. */
27
+ export declare const OPENCODE_PROVIDER_ID = "opencode";
28
+ /**
29
+ * OpenCode's paid Go tier (OpenAI-compatible endpoint). The operator can
30
+ * override per deployment via the plugin's scoped settings (`baseUrl`) —
31
+ * see {@link resolveOpencodeBaseUrl}.
32
+ */
33
+ export declare const OPENCODE_BASE_URL = "https://opencode.ai/zen/go/v1";
34
+ /** Ambient fallback when auth.json holds no credential. */
35
+ export declare const OPENCODE_API_KEY_ENV = "OPENCODE_GO_API_KEY";
36
+ /**
37
+ * Structural slice of sigrun's seam host (src/harness/seam-host.ts) a
38
+ * provider plugin may consume. Declared here — structurally, like the
39
+ * bridge's pi-ai re-declarations — because the plugin cannot import
40
+ * sigrun core. The scoped settings store is async-only, which is why the
41
+ * loader awaits factory results.
42
+ */
43
+ export interface SigrunPluginHost {
44
+ readonly stores: {
45
+ /** This plugin's own `settings/<slug>.json`; absent file → `{}`. */
46
+ readonly settings: {
47
+ get(): Promise<Record<string, unknown>>;
48
+ set(value: Record<string, unknown>): Promise<void>;
49
+ };
50
+ };
51
+ }
52
+ /** Sigrun's three-state per-provider readiness vocabulary (Task 6). */
53
+ export type ProviderReadinessStatus = "not_configured" | "degraded" | "ready";
54
+ /** One tracker report: the status plus optional operator-facing detail. */
55
+ export interface ProviderReadiness {
56
+ readonly status: ProviderReadinessStatus;
57
+ readonly detail?: string;
58
+ }
59
+ /** Sigrun's §3 health vocabulary. */
60
+ export type HealthStatus = "ok" | "degraded";
61
+ /**
62
+ * The provider-plugin factory result (the loader's Task 1 contract):
63
+ * a provider-only contributor — `{ name, providers, health? }`, no
64
+ * `extension`. Structural, like the host slice above.
65
+ */
66
+ export interface ProviderPluginResult {
67
+ readonly name: string;
68
+ readonly providers: readonly {
69
+ readonly provider: Provider;
70
+ readonly readiness?: () => ProviderReadiness;
71
+ }[];
72
+ readonly health?: () => HealthStatus;
73
+ }
74
+ /**
75
+ * Parse an OpenAI-compatible `[OI]` `/models` body into pi-ai chat
76
+ * models. Every entry is stamped `provider: "opencode"` — pi-ai's
77
+ * restore path drops stored models whose `provider` field differs from
78
+ * the registered id, so an unstamped catalog silently vanishes on cold
79
+ * boot. Malformed bodies throw (a refresh failure retains the previous
80
+ * catalog; garbage must never publish). Extra entry fields (the live
81
+ * endpoint serves id/object/created/owned_by) are ignored.
82
+ */
83
+ export declare function parseOpencodeCatalog(body: unknown, baseUrl: string): Model<"openai-completions">[];
84
+ /** One built plugin: the contribution's parts, sharing the tracker closure. */
85
+ export interface OpencodePlugin {
86
+ readonly provider: Provider;
87
+ /** Synchronous read over plugin-owned state (never network/storage). */
88
+ readonly readiness: () => ProviderReadiness;
89
+ readonly health: () => HealthStatus;
90
+ }
91
+ /**
92
+ * Build the OpenCode Go provider plus its readiness/health trackers.
93
+ * `baseUrl` is overridable so hermetic tests drive a fixture server; the
94
+ * default is the real Go-tier endpoint.
95
+ */
96
+ export declare function createOpencodePlugin(baseUrl?: string): OpencodePlugin;
97
+ /**
98
+ * Resolve the plugin's base URL from its scoped settings: `baseUrl`
99
+ * (string) overrides the {@link OPENCODE_BASE_URL} constant. Absent → the
100
+ * constant; present-but-not-a-non-empty-string → throw (a malformed
101
+ * override must quarantine loudly, not silently fall back to a possibly
102
+ * wrong constant).
103
+ */
104
+ export declare function resolveOpencodeBaseUrl(settings: Record<string, unknown>): string;
105
+ /**
106
+ * The sigrun extension-package factory (provider-only contributor).
107
+ * Arity 1 declares the host-taking signature — sigrun's loader passes the
108
+ * seam host only to factories that declare one (`factory.length > 0`) —
109
+ * and the factory is ASYNC because the scoped settings store is
110
+ * async-only; sigrun's loader awaits factory results. The base URL is
111
+ * deployment-configurable through the scoped settings (`baseUrl` in
112
+ * `settings/provider-opencode.json`, constant fallback): deployments behind
113
+ * a proxy or mirror — and hermetic test fixtures — redirect the endpoint
114
+ * that way (restart-required, exactly like sigrun-pareto/sigrun-bifrost).
115
+ */
116
+ export default function sigrunOpencode(host: SigrunPluginHost): Promise<ProviderPluginResult>;
package/dist/index.js ADDED
@@ -0,0 +1,249 @@
1
+ /**
2
+ * @sigrun/provider-opencode — a sigrun provider plugin (provider plugins Task 8):
3
+ * OpenCode's PAID Go tier (`https://opencode.ai/zen/go/v1`) as one
4
+ * dynamic OpenAI-compatible provider. The `/zen/v1` free tier is
5
+ * client-gated by design (FreeTierError for non-CLI requests) and is out
6
+ * of scope.
7
+ *
8
+ * Structurally identical to sigrun-ollama (the Task 7 reference plugin):
9
+ * the default export is the sigrun extension-package factory returning a
10
+ * PROVIDER-ONLY contributor — `{ name, providers, health }` with no
11
+ * `extension` field — consumed structurally by sigrun's loader. The only
12
+ * intended deltas from the reference are the provider id, the base URL,
13
+ * and the ambient env key. Credentials resolve through sigrun's auth.json
14
+ * bridge (the stored credential arrives as `credential` on the auth
15
+ * resolver, ambient env as `ctx.env`) with `OPENCODE_GO_API_KEY` as the
16
+ * fallback; the plugin never reads auth.json or `process.env` itself.
17
+ *
18
+ * Runtime imports from pi-ai are deliberate (`createProvider` + the
19
+ * OpenAI-completions api); the package still declares pi-ai/pi-durable as
20
+ * devDependencies only, because sigrun (the host) ships them and
21
+ * `syncPackages` installs with `--omit=dev` — the provider-plugins spec's
22
+ * dependency posture.
23
+ */
24
+ import { createProvider } from "@earendil-works/pi-ai/models";
25
+ import { openAICompletionsApi } from "@earendil-works/pi-ai/api/openai-completions.lazy";
26
+ /** The provider id models are registered (and stamped) under. */
27
+ export const OPENCODE_PROVIDER_ID = "opencode";
28
+ /**
29
+ * OpenCode's paid Go tier (OpenAI-compatible endpoint). The operator can
30
+ * override per deployment via the plugin's scoped settings (`baseUrl`) —
31
+ * see {@link resolveOpencodeBaseUrl}.
32
+ */
33
+ export const OPENCODE_BASE_URL = "https://opencode.ai/zen/go/v1";
34
+ /** Ambient fallback when auth.json holds no credential. */
35
+ export const OPENCODE_API_KEY_ENV = "OPENCODE_GO_API_KEY";
36
+ /**
37
+ * Conservative placeholders for catalog entries the Go tier's `/models`
38
+ * shape gives no metadata for (no window, no limits, no rates — entries
39
+ * carry only id/object/created/owned_by). Same values as the reference
40
+ * plugin and sigrun's degenerate env provider, keeping context
41
+ * management deterministic against unknown windows.
42
+ */
43
+ const DEFAULT_CONTEXT_WINDOW = 128_000;
44
+ const DEFAULT_MAX_TOKENS = 16_384;
45
+ /**
46
+ * Parse an OpenAI-compatible `[OI]` `/models` body into pi-ai chat
47
+ * models. Every entry is stamped `provider: "opencode"` — pi-ai's
48
+ * restore path drops stored models whose `provider` field differs from
49
+ * the registered id, so an unstamped catalog silently vanishes on cold
50
+ * boot. Malformed bodies throw (a refresh failure retains the previous
51
+ * catalog; garbage must never publish). Extra entry fields (the live
52
+ * endpoint serves id/object/created/owned_by) are ignored.
53
+ */
54
+ export function parseOpencodeCatalog(body, baseUrl) {
55
+ if (typeof body !== "object" || body === null) {
56
+ throw new Error("opencode /models body is not a JSON object");
57
+ }
58
+ const data = body.data;
59
+ if (!Array.isArray(data)) {
60
+ throw new Error("opencode /models body has no data array");
61
+ }
62
+ return data.map((entry) => {
63
+ const id = entry?.id;
64
+ if (typeof id !== "string" || id.length === 0) {
65
+ throw new Error("opencode /models entry without a string id");
66
+ }
67
+ const model = {
68
+ id,
69
+ name: id,
70
+ api: "openai-completions",
71
+ // THE stamp — see the doc comment above.
72
+ provider: OPENCODE_PROVIDER_ID,
73
+ baseUrl,
74
+ // The Go tier's /models carries no modality metadata; text is
75
+ // the safe floor (claiming image without evidence would ship
76
+ // image parts to models that cannot read them).
77
+ input: ["text"],
78
+ // Unknown rates: zeroed, usage tracking stays structural.
79
+ cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
80
+ reasoning: false,
81
+ contextWindow: DEFAULT_CONTEXT_WINDOW,
82
+ maxTokens: DEFAULT_MAX_TOKENS,
83
+ };
84
+ return model;
85
+ });
86
+ }
87
+ /**
88
+ * Build the OpenCode Go provider plus its readiness/health trackers.
89
+ * `baseUrl` is overridable so hermetic tests drive a fixture server; the
90
+ * default is the real Go-tier endpoint.
91
+ */
92
+ export function createOpencodePlugin(baseUrl = OPENCODE_BASE_URL) {
93
+ // Tracker state: seeded from credential resolution (pi-ai never
94
+ // refreshes an unconfigured provider, so refresh outcomes alone cannot
95
+ // express not_configured), updated per refresh outcome. Readiness and
96
+ // health are synchronous derivations over this state.
97
+ let credentialResolved = false;
98
+ let lastRefreshFailed = false;
99
+ let lastRefreshDetail;
100
+ let lastAnnounced;
101
+ function status() {
102
+ if (!credentialResolved)
103
+ return "not_configured";
104
+ return lastRefreshFailed ? "degraded" : "ready";
105
+ }
106
+ /** Apply a state change; `console.warn` on transitions only. */
107
+ function announce() {
108
+ const next = status();
109
+ if (next === lastAnnounced)
110
+ return;
111
+ console.warn(`provider-opencode: opencode ${lastAnnounced ?? "startup"} → ${next}` +
112
+ (lastRefreshDetail !== undefined && next === "degraded"
113
+ ? ` (${lastRefreshDetail})`
114
+ : ""));
115
+ lastAnnounced = next;
116
+ }
117
+ function noteCredential(resolved) {
118
+ credentialResolved = resolved;
119
+ announce();
120
+ }
121
+ function noteRefresh(ok, detail) {
122
+ lastRefreshFailed = !ok;
123
+ lastRefreshDetail = ok ? undefined : detail;
124
+ announce();
125
+ }
126
+ const provider = createProvider({
127
+ id: OPENCODE_PROVIDER_ID,
128
+ name: "OpenCode Go",
129
+ baseUrl,
130
+ // REQUIRED baseline: createProvider iterates it (omitting throws
131
+ // "baselineModels is not iterable"); empty = purely dynamic.
132
+ models: [],
133
+ auth: {
134
+ apiKey: {
135
+ name: "OpenCode Go API key (auth.json or OPENCODE_GO_API_KEY)",
136
+ resolve: async ({ credential, ctx }) => {
137
+ // Bridged auth.json credential (sigrun's bridge: the
138
+ // "default" entry first, then the provider's own) wins;
139
+ // the env var is the ambient fallback.
140
+ const stored = credential?.key?.trim() || undefined;
141
+ if (stored !== undefined) {
142
+ noteCredential(true);
143
+ return { auth: { apiKey: stored }, source: "auth.json" };
144
+ }
145
+ const envKey = (await ctx.env(OPENCODE_API_KEY_ENV))?.trim() || undefined;
146
+ if (envKey !== undefined) {
147
+ noteCredential(true);
148
+ return { auth: { apiKey: envKey }, source: OPENCODE_API_KEY_ENV };
149
+ }
150
+ // Unconfigured: undefined makes pi-ai skip the refresh
151
+ // phase entirely and requests fail loud.
152
+ noteCredential(false);
153
+ return undefined;
154
+ },
155
+ },
156
+ },
157
+ fetchModels: async (context) => {
158
+ // The context credential is whatever auth resolution produced —
159
+ // for this plugin always the merged api key; narrow rather than
160
+ // trust the union.
161
+ const key = context.credential?.type === "api_key"
162
+ ? context.credential.key?.trim() || undefined
163
+ : undefined;
164
+ if (key === undefined) {
165
+ noteCredential(false);
166
+ // NEVER return [] — the createProvider wrapper publishes +
167
+ // persists whatever fetchModels returns; an empty array
168
+ // would wipe the catalog.
169
+ throw new Error(`provider-opencode: OpenCode Go is not configured ` +
170
+ `(no auth.json credential and no ${OPENCODE_API_KEY_ENV}); ` +
171
+ "refusing to publish an empty catalog");
172
+ }
173
+ const effectiveBase = context.baseUrl ?? baseUrl;
174
+ // URL-join needs the trailing slash ("models" against a
175
+ // slash-less base would resolve to the host root).
176
+ const root = effectiveBase.endsWith("/") ? effectiveBase : `${effectiveBase}/`;
177
+ try {
178
+ const response = await fetch(new URL("models", root), {
179
+ headers: { Authorization: `Bearer ${key}` },
180
+ signal: context.signal,
181
+ });
182
+ if (!response.ok) {
183
+ throw new Error(`HTTP ${response.status}`);
184
+ }
185
+ const models = parseOpencodeCatalog(await response.json(), effectiveBase);
186
+ noteRefresh(true);
187
+ return models;
188
+ }
189
+ catch (cause) {
190
+ // An aborted refresh is not a failed one: pi-ai discards
191
+ // the outcome anyway, so the tracker must not move.
192
+ if (context.signal.aborted)
193
+ throw cause;
194
+ const message = cause instanceof Error ? cause.message : String(cause);
195
+ noteRefresh(false, message);
196
+ throw new Error(`provider-opencode: OpenCode Go catalog refresh failed: ${message}`);
197
+ }
198
+ },
199
+ api: openAICompletionsApi(),
200
+ });
201
+ return {
202
+ provider,
203
+ readiness: () => ({
204
+ status: status(),
205
+ ...(lastRefreshDetail !== undefined && status() === "degraded"
206
+ ? { detail: lastRefreshDetail }
207
+ : {}),
208
+ }),
209
+ // not_configured → ok (an unconfigured optional plugin is not a
210
+ // degraded process); configured-and-failing → degraded; else ok.
211
+ health: () => (status() === "degraded" ? "degraded" : "ok"),
212
+ };
213
+ }
214
+ /**
215
+ * Resolve the plugin's base URL from its scoped settings: `baseUrl`
216
+ * (string) overrides the {@link OPENCODE_BASE_URL} constant. Absent → the
217
+ * constant; present-but-not-a-non-empty-string → throw (a malformed
218
+ * override must quarantine loudly, not silently fall back to a possibly
219
+ * wrong constant).
220
+ */
221
+ export function resolveOpencodeBaseUrl(settings) {
222
+ if (!("baseUrl" in settings))
223
+ return OPENCODE_BASE_URL;
224
+ const value = settings.baseUrl;
225
+ if (typeof value !== "string" || value.trim().length === 0) {
226
+ throw new Error("provider-opencode: settings/provider-opencode.json baseUrl must be a non-empty string when present");
227
+ }
228
+ return value.trim();
229
+ }
230
+ /**
231
+ * The sigrun extension-package factory (provider-only contributor).
232
+ * Arity 1 declares the host-taking signature — sigrun's loader passes the
233
+ * seam host only to factories that declare one (`factory.length > 0`) —
234
+ * and the factory is ASYNC because the scoped settings store is
235
+ * async-only; sigrun's loader awaits factory results. The base URL is
236
+ * deployment-configurable through the scoped settings (`baseUrl` in
237
+ * `settings/provider-opencode.json`, constant fallback): deployments behind
238
+ * a proxy or mirror — and hermetic test fixtures — redirect the endpoint
239
+ * that way (restart-required, exactly like sigrun-pareto/sigrun-bifrost).
240
+ */
241
+ export default async function sigrunOpencode(host) {
242
+ const settings = await host.stores.settings.get();
243
+ const plugin = createOpencodePlugin(resolveOpencodeBaseUrl(settings));
244
+ return {
245
+ name: "provider-opencode",
246
+ providers: [{ provider: plugin.provider, readiness: plugin.readiness }],
247
+ health: plugin.health,
248
+ };
249
+ }
package/package.json CHANGED
@@ -1,6 +1,51 @@
1
1
  {
2
- "name": "@sigrun/provider-opencode",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
2
+ "name": "@sigrun/provider-opencode",
3
+ "version": "0.1.1",
4
+ "description": "Sigrun provider plugin: OpenCode's paid Go tier (OpenAI-compatible /zen/go/v1) as a dynamic provider",
5
+ "type": "module",
6
+ "engines": {
7
+ "node": ">=22.19.0"
8
+ },
9
+ "main": "dist/index.js",
10
+ "types": "dist/index.d.ts",
11
+ "exports": {
12
+ ".": {
13
+ "types": "./dist/index.d.ts",
14
+ "default": "./dist/index.js"
15
+ }
16
+ },
17
+ "files": [
18
+ "dist",
19
+ "CHANGELOG.md"
20
+ ],
21
+ "scripts": {
22
+ "test": "node scripts/check-manifest.mjs --expect-name @sigrun/provider-opencode && vitest run",
23
+ "typecheck": "tsc --noEmit",
24
+ "build": "tsc -p tsconfig.build.json",
25
+ "prepare": "npm run build"
26
+ },
27
+ "devDependencies": {
28
+ "@earendil-works/pi-ai": "1.3.0",
29
+ "@earendil-works/pi-durable": "1.3.0",
30
+ "@types/node": "^26.6.4",
31
+ "typescript": "^7.0.2",
32
+ "vitest": "^5.0.3"
33
+ },
34
+ "license": "MIT",
35
+ "repository": {
36
+ "type": "github",
37
+ "url": "git+https://github.com/sigrun-ai/sigrun-provider-opencode.git"
38
+ },
39
+ "bugs": {
40
+ "url": "https://github.com/sigrun-ai/sigrun-provider-opencode/issues"
41
+ },
42
+ "homepage": "https://github.com/sigrun-ai/sigrun-provider-opencode#readme",
43
+ "publishConfig": {
44
+ "access": "public",
45
+ "provenance": true
46
+ },
47
+ "peerDependencies": {
48
+ "@earendil-works/pi-ai": "1.3.0",
49
+ "@earendil-works/pi-durable": "1.3.0"
50
+ }
51
+ }