@dan-ai-studio/dshopencodego 0.1.21 → 0.1.23

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/lib/index.js CHANGED
@@ -73,7 +73,8 @@ import { attributionHeaders as attributionHeaders3, LlmError as LlmError4 } from
73
73
  // src/catalog/constants.ts
74
74
  var PROVIDER_ID = "opencode-go";
75
75
  var DISPLAY_NAME = "OpenCode Go";
76
- var DEFAULT_BASE_URL = "https://opencode.ai/zen/go/v1";
76
+ var DEFAULT_BASE_URL = "https://opencode.ai/inference/go/v1";
77
+ var CONSOLE_BASE_URL = "https://opencode.ai/inference/v1";
77
78
  var MODEL_METADATA_URL = "https://models.dev/api.json";
78
79
  var MODEL_METADATA_PROVIDER = "opencode-go";
79
80
  var METADATA_FETCH_TIMEOUT_MS = 1e4;
@@ -1177,6 +1178,7 @@ var ACCOUNT_QUOTA_CODE = "ACCOUNT_QUOTA";
1177
1178
  function classifyPiAiError(message) {
1178
1179
  if (/\b(?:401|403)\b/.test(message)) return "AUTH";
1179
1180
  if (isQuotaExceededError(message)) return ACCOUNT_QUOTA_CODE;
1181
+ if (/server_error/i.test(message)) return "SERVER";
1180
1182
  if (/\b429\b|rate.?limit/i.test(message)) return "RATE_LIMIT";
1181
1183
  if (/\b413\b|payload too large|request body too large|length limit exceeded/i.test(message)) return "INVALID_REQUEST";
1182
1184
  if (/\b400\b|invalid.?request/i.test(message)) return "INVALID_REQUEST";
@@ -1347,7 +1349,7 @@ var SEED_QUOTAS = {
1347
1349
  "deepseek-v4-flash-vision-exp": { monthlyUsd: 15, monthlyRequests: 32500 },
1348
1350
  "hy4-preview": { monthlyUsd: 30, monthlyRequests: 6770 },
1349
1351
  "hy3": { monthlyUsd: 60, monthlyRequests: 21500 },
1350
- "space-bunny-free": { monthlyUsd: "unlimited", monthlyRequests: "unlimited" },
1352
+ // 注意:space-bunny-free 并不在 Go 端点支持的 37 个模型列表中(请求会返回 400 错误),已依据 R-B4 予以剔除
1351
1353
  "grok-4.7": { monthlyUsd: 15, monthlyRequests: 845 },
1352
1354
  "grok-4.6": { monthlyUsd: 15, monthlyRequests: 845 },
1353
1355
  "gpt-6-luna": { monthlyUsd: 15, monthlyRequests: 21130 },
@@ -1396,12 +1398,15 @@ function sortModels(models, now = Date.now()) {
1396
1398
  import { randomUUID } from "node:crypto";
1397
1399
  import { attributionHeaders as attributionHeaders4 } from "@deepseek-ai/dsh-llm";
1398
1400
  var SESSION_HEADER = "x-opencode-session";
1401
+ var SESSION_AFFINITY_HEADER = "x-session-affinity";
1399
1402
  function opencodeSessionValue(sessionId) {
1400
1403
  return sessionId !== void 0 && sessionId.length > 0 ? sessionId : randomUUID();
1401
1404
  }
1402
1405
  function providerHeaders(sessionId) {
1406
+ const session = opencodeSessionValue(sessionId);
1403
1407
  return {
1404
- [SESSION_HEADER]: opencodeSessionValue(sessionId),
1408
+ [SESSION_HEADER]: session,
1409
+ [SESSION_AFFINITY_HEADER]: session,
1405
1410
  ...attributionHeaders4()
1406
1411
  };
1407
1412
  }
@@ -1762,6 +1767,44 @@ var catalogRemote = {
1762
1767
  ]
1763
1768
  };
1764
1769
 
1770
+ // src/update/contract.ts
1771
+ function parseUpdateStatus(value) {
1772
+ if (value === null || typeof value !== "object") throw new Error("invalid update reading");
1773
+ const row = value;
1774
+ if (typeof row["current"] !== "string") throw new Error("invalid update reading");
1775
+ if (row["latest"] !== void 0 && typeof row["latest"] !== "string") throw new Error("invalid update reading");
1776
+ if (typeof row["available"] !== "boolean") throw new Error("invalid update reading");
1777
+ if (typeof row["checkedAt"] !== "number" || !Number.isFinite(row["checkedAt"])) {
1778
+ throw new Error("invalid update reading");
1779
+ }
1780
+ return {
1781
+ current: row["current"],
1782
+ latest: typeof row["latest"] === "string" ? row["latest"] : void 0,
1783
+ available: row["available"],
1784
+ checkedAt: row["checkedAt"]
1785
+ };
1786
+ }
1787
+ var codec2 = (typeSymbol, parse) => ({
1788
+ mode: "strict",
1789
+ typeSymbol,
1790
+ schema: { parse },
1791
+ create: () => ({ parse })
1792
+ });
1793
+ var updateRemote = {
1794
+ package: "@dan-ai-studio/dshopencodego",
1795
+ descriptors: [
1796
+ {
1797
+ id: "@dan-ai-studio/dshopencodego#opencodeGoUpdate/readUpdate",
1798
+ service: "opencodeGoUpdate",
1799
+ namespace: "opencodeGoUpdate",
1800
+ method: "readUpdate",
1801
+ invocation: { kind: "direct" },
1802
+ parameters: [],
1803
+ result: codec2("@dan-ai-studio/dshopencodego#UpdateStatus", parseUpdateStatus)
1804
+ }
1805
+ ]
1806
+ };
1807
+
1765
1808
  // src/usage/contract.ts
1766
1809
  function parseWindow(value, key) {
1767
1810
  if (value === null || typeof value !== "object") throw new Error(`invalid usage window "${key}"`);
@@ -1816,7 +1859,7 @@ function parseGoMeter(value) {
1816
1859
  })
1817
1860
  };
1818
1861
  }
1819
- var codec2 = (typeSymbol, parse) => ({
1862
+ var codec3 = (typeSymbol, parse) => ({
1820
1863
  mode: "strict",
1821
1864
  typeSymbol,
1822
1865
  schema: { parse },
@@ -1832,7 +1875,7 @@ var usageRemote = {
1832
1875
  method: "readWindows",
1833
1876
  invocation: { kind: "direct" },
1834
1877
  parameters: [],
1835
- result: codec2("@dan-ai-studio/dshopencodego#GoUsageWindows", parseUsageWindows)
1878
+ result: codec3("@dan-ai-studio/dshopencodego#GoUsageWindows", parseUsageWindows)
1836
1879
  },
1837
1880
  {
1838
1881
  id: "@dan-ai-studio/dshopencodego#opencodeGoUsage/readMeter",
@@ -1841,7 +1884,7 @@ var usageRemote = {
1841
1884
  method: "readMeter",
1842
1885
  invocation: { kind: "direct" },
1843
1886
  parameters: [],
1844
- result: codec2("@dan-ai-studio/dshopencodego#GoMeter", parseGoMeter)
1887
+ result: codec3("@dan-ai-studio/dshopencodego#GoMeter", parseGoMeter)
1845
1888
  }
1846
1889
  ]
1847
1890
  };
@@ -1849,7 +1892,7 @@ var usageRemote = {
1849
1892
  // src/remotes.ts
1850
1893
  var contribution = {
1851
1894
  package: usageRemote.package,
1852
- descriptors: [...usageRemote.descriptors, ...catalogRemote.descriptors]
1895
+ descriptors: [...usageRemote.descriptors, ...catalogRemote.descriptors, ...updateRemote.descriptors]
1853
1896
  };
1854
1897
  function registerRemotes(ctx) {
1855
1898
  ctx.inject(["typert"], (scope) => {
@@ -1944,6 +1987,106 @@ var OpencodeGoCatalogService = class extends TypertRemoteService {
1944
1987
  }
1945
1988
  };
1946
1989
 
1990
+ // src/update/check.ts
1991
+ var PACKAGE_NAME = "@dan-ai-studio/dshopencodego";
1992
+ var REGISTRY_LATEST_URL = `https://registry.npmjs.org/${encodeURIComponent(PACKAGE_NAME)}/latest`;
1993
+ var CHECK_CACHE_MS = 6 * 60 * 60 * 1e3;
1994
+ var CHECK_TIMEOUT_MS = 1e4;
1995
+ function parseVersion(value) {
1996
+ const match = /^(\d+)\.(\d+)\.(\d+)(?:-([0-9a-zA-Z.-]+))?(?:\+[0-9a-zA-Z.-]+)?$/.exec(value.trim());
1997
+ if (match === void 0 || match === null) return void 0;
1998
+ const [, major, minor, patch, prerelease] = match;
1999
+ return {
2000
+ core: [Number(major), Number(minor), Number(patch)],
2001
+ prerelease: prerelease === void 0 ? void 0 : prerelease.split(".")
2002
+ };
2003
+ }
2004
+ function compareVersions(left, right) {
2005
+ const ours = parseVersion(left);
2006
+ const theirs = parseVersion(right);
2007
+ if (ours === void 0 || theirs === void 0) throw new Error(`dshopencodego: cannot compare versions "${left}" and "${right}"`);
2008
+ for (let index = 0; index < 3; index += 1) {
2009
+ const difference = (ours.core[index] ?? 0) - (theirs.core[index] ?? 0);
2010
+ if (difference !== 0) return difference;
2011
+ }
2012
+ if (ours.prerelease === void 0 && theirs.prerelease === void 0) return 0;
2013
+ if (ours.prerelease === void 0) return 1;
2014
+ if (theirs.prerelease === void 0) return -1;
2015
+ const width = Math.max(ours.prerelease.length, theirs.prerelease.length);
2016
+ for (let index = 0; index < width; index += 1) {
2017
+ const oursPart = ours.prerelease[index];
2018
+ const theirsPart = theirs.prerelease[index];
2019
+ if (oursPart === void 0) return -1;
2020
+ if (theirsPart === void 0) return 1;
2021
+ if (oursPart !== theirsPart) return oursPart < theirsPart ? -1 : 1;
2022
+ }
2023
+ return 0;
2024
+ }
2025
+ function isUpdateAvailable(current, latest) {
2026
+ if (latest === void 0) return false;
2027
+ try {
2028
+ return compareVersions(latest, current) > 0;
2029
+ } catch {
2030
+ return false;
2031
+ }
2032
+ }
2033
+ function parseLatestVersion(body) {
2034
+ if (body === null || typeof body !== "object") return void 0;
2035
+ const version = body["version"];
2036
+ return typeof version === "string" && parseVersion(version) !== void 0 ? version : void 0;
2037
+ }
2038
+ function createUpdateChecker(deps) {
2039
+ const now = deps.now ?? Date.now;
2040
+ const fetchImpl = deps.fetchImpl ?? fetch;
2041
+ let cached;
2042
+ let inflight;
2043
+ async function lookup() {
2044
+ const checkedAt = now();
2045
+ try {
2046
+ const response = await fetchImpl(REGISTRY_LATEST_URL, { signal: AbortSignal.timeout(CHECK_TIMEOUT_MS) });
2047
+ if (!response.ok) throw new Error(`registry answered ${response.status}`);
2048
+ const latest = parseLatestVersion(await response.json());
2049
+ return {
2050
+ current: deps.currentVersion,
2051
+ latest,
2052
+ available: isUpdateAvailable(deps.currentVersion, latest),
2053
+ checkedAt
2054
+ };
2055
+ } catch {
2056
+ return { current: deps.currentVersion, latest: void 0, available: false, checkedAt };
2057
+ }
2058
+ }
2059
+ return {
2060
+ check(force = false) {
2061
+ if (!force && cached !== void 0 && now() - cached.checkedAt < CHECK_CACHE_MS) return Promise.resolve(cached);
2062
+ inflight ??= lookup().then((status) => {
2063
+ cached = status;
2064
+ inflight = void 0;
2065
+ return status;
2066
+ });
2067
+ return inflight;
2068
+ }
2069
+ };
2070
+ }
2071
+
2072
+ // src/update/version.ts
2073
+ var PLUGIN_VERSION = true ? "0.1.23" : "0.0.0-dev";
2074
+
2075
+ // src/update/service.ts
2076
+ import { TypertRemoteService as TypertRemoteService2 } from "@deepseek-ai/dsh-typert-protocol";
2077
+ var OpencodeGoUpdateService = class extends TypertRemoteService2 {
2078
+ check;
2079
+ constructor(ctx, options) {
2080
+ super(ctx, "opencodeGoUpdate");
2081
+ const checker = createUpdateChecker({ currentVersion: options.currentVersion });
2082
+ this.check = (force) => checker.check(force);
2083
+ }
2084
+ /** Whether a newer release is published; `force` bypasses the cache. */
2085
+ readUpdate(force) {
2086
+ return this.check(force);
2087
+ }
2088
+ };
2089
+
1947
2090
  // src/usage/meter.ts
1948
2091
  var EMPTY = {
1949
2092
  calls: 0,
@@ -2049,7 +2192,7 @@ async function readUsageWindows(options) {
2049
2192
 
2050
2193
  // src/usage/service.ts
2051
2194
  import { randomUUID as randomUUID2 } from "node:crypto";
2052
- import { RemoteError as RemoteError2, TypertRemoteService as TypertRemoteService2 } from "@deepseek-ai/dsh-typert-protocol";
2195
+ import { RemoteError as RemoteError2, TypertRemoteService as TypertRemoteService3 } from "@deepseek-ai/dsh-typert-protocol";
2053
2196
  import { LlmError as LlmError11 } from "@deepseek-ai/dsh-llm";
2054
2197
  function usageFailure(error, source) {
2055
2198
  const missing = error instanceof LlmError11 && error.code === "MISSING_CREDENTIAL";
@@ -2060,7 +2203,7 @@ function usageFailure(error, source) {
2060
2203
  { cause: error }
2061
2204
  );
2062
2205
  }
2063
- var OpencodeGoUsageService = class extends TypertRemoteService2 {
2206
+ var OpencodeGoUsageService = class extends TypertRemoteService3 {
2064
2207
  identity;
2065
2208
  options;
2066
2209
  constructor(ctx, options) {
@@ -2153,6 +2296,7 @@ function apply(ctx, raw) {
2153
2296
  catalog: () => adapter.catalogOf(current()),
2154
2297
  visibility: () => current().modelVisibility
2155
2298
  });
2299
+ ctx.plugin(OpencodeGoUpdateService, { currentVersion: PLUGIN_VERSION });
2156
2300
  let registration;
2157
2301
  let visibilityFacts = JSON.stringify(current().modelVisibility);
2158
2302
  const applyRoute = (credentialConfigured) => {
@@ -2195,7 +2339,7 @@ function apply(ctx, raw) {
2195
2339
  const undiscover = ctx.llm.registerModelDiscovery(name, async (request) => {
2196
2340
  if (request.provider !== PROVIDER_ID && !(request.baseURL ?? "").includes("opencode.ai")) {
2197
2341
  throw new LlmError12(
2198
- "dshopencodego discovers only OpenCode zen/go endpoints; enter this provider's models by hand",
2342
+ "dshopencodego discovers only OpenCode Go/Console endpoints; enter this provider's models by hand",
2199
2343
  "DISCOVERY_UNSUPPORTED"
2200
2344
  );
2201
2345
  }
@@ -2220,6 +2364,7 @@ function apply(ctx, raw) {
2220
2364
  ctx.logger.info(`dshopencodego: route "${PROVIDER_ID}" registered as ${DISPLAY_NAME}`);
2221
2365
  }
2222
2366
  export {
2367
+ CONSOLE_BASE_URL,
2223
2368
  Config,
2224
2369
  DEFAULT_API_KEY_ENV,
2225
2370
  DEFAULT_BASE_URL,
@@ -2228,6 +2373,7 @@ export {
2228
2373
  OpencodeGoCatalog,
2229
2374
  PROVIDER_ID,
2230
2375
  PlainConfig,
2376
+ SESSION_AFFINITY_HEADER,
2231
2377
  SESSION_HEADER,
2232
2378
  apply,
2233
2379
  assertBaseURL,
@@ -4,7 +4,9 @@ export declare const PROVIDER_ID = "opencode-go";
4
4
  /** Selector label for the route. */
5
5
  export declare const DISPLAY_NAME = "OpenCode Go";
6
6
  /** Gateway base URL; `/models` and `/usage` hang off it. */
7
- export declare const DEFAULT_BASE_URL = "https://opencode.ai/zen/go/v1";
7
+ export declare const DEFAULT_BASE_URL = "https://opencode.ai/inference/go/v1";
8
+ /** Console base URL (on-demand / free models, pay-as-you-go). */
9
+ export declare const CONSOLE_BASE_URL = "https://opencode.ai/inference/v1";
8
10
  /** Online model metadata: capability, lifecycle, and pricing per model id. */
9
11
  export declare const MODEL_METADATA_URL = "https://models.dev/api.json";
10
12
  /** The models.dev provider key this plugin reads. */
@@ -4,7 +4,9 @@ export const PROVIDER_ID = 'opencode-go';
4
4
  /** Selector label for the route. */
5
5
  export const DISPLAY_NAME = 'OpenCode Go';
6
6
  /** Gateway base URL; `/models` and `/usage` hang off it. */
7
- export const DEFAULT_BASE_URL = 'https://opencode.ai/zen/go/v1';
7
+ export const DEFAULT_BASE_URL = 'https://opencode.ai/inference/go/v1';
8
+ /** Console base URL (on-demand / free models, pay-as-you-go). */
9
+ export const CONSOLE_BASE_URL = 'https://opencode.ai/inference/v1';
8
10
  /** Online model metadata: capability, lifecycle, and pricing per model id. */
9
11
  export const MODEL_METADATA_URL = 'https://models.dev/api.json';
10
12
  /** The models.dev provider key this plugin reads. */
@@ -23,7 +23,7 @@ import type { LlmDiscoveredModel } from '@deepseek-ai/dsh-llm';
23
23
  import type { GoDoc } from './go-doc.ts';
24
24
  import type { ModelDefaults, ModelFacts } from './metadata.ts';
25
25
  import type { GoQuota } from '../go-limits.ts';
26
- export { PROVIDER_ID, DISPLAY_NAME, DEFAULT_BASE_URL } from './constants.ts';
26
+ export { PROVIDER_ID, DISPLAY_NAME, DEFAULT_BASE_URL, CONSOLE_BASE_URL } from './constants.ts';
27
27
  export { readModelIds, fetchModelIds } from './gateway.ts';
28
28
  export { readOnlineMetadata, toPiModel, modelBaseURL } from './metadata.ts';
29
29
  export type { ModelFacts, OnlineMetadata, ModelDefaults } from './metadata.ts';
@@ -29,7 +29,7 @@ import { fetchModelIds } from "./gateway.js";
29
29
  import { fetchGoDocument, GO_DOC_URLS } from "./go-doc.js";
30
30
  import { readBoundedJson } from "./json-response.js";
31
31
  import { readOnlineMetadata, toPiModel } from "./metadata.js";
32
- export { PROVIDER_ID, DISPLAY_NAME, DEFAULT_BASE_URL } from "./constants.js";
32
+ export { PROVIDER_ID, DISPLAY_NAME, DEFAULT_BASE_URL, CONSOLE_BASE_URL } from "./constants.js";
33
33
  export { readModelIds, fetchModelIds } from "./gateway.js";
34
34
  export { readOnlineMetadata, toPiModel, modelBaseURL } from "./metadata.js";
35
35
  export { decideProtocol, inferProtocol, protocolOfNpm } from "./protocol.js";
@@ -51,6 +51,12 @@ export function classifyPiAiError(message) {
51
51
  return 'AUTH';
52
52
  if (isQuotaExceededError(message))
53
53
  return ACCOUNT_QUOTA_CODE;
54
+ // A 429 typed as `server_error` is the gateway reporting an unavailable
55
+ // upstream, not the caller exceeding a rate limit: the remedy is waiting for
56
+ // the endpoint to recover rather than slowing down, so it classifies as
57
+ // SERVER even though the status code alone reads as a rate limit.
58
+ if (/server_error/i.test(message))
59
+ return 'SERVER';
54
60
  if (/\b429\b|rate.?limit/i.test(message))
55
61
  return 'RATE_LIMIT';
56
62
  if (/\b413\b|payload too large|request body too large|length limit exceeded/i.test(message))
@@ -58,7 +58,7 @@ export const SEED_QUOTAS = {
58
58
  'deepseek-v4-flash-vision-exp': { monthlyUsd: 15, monthlyRequests: 32_500 },
59
59
  'hy4-preview': { monthlyUsd: 30, monthlyRequests: 6_770 },
60
60
  'hy3': { monthlyUsd: 60, monthlyRequests: 21_500 },
61
- 'space-bunny-free': { monthlyUsd: 'unlimited', monthlyRequests: 'unlimited' },
61
+ // 注意:space-bunny-free 并不在 Go 端点支持的 37 个模型列表中(请求会返回 400 错误),已依据 R-B4 予以剔除
62
62
  'grok-4.7': { monthlyUsd: 15, monthlyRequests: 845 },
63
63
  'grok-4.6': { monthlyUsd: 15, monthlyRequests: 845 },
64
64
  'gpt-6-luna': { monthlyUsd: 15, monthlyRequests: 21_130 },
@@ -19,7 +19,7 @@
19
19
  * config:
20
20
  * enabled: true # false withdraws the route only
21
21
  * apiKeyEnv: OPENCODE_GO_API_KEY # default
22
- * baseURL: https://opencode.ai/zen/go/v1 # default
22
+ * baseURL: https://opencode.ai/inference/go/v1 # default
23
23
  * refreshMinutes: 60 # live catalog TTL
24
24
  * modelProtocols: # last-resort protocol override
25
25
  * some-model: openai-responses
@@ -35,10 +35,10 @@ declare module '@deepseek-ai/cordis' {
35
35
  }
36
36
  export { OpencodeGoAdapter } from './adapter.ts';
37
37
  export type { OpencodeGoAdapterOptions, OpencodeGoImageAccess } from './adapter.ts';
38
- export { DEFAULT_BASE_URL, DISPLAY_NAME, PROVIDER_ID, OpencodeGoCatalog, discoverCatalogModels } from './catalog/index.ts';
38
+ export { DEFAULT_BASE_URL, CONSOLE_BASE_URL, DISPLAY_NAME, PROVIDER_ID, OpencodeGoCatalog, discoverCatalogModels } from './catalog/index.ts';
39
39
  export { Config, PlainConfig, assertBaseURL, DEFAULT_API_KEY_ENV } from './config.ts';
40
40
  export type { OpencodeGoConfig } from './config.ts';
41
- export { SESSION_HEADER, opencodeSessionValue, providerHeaders } from './session-header.ts';
41
+ export { SESSION_HEADER, SESSION_AFFINITY_HEADER, opencodeSessionValue, providerHeaders } from './session-header.ts';
42
42
  export { isModelEnabled, sortModels } from './models.ts';
43
43
  export type { ModelSummary } from './models.ts';
44
44
  export declare const name = "dshopencodego";
@@ -19,7 +19,7 @@
19
19
  * config:
20
20
  * enabled: true # false withdraws the route only
21
21
  * apiKeyEnv: OPENCODE_GO_API_KEY # default
22
- * baseURL: https://opencode.ai/zen/go/v1 # default
22
+ * baseURL: https://opencode.ai/inference/go/v1 # default
23
23
  * refreshMinutes: 60 # live catalog TTL
24
24
  * modelProtocols: # last-resort protocol override
25
25
  * some-model: openai-responses
@@ -36,11 +36,13 @@ import { DISPLAY_NAME, PROVIDER_ID, discoverCatalogModels } from "./catalog/inde
36
36
  import { assertBaseURL, PlainConfig, readConfig } from "./config.js";
37
37
  import { registerRemotes } from "./remotes.js";
38
38
  import { OpencodeGoCatalogService } from "./catalog/service.js";
39
+ import { OpencodeGoUpdateService } from "./update/index.js";
40
+ import { PLUGIN_VERSION } from "./update/index.js";
39
41
  import { OpencodeGoUsageService, UsageMeter } from "./usage/index.js";
40
42
  export { OpencodeGoAdapter } from "./adapter.js";
41
- export { DEFAULT_BASE_URL, DISPLAY_NAME, PROVIDER_ID, OpencodeGoCatalog, discoverCatalogModels } from "./catalog/index.js";
43
+ export { DEFAULT_BASE_URL, CONSOLE_BASE_URL, DISPLAY_NAME, PROVIDER_ID, OpencodeGoCatalog, discoverCatalogModels } from "./catalog/index.js";
42
44
  export { Config, PlainConfig, assertBaseURL, DEFAULT_API_KEY_ENV } from "./config.js";
43
- export { SESSION_HEADER, opencodeSessionValue, providerHeaders } from "./session-header.js";
45
+ export { SESSION_HEADER, SESSION_AFFINITY_HEADER, opencodeSessionValue, providerHeaders } from "./session-header.js";
44
46
  export { isModelEnabled, sortModels } from "./models.js";
45
47
  export const name = 'dshopencodego';
46
48
  export const inject = ['llm'];
@@ -107,6 +109,7 @@ export function apply(ctx, raw) {
107
109
  catalog: () => adapter.catalogOf(current()),
108
110
  visibility: () => current().modelVisibility,
109
111
  });
112
+ ctx.plugin(OpencodeGoUpdateService, { currentVersion: PLUGIN_VERSION });
110
113
  let registration;
111
114
  let visibilityFacts = JSON.stringify(current().modelVisibility);
112
115
  /**
@@ -166,7 +169,7 @@ export function apply(ctx, raw) {
166
169
  syncRoute();
167
170
  const undiscover = ctx.llm.registerModelDiscovery(name, async (request) => {
168
171
  if (request.provider !== PROVIDER_ID && !(request.baseURL ?? '').includes('opencode.ai')) {
169
- throw new LlmError('dshopencodego discovers only OpenCode zen/go endpoints; enter this provider\'s models by hand', 'DISCOVERY_UNSUPPORTED');
172
+ throw new LlmError('dshopencodego discovers only OpenCode Go/Console endpoints; enter this provider\'s models by hand', 'DISCOVERY_UNSUPPORTED');
170
173
  }
171
174
  return discoverCatalogModels(adapter.catalogOf(current()));
172
175
  });
@@ -8,11 +8,12 @@
8
8
  * @module @dan-ai-studio/dshopencodego/remotes
9
9
  */
10
10
  import { catalogRemote } from "./catalog/contract.js";
11
+ import { updateRemote } from "./update/contract.js";
11
12
  import { usageRemote } from "./usage/contract.js";
12
13
  /** Every Remote method this package owns, mounted as one contribution. */
13
14
  const contribution = {
14
15
  package: usageRemote.package,
15
- descriptors: [...usageRemote.descriptors, ...catalogRemote.descriptors],
16
+ descriptors: [...usageRemote.descriptors, ...catalogRemote.descriptors, ...updateRemote.descriptors],
16
17
  };
17
18
  /** Register this package's Remote contribution for the mount's lifetime. */
18
19
  export function registerRemotes(ctx) {
@@ -17,6 +17,8 @@
17
17
  */
18
18
  /** Header name the gateway requires. */
19
19
  export declare const SESSION_HEADER = "x-opencode-session";
20
+ /** Session affinity header for gateway routing and prompt caching. */
21
+ export declare const SESSION_AFFINITY_HEADER = "x-session-affinity";
20
22
  /**
21
23
  * The header value for one request.
22
24
  * @param sessionId - the request's session id, if it names one.
@@ -27,9 +29,9 @@ export declare function opencodeSessionValue(sessionId: string | undefined): str
27
29
  * Every header one provider request carries.
28
30
  *
29
31
  * The attribution `user-agent` is Harness-owned and always present; the session
30
- * header is added on top of it. A caller-supplied header of the same name can
31
- * never win, because the gateway's routing decision must follow the session,
32
- * not a deployment string.
32
+ * and affinity headers are added on top of it. A caller-supplied header of the
33
+ * same name can never win, because the gateway's routing decision must follow
34
+ * the session, not a deployment string.
33
35
  * @param sessionId - the request's session id, if it names one.
34
36
  * @returns headers for the pi-ai request options.
35
37
  */
@@ -19,6 +19,8 @@ import { randomUUID } from 'node:crypto';
19
19
  import { attributionHeaders } from '@deepseek-ai/dsh-llm';
20
20
  /** Header name the gateway requires. */
21
21
  export const SESSION_HEADER = 'x-opencode-session';
22
+ /** Session affinity header for gateway routing and prompt caching. */
23
+ export const SESSION_AFFINITY_HEADER = 'x-session-affinity';
22
24
  /**
23
25
  * The header value for one request.
24
26
  * @param sessionId - the request's session id, if it names one.
@@ -31,15 +33,17 @@ export function opencodeSessionValue(sessionId) {
31
33
  * Every header one provider request carries.
32
34
  *
33
35
  * The attribution `user-agent` is Harness-owned and always present; the session
34
- * header is added on top of it. A caller-supplied header of the same name can
35
- * never win, because the gateway's routing decision must follow the session,
36
- * not a deployment string.
36
+ * and affinity headers are added on top of it. A caller-supplied header of the
37
+ * same name can never win, because the gateway's routing decision must follow
38
+ * the session, not a deployment string.
37
39
  * @param sessionId - the request's session id, if it names one.
38
40
  * @returns headers for the pi-ai request options.
39
41
  */
40
42
  export function providerHeaders(sessionId) {
43
+ const session = opencodeSessionValue(sessionId);
41
44
  return {
42
- [SESSION_HEADER]: opencodeSessionValue(sessionId),
45
+ [SESSION_HEADER]: session,
46
+ [SESSION_AFFINITY_HEADER]: session,
43
47
  ...attributionHeaders(),
44
48
  };
45
49
  }
@@ -0,0 +1,74 @@
1
+ /**
2
+ * Self update check: is a newer release of this package published?
3
+ *
4
+ * This module only *reads*: it asks the npm registry for the latest published
5
+ * version and compares it with the running build. It never downloads,
6
+ * installs, or replaces anything — the actual update stays with the host's
7
+ * `pluginManager` (`removeBundle` + `installBundle`), driven by an explicit
8
+ * user click on the settings page. Automatic self-replacement would bypass
9
+ * the host's manifest lock, peer-compatibility checks, build approvals, and
10
+ * HMR lifecycle, so it is deliberately not implemented here.
11
+ *
12
+ * The check runs only when the settings page asks (page open or manual
13
+ * refresh), never on the model-request path, and fails silent: an unreachable
14
+ * registry simply hides the banner instead of erroring the page.
15
+ *
16
+ * @module @dan-ai-studio/dshopencodego/update/check
17
+ */
18
+ /** This package as addressed on the registry. */
19
+ export declare const PACKAGE_NAME = "@dan-ai-studio/dshopencodego";
20
+ /** Document address answering `{ "version": "x.y.z", ... }`. */
21
+ export declare const REGISTRY_LATEST_URL: string;
22
+ /** How long one lookup answers for before the registry is asked again. */
23
+ export declare const CHECK_CACHE_MS: number;
24
+ /** Bound on one registry round trip; expiry fails silent, never throws. */
25
+ export declare const CHECK_TIMEOUT_MS = 10000;
26
+ /** What the settings page renders. */
27
+ export interface UpdateStatus {
28
+ /** Version of the running build. */
29
+ readonly current: string;
30
+ /** Latest published version, when the registry answered. */
31
+ readonly latest: string | undefined;
32
+ /** True when the registry answered and `latest` is newer than `current`. */
33
+ readonly available: boolean;
34
+ /** Milliseconds since the epoch of the lookup backing this reading. */
35
+ readonly checkedAt: number;
36
+ }
37
+ interface ParsedVersion {
38
+ readonly core: readonly number[];
39
+ /** Absent for a release; present (lower precedence) for `1.2.3-tag.4`. */
40
+ readonly prerelease: readonly string[] | undefined;
41
+ }
42
+ /** Split `1.2.3-tag.4+build` into comparable parts; invalid input yields undefined. */
43
+ export declare function parseVersion(value: string): ParsedVersion | undefined;
44
+ /**
45
+ * Compare two version strings.
46
+ * @returns negative when `left` is older, positive when newer, zero when equal.
47
+ * @throws {Error} when either side is not a version.
48
+ */
49
+ export declare function compareVersions(left: string, right: string): number;
50
+ /** True when `latest` is a newer release than `current`. Unparsable input is never an update. */
51
+ export declare function isUpdateAvailable(current: string, latest: string | undefined): boolean;
52
+ /** Read the `version` field out of a registry `latest` document. */
53
+ export declare function parseLatestVersion(body: unknown): string | undefined;
54
+ /** Dependencies the check needs, injectable for tests. */
55
+ export interface UpdateCheckDeps {
56
+ /** Running build's version (build-time constant in production). */
57
+ readonly currentVersion: string;
58
+ /** Fetch implementation; defaults to the global one. */
59
+ readonly fetchImpl?: typeof fetch;
60
+ /** Clock; defaults to `Date.now`. */
61
+ readonly now?: () => number;
62
+ }
63
+ /**
64
+ * Ask the registry for the latest published version, cached per process.
65
+ *
66
+ * Concurrent callers share one in-flight lookup. A lookup that fails for any
67
+ * reason — network, timeout, malformed document — resolves as "no update
68
+ * known" rather than rejecting, so the settings page never breaks because
69
+ * the registry is unreachable.
70
+ */
71
+ export declare function createUpdateChecker(deps: UpdateCheckDeps): {
72
+ check(force?: boolean): Promise<UpdateStatus>;
73
+ };
74
+ export {};