dsh-opencode-go 0.1.1 → 0.1.3

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/README.md CHANGED
@@ -2,11 +2,12 @@
2
2
 
3
3
  在 [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) 中使用 OpenCode Go 订阅模型,支持流式回复、工具调用、图片输入和 Web 设置页。
4
4
 
5
- 插件自动添加 OpenCode Go 所需的会话请求头,并从网关获取可用模型目录。通过 DSH 插件命令安装,无需修改 DSH 源码。
5
+ 插件自动添加 OpenCode Go 所需的会话请求头,并从网关获取可用模型目录,显示套餐剩余额度。通过 DSH 插件命令安装,无需修改 DSH 源码。
6
+
7
+ 无需任何设置,一key开始
6
8
 
7
- ## 安装与使用
8
9
 
9
- 已验证兼容 **DSH `0.1.6-alpha.1`**。其他版本尚未验证。
10
+ ## 安装与使用
10
11
 
11
12
  ### Web
12
13
 
@@ -49,44 +50,34 @@ dsh --profile headless --patch ./headless.patch.yml "你好"
49
50
 
50
51
  模型 ID 须在当前网关目录中可用。Web 和 Headless 使用各自的 profile,需要分别安装插件。
51
52
 
52
- ## 订阅用量
53
+ ## 升级插件
53
54
 
54
- Web 中选择 `opencode-go` 提供方的模型后,模型名左侧显示 **Go · 5h 已用百分比 · 周已用百分比**。点击可查看 5 小时、每周、每月用量和本地时区的重置时间。
55
+ 更新 Web profile 中的插件到 npm 最新版本:
55
56
 
56
- 数据通过 Host 使用已配置的 API Key 查询 `GET /zen/go/v1/usage`,每分钟刷新;浏览器不接收 API Key。切换到其他提供方后隐藏,页面不可见时暂停查询。接口失败或响应缺失时显示“暂不可用”,不会填成 0%。接口返回的是账号订阅额度,不是当前会话 token 数。
57
+ ```sh
58
+ dsh plugin --profile web update dsh-opencode-go --latest
59
+ ```
57
60
 
58
- 这个接口可在 [OpenCode 官方源码](https://github.com/anomalyco/opencode/blob/dev/packages/console/app/src/routes/zen/go/v1/usage.ts) 中查到;当前返回 `rolling`、`weekly`、`monthly` 各窗口的 `status`、`percent` 和 `resetsAt`,不返回缓存命中率。
61
+ 完成后重启 `dsh web` 并刷新浏览器。Headless 用户将 `web` 换成 `headless`;如果两个 profile 都安装了插件,需要分别升级。
59
62
 
60
- 底部的缓存命中率由每次模型响应的缓存 token 统计计算。插件支持 `prompt_tokens_details.cached_tokens`、`prompt_cache_hit_tokens` 和顶层 `cached_tokens`。上游未返回缓存统计时,当前 pi-ai / Harness 统计链路也可能显示 0;可在 OpenCode 控制台的使用历史中展开对应请求的输入明细进行核对。新会话首次请求出现 0 命中是可能的,同一会话后续有可复用的提示前缀才可能命中。
63
+ 如果此前安装的是本地目录或 `.tgz` 文件,用以下命令切换到 npm 最新版:
61
64
 
62
- ## 配置
65
+ ```sh
66
+ dsh plugin --profile web add dsh-opencode-go@latest
67
+ ```
63
68
 
64
- Web 用户可直接在 **设置 → OpenCode Go** 中修改配置。启用开关立即生效;保存其他配置后,后续请求使用新值。
69
+ 升级插件无需卸载,也无需重新填写 API Key。模型目录会自动同步,正常新增模型不需要再次升级插件。
65
70
 
66
- 需要通过文件配置时,在对应 profile 的 `cordis.patch.yml` 中添加以下内容。所有字段均有默认值,通常只需配置 API Key。
71
+ ## 订阅用量显示
67
72
 
68
- ```yaml
69
- - id: opencode-go
70
- config:
71
- enabled: true
72
- apiKeyEnv: OPENCODE_API_KEY
73
- baseURL: https://opencode.ai/zen/go/v1
74
- refreshMinutes: 60
75
- streamIdleTimeoutMs: 300000
76
- maxRequestImageBytes: 20971520
77
- requestImagePixelBudget: 4194304
78
- requestImageMaxBytes: 1048576
79
- ```
73
+ ![alt text](image.png)
80
74
 
81
- `apiKeyEnv` 是凭据引用名,不是密钥。插件通过 DSH credentials 服务解析凭据;未挂载该服务时读取进程环境。Web 保存的设置可覆盖文件中的基础配置。
75
+ ## 配置
82
76
 
83
- 关闭插件开关或移除密钥会撤下提供方路由,设置页仍可访问。
77
+ Web 用户可直接在 **设置 → OpenCode Go** 中修改配置。启用开关立即生效;
84
78
 
85
79
  ## 常见问题
86
80
 
87
- ### 安装提示依赖构建脚本被拦截
88
-
89
- 按安装器提示,在该 profile 的 `pnpm-workspace.yaml` 中逐项设置 `allowBuilds`。OpenCode Go 已验证的运行路径不需要执行 `@google/genai` 和 `protobufjs` 的安装脚本,可以将这两项设为 `false` 后重试。保留文件中的其他配置,无需全局放开安装脚本。
90
81
 
91
82
  ### 提示 `opencode-go` 路由已被占用
92
83
 
@@ -94,16 +85,20 @@ Web 用户可直接在 **设置 → OpenCode Go** 中修改配置。启用开关
94
85
 
95
86
  ### 没有出现预期的模型
96
87
 
97
- 先确认插件已启用且 API Key 已配置,再刷新设置页中的模型列表。插件仅展示本地适配表与网关实时目录的交集;尚未适配协议的新模型不会自动加入。
88
+ 先确认插件已启用且 API Key 已配置,再刷新设置页中的模型列表。插件每次读取模型列表都会请求网关 `/models`,并同步 [models.dev 的 OpenCode Go 配置](https://models.dev/api.json)。模型的协议、上下文长度、输出上限和图片能力来自在线配置,新模型无需等待本插件或 pi-ai 发布新版本。
89
+
90
+ 网关和在线配置已收录、且使用 Anthropic Messages、OpenAI Chat Completions 或 OpenAI Responses 协议的新模型,在下次读取或刷新列表时即可使用。刷新会绕过已有会话的目录缓存;直接请求尚未缓存的新模型也会立即重新同步。设置页显示完整模型列表。
91
+
92
+ 网关已公布 ID 但尚无有效协议/能力配置的模型仍会显示,并标注配置暂不可用;调用时会说明原因。仅凭模型 ID 无法可靠推断调用方式。上游新增全新协议或协议特例时,仍可能需要适配。
98
93
 
99
- 实时目录获取失败时,适配器可使用本地表继续处理请求;设置页中的模型发现会显示失败,便于重新尝试。
94
+ 在线配置暂时不可达时,优先复用本次运行中成功获取的配置,以 pi-ai 内置配置作为备用。网关目录不可达时,已有请求可以使用上次目录;设置页刷新会显示失败,避免把旧目录误认为最新结果。`refreshMinutes` 只控制已有模型请求的缓存时长,不阻止主动读取列表获取新模型。
100
95
 
101
96
  ## 功能说明
102
97
 
103
98
  - **会话请求头**:每次请求包含 Harness User-Agent 和 `x-opencode-session`。同一会话保持相同 ID,无会话 ID 的请求使用独立随机值。
104
99
  - **流式与历史**:支持流式输出、工具调用及历史回放,协议请求由 pi-ai 执行。
105
100
  - **图片输入**:支持目录中声明图片能力的模型,需要 DSH attachment 服务。
106
- - **提示与缓存**:插件不增加隐藏系统提示;会话 ID 用于网关路由,历史前缀及图片编码影响缓存复用。
101
+ - **提示与缓存**:插件不增加隐藏系统提示;会话 ID 用于网关路由;
107
102
 
108
103
  ## 卸载
109
104
 
@@ -115,26 +110,11 @@ dsh plugin --profile web remove dsh-opencode-go
115
110
  dsh plugin --profile headless remove dsh-opencode-go
116
111
  ```
117
112
 
118
- 如果默认模型仍指向 `opencode-go`,请改选其他提供方。凭据和历史会话由 DSH 管理。
119
-
120
- ## 开发
121
-
122
- 需要 Node.js `^22.19.0 || >=24.0.0` 和 npm。
123
-
124
- ```sh
125
- npm ci
126
- npm run typecheck
127
- npm test
128
- npm pack
129
- ```
130
113
 
131
- `npm test` 会先构建再执行测试;`npm pack` 生成可安装的 `.tgz` 包。依赖均通过 npm 安装,不需要 DSH 源码仓库。
132
114
 
133
- - [验证说明](docs/verification.md):测试范围、隔离安装与 Headless 验证方法。
134
- - [架构与依赖](docs/independent-package.md):Host、Client 构建方式和转换代码的维护方式。
135
- - [第三方声明](THIRD_PARTY_NOTICES.md):代码来源与许可。
115
+ ## 反馈
136
116
 
137
- 自动化测试使用本地模拟网关,不会发起付费模型请求。真实账号、Desktop 和其他操作系统尚未验证。
117
+ 遇到bug或有功能建议请提issue
138
118
 
139
119
  ## 许可证
140
120
 
@@ -51,6 +51,12 @@ The usage tests invoke the actual published Typert Host gateway against a local
51
51
 
52
52
  The local Web profile was also checked against the real read-only OpenCode Go usage endpoint. The button renders immediately before the model selector, and its details show rolling, weekly, and monthly percentages and local reset times. This check sends no model-generation request. The manual Remote codec supports both the published DSH `schema` shape and the current source build's `create()` shape.
53
53
 
54
+ ## Dynamic model catalog verification
55
+
56
+ The dynamic catalog change passes all 140 tests plus the Host/Client build. Its offline regression fixtures cover Union Alpha and an arbitrary future id absent from pi-ai, immediate discovery and picker refresh, metadata arriving after a model id, HTTP ETag revalidation, concurrent refreshes, metadata outages, gateway outages, removal of retired models, malformed metadata, and supported reasoning controls. Real SDK streams against a local HTTP gateway verify Chat Completions, Responses, and Anthropic Messages paths for models with no built-in entry, including the Anthropic SDK's `/v1/messages` suffix.
57
+
58
+ A read-only check against the live OpenCode Go listing and models.dev resolved 36 of 38 advertised ids, including `union-alpha`; `deepseek-flash` and `hy3-preview` had no metadata. Those ids remain visible with a configuration diagnostic. This check did not send a generation request or establish that every advertised id is currently callable for an account.
59
+
54
60
  ## Remaining limits
55
61
 
56
62
  Real paid OpenCode Go completions, Desktop, non-macOS platforms, and other DSH releases are not verified. The automated gateway tests preserve the adapter's request and replay semantics but cannot establish account validity or live provider availability.
package/lib/client.js CHANGED
@@ -40,7 +40,7 @@ if (!document.querySelector("style[data-plugin-css=" + JSON.stringify(id) + "]")
40
40
  style.textContent = ".BtKYva_field{flex-direction:column;gap:6px;padding:12px 0;display:flex}.BtKYva_field+.BtKYva_field{border-top:.5px solid var(--dsw-alias-border-l2)}.BtKYva_head{align-items:center;gap:8px;display:flex}.BtKYva_label{min-width:0;color:var(--dsw-alias-label-primary);flex:1;font-size:13px;font-weight:500;line-height:1.5}.BtKYva_badges{align-items:center;gap:8px;display:inline-flex}.BtKYva_reset{font:inherit;color:var(--dsw-alias-label-secondary);cursor:pointer;background:0 0;border:none;padding:0;font-size:12px;line-height:1.5}.BtKYva_reset:hover:not(:disabled){color:var(--dsw-alias-label-primary)}.BtKYva_reset:disabled{cursor:default}.BtKYva_input{border:.5px solid var(--dsw-alias-border-l4);background:var(--dsw-alias-bg-layer-3);height:34px;font:inherit;color:var(--dsw-alias-label-primary);border-radius:8px;padding:0 12px;font-size:13px;line-height:1.5}.BtKYva_input:focus-visible{border-color:var(--dsw-alias-brand-primary);outline:none}.BtKYva_input:disabled{color:var(--dsw-alias-label-tertiary);cursor:default}.BtKYva_inputInvalid{border-color:var(--dsw-alias-label-error);}.BtKYva_invalid{color:var(--dsw-alias-label-error);margin:0;font-size:12px;line-height:1.5}.BtKYva_hint{color:var(--dsw-alias-label-tertiary);margin:0;font-size:12px;line-height:1.5}.BtKYva_intro{color:var(--dsw-alias-label-secondary);margin:0 0 8px;font-size:13px;line-height:1.5}.BtKYva_disclosure{font:inherit;text-align:left;cursor:pointer;background:0 0;border:none;align-items:center;gap:8px;margin:0;padding:0;display:flex}.BtKYva_disclosure:hover .BtKYva_label{color:var(--dsw-alias-brand-primary)}.BtKYva_chevron,.BtKYva_chevronOpen{color:var(--dsw-alias-label-tertiary);flex:none}.BtKYva_chevron{transform:rotate(-90deg)}.BtKYva_advanced{flex-direction:column;gap:4px;padding:4px 0 0 22px;display:flex}.BtKYva_advanced .BtKYva_field+.BtKYva_field{border-top:none}.BtKYva_chips{flex-wrap:wrap;gap:6px;display:flex}.BtKYva_actions{align-items:center;gap:8px;padding-top:12px;display:flex}.BtKYva_failedNote{color:var(--dsw-alias-label-error);margin:0;font-size:12px;line-height:1.5}";
41
41
  document.head.appendChild(style);
42
42
  }
43
- var Section_default = { "reset": "BtKYva_reset", "badges": "BtKYva_badges", "inputInvalid": "BtKYva_inputInvalid", "label": "BtKYva_label", "invalid": "BtKYva_invalid", "failedNote": "BtKYva_failedNote", "input": "BtKYva_input", "disclosure": "BtKYva_disclosure", "intro": "BtKYva_intro", "field": "BtKYva_field", "advanced": "BtKYva_advanced", "actions": "BtKYva_actions", "head": "BtKYva_head", "hint": "BtKYva_hint", "chevron": "BtKYva_chevron", "chevronOpen": "BtKYva_chevronOpen", "chips": "BtKYva_chips" };
43
+ var Section_default = { "badges": "BtKYva_badges", "field": "BtKYva_field", "inputInvalid": "BtKYva_inputInvalid", "input": "BtKYva_input", "hint": "BtKYva_hint", "actions": "BtKYva_actions", "label": "BtKYva_label", "invalid": "BtKYva_invalid", "reset": "BtKYva_reset", "intro": "BtKYva_intro", "disclosure": "BtKYva_disclosure", "head": "BtKYva_head", "chevronOpen": "BtKYva_chevronOpen", "chips": "BtKYva_chips", "failedNote": "BtKYva_failedNote", "chevron": "BtKYva_chevron", "advanced": "BtKYva_advanced" };
44
44
 
45
45
  // src/client/Section.tsx
46
46
  var import_jsx_runtime = require("react/jsx-runtime");
@@ -553,7 +553,6 @@ var OPENCODE_GO_NS = "llm-opencode-go";
553
553
  var DEFAULT_API_KEY_REF = "OPENCODE_API_KEY";
554
554
  var API_KEY_FIELD = "apiKey";
555
555
  var OPENCODE_GO_PROVIDER = "opencode-go";
556
- var MODEL_PREVIEW_LIMIT = 6;
557
556
  var OpencodeGoSectionController = class {
558
557
  /**
559
558
  * @param scope - the bound settings scope for the `llm-opencode-go` namespace.
@@ -646,7 +645,7 @@ var OpencodeGoSectionController = class {
646
645
  this.models = response.ok ? {
647
646
  status: "ready",
648
647
  count: response.value.length,
649
- preview: response.value.slice(0, MODEL_PREVIEW_LIMIT).map((model) => model.name ?? model.id)
648
+ preview: response.value.map((model) => model.name ?? model.id)
650
649
  } : { status: "failed", message: response.error.message };
651
650
  this.store.set(this.projection());
652
651
  }).catch((error) => {
@@ -766,7 +765,7 @@ var en = {
766
765
  baseURLLabel: "Gateway URL",
767
766
  baseURLHint: "The endpoint that serves requests and the live model list.",
768
767
  refreshMinutesLabel: "Catalog refresh (minutes)",
769
- refreshMinutesHint: "How long one live model-listing resolution stays authoritative before re-fetching.",
768
+ refreshMinutesHint: "Cache lifetime for ongoing requests. Reading or refreshing the model list always checks the gateway and online model metadata immediately.",
770
769
  streamIdleTimeoutMsLabel: "Stream idle timeout (ms)",
771
770
  streamIdleTimeoutMsHint: "Largest quiet gap between stream events before the request fails.",
772
771
  maxRequestImageBytesLabel: "Request image payload cap (bytes)",
@@ -821,7 +820,7 @@ var zh = {
821
820
  baseURLLabel: "\u7F51\u5173\u5730\u5740",
822
821
  baseURLHint: "\u540C\u65F6\u63D0\u4F9B\u6A21\u578B\u8BF7\u6C42\u4E0E\u5B9E\u65F6\u6A21\u578B\u5217\u8868\u7684\u7AEF\u70B9\u3002",
823
822
  refreshMinutesLabel: "\u76EE\u5F55\u5237\u65B0\uFF08\u5206\u949F\uFF09",
824
- refreshMinutesHint: "\u4E00\u6B21\u5B9E\u65F6\u6A21\u578B\u5217\u8868\u89E3\u6790\u4FDD\u6301\u6709\u6548\u7684\u65F6\u957F\uFF0C\u5230\u671F\u540E\u91CD\u65B0\u62C9\u53D6\u3002",
823
+ refreshMinutesHint: "\u5DF2\u6709\u8BF7\u6C42\u7684\u76EE\u5F55\u7F13\u5B58\u65F6\u957F\u3002\u8BFB\u53D6\u6216\u5237\u65B0\u6A21\u578B\u5217\u8868\u65F6\uFF0C\u4F1A\u7ACB\u5373\u6838\u5BF9\u7F51\u5173\u4E0E\u5728\u7EBF\u6A21\u578B\u914D\u7F6E\u3002",
825
824
  streamIdleTimeoutMsLabel: "\u6D41\u7A7A\u95F2\u8D85\u65F6\uFF08\u6BEB\u79D2\uFF09",
826
825
  streamIdleTimeoutMsHint: "\u6D41\u4E8B\u4EF6\u95F4\u5141\u8BB8\u7684\u6700\u5927\u9759\u9ED8\u95F4\u9694\uFF0C\u8D85\u51FA\u540E\u8BF7\u6C42\u5931\u8D25\u3002",
827
826
  maxRequestImageBytesLabel: "\u8BF7\u6C42\u56FE\u7247\u8F7D\u8377\u4E0A\u9650\uFF08\u5B57\u8282\uFF09",
@@ -886,7 +885,7 @@ if (!document.querySelector("style[data-plugin-css=" + JSON.stringify(id2) + "]"
886
885
  style.textContent = ".dmPm3q_root{min-width:0;display:inline-flex;position:relative}.dmPm3q_trigger{color:inherit;opacity:.75;font:inherit;cursor:pointer;white-space:nowrap;background:0 0;border:0;border-radius:6px;padding:4px 6px;font-size:12px}.dmPm3q_trigger:hover,.dmPm3q_trigger:focus-visible{opacity:1;background:color-mix(in srgb, currentColor 8%, transparent)}.dmPm3q_panel{z-index:100;border:1px solid color-mix(in srgb, currentColor 18%, transparent);width:280px;max-width:calc(100vw - 32px);color:var(--dsw-alias-label-primary,CanvasText);background:var(--dsw-specific-menu,Canvas);border-radius:12px;padding:16px;font-size:12px;position:absolute;bottom:calc(100% + 12px);right:0;box-shadow:0 8px 30px #0002}.dmPm3q_hint{opacity:.65;font-size:11px;line-height:1.6}.dmPm3q_window{margin-top:14px}.dmPm3q_row{justify-content:space-between;margin-bottom:5px;display:flex}.dmPm3q_window progress{accent-color:#39a878;width:100%;height:5px;margin-bottom:5px;display:block}.dmPm3q_window progress::-webkit-progress-bar{background:color-mix(in srgb, currentColor 15%, transparent);border-radius:4px}.dmPm3q_window progress::-webkit-progress-value{background:#39a878;border-radius:4px}.dmPm3q_window progress::-moz-progress-bar{background:#39a878;border-radius:4px}";
887
886
  document.head.appendChild(style);
888
887
  }
889
- var UsagePill_default = { "root": "dmPm3q_root", "window": "dmPm3q_window", "panel": "dmPm3q_panel", "row": "dmPm3q_row", "hint": "dmPm3q_hint", "trigger": "dmPm3q_trigger" };
888
+ var UsagePill_default = { "hint": "dmPm3q_hint", "row": "dmPm3q_row", "window": "dmPm3q_window", "root": "dmPm3q_root", "trigger": "dmPm3q_trigger", "panel": "dmPm3q_panel" };
890
889
 
891
890
  // src/client/UsagePill.tsx
892
891
  var import_jsx_runtime2 = require("react/jsx-runtime");
package/lib/index.js CHANGED
@@ -641,45 +641,117 @@ import { anthropicMessagesApi } from "@earendil-works/pi-ai/api/anthropic-messag
641
641
  import { openAICompletionsApi } from "@earendil-works/pi-ai/api/openai-completions.lazy";
642
642
  import { openAIResponsesApi } from "@earendil-works/pi-ai/api/openai-responses.lazy";
643
643
  import { attributionHeaders, LlmError as LlmError4 } from "@deepseek-ai/dsh-llm";
644
- var PROVIDER_ID = "opencode-go";
645
- var DISPLAY_NAME = "OpenCode Go";
646
- var DEFAULT_BASE_URL = "https://opencode.ai/zen/go/v1";
647
- var CATALOG_ADDITIONS = [
648
- {
649
- id: "deepseek-v4.1-flash",
650
- siblingId: "deepseek-v4-flash",
651
- inputSiblingId: "deepseek-v4-flash-vision-exp",
652
- name: "DeepSeek V4.1 Flash"
644
+
645
+ // src/model-metadata.ts
646
+ var MODEL_METADATA_URL = "https://models.dev/api.json";
647
+ function record(value) {
648
+ return value !== null && typeof value === "object" && !Array.isArray(value) ? value : {};
649
+ }
650
+ function positiveInteger(value, field) {
651
+ if (typeof value !== "number" || !Number.isSafeInteger(value) || value <= 0) {
652
+ throw new Error(`missing or invalid ${field}`);
653
653
  }
654
- ];
655
- var MODELS_FETCH_TIMEOUT_MS = 1e4;
656
- function curatedModels(baseURL) {
657
- const models = /* @__PURE__ */ new Map();
658
- for (const model of getBuiltinModels("opencode-go")) {
659
- models.set(model.id, { ...model, provider: PROVIDER_ID, baseUrl: baseURL });
654
+ return value;
655
+ }
656
+ function rates(value) {
657
+ const cost = record(value);
658
+ const rate = (key) => {
659
+ const value2 = cost[key];
660
+ return typeof value2 === "number" && Number.isFinite(value2) && value2 >= 0 ? value2 : 0;
661
+ };
662
+ return { input: rate("input"), output: rate("output"), cacheRead: rate("cache_read"), cacheWrite: rate("cache_write") };
663
+ }
664
+ var LEVELS = ["off", "minimal", "low", "medium", "high", "xhigh", "max"];
665
+ function thinkingLevels(metadata, known) {
666
+ if (!Array.isArray(metadata.reasoning_options) && known?.thinkingLevelMap !== void 0) return known.thinkingLevelMap;
667
+ const map = Object.fromEntries(LEVELS.map((level) => [level, null]));
668
+ for (const item of Array.isArray(metadata.reasoning_options) ? metadata.reasoning_options : []) {
669
+ const option = record(item);
670
+ if (option.type === "toggle" || option.type === "budget_tokens") {
671
+ map.off = "off";
672
+ map.high = "high";
673
+ }
674
+ if (option.type !== "effort" || !Array.isArray(option.values)) continue;
675
+ for (const value of option.values) {
676
+ const level = value === "none" ? "off" : value;
677
+ if (LEVELS.includes(level)) map[level] = String(value);
678
+ }
660
679
  }
661
- for (const addition of CATALOG_ADDITIONS) {
662
- if (models.has(addition.id)) continue;
663
- const sibling = models.get(addition.siblingId);
664
- if (sibling === void 0) continue;
665
- const inputSibling = models.get(addition.inputSiblingId);
666
- if (inputSibling === void 0) continue;
667
- models.set(addition.id, {
668
- ...sibling,
669
- id: addition.id,
670
- name: addition.name,
671
- input: [...inputSibling.input],
672
- provider: PROVIDER_ID,
673
- baseUrl: baseURL
674
- });
680
+ if (known?.reasoning && known.thinkingLevelMap?.off !== null) map.off ??= known.thinkingLevelMap?.off ?? "off";
681
+ return map;
682
+ }
683
+ function modelBaseURL(api, baseURL) {
684
+ const base = baseURL.replace(/\/+$/, "");
685
+ return api === "anthropic-messages" ? base.replace(/\/v1$/, "") : base;
686
+ }
687
+ function readModelMetadata(body, baseURL, builtin) {
688
+ const provider = record(record(body)["opencode-go"]);
689
+ if (provider.models === null || typeof provider.models !== "object" || Array.isArray(provider.models)) {
690
+ throw new Error("models.dev has no opencode-go models object");
675
691
  }
676
- if (models.size === 0) {
677
- throw new LlmError4(
678
- "llm-opencode-go: pi-ai's installed catalog describes no opencode-go models; cannot seed the curated table",
679
- "INVALID_CONFIG"
680
- );
692
+ const entries = record(provider.models);
693
+ const models = /* @__PURE__ */ new Map();
694
+ const errors = /* @__PURE__ */ new Map();
695
+ for (const [id, value] of Object.entries(entries)) {
696
+ try {
697
+ const metadata = record(value);
698
+ const npm = record(metadata.provider).npm ?? provider.npm;
699
+ const api = npm === "@ai-sdk/anthropic" ? "anthropic-messages" : npm === "@ai-sdk/openai" ? "openai-responses" : npm === "@ai-sdk/openai-compatible" ? "openai-completions" : void 0;
700
+ if (api === void 0) throw new Error(`unsupported model protocol ${String(npm)}`);
701
+ const limit = record(metadata.limit);
702
+ const input = record(metadata.modalities).input;
703
+ if (!Array.isArray(input) || !input.includes("text")) throw new Error("missing text input modality");
704
+ if (typeof metadata.reasoning !== "boolean") throw new Error("missing reasoning capability");
705
+ const exact = builtin.get(id);
706
+ const family = typeof metadata.family === "string" ? Object.keys(entries).find((key) => record(entries[key]).family === metadata.family && builtin.get(key)?.api === api) : void 0;
707
+ const known = exact?.api === api ? exact : family === void 0 ? void 0 : builtin.get(family);
708
+ const compat = api === "openai-completions" ? {
709
+ supportsStore: false,
710
+ supportsDeveloperRole: false,
711
+ maxTokensField: "max_tokens",
712
+ ...record(metadata.interleaved).field === "reasoning_content" ? { requiresReasoningContentOnAssistantMessages: true } : {},
713
+ ...known?.compat
714
+ } : api === "openai-responses" ? { sessionAffinityFormat: "openai-nosession", ...known?.compat } : { ...known?.compat };
715
+ const cost = rates(metadata.cost);
716
+ const tiers = record(metadata.cost).tiers;
717
+ if (Array.isArray(tiers)) {
718
+ cost.tiers = tiers.flatMap((item) => {
719
+ const tier = record(record(item).tier);
720
+ return tier.type === "context" && typeof tier.size === "number" && Number.isSafeInteger(tier.size) && tier.size > 0 ? [{ ...rates(item), inputTokensAbove: tier.size }] : [];
721
+ }).sort((a, b) => a.inputTokensAbove - b.inputTokensAbove);
722
+ }
723
+ models.set(id, {
724
+ id,
725
+ name: typeof metadata.name === "string" && metadata.name.length > 0 ? metadata.name : id,
726
+ provider: "opencode-go",
727
+ api,
728
+ baseUrl: modelBaseURL(api, baseURL),
729
+ reasoning: metadata.reasoning,
730
+ thinkingLevelMap: thinkingLevels(metadata, known),
731
+ input: input.includes("image") ? ["text", "image"] : ["text"],
732
+ contextWindow: positiveInteger(limit.context, "context limit"),
733
+ maxTokens: positiveInteger(limit.output, "output limit"),
734
+ cost,
735
+ compat
736
+ });
737
+ } catch (error) {
738
+ errors.set(id, error instanceof Error ? error.message : String(error));
739
+ }
681
740
  }
682
- return models;
741
+ return { models, errors };
742
+ }
743
+
744
+ // src/catalog.ts
745
+ var PROVIDER_ID = "opencode-go";
746
+ var DISPLAY_NAME = "OpenCode Go";
747
+ var DEFAULT_BASE_URL = "https://opencode.ai/zen/go/v1";
748
+ var MODELS_FETCH_TIMEOUT_MS = 1e4;
749
+ function builtinModels(baseURL) {
750
+ return new Map(getBuiltinModels("opencode-go").map((model) => [model.id, {
751
+ ...model,
752
+ provider: PROVIDER_ID,
753
+ baseUrl: modelBaseURL(model.api, baseURL)
754
+ }]));
683
755
  }
684
756
  function readLiveModelIds(body) {
685
757
  const data = body?.data;
@@ -689,7 +761,7 @@ function readLiveModelIds(body) {
689
761
  const id = entry?.id;
690
762
  if (typeof id === "string" && id.length > 0) ids.push(id);
691
763
  }
692
- return ids;
764
+ return [...new Set(ids)];
693
765
  }
694
766
  async function fetchLiveModelIds(baseURL) {
695
767
  const url = `${baseURL.replace(/\/+$/, "")}/models`;
@@ -697,25 +769,21 @@ async function fetchLiveModelIds(baseURL) {
697
769
  try {
698
770
  response = await fetch(url, {
699
771
  method: "GET",
772
+ cache: "no-cache",
700
773
  headers: { accept: "application/json", ...attributionHeaders() },
701
774
  signal: AbortSignal.timeout(MODELS_FETCH_TIMEOUT_MS)
702
775
  });
703
776
  } catch (error) {
704
777
  throw new LlmError4(`could not reach ${url}`, "DISCOVERY_FAILED", { cause: error });
705
778
  }
706
- if (!response.ok) {
707
- throw new LlmError4(`${url} answered ${response.status}`, "DISCOVERY_FAILED");
708
- }
779
+ if (!response.ok) throw new LlmError4(`${url} answered ${response.status}`, "DISCOVERY_FAILED");
709
780
  return readLiveModelIds(await response.json());
710
781
  }
711
782
  function harnessApiKeyAuth() {
712
- return {
713
- apiKey: {
714
- name: "OpenCode API key",
715
- /* v8 ignore next -- pi-ai calls resolve only for a request without an apiKey override, and this adapter always passes one */
716
- resolve: () => Promise.resolve({ auth: {}, source: "OpenCode API key" })
717
- }
718
- };
783
+ return { apiKey: {
784
+ name: "OpenCode API key",
785
+ resolve: () => Promise.resolve({ auth: {}, source: "OpenCode API key" })
786
+ } };
719
787
  }
720
788
  function buildProvider(baseURL, models) {
721
789
  return createProvider({
@@ -732,13 +800,6 @@ function buildProvider(baseURL, models) {
732
800
  });
733
801
  }
734
802
  var OpencodeGoCatalog = class {
735
- /**
736
- * @param baseURL - The endpoint the gateway serves; also the listing base.
737
- * @param refreshMs - How long one live resolution stays authoritative.
738
- * @param onFallback - Observes a failed live listing and how many curated
739
- * models kept serving because of it.
740
- * @param onOmitted - Observes live ids the curated table cannot route.
741
- */
742
803
  constructor(baseURL, refreshMs, onFallback, onOmitted) {
743
804
  this.baseURL = baseURL;
744
805
  this.refreshMs = refreshMs;
@@ -747,12 +808,10 @@ var OpencodeGoCatalog = class {
747
808
  }
748
809
  served;
749
810
  pending;
750
- /**
751
- * The current catalog, fetching when expired or never fetched.
752
- * @returns the snapshot now serving, shared by concurrent callers.
753
- */
754
- snapshot() {
755
- if (this.served !== void 0 && Date.now() - this.served.fetchedAtMs < this.refreshMs) {
811
+ metadata;
812
+ metadataETag;
813
+ snapshot(force = false) {
814
+ if (!force && this.served !== void 0 && Date.now() - this.served.fetchedAtMs < this.refreshMs) {
756
815
  return Promise.resolve(this.served);
757
816
  }
758
817
  this.pending ??= this.build().then((snapshot) => {
@@ -763,48 +822,95 @@ var OpencodeGoCatalog = class {
763
822
  });
764
823
  return this.pending;
765
824
  }
766
- /** One resolution: intersect the curated table with the live listing. */
825
+ /** Conditional HTTP requests save bandwidth while still checking for updated metadata. */
826
+ async refreshMetadata(builtin) {
827
+ const response = await fetch(MODEL_METADATA_URL, {
828
+ headers: {
829
+ accept: "application/json",
830
+ ...attributionHeaders(),
831
+ ...this.metadataETag === void 0 ? {} : { "if-none-match": this.metadataETag }
832
+ },
833
+ cache: "no-cache",
834
+ signal: AbortSignal.timeout(MODELS_FETCH_TIMEOUT_MS)
835
+ });
836
+ if (response.status === 304 && this.metadata !== void 0) return this.metadata;
837
+ if (!response.ok) throw new Error(`models.dev answered ${response.status}`);
838
+ const metadata = readModelMetadata(await response.json(), this.baseURL, builtin);
839
+ this.metadata = metadata;
840
+ this.metadataETag = response.headers.get("etag") ?? void 0;
841
+ return metadata;
842
+ }
843
+ /** Gateway ids decide membership; online metadata decides how to call each model. */
767
844
  async build() {
768
- const curated = curatedModels(this.baseURL);
769
- let liveIds;
770
- let failure;
771
- try {
772
- liveIds = await fetchLiveModelIds(this.baseURL);
773
- } catch (error) {
774
- failure = error;
845
+ const builtin = builtinModels(this.baseURL);
846
+ const [listing, metadataResult] = await Promise.allSettled([
847
+ fetchLiveModelIds(this.baseURL),
848
+ this.refreshMetadata(builtin)
849
+ ]);
850
+ const metadata = metadataResult.status === "fulfilled" ? metadataResult.value : this.metadata;
851
+ const known = new Map([...builtin, ...this.served?.models ?? [], ...metadata?.models ?? []]);
852
+ if (metadataResult.status === "rejected") {
853
+ this.onFallback({ url: MODEL_METADATA_URL, error: metadataResult.reason, kept: known.size });
775
854
  }
776
- if (liveIds === void 0) {
777
- this.onFallback({ url: `${this.baseURL.replace(/\/+$/, "")}/models`, error: failure, kept: curated.size });
855
+ if (listing.status === "rejected") {
856
+ const models2 = this.served?.models ?? known;
857
+ this.onFallback({ url: `${this.baseURL.replace(/\/+$/, "")}/models`, error: listing.reason, kept: models2.size });
778
858
  return {
779
- models: curated,
780
- provider: buildProvider(this.baseURL, [...curated.values()]),
859
+ models: models2,
860
+ unavailable: this.served?.unavailable ?? /* @__PURE__ */ new Map(),
861
+ provider: buildProvider(this.baseURL, [...models2.values()]),
781
862
  live: false,
782
863
  fetchedAtMs: Date.now()
783
864
  };
784
865
  }
785
- const live = new Set(liveIds);
786
- const omitted = liveIds.filter((id) => !curated.has(id));
787
- if (omitted.length > 0) this.onOmitted(omitted);
788
- const served = [...curated.values()].filter((model) => live.has(model.id));
866
+ const models = /* @__PURE__ */ new Map();
867
+ const unavailable = /* @__PURE__ */ new Map();
868
+ for (const id of listing.value) {
869
+ const error = metadata?.errors.get(id);
870
+ const model = known.get(id);
871
+ if (error !== void 0 || model === void 0) {
872
+ unavailable.set(id, error ?? "model metadata has not been published on models.dev yet");
873
+ } else {
874
+ models.set(id, model);
875
+ }
876
+ }
877
+ if (unavailable.size > 0) this.onOmitted([...unavailable.keys()]);
789
878
  return {
790
- models: new Map(served.map((model) => [model.id, model])),
791
- provider: buildProvider(this.baseURL, served),
879
+ models,
880
+ unavailable,
881
+ provider: buildProvider(this.baseURL, [...models.values()]),
792
882
  live: true,
793
883
  fetchedAtMs: Date.now()
794
884
  };
795
885
  }
886
+ /** New or previously unconfigured ids get a fresh lookup even during the runtime TTL. */
887
+ async forModel(id) {
888
+ const cached = this.served;
889
+ let snapshot = await this.snapshot();
890
+ if (!snapshot.models.has(id) && snapshot === cached) snapshot = await this.snapshot(true);
891
+ if (snapshot.unavailable.has(id)) {
892
+ throw new LlmError4(
893
+ `opencode-go model "${id}" is advertised but cannot be configured: ${snapshot.unavailable.get(id)}; refresh the model list to retry`,
894
+ "MODEL_METADATA_UNAVAILABLE"
895
+ );
896
+ }
897
+ return snapshot;
898
+ }
796
899
  };
797
900
  async function discoverCatalogModels(catalog) {
798
- const snapshot = await catalog.snapshot();
901
+ const snapshot = await catalog.snapshot(true);
799
902
  if (!snapshot.live) {
800
903
  throw new LlmError4("llm-opencode-go: the live model listing is unreachable; try again later", "DISCOVERY_FAILED");
801
904
  }
802
- return [...snapshot.models.values()].map((model) => ({
803
- id: model.id,
804
- name: model.name,
805
- contextWindow: model.contextWindow,
806
- maxTokens: model.maxTokens
807
- }));
905
+ return [
906
+ ...[...snapshot.models.values()].map((model) => ({
907
+ id: model.id,
908
+ name: model.name,
909
+ contextWindow: model.contextWindow,
910
+ maxTokens: model.maxTokens
911
+ })),
912
+ ...[...snapshot.unavailable].map(([id, reason]) => ({ id, name: `${id} (metadata unavailable: ${reason})` }))
913
+ ];
808
914
  }
809
915
 
810
916
  // src/config.ts
@@ -888,16 +994,21 @@ var OpencodeGoAdapter = class extends LlmAdapter {
888
994
  return { id: provider, name: DISPLAY_NAME };
889
995
  }
890
996
  async listModels(_provider) {
891
- const snapshot = await this.catalogOf(this.options.config()).snapshot();
892
- return [...snapshot.models.values()].map((model) => ({
997
+ const snapshot = await this.catalogOf(this.options.config()).snapshot(true);
998
+ return [...[...snapshot.models.values()].map((model) => ({
893
999
  provider: PROVIDER_ID,
894
1000
  id: model.id,
895
1001
  name: model.name,
896
1002
  inputModalities: [...model.input]
897
- }));
1003
+ })), ...[...snapshot.unavailable].map(([id, reason]) => ({
1004
+ provider: PROVIDER_ID,
1005
+ id,
1006
+ name: id,
1007
+ description: `Metadata unavailable: ${reason}`
1008
+ }))];
898
1009
  }
899
1010
  async resolveModel(_provider, model, _signal) {
900
- const snapshot = await this.catalogOf(this.options.config()).snapshot();
1011
+ const snapshot = await this.catalogOf(this.options.config()).forModel(model);
901
1012
  const resolved = snapshot.models.get(model);
902
1013
  if (resolved === void 0) {
903
1014
  throw new LlmError5(`opencode-go has no model "${model}"`, "UNKNOWN_MODEL");
@@ -941,7 +1052,7 @@ var OpencodeGoAdapter = class extends LlmAdapter {
941
1052
  throw new LlmError5("llm-opencode-go does not support GenerateOptions.stop", "UNSUPPORTED_OPTION");
942
1053
  }
943
1054
  const config = this.options.config();
944
- const snapshot = await this.catalogOf(config).snapshot();
1055
+ const snapshot = await this.catalogOf(config).forModel(options.model);
945
1056
  const model = snapshot.models.get(options.model);
946
1057
  if (model === void 0) {
947
1058
  throw new LlmError5(`opencode-go has no model "${options.model}"`, "UNKNOWN_MODEL");
@@ -1122,11 +1233,11 @@ function apply(ctx, raw) {
1122
1233
  };
1123
1234
  ctx.plugin(GoUsageService, { baseURL: () => current().baseURL, resolveApiKey });
1124
1235
  const logger = {
1125
- fallback: ({ error }) => {
1126
- ctx.logger.warn(`llm-opencode-go: live model listing unreachable; serving the curated table until the next refresh (${String(error)})`);
1236
+ fallback: ({ url, error }) => {
1237
+ ctx.logger.warn(`llm-opencode-go: could not refresh ${url}; using last-known model data (${String(error)})`);
1127
1238
  },
1128
1239
  omitted: (ids) => {
1129
- ctx.logger.info(`llm-opencode-go: live listing carries ids the curated table cannot route; omitted: ${ids.join(", ")}`);
1240
+ ctx.logger.warn(`llm-opencode-go: gateway models awaiting usable online metadata: ${ids.join(", ")}`);
1130
1241
  }
1131
1242
  };
1132
1243
  const adapter = new OpencodeGoAdapter({
@@ -1,95 +1,43 @@
1
- /**
2
- * The OpenCode Go model catalog: a curated table intersected with the gateway's
3
- * live model list.
4
- *
5
- * The curated table is seeded from pi-ai's installed `opencode-go` catalog —
6
- * its entries carry the wire protocol, compat quirks, thinking-level spellings,
7
- * and capacities that the gateway's own listing endpoint does not disclose —
8
- * plus this package's additions for models the installed catalog has not
9
- * caught up with. The gateway rotates its list faster than either pi-ai or
10
- * this package releases, so the served catalog is the curated table **minus**
11
- * models the live listing no longer advertises: a model the gateway retired
12
- * must disappear rather than error mid-session. Live ids the curated table
13
- * does not describe are omitted too, because without a protocol mapping they
14
- * cannot be routed correctly.
15
- *
16
- * The live listing at `{baseURL}/models` is public and answers ids only. A
17
- * fetch failure never takes the route down: the curated table is served as-is
18
- * and the failure is reported, on the grounds that a network blip should not
19
- * deny service, while model discovery — whose whole purpose is the live
20
- * answer — fails loud instead.
21
- *
22
- * @module dsh-llm-opencode-go/catalog
23
- */
24
1
  import type { Api, Model, Provider } from '@earendil-works/pi-ai';
25
2
  import type { LlmDiscoveredModel } from '@deepseek-ai/dsh-llm';
26
- /** Provider route key this plugin registers and every materialized model carries. */
27
3
  export declare const PROVIDER_ID = "opencode-go";
28
- /** Display name for selectors and status labels. */
29
4
  export declare const DISPLAY_NAME = "OpenCode Go";
30
- /** Endpoint serving both model requests and the model listing. */
31
5
  export declare const DEFAULT_BASE_URL = "https://opencode.ai/zen/go/v1";
32
- /** One immutable resolution of the served catalog. */
33
6
  export interface CatalogSnapshot {
34
- /** Served models by id: the curated table minus models absent from the live listing. */
35
7
  readonly models: ReadonlyMap<string, Model<Api>>;
36
- /** The provider serving exactly these models. */
8
+ /** Advertised ids with missing/unsupported metadata stay visible with a diagnostic. */
9
+ readonly unavailable: ReadonlyMap<string, string>;
37
10
  readonly provider: Provider;
38
- /** Whether the live listing answered this resolution; `false` on the fallback path. */
39
11
  readonly live: boolean;
40
- /** When this resolution completed, for the refresh interval. */
41
12
  readonly fetchedAtMs: number;
42
13
  }
43
- /**
44
- * Read one live listing reply. The endpoint answers the standard OpenAI
45
- * `data` array with entries carrying ids only; anything else is a fault the
46
- * caller treats as "live unavailable" and falls back. An honest empty array
47
- * is a valid answer: the gateway says it serves nothing.
48
- * @param body - the decoded JSON reply.
49
- * @returns the non-empty ids it advertises, in reply order.
50
- * @throws {Error} when the reply carries no `data` array.
51
- */
14
+ /** A valid empty listing means the gateway serves nothing; malformed replies are failures. */
52
15
  export declare function readLiveModelIds(body: unknown): readonly string[];
53
- /**
54
- * TTL-cached resolution of the served catalog. Concurrent callers share one
55
- * in-flight fetch; a completed snapshot serves unchanged until the refresh
56
- * interval elapses, so one stream call never observes two catalog generations.
57
- */
16
+ /** Runtime requests reuse a snapshot; discovery revalidates it. Concurrent reads coalesce. */
58
17
  export declare class OpencodeGoCatalog {
59
18
  private readonly baseURL;
60
19
  private readonly refreshMs;
61
20
  private readonly onFallback;
21
+ /** Kept for API compatibility; now reports unconfigured ids rather than hiding them. */
62
22
  private readonly onOmitted;
63
23
  private served;
64
24
  private pending;
65
- /**
66
- * @param baseURL - The endpoint the gateway serves; also the listing base.
67
- * @param refreshMs - How long one live resolution stays authoritative.
68
- * @param onFallback - Observes a failed live listing and how many curated
69
- * models kept serving because of it.
70
- * @param onOmitted - Observes live ids the curated table cannot route.
71
- */
25
+ private metadata;
26
+ private metadataETag;
72
27
  constructor(baseURL: string, refreshMs: number, onFallback: (detail: {
73
28
  url: string;
74
29
  error: unknown;
75
30
  kept: number;
76
- }) => void, onOmitted: (ids: readonly string[]) => void);
77
- /**
78
- * The current catalog, fetching when expired or never fetched.
79
- * @returns the snapshot now serving, shared by concurrent callers.
80
- */
81
- snapshot(): Promise<CatalogSnapshot>;
82
- /** One resolution: intersect the curated table with the live listing. */
31
+ }) => void,
32
+ /** Kept for API compatibility; now reports unconfigured ids rather than hiding them. */
33
+ onOmitted: (ids: readonly string[]) => void);
34
+ snapshot(force?: boolean): Promise<CatalogSnapshot>;
35
+ /** Conditional HTTP requests save bandwidth while still checking for updated metadata. */
36
+ private refreshMetadata;
37
+ /** Gateway ids decide membership; online metadata decides how to call each model. */
83
38
  private build;
39
+ /** New or previously unconfigured ids get a fresh lookup even during the runtime TTL. */
40
+ forModel(id: string): Promise<CatalogSnapshot>;
84
41
  }
85
- /**
86
- * Candidate models for the configuration surface's "fetch available models"
87
- * action: the same live intersection the route would serve, with the curated
88
- * capacities the listing endpoint does not disclose. Unlike route resolution,
89
- * discovery fails when the live listing is unreachable: its whole purpose is
90
- * the live answer, and a stale answer would offer models the gateway retired.
91
- * @param catalog - The route's catalog resolver.
92
- * @returns the served models in curated order.
93
- * @throws LlmError `DISCOVERY_FAILED` when the live listing is unreachable.
94
- */
42
+ /** Explicit discovery always revalidates both sources, including during the runtime TTL. */
95
43
  export declare function discoverCatalogModels(catalog: OpencodeGoCatalog): Promise<readonly LlmDiscoveredModel[]>;
@@ -11,7 +11,7 @@
11
11
  import z from '@deepseek-ai/schemastery';
12
12
  /** Environment variable resolving the OpenCode API key. */
13
13
  export declare const DEFAULT_API_KEY_ENV = "OPENCODE_API_KEY";
14
- /** Default interval between live catalog re-resolutions. */
14
+ /** Runtime request cache lifetime; model listing/discovery always revalidates immediately. */
15
15
  export declare const DEFAULT_REFRESH_MINUTES = 60;
16
16
  /** Default maximum idle interval while a stream read is outstanding. */
17
17
  export declare const DEFAULT_STREAM_IDLE_TIMEOUT_MS = 300000;
@@ -29,7 +29,7 @@ export interface OpencodeGoConfig {
29
29
  apiKeyEnv: string;
30
30
  /** The gateway endpoint; also the base of the live model listing. */
31
31
  baseURL: string;
32
- /** How long one live catalog resolution stays authoritative, in minutes. */
32
+ /** Runtime request cache lifetime in minutes; explicit catalog reads bypass it. */
33
33
  refreshMinutes: number;
34
34
  /** Largest idle gap between stream events before the request fails. */
35
35
  streamIdleTimeoutMs: number;
@@ -0,0 +1,15 @@
1
+ /** Convert OpenCode's online models.dev metadata into the SDK's three wire protocols. */
2
+ import type { Api, Model } from '@earendil-works/pi-ai';
3
+ export declare const MODEL_METADATA_URL = "https://models.dev/api.json";
4
+ export interface ModelMetadata {
5
+ readonly models: ReadonlyMap<string, Model<Api>>;
6
+ readonly errors: ReadonlyMap<string, string>;
7
+ }
8
+ /** Anthropic's SDK appends /v1/messages; the OpenAI SDKs append paths below /v1. */
9
+ export declare function modelBaseURL(api: Api, baseURL: string): string;
10
+ /**
11
+ * Only read the opencode-go record. Online endpoints, headers and credentials
12
+ * are deliberately ignored: model traffic always stays on the configured gateway.
13
+ * A bad entry is isolated instead of discarding every other model.
14
+ */
15
+ export declare function readModelMetadata(body: unknown, baseURL: string, builtin: ReadonlyMap<string, Model<Api>>): ModelMetadata;
package/package.json CHANGED
@@ -1,9 +1,17 @@
1
1
  {
2
2
  "name": "dsh-opencode-go",
3
- "version": "0.1.1",
3
+ "version": "0.1.3",
4
4
  "description": "OpenCode Go model provider and settings UI for DeepSeek Harness",
5
5
  "type": "module",
6
6
  "license": "MIT",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/Duskriver/dsh-opencode-go.git"
10
+ },
11
+ "homepage": "https://github.com/Duskriver/dsh-opencode-go#readme",
12
+ "bugs": {
13
+ "url": "https://github.com/Duskriver/dsh-opencode-go/issues"
14
+ },
7
15
  "engines": {
8
16
  "node": "^22.19.0 || >=24.0.0"
9
17
  },
@@ -178,6 +186,7 @@
178
186
  },
179
187
  "keywords": [
180
188
  "deepseek-harness",
189
+ "dsh-plugin",
181
190
  "dsh",
182
191
  "opencode",
183
192
  "opencode-go",