dsh-opencode 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.
- package/LICENSE +21 -0
- package/README.md +108 -0
- package/cordis.patch.yml +20 -0
- package/lib/adapter.js +109 -0
- package/lib/cache.js +171 -0
- package/lib/catalog.js +499 -0
- package/lib/commands.js +177 -0
- package/lib/config.d.ts +38 -0
- package/lib/config.js +142 -0
- package/lib/credentials.js +88 -0
- package/lib/index.d.ts +19 -0
- package/lib/index.js +208 -0
- package/lib/normalize.d.ts +10 -0
- package/lib/normalize.js +386 -0
- package/lib/snapshot.js +100 -0
- package/lib/transport.js +199 -0
- package/package.json +79 -0
package/lib/config.d.ts
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { Product } from "./normalize.js";
|
|
2
|
+
import { RetryPolicyConfig } from "@deepseek-ai/dsh-llm";
|
|
3
|
+
import z from "@deepseek-ai/schemastery";
|
|
4
|
+
//#region src/config.d.ts
|
|
5
|
+
/** Configuration for one fixed live route; the `providers` dict key IS the route. */
|
|
6
|
+
interface OpenCodeProviderConfig {
|
|
7
|
+
/** Which OpenCode product this route serves; must match the route key. */
|
|
8
|
+
product?: Product;
|
|
9
|
+
/** Credential reference (environment-variable name) resolved per request. */
|
|
10
|
+
apiKeyEnv?: string;
|
|
11
|
+
/** Display name shown by configuration surfaces. */
|
|
12
|
+
displayName?: string;
|
|
13
|
+
/** Additional deployment-owned request headers. */
|
|
14
|
+
headers?: Record<string, string>;
|
|
15
|
+
/** Provider-owned model-request retry policy. */
|
|
16
|
+
retryPolicy?: RetryPolicyConfig;
|
|
17
|
+
/** Maximum provider idle time while one stream read is outstanding. */
|
|
18
|
+
streamIdleTimeoutMs?: number;
|
|
19
|
+
}
|
|
20
|
+
/** Catalog refresh configuration. */
|
|
21
|
+
interface CatalogConfigInput {
|
|
22
|
+
refreshIntervalMs?: number;
|
|
23
|
+
listRevalidateAfterMs?: number;
|
|
24
|
+
timeoutMs?: number;
|
|
25
|
+
maxStaleMs?: number;
|
|
26
|
+
requireFresh?: boolean;
|
|
27
|
+
/** Cache file location; defaults under the DSH home. */
|
|
28
|
+
cachePath?: string;
|
|
29
|
+
}
|
|
30
|
+
/** Plugin configuration. */
|
|
31
|
+
interface Config {
|
|
32
|
+
providers?: Record<string, OpenCodeProviderConfig>;
|
|
33
|
+
catalog?: CatalogConfigInput;
|
|
34
|
+
}
|
|
35
|
+
/** Runtime schema for {@link Config}. */
|
|
36
|
+
declare const Config: z<Config>;
|
|
37
|
+
//#endregion
|
|
38
|
+
export { CatalogConfigInput, Config, OpenCodeProviderConfig };
|
package/lib/config.js
ADDED
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import { PRODUCT_BY_ROUTE, ROUTE_BY_PRODUCT } from "./normalize.js";
|
|
2
|
+
import { RetryPolicySchema, resolveRetryPolicy } from "@deepseek-ai/dsh-llm";
|
|
3
|
+
import z from "@deepseek-ai/schemastery";
|
|
4
|
+
import { credentialRef } from "@deepseek-ai/dsh-credentials";
|
|
5
|
+
//#region src/config.ts
|
|
6
|
+
/**
|
|
7
|
+
* Non-secret configuration schema for the `opencode-live` plugin.
|
|
8
|
+
*
|
|
9
|
+
* The composition base (cordis.patch.yml) and the optional user-settings
|
|
10
|
+
* section share this schema. Configuration carries references, never secret
|
|
11
|
+
* values: the API key is named through `apiKeyEnv` and resolved per request
|
|
12
|
+
* through the DSH credential seam.
|
|
13
|
+
*
|
|
14
|
+
* The plugin owns exactly two fixed routes. A provider entry keyed anything
|
|
15
|
+
* else, or keyed with the wrong product, is refused where it is written.
|
|
16
|
+
*
|
|
17
|
+
* @module opencode-live/config
|
|
18
|
+
*/
|
|
19
|
+
/** Upper bound shared with DSH's timer facilities. */
|
|
20
|
+
const MAX_TIMER_DELAY_MS = 2147483647;
|
|
21
|
+
/** The credential reference both OpenCode products document. */
|
|
22
|
+
const DEFAULT_API_KEY_ENV = "OPENCODE_API_KEY";
|
|
23
|
+
const DEFAULT_REFRESH_INTERVAL_MS = 9e5;
|
|
24
|
+
const DEFAULT_LIST_REVALIDATE_AFTER_MS = 6e4;
|
|
25
|
+
const DEFAULT_TIMEOUT_MS = 15e3;
|
|
26
|
+
const DEFAULT_MAX_STALE_MS = 6048e5;
|
|
27
|
+
const DEFAULT_STREAM_IDLE_TIMEOUT_MS = 3e5;
|
|
28
|
+
const DEFAULT_MAX_REQUEST_IMAGE_BYTES = 20 * 1024 * 1024;
|
|
29
|
+
const DEFAULT_REQUEST_IMAGE_PIXEL_BUDGET = 2048 * 2048;
|
|
30
|
+
const DEFAULT_REQUEST_IMAGE_MAX_BYTES = 1024 * 1024;
|
|
31
|
+
const productSchema = z.union([z.const("zen").required(), z.const("go").required()]);
|
|
32
|
+
const providerSchema = z.object({
|
|
33
|
+
product: productSchema,
|
|
34
|
+
apiKeyEnv: z.string().role("credential-ref"),
|
|
35
|
+
displayName: z.string(),
|
|
36
|
+
headers: z.dict(z.string()),
|
|
37
|
+
retryPolicy: RetryPolicySchema,
|
|
38
|
+
streamIdleTimeoutMs: z.number().min(Number.MIN_VALUE).max(MAX_TIMER_DELAY_MS)
|
|
39
|
+
});
|
|
40
|
+
const catalogSchema = z.object({
|
|
41
|
+
refreshIntervalMs: z.number().step(1).min(1),
|
|
42
|
+
listRevalidateAfterMs: z.number().step(1).min(1),
|
|
43
|
+
timeoutMs: z.number().step(1).min(1),
|
|
44
|
+
maxStaleMs: z.number().step(1).min(1),
|
|
45
|
+
requireFresh: z.boolean(),
|
|
46
|
+
cachePath: z.string()
|
|
47
|
+
});
|
|
48
|
+
/** Runtime schema for {@link Config}. */
|
|
49
|
+
const Config = z.object({
|
|
50
|
+
providers: z.dict(providerSchema),
|
|
51
|
+
catalog: catalogSchema.default({})
|
|
52
|
+
});
|
|
53
|
+
/** Whether one number is a positive finite integer within timer bounds. */
|
|
54
|
+
function isBoundedPositiveInteger(value, max) {
|
|
55
|
+
return typeof value === "number" && Number.isSafeInteger(value) && value > 0 && value <= max;
|
|
56
|
+
}
|
|
57
|
+
/** Validate one bounded catalog interval field. */
|
|
58
|
+
function requireInterval(value, fallback, label, max = MAX_TIMER_DELAY_MS) {
|
|
59
|
+
const resolved = value ?? fallback;
|
|
60
|
+
if (!isBoundedPositiveInteger(resolved, max)) throw new Error(`opencode-live: catalog.${label} must be a positive integer no greater than ${max}`);
|
|
61
|
+
return resolved;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Validate configuration and resolve every default. This is the one explicit
|
|
65
|
+
* resolve step; an invalid route key, a product/route mismatch, or an out-of
|
|
66
|
+
* -bound interval fails loudly here instead of disabling a route silently.
|
|
67
|
+
* @param config - the raw configuration value.
|
|
68
|
+
* @returns detached validated configuration.
|
|
69
|
+
*/
|
|
70
|
+
function resolveConfig(config) {
|
|
71
|
+
const providers = /* @__PURE__ */ new Map();
|
|
72
|
+
const entries = Object.entries(config.providers ?? {});
|
|
73
|
+
if (entries.length === 0) for (const route of [ROUTE_BY_PRODUCT.zen, ROUTE_BY_PRODUCT.go]) providers.set(route, resolveProvider(route, {
|
|
74
|
+
product: PRODUCT_BY_ROUTE[route],
|
|
75
|
+
apiKeyEnv: DEFAULT_API_KEY_ENV
|
|
76
|
+
}));
|
|
77
|
+
for (const [route, source] of entries) {
|
|
78
|
+
if (route !== ROUTE_BY_PRODUCT.zen && route !== ROUTE_BY_PRODUCT.go) throw new Error(`opencode-live: provider "${route}" is not a route this plugin owns; the fixed routes are ${ROUTE_BY_PRODUCT.zen} and ${ROUTE_BY_PRODUCT.go}`);
|
|
79
|
+
const resolved = resolveProvider(route, source);
|
|
80
|
+
if (providers.get(resolved.route) !== void 0) throw new Error(`opencode-live: provider "${route}" is declared twice`);
|
|
81
|
+
providers.set(resolved.route, resolved);
|
|
82
|
+
}
|
|
83
|
+
return {
|
|
84
|
+
providers,
|
|
85
|
+
catalog: {
|
|
86
|
+
refreshIntervalMs: requireInterval(config.catalog?.refreshIntervalMs, DEFAULT_REFRESH_INTERVAL_MS, "refreshIntervalMs"),
|
|
87
|
+
listRevalidateAfterMs: requireInterval(config.catalog?.listRevalidateAfterMs, DEFAULT_LIST_REVALIDATE_AFTER_MS, "listRevalidateAfterMs"),
|
|
88
|
+
timeoutMs: requireInterval(config.catalog?.timeoutMs, DEFAULT_TIMEOUT_MS, "timeoutMs"),
|
|
89
|
+
maxStaleMs: requireInterval(config.catalog?.maxStaleMs, DEFAULT_MAX_STALE_MS, "maxStaleMs"),
|
|
90
|
+
requireFresh: config.catalog?.requireFresh ?? false,
|
|
91
|
+
...config.catalog?.cachePath !== void 0 ? { cachePath: requireCachePath(config.catalog.cachePath) } : {}
|
|
92
|
+
}
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
/** Validate one provider entry against its fixed route. */
|
|
96
|
+
function resolveProvider(route, source) {
|
|
97
|
+
const product = source.product ?? PRODUCT_BY_ROUTE[route];
|
|
98
|
+
if (PRODUCT_BY_ROUTE[route] !== product) throw new Error(`opencode-live: provider "${route}" declares product "${source.product}", but this route serves "${PRODUCT_BY_ROUTE[route]}"`);
|
|
99
|
+
const apiKeyEnv = credentialRef(source.apiKeyEnv ?? "OPENCODE_API_KEY");
|
|
100
|
+
const streamIdleTimeoutMs = source.streamIdleTimeoutMs ?? 3e5;
|
|
101
|
+
if (!Number.isFinite(streamIdleTimeoutMs) || streamIdleTimeoutMs <= 0 || streamIdleTimeoutMs > MAX_TIMER_DELAY_MS) throw new Error(`opencode-live: provider "${route}" streamIdleTimeoutMs must be a positive finite number no greater than ${MAX_TIMER_DELAY_MS}`);
|
|
102
|
+
if (source.displayName !== void 0 && source.displayName.length === 0) throw new Error(`opencode-live: provider "${route}" has an empty displayName`);
|
|
103
|
+
assertValidHeaders(route, source.headers);
|
|
104
|
+
return {
|
|
105
|
+
route,
|
|
106
|
+
product,
|
|
107
|
+
apiKeyEnv,
|
|
108
|
+
displayName: source.displayName ?? defaultDisplayName(route),
|
|
109
|
+
...source.headers !== void 0 ? { headers: { ...source.headers } } : {},
|
|
110
|
+
retryPolicy: resolveRetryPolicy(source.retryPolicy, `opencode-live: provider "${route}" retryPolicy`),
|
|
111
|
+
streamIdleTimeoutMs
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
/** The default display name for one fixed route. */
|
|
115
|
+
function defaultDisplayName(route) {
|
|
116
|
+
return route === ROUTE_BY_PRODUCT.zen ? "OpenCode Zen (Live)" : "OpenCode Go (Live)";
|
|
117
|
+
}
|
|
118
|
+
/** Reject a header Fetch cannot carry, naming the route and field. */
|
|
119
|
+
function assertValidHeaders(route, headers) {
|
|
120
|
+
for (const [name, value] of Object.entries(headers ?? {})) try {
|
|
121
|
+
new Headers([[name, value]]);
|
|
122
|
+
} catch {
|
|
123
|
+
throw new Error(`opencode-live: provider "${route}" header "${name}" is not valid for Fetch; use a valid HTTP field name and a single-line value representable as bytes`);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
/** Validate the optional cache path override. */
|
|
127
|
+
function requireCachePath(path) {
|
|
128
|
+
if (path.length === 0) throw new Error("opencode-live: catalog.cachePath must be a non-empty path when set");
|
|
129
|
+
return path;
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Refuse a section this plugin could not serve. Registered as the settings
|
|
133
|
+
* namespace's validator so an invalid route or interval is rejected where it
|
|
134
|
+
* is written instead of silently disabling a route.
|
|
135
|
+
* @param config - the resolved section to check.
|
|
136
|
+
* @throws Error naming the offending configuration entry.
|
|
137
|
+
*/
|
|
138
|
+
function assertServiceable(config) {
|
|
139
|
+
resolveConfig(config);
|
|
140
|
+
}
|
|
141
|
+
//#endregion
|
|
142
|
+
export { Config, DEFAULT_API_KEY_ENV, DEFAULT_LIST_REVALIDATE_AFTER_MS, DEFAULT_MAX_REQUEST_IMAGE_BYTES, DEFAULT_MAX_STALE_MS, DEFAULT_REFRESH_INTERVAL_MS, DEFAULT_REQUEST_IMAGE_MAX_BYTES, DEFAULT_REQUEST_IMAGE_PIXEL_BUDGET, DEFAULT_STREAM_IDLE_TIMEOUT_MS, DEFAULT_TIMEOUT_MS, assertServiceable, resolveConfig };
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { LlmError, assertUsableApiKey } from "@deepseek-ai/dsh-llm";
|
|
2
|
+
import "@deepseek-ai/dsh-credentials";
|
|
3
|
+
import { launchEnvironmentOf } from "@deepseek-ai/dsh-launch-environment";
|
|
4
|
+
//#region src/credentials.ts
|
|
5
|
+
/**
|
|
6
|
+
* Resolve the API key for one inference call.
|
|
7
|
+
*
|
|
8
|
+
* Mirrors the fail-loud reference semantics of the DSH pi-ai adapter: a named
|
|
9
|
+
* reference that misses throws `MISSING_CREDENTIAL` naming the route and the
|
|
10
|
+
* reference, never a key fragment, and never falls back to an ambient key
|
|
11
|
+
* another provider might have left in the environment.
|
|
12
|
+
* @param ctx - the plugin context carrying the optional credential service.
|
|
13
|
+
* @param route - the live route the credential is resolved for.
|
|
14
|
+
* @param profile - the resolved profile naming the credential reference.
|
|
15
|
+
* @returns the validated key for this call.
|
|
16
|
+
* @throws {LlmError} code `MISSING_CREDENTIAL` when the reference is unset.
|
|
17
|
+
*/
|
|
18
|
+
async function resolveApiKeyFor(ctx, route, profile) {
|
|
19
|
+
const ref = profile.apiKeyEnv;
|
|
20
|
+
if (ref === void 0) return void 0;
|
|
21
|
+
const credentials = ctx.get("credentials");
|
|
22
|
+
const hit = credentials !== void 0 ? (await credentials.resolve(ref))?.value : launchEnvironmentOf(ctx).get(ref)?.value;
|
|
23
|
+
if (hit !== void 0 && hit.length > 0) return assertUsableApiKey(hit, "opencode-live", ref);
|
|
24
|
+
throw new LlmError(`opencode-live: no credential for provider route "${route}"; its profile resolves ${ref}, which is not set — store it through the credentials service (the web Models page writes it) or export it in the launching environment`, "MISSING_CREDENTIAL");
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Api-key auth for a route the plugin authenticates itself.
|
|
28
|
+
*
|
|
29
|
+
* `Models` calls this when the request carries no `apiKey` override (for
|
|
30
|
+
* example a status check): it reports the route as unconfigured rather than
|
|
31
|
+
* inventing ambient credentials. The per-request key override remains the
|
|
32
|
+
* only real authentication path.
|
|
33
|
+
* @param name - display name used as the resolution's status label.
|
|
34
|
+
* @returns the api-key auth method.
|
|
35
|
+
*/
|
|
36
|
+
function apiKeyOnlyAuth(name) {
|
|
37
|
+
return {
|
|
38
|
+
name,
|
|
39
|
+
resolve: ({ credential }) => Promise.resolve({
|
|
40
|
+
auth: credential?.key === void 0 ? {} : { apiKey: credential.key },
|
|
41
|
+
source: name
|
|
42
|
+
})
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Whether the configured reference is currently set, for status display.
|
|
47
|
+
* Presence is reported without ever exposing the value.
|
|
48
|
+
* @param ctx - the plugin context.
|
|
49
|
+
* @param ref - the credential reference to describe.
|
|
50
|
+
* @returns presence facts, or `undefined` without a credential service.
|
|
51
|
+
*/
|
|
52
|
+
async function describeCredential(ctx, ref) {
|
|
53
|
+
const credentials = ctx.get("credentials");
|
|
54
|
+
if (credentials === void 0) return void 0;
|
|
55
|
+
return credentials.describe(ref);
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* The stable auth bridge handed to `PiAiAdapter`.
|
|
59
|
+
*
|
|
60
|
+
* pi-ai requires both members; this plugin's routes authenticate exclusively
|
|
61
|
+
* through the per-request key, so the store is inert (nothing to read, no
|
|
62
|
+
* sign-in to persist) and the context never answers ambient questions. A
|
|
63
|
+
* write attempt is refused loudly rather than silently dropped: a login that
|
|
64
|
+
* believed it persisted would fail every later request.
|
|
65
|
+
* @returns the injection for `createModels()`.
|
|
66
|
+
*/
|
|
67
|
+
function staticAuthBridge() {
|
|
68
|
+
return {
|
|
69
|
+
credentials: {
|
|
70
|
+
async read() {},
|
|
71
|
+
async list() {
|
|
72
|
+
return [];
|
|
73
|
+
},
|
|
74
|
+
async modify() {
|
|
75
|
+
throw new LlmError("opencode-live: stored credential sign-ins are not supported; its routes authenticate through the apiKeyEnv reference resolved by the DSH credential service", "NO_CREDENTIAL_STORE");
|
|
76
|
+
},
|
|
77
|
+
async delete() {}
|
|
78
|
+
},
|
|
79
|
+
authContext: {
|
|
80
|
+
async env() {},
|
|
81
|
+
async fileExists() {
|
|
82
|
+
return false;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
//#endregion
|
|
88
|
+
export { apiKeyOnlyAuth, describeCredential, resolveApiKeyFor, staticAuthBridge };
|
package/lib/index.d.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { Product, ROUTE_BY_PRODUCT, RouteId } from "./normalize.js";
|
|
2
|
+
import { Config } from "./config.js";
|
|
3
|
+
import { Context } from "@deepseek-ai/cordis";
|
|
4
|
+
|
|
5
|
+
//#region src/index.d.ts
|
|
6
|
+
/** Cordis plugin name. */
|
|
7
|
+
declare const name = "opencode-live";
|
|
8
|
+
/** Services required before the plugin can activate. */
|
|
9
|
+
declare const inject: string[];
|
|
10
|
+
/** The settings namespace this plugin owns. */
|
|
11
|
+
declare const SETTINGS_NAMESPACE = "opencode-live";
|
|
12
|
+
/**
|
|
13
|
+
* Register the plugin against one composition context.
|
|
14
|
+
* @param ctx - the Cordis context.
|
|
15
|
+
* @param config - the composition base configuration for this plugin.
|
|
16
|
+
*/
|
|
17
|
+
declare function apply(ctx: Context, config?: Config): void;
|
|
18
|
+
//#endregion
|
|
19
|
+
export { Config, type Product, ROUTE_BY_PRODUCT, type RouteId, SETTINGS_NAMESPACE, apply, inject, name };
|
package/lib/index.js
ADDED
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
import { ROUTE_BY_PRODUCT } from "./normalize.js";
|
|
2
|
+
import { LiveOpenCodeAdapter } from "./adapter.js";
|
|
3
|
+
import { readySetHash } from "./snapshot.js";
|
|
4
|
+
import { CatalogManager } from "./catalog.js";
|
|
5
|
+
import { Config, DEFAULT_MAX_REQUEST_IMAGE_BYTES, DEFAULT_REQUEST_IMAGE_MAX_BYTES, DEFAULT_REQUEST_IMAGE_PIXEL_BUDGET, assertServiceable, resolveConfig } from "./config.js";
|
|
6
|
+
import { apiKeyOnlyAuth, describeCredential, resolveApiKeyFor, staticAuthBridge } from "./credentials.js";
|
|
7
|
+
import { registerCommands } from "./commands.js";
|
|
8
|
+
import { buildRouteProvider } from "./transport.js";
|
|
9
|
+
import { resolveImageAttachmentAccess, resolveRetryPolicy } from "@deepseek-ai/dsh-llm";
|
|
10
|
+
import { dshHomePath } from "@deepseek-ai/dsh-home-paths";
|
|
11
|
+
//#region src/index.ts
|
|
12
|
+
/** Cordis plugin name. */
|
|
13
|
+
const name = "opencode-live";
|
|
14
|
+
/** Services required before the plugin can activate. */
|
|
15
|
+
const inject = ["llm"];
|
|
16
|
+
/** The settings namespace this plugin owns. */
|
|
17
|
+
const SETTINGS_NAMESPACE = "opencode-live";
|
|
18
|
+
/**
|
|
19
|
+
* Register the plugin against one composition context.
|
|
20
|
+
* @param ctx - the Cordis context.
|
|
21
|
+
* @param config - the composition base configuration for this plugin.
|
|
22
|
+
*/
|
|
23
|
+
function apply(ctx, config = {}) {
|
|
24
|
+
const resolved = resolveConfig(config);
|
|
25
|
+
let currentConfig = resolved;
|
|
26
|
+
let configRevision = 0;
|
|
27
|
+
const catalog = new CatalogManager({
|
|
28
|
+
config: {
|
|
29
|
+
...resolved.catalog,
|
|
30
|
+
...resolved.catalog.cachePath === void 0 ? { cachePath: defaultCachePath() } : {}
|
|
31
|
+
},
|
|
32
|
+
onChange: (snapshot) => onCatalogChange(snapshot),
|
|
33
|
+
warn: (message) => ctx.logger.warn(message)
|
|
34
|
+
});
|
|
35
|
+
const adapter = new LiveOpenCodeAdapter({
|
|
36
|
+
profiles,
|
|
37
|
+
resolveApiKey: (route, profile) => resolveApiKeyFor(ctx, route, profile),
|
|
38
|
+
auth: staticAuthBridge(),
|
|
39
|
+
resolveAttachments: () => ctx.get("attachments"),
|
|
40
|
+
resolveImageAccess: (attachments, ref) => resolveImageAttachmentAccess(attachments, (hostPath) => ctx.get("fs")?.processPathFromHostPath(hostPath), ref),
|
|
41
|
+
catalog,
|
|
42
|
+
initialWaitMs: currentConfig.catalog.timeoutMs,
|
|
43
|
+
requireFresh: currentConfig.catalog.requireFresh,
|
|
44
|
+
onReplayDegrade: ({ provider, model, reason }) => {
|
|
45
|
+
ctx.logger.warn(`opencode-live: unusable replay state on assistant history for route "${provider}/${model}"; sending that message as provider-neutral content (${reason})`);
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
/** Memoized profiles keyed by configuration revision and catalog content. */
|
|
49
|
+
let memoKey;
|
|
50
|
+
let memoProfiles;
|
|
51
|
+
function profiles() {
|
|
52
|
+
const key = `${configRevision}:${catalog.current?.contentHash ?? "empty"}`;
|
|
53
|
+
if (memoKey === key && memoProfiles !== void 0) return memoProfiles;
|
|
54
|
+
const map = /* @__PURE__ */ new Map();
|
|
55
|
+
for (const [route, providerConfig] of currentConfig.providers) {
|
|
56
|
+
const view = catalog.current?.products[providerConfig.product];
|
|
57
|
+
const ready = view === void 0 ? [] : view.readyIds.map((id) => view.candidates.get(id)).filter((candidate) => candidate !== void 0);
|
|
58
|
+
map.set(route, {
|
|
59
|
+
provider: route,
|
|
60
|
+
displayName: providerConfig.displayName,
|
|
61
|
+
apiKeyEnv: providerConfig.apiKeyEnv,
|
|
62
|
+
streamIdleTimeoutMs: providerConfig.streamIdleTimeoutMs,
|
|
63
|
+
maxRequestImageBytes: DEFAULT_MAX_REQUEST_IMAGE_BYTES,
|
|
64
|
+
requestImagePixelBudget: DEFAULT_REQUEST_IMAGE_PIXEL_BUDGET,
|
|
65
|
+
requestImageMaxBytes: DEFAULT_REQUEST_IMAGE_MAX_BYTES,
|
|
66
|
+
retryPolicy: providerConfig.retryPolicy ?? resolveRetryPolicy(void 0, `opencode-live: provider "${route}" retryPolicy`),
|
|
67
|
+
...providerConfig.headers !== void 0 ? { headers: { ...providerConfig.headers } } : {},
|
|
68
|
+
configuredMaxTokens: /* @__PURE__ */ new Map(),
|
|
69
|
+
piProvider: buildRouteProvider({
|
|
70
|
+
route,
|
|
71
|
+
product: providerConfig.product,
|
|
72
|
+
displayName: providerConfig.displayName,
|
|
73
|
+
ready,
|
|
74
|
+
auth: { apiKey: apiKeyOnlyAuth(providerConfig.displayName) }
|
|
75
|
+
})
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
memoKey = key;
|
|
79
|
+
memoProfiles = map;
|
|
80
|
+
return map;
|
|
81
|
+
}
|
|
82
|
+
let registration;
|
|
83
|
+
let registeredFacts;
|
|
84
|
+
let lastReadyHash;
|
|
85
|
+
/** The current route list in fixed order. */
|
|
86
|
+
function routeList() {
|
|
87
|
+
return [...currentConfig.providers.keys()];
|
|
88
|
+
}
|
|
89
|
+
/** Registration facts: routes with their display names and retry policies. */
|
|
90
|
+
function registrationFacts() {
|
|
91
|
+
return JSON.stringify(routeList().map((route) => {
|
|
92
|
+
const provider = currentConfig.providers.get(route);
|
|
93
|
+
return {
|
|
94
|
+
route,
|
|
95
|
+
displayName: provider?.displayName,
|
|
96
|
+
retryPolicy: provider?.retryPolicy
|
|
97
|
+
};
|
|
98
|
+
}));
|
|
99
|
+
}
|
|
100
|
+
/** Register or atomically replace this adapter's routes. */
|
|
101
|
+
function ensureRegistration() {
|
|
102
|
+
const routes = routeList();
|
|
103
|
+
const facts = registrationFacts();
|
|
104
|
+
if (registration === void 0) {
|
|
105
|
+
if (routes.length === 0) {
|
|
106
|
+
registeredFacts = facts;
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
registration = ctx.llm.registerAdapter(routes, adapter);
|
|
110
|
+
registeredFacts = facts;
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
registration.replace(routes);
|
|
114
|
+
registeredFacts = facts;
|
|
115
|
+
}
|
|
116
|
+
/** The directory entries for the two fixed routes. */
|
|
117
|
+
function directoryEntries() {
|
|
118
|
+
return routeList().map((route) => ({
|
|
119
|
+
provider: route,
|
|
120
|
+
displayName: currentConfig.providers.get(route)?.displayName ?? route,
|
|
121
|
+
settingsNs: SETTINGS_NAMESPACE,
|
|
122
|
+
settingsPath: ["providers", route],
|
|
123
|
+
declared: true
|
|
124
|
+
}));
|
|
125
|
+
}
|
|
126
|
+
let directory;
|
|
127
|
+
/** Register or atomically replace the configurable-provider directory. */
|
|
128
|
+
function ensureDirectory() {
|
|
129
|
+
const entries = directoryEntries();
|
|
130
|
+
JSON.stringify(entries);
|
|
131
|
+
if (directory === void 0) {
|
|
132
|
+
if (entries.length === 0) return;
|
|
133
|
+
directory = ctx.llm.registerConfigurableProviders(entries);
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
directory.replace(entries);
|
|
137
|
+
}
|
|
138
|
+
/** Re-register only when the executable set actually changed. */
|
|
139
|
+
function onCatalogChange(snapshot) {
|
|
140
|
+
if (snapshot === void 0) return;
|
|
141
|
+
const hash = readySetHash(snapshot);
|
|
142
|
+
if (hash === lastReadyHash) return;
|
|
143
|
+
lastReadyHash = hash;
|
|
144
|
+
if (registration !== void 0) ensureRegistration();
|
|
145
|
+
}
|
|
146
|
+
ensureRegistration();
|
|
147
|
+
ensureDirectory();
|
|
148
|
+
let commandDisposers = [];
|
|
149
|
+
ctx.inject(["commands"], (commandsCtx) => {
|
|
150
|
+
commandDisposers = registerCommands(commandsCtx, {
|
|
151
|
+
catalog,
|
|
152
|
+
config: () => currentConfig,
|
|
153
|
+
describeCredential: async (route) => {
|
|
154
|
+
const provider = currentConfig.providers.get(route);
|
|
155
|
+
if (provider === void 0) return void 0;
|
|
156
|
+
return describeCredential(ctx, provider.apiKeyEnv);
|
|
157
|
+
}
|
|
158
|
+
});
|
|
159
|
+
});
|
|
160
|
+
let source = () => config;
|
|
161
|
+
ctx.inject(["settings"], (settingsCtx) => {
|
|
162
|
+
settingsCtx.settings.installSection(ctx, SETTINGS_NAMESPACE, Config, config, {
|
|
163
|
+
validate: assertServiceable,
|
|
164
|
+
setSource: (next) => {
|
|
165
|
+
source = next;
|
|
166
|
+
},
|
|
167
|
+
onChange: () => {
|
|
168
|
+
try {
|
|
169
|
+
const next = resolveConfig(source());
|
|
170
|
+
const catalogChanged = JSON.stringify(next.catalog) !== JSON.stringify(currentConfig.catalog);
|
|
171
|
+
const routesChanged = registrationFacts() !== registeredFacts;
|
|
172
|
+
currentConfig = next;
|
|
173
|
+
configRevision += 1;
|
|
174
|
+
adapter.updateOptions({
|
|
175
|
+
initialWaitMs: next.catalog.timeoutMs,
|
|
176
|
+
requireFresh: next.catalog.requireFresh
|
|
177
|
+
});
|
|
178
|
+
if (catalogChanged) catalog.reconfigure({
|
|
179
|
+
...next.catalog,
|
|
180
|
+
...next.catalog.cachePath === void 0 ? {} : { cachePath: next.catalog.cachePath }
|
|
181
|
+
});
|
|
182
|
+
if (routesChanged) ensureRegistration();
|
|
183
|
+
ensureDirectory();
|
|
184
|
+
} catch (error) {
|
|
185
|
+
ctx.logger.error("opencode-live: keeping the previously registered routes after a refused settings update");
|
|
186
|
+
ctx.logger.error(error);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
});
|
|
190
|
+
});
|
|
191
|
+
ctx.effect(function* () {
|
|
192
|
+
yield () => {
|
|
193
|
+
catalog.stop();
|
|
194
|
+
for (const dispose of commandDisposers) dispose();
|
|
195
|
+
commandDisposers = [];
|
|
196
|
+
};
|
|
197
|
+
}, "opencode-live effects");
|
|
198
|
+
catalog.start().catch((error) => {
|
|
199
|
+
ctx.logger.warn("opencode-live: the initial catalog refresh failed; the catalog will retry periodically");
|
|
200
|
+
ctx.logger.warn(error);
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
/** The default cache location under the resolved DSH home. */
|
|
204
|
+
function defaultCachePath() {
|
|
205
|
+
return dshHomePath("cache", "opencode-live", "catalog.json");
|
|
206
|
+
}
|
|
207
|
+
//#endregion
|
|
208
|
+
export { Config, ROUTE_BY_PRODUCT, SETTINGS_NAMESPACE, apply, inject, name };
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
//#region src/normalize.d.ts
|
|
2
|
+
/** The two OpenCode products this plugin serves. */
|
|
3
|
+
type Product = 'zen' | 'go';
|
|
4
|
+
/** DSH route keys this plugin registers; fixed for the plugin's lifetime. */
|
|
5
|
+
declare const ROUTE_ZEN = "opencode-zen-live";
|
|
6
|
+
declare const ROUTE_GO = "opencode-go-live";
|
|
7
|
+
type RouteId = typeof ROUTE_ZEN | typeof ROUTE_GO;
|
|
8
|
+
declare const ROUTE_BY_PRODUCT: Readonly<Record<Product, RouteId>>;
|
|
9
|
+
//#endregion
|
|
10
|
+
export { Product, ROUTE_BY_PRODUCT, ROUTE_GO, ROUTE_ZEN, RouteId };
|