dsh-opencode-go 0.1.2 → 0.1.4
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 +30 -48
- package/docs/verification.md +6 -0
- package/lib/client.js +5 -6
- package/lib/index.js +199 -92
- package/lib/types/catalog.d.ts +17 -69
- package/lib/types/config.d.ts +2 -2
- package/lib/types/model-metadata.d.ts +15 -0
- package/package.json +1 -1
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
|
|
5
|
+
插件自动添加 OpenCode Go 所需的会话请求头,并从网关获取可用模型目录,显示套餐剩余额度。通过 DSH 插件命令安装,无需修改 DSH 源码。
|
|
6
|
+
|
|
7
|
+
无需任何设置,一key开始
|
|
6
8
|
|
|
7
|
-
## 安装与使用
|
|
8
9
|
|
|
9
|
-
|
|
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
|
|
55
|
+
更新 Web profile 中的插件到 npm 最新版本:
|
|
55
56
|
|
|
56
|
-
|
|
57
|
+
```sh
|
|
58
|
+
dsh plugin --profile web update dsh-opencode-go --latest
|
|
59
|
+
```
|
|
57
60
|
|
|
58
|
-
|
|
61
|
+
完成后重启 `dsh web` 并刷新浏览器。Headless 用户将 `web` 换成 `headless`;如果两个 profile 都安装了插件,需要分别升级。
|
|
59
62
|
|
|
60
|
-
|
|
63
|
+
如果此前安装的是本地目录或 `.tgz` 文件,用以下命令切换到 npm 最新版:
|
|
61
64
|
|
|
62
|
-
|
|
65
|
+
```sh
|
|
66
|
+
dsh plugin --profile web add dsh-opencode-go@latest
|
|
67
|
+
```
|
|
63
68
|
|
|
64
|
-
|
|
69
|
+
升级插件无需卸载,也无需重新填写 API Key。模型目录会自动同步,正常新增模型不需要再次升级插件。
|
|
65
70
|
|
|
66
|
-
|
|
71
|
+
## 订阅用量显示
|
|
67
72
|
|
|
68
|
-
|
|
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
|
+

|
|
80
74
|
|
|
81
|
-
|
|
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,22 @@ 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 无法可靠推断调用方式。上游新增全新协议或协议特例时,仍可能需要适配。
|
|
93
|
+
|
|
94
|
+
模型具有推理能力但没有可调节的推理档位时(如 `union-alpha`),仍可正常选择和使用,只是不显示推理强度选项。
|
|
98
95
|
|
|
99
|
-
|
|
96
|
+
在线配置暂时不可达时,优先复用本次运行中成功获取的配置,以 pi-ai 内置配置作为备用。网关目录不可达时,已有请求可以使用上次目录;设置页刷新会显示失败,避免把旧目录误认为最新结果。`refreshMinutes` 只控制已有模型请求的缓存时长,不阻止主动读取列表获取新模型。
|
|
100
97
|
|
|
101
98
|
## 功能说明
|
|
102
99
|
|
|
103
100
|
- **会话请求头**:每次请求包含 Harness User-Agent 和 `x-opencode-session`。同一会话保持相同 ID,无会话 ID 的请求使用独立随机值。
|
|
104
101
|
- **流式与历史**:支持流式输出、工具调用及历史回放,协议请求由 pi-ai 执行。
|
|
105
102
|
- **图片输入**:支持目录中声明图片能力的模型,需要 DSH attachment 服务。
|
|
106
|
-
- **提示与缓存**:插件不增加隐藏系统提示;会话 ID
|
|
103
|
+
- **提示与缓存**:插件不增加隐藏系统提示;会话 ID 用于网关路由;
|
|
107
104
|
|
|
108
105
|
## 卸载
|
|
109
106
|
|
|
@@ -115,26 +112,11 @@ dsh plugin --profile web remove dsh-opencode-go
|
|
|
115
112
|
dsh plugin --profile headless remove dsh-opencode-go
|
|
116
113
|
```
|
|
117
114
|
|
|
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
115
|
|
|
131
|
-
`npm test` 会先构建再执行测试;`npm pack` 生成可安装的 `.tgz` 包。依赖均通过 npm 安装,不需要 DSH 源码仓库。
|
|
132
116
|
|
|
133
|
-
|
|
134
|
-
- [架构与依赖](docs/independent-package.md):Host、Client 构建方式和转换代码的维护方式。
|
|
135
|
-
- [第三方声明](THIRD_PARTY_NOTICES.md):代码来源与许可。
|
|
117
|
+
## 反馈
|
|
136
118
|
|
|
137
|
-
|
|
119
|
+
遇到bug或有功能建议请提issue
|
|
138
120
|
|
|
139
121
|
## 许可证
|
|
140
122
|
|
package/docs/verification.md
CHANGED
|
@@ -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 = { "
|
|
43
|
+
var Section_default = { "intro": "BtKYva_intro", "head": "BtKYva_head", "label": "BtKYva_label", "invalid": "BtKYva_invalid", "hint": "BtKYva_hint", "chevron": "BtKYva_chevron", "chips": "BtKYva_chips", "failedNote": "BtKYva_failedNote", "advanced": "BtKYva_advanced", "input": "BtKYva_input", "reset": "BtKYva_reset", "badges": "BtKYva_badges", "actions": "BtKYva_actions", "inputInvalid": "BtKYva_inputInvalid", "disclosure": "BtKYva_disclosure", "chevronOpen": "BtKYva_chevronOpen", "field": "BtKYva_field" };
|
|
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.
|
|
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: "
|
|
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: "\
|
|
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 = { "
|
|
888
|
+
var UsagePill_default = { "panel": "dmPm3q_panel", "trigger": "dmPm3q_trigger", "hint": "dmPm3q_hint", "root": "dmPm3q_root", "window": "dmPm3q_window", "row": "dmPm3q_row" };
|
|
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
|
-
|
|
645
|
-
|
|
646
|
-
var
|
|
647
|
-
|
|
648
|
-
{
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
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
|
-
|
|
656
|
-
function
|
|
657
|
-
const
|
|
658
|
-
|
|
659
|
-
|
|
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
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
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
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
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
|
-
|
|
714
|
-
|
|
715
|
-
|
|
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
|
-
|
|
752
|
-
|
|
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
|
-
/**
|
|
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
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
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 (
|
|
777
|
-
|
|
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:
|
|
780
|
-
|
|
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
|
|
786
|
-
const
|
|
787
|
-
|
|
788
|
-
|
|
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
|
|
791
|
-
|
|
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 [
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
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,7 +994,7 @@ 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();
|
|
997
|
+
const snapshot = await this.catalogOf(this.options.config()).snapshot(true);
|
|
892
998
|
return [...snapshot.models.values()].map((model) => ({
|
|
893
999
|
provider: PROVIDER_ID,
|
|
894
1000
|
id: model.id,
|
|
@@ -897,7 +1003,7 @@ var OpencodeGoAdapter = class extends LlmAdapter {
|
|
|
897
1003
|
}));
|
|
898
1004
|
}
|
|
899
1005
|
async resolveModel(_provider, model, _signal) {
|
|
900
|
-
const snapshot = await this.catalogOf(this.options.config()).
|
|
1006
|
+
const snapshot = await this.catalogOf(this.options.config()).forModel(model);
|
|
901
1007
|
const resolved = snapshot.models.get(model);
|
|
902
1008
|
if (resolved === void 0) {
|
|
903
1009
|
throw new LlmError5(`opencode-go has no model "${model}"`, "UNKNOWN_MODEL");
|
|
@@ -907,9 +1013,10 @@ var OpencodeGoAdapter = class extends LlmAdapter {
|
|
|
907
1013
|
/** Describe one model: capacities plus the reasoning levels it actually offers. */
|
|
908
1014
|
modelInfo(model) {
|
|
909
1015
|
const reasoning = {};
|
|
910
|
-
|
|
1016
|
+
const levels = model.reasoning ? getSupportedThinkingLevels(model) : [];
|
|
1017
|
+
if (levels.length > 0) {
|
|
911
1018
|
reasoning.reasoning = {
|
|
912
|
-
efforts:
|
|
1019
|
+
efforts: levels.map((level) => ({
|
|
913
1020
|
id: ReasoningEffortId(level),
|
|
914
1021
|
name: `${level.charAt(0).toUpperCase()}${level.slice(1)}`
|
|
915
1022
|
}))
|
|
@@ -941,7 +1048,7 @@ var OpencodeGoAdapter = class extends LlmAdapter {
|
|
|
941
1048
|
throw new LlmError5("llm-opencode-go does not support GenerateOptions.stop", "UNSUPPORTED_OPTION");
|
|
942
1049
|
}
|
|
943
1050
|
const config = this.options.config();
|
|
944
|
-
const snapshot = await this.catalogOf(config).
|
|
1051
|
+
const snapshot = await this.catalogOf(config).forModel(options.model);
|
|
945
1052
|
const model = snapshot.models.get(options.model);
|
|
946
1053
|
if (model === void 0) {
|
|
947
1054
|
throw new LlmError5(`opencode-go has no model "${options.model}"`, "UNKNOWN_MODEL");
|
|
@@ -1122,11 +1229,11 @@ function apply(ctx, raw) {
|
|
|
1122
1229
|
};
|
|
1123
1230
|
ctx.plugin(GoUsageService, { baseURL: () => current().baseURL, resolveApiKey });
|
|
1124
1231
|
const logger = {
|
|
1125
|
-
fallback: ({ error }) => {
|
|
1126
|
-
ctx.logger.warn(`llm-opencode-go:
|
|
1232
|
+
fallback: ({ url, error }) => {
|
|
1233
|
+
ctx.logger.warn(`llm-opencode-go: could not refresh ${url}; using last-known model data (${String(error)})`);
|
|
1127
1234
|
},
|
|
1128
1235
|
omitted: (ids) => {
|
|
1129
|
-
ctx.logger.
|
|
1236
|
+
ctx.logger.warn(`llm-opencode-go: gateway models awaiting usable online metadata: ${ids.join(", ")}`);
|
|
1130
1237
|
}
|
|
1131
1238
|
};
|
|
1132
1239
|
const adapter = new OpencodeGoAdapter({
|
package/lib/types/catalog.d.ts
CHANGED
|
@@ -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
|
-
/**
|
|
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
|
-
|
|
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,
|
|
77
|
-
/**
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
/**
|
|
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[]>;
|
package/lib/types/config.d.ts
CHANGED
|
@@ -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
|
-
/**
|
|
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
|
-
/**
|
|
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;
|