@goodandready/dsh-opencode-provider 0.5.5 → 0.5.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +5 -0
- package/README.md +4 -4
- package/README.ru.md +4 -4
- package/README.zh.md +4 -4
- package/lib/index.d.ts +9 -0
- package/lib/index.js +58 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.5.6
|
|
4
|
+
|
|
5
|
+
- **Persistent sticky routing**: the last successful account/model per conversation survives adapter and Desktop restarts. `~/.dsh/storages/dsh-opencode/sticky.json` stores hashed session ids and key fingerprints only (no keys or prompt text), keeps 500 entries for 14 days.
|
|
6
|
+
- 88 tests.
|
|
7
|
+
|
|
3
8
|
## 0.5.5
|
|
4
9
|
|
|
5
10
|
- **Per-account request stats**: every upstream attempt now records its outcome, latency and TTFT in the local request log; the heatmap adds a per-account table with requests, errors and average latency/TTFT. No prompt or response content is stored.
|
package/README.md
CHANGED
|
@@ -95,7 +95,7 @@ graph LR
|
|
|
95
95
|
- **`/opencode` command** for status, usage, accounts, models, rules, and `test [model]`.
|
|
96
96
|
- **TUI settings section** for the terminal profile (`/settings → OpenCode`) with the key, endpoints, allowlist, active account, fallback model, sticky routing and timeouts.
|
|
97
97
|
- **Quota fallback**: choose a free model in Settings → OpenCode; a Go quota rejection retries once through Console/Zen.
|
|
98
|
-
- **Sticky account**: the last successful account is preferred for the same conversation; explicit model rules and pinned accounts win.
|
|
98
|
+
- **Sticky account**: the last successful account is preferred for the same conversation and survives restarts via a local `sticky.json` (hashed session ids and key fingerprints only); explicit model rules and pinned accounts win.
|
|
99
99
|
- **Routing trace** in the quota panel and per-account heatmap filter.
|
|
100
100
|
- **One-click plugin update** in Settings → OpenCode: current/latest version, an update badge, and an **Update now** button that installs the exact npm version through the DSH CLI and asks for a restart. The host endpoint is `GET/POST /dsh-opencode/update`.
|
|
101
101
|
- **Plan-aware Go picker**: hides models outside the Go table unless turned off; descriptions carry `Go`, `Console` or `FREE`.
|
|
@@ -123,12 +123,12 @@ npm pack
|
|
|
123
123
|
Install the **packed tarball** into a profile:
|
|
124
124
|
|
|
125
125
|
```bash
|
|
126
|
-
dsh plugin --profile web add /absolute/path/to/goodandready-dsh-opencode-provider-0.5.
|
|
126
|
+
dsh plugin --profile web add /absolute/path/to/goodandready-dsh-opencode-provider-0.5.6.tgz
|
|
127
127
|
# DSH Desktop:
|
|
128
|
-
dsh plugin --profile desktop add /absolute/path/to/goodandready-dsh-opencode-provider-0.5.
|
|
128
|
+
dsh plugin --profile desktop add /absolute/path/to/goodandready-dsh-opencode-provider-0.5.6.tgz
|
|
129
129
|
|
|
130
130
|
# Or install the published package:
|
|
131
|
-
dsh plugin --profile web add @goodandready/dsh-opencode-provider@0.5.
|
|
131
|
+
dsh plugin --profile web add @goodandready/dsh-opencode-provider@0.5.6
|
|
132
132
|
```
|
|
133
133
|
|
|
134
134
|
Restart dsh (or DSH Desktop), then pick `OpenCode` or `OpenCode Go` in the model picker.
|
package/README.ru.md
CHANGED
|
@@ -94,7 +94,7 @@ graph LR
|
|
|
94
94
|
- **Онбординг аккаунтов**: **Import all from auth.json** переносит все ключи OpenCode в отдельные credential ref и добавляет аккаунты; **Check all accounts** обновляет квоты и health по каждому аккаунту.
|
|
95
95
|
- **Правила «модель ➔ аккаунт»**: сопоставление по glob-паттернам закрепляет предпочтительный аккаунт за моделью; остальные аккаунты остаются в резерве.
|
|
96
96
|
- **Квотный fallback**: выберите бесплатную модель в настройках; при ошибке квоты Go тот же запрос один раз повторяется через Console/Zen. Если fallback тоже не сработал, возвращается исходная ошибка квоты.
|
|
97
|
-
- **Липкий аккаунт**: последний успешный аккаунт предпочитается в том же
|
|
97
|
+
- **Липкий аккаунт**: последний успешный аккаунт предпочитается в том же диалоге и сохраняется между перезапусками в локальном `sticky.json` (только хешированные session id и fingerprint ключей); явные правила модели, закреплённый аккаунт и квотный fallback имеют приоритет.
|
|
98
98
|
- **Трассировка маршрутизации**: панель квот показывает последние решения (`rule`, `sticky`, `rotation`, `quota-fallback`) без промптов и ключей.
|
|
99
99
|
- **Статистика по аккаунтам**: в request log пишутся `ok`, `latencyMs`, `ttftMs` и `error`; heatmap показывает таблицу запросов, ошибок и средних latency/TTFT по каждому аккаунту.
|
|
100
100
|
- **Почасовой Heatmap запросов**: сетка «день недели × час» в локальном времени плюс суточная полоса агрегации. Фильтры переключают отображение между всеми запросами, конкретной моделью и аккаунтом. Лог `~/.dsh/storages/dsh-opencode/requests.jsonl` хранит только метаданные `{at, provider, model, purpose, account, reason, ok, latencyMs, ttftMs, error}` (промпты и ответы не сохраняются).
|
|
@@ -122,12 +122,12 @@ npm pack
|
|
|
122
122
|
Установка из **упакованного tarball**:
|
|
123
123
|
|
|
124
124
|
```bash
|
|
125
|
-
dsh plugin --profile web add /absolute/path/to/goodandready-dsh-opencode-provider-0.5.
|
|
125
|
+
dsh plugin --profile web add /absolute/path/to/goodandready-dsh-opencode-provider-0.5.6.tgz
|
|
126
126
|
# DSH Desktop:
|
|
127
|
-
dsh plugin --profile desktop add /absolute/path/to/goodandready-dsh-opencode-provider-0.5.
|
|
127
|
+
dsh plugin --profile desktop add /absolute/path/to/goodandready-dsh-opencode-provider-0.5.6.tgz
|
|
128
128
|
|
|
129
129
|
# Либо установка опубликованного пакета из npm:
|
|
130
|
-
dsh plugin --profile web add @goodandready/dsh-opencode-provider@0.5.
|
|
130
|
+
dsh plugin --profile web add @goodandready/dsh-opencode-provider@0.5.6
|
|
131
131
|
```
|
|
132
132
|
|
|
133
133
|
После установки перезапустите dsh (или DSH Desktop) и выберите **OpenCode** или **OpenCode Go** в списке моделей.
|
package/README.zh.md
CHANGED
|
@@ -94,7 +94,7 @@ graph LR
|
|
|
94
94
|
- **每账户预计可用时长**:按各窗口的平均消耗速度估算耗尽时间,并标记在重置前最先耗尽的窗口;API 仅提供当前百分比与重置时间,因此为近似值。
|
|
95
95
|
- **模型 ➔ 账户路由规则**:支持 Glob 通配符(如 `*free*`、`deepseek-*`)将模型优先绑定至特定账户,其他账户保留为备用。
|
|
96
96
|
- **配额回退**:在设置中选择免费模型;Go 返回配额错误时,同一请求通过 Console/Zen 重试一次。若回退也失败,返回原始配额错误。
|
|
97
|
-
-
|
|
97
|
+
- **会话粘性账户**:同一会话优先使用上次成功的账户,并通过本地 `sticky.json` 跨重启保留(仅哈希后的 session id 和密钥指纹);显式模型规则、固定账户和配额回退优先。
|
|
98
98
|
- **路由追踪**:配额面板显示最近的账户/模型决策(`rule`、`sticky`、`rotation`、`quota-fallback`),不记录提示词或密钥。
|
|
99
99
|
- **按账户请求统计**:请求日志记录 `ok`、`latencyMs`、`ttftMs` 和 `error`;热力图表格显示每个账户的请求数、错误数及平均延迟/TTFT。
|
|
100
100
|
- **本地时间请求热力图**:星期×小时交互式热力图,排除系统心跳与重复记录,可按模型或账户过滤,仅保存 `{at, provider, model, purpose, account, reason, ok, latencyMs, ttftMs, error}` 元数据,不保存任何提示词与回复。
|
|
@@ -119,12 +119,12 @@ npm pack
|
|
|
119
119
|
通过**打包文件 (.tgz)**安装:
|
|
120
120
|
|
|
121
121
|
```bash
|
|
122
|
-
dsh plugin --profile web add /absolute/path/to/goodandready-dsh-opencode-provider-0.5.
|
|
122
|
+
dsh plugin --profile web add /absolute/path/to/goodandready-dsh-opencode-provider-0.5.6.tgz
|
|
123
123
|
# DSH 桌面端:
|
|
124
|
-
dsh plugin --profile desktop add /absolute/path/to/goodandready-dsh-opencode-provider-0.5.
|
|
124
|
+
dsh plugin --profile desktop add /absolute/path/to/goodandready-dsh-opencode-provider-0.5.6.tgz
|
|
125
125
|
|
|
126
126
|
# 或从 npm 安装已发布版本:
|
|
127
|
-
dsh plugin --profile web add @goodandready/dsh-opencode-provider@0.5.
|
|
127
|
+
dsh plugin --profile web add @goodandready/dsh-opencode-provider@0.5.6
|
|
128
128
|
```
|
|
129
129
|
|
|
130
130
|
安装完成后重启 dsh 或 DSH 桌面端,在模型选择器中选择 **OpenCode** 或 **OpenCode Go** 即可。
|
package/lib/index.d.ts
CHANGED
|
@@ -176,6 +176,8 @@ interface OpenCodeAdapterDeps {
|
|
|
176
176
|
readonly recordRequest?: (provider: string, model: string, purpose?: string, account?: string, reason?: string, outcome?: RequestOutcome) => void;
|
|
177
177
|
/** Test seam; defaults to ~/.dsh/storages/dsh-opencode/cooldowns.json. */
|
|
178
178
|
readonly cooldownPath?: string;
|
|
179
|
+
/** Test seam; defaults to ~/.dsh/storages/dsh-opencode/sticky.json. */
|
|
180
|
+
readonly stickyPath?: string;
|
|
179
181
|
/** Receives a one-time warning when the provider tool limit drops tools. */
|
|
180
182
|
readonly warn?: (message: string) => void;
|
|
181
183
|
}
|
|
@@ -183,7 +185,9 @@ declare class OpenCodeAdapter extends LlmAdapter {
|
|
|
183
185
|
private readonly deps;
|
|
184
186
|
private readonly fetchImpl;
|
|
185
187
|
private readonly cooldownPath;
|
|
188
|
+
private readonly stickyPath;
|
|
186
189
|
private cooldownWrites;
|
|
190
|
+
private stickyWrites;
|
|
187
191
|
/** key fingerprint → cooldown deadline and the rejection that caused it. */
|
|
188
192
|
private readonly cooldowns;
|
|
189
193
|
private readonly sticky;
|
|
@@ -201,6 +205,11 @@ declare class OpenCodeAdapter extends LlmAdapter {
|
|
|
201
205
|
stream(options: GenerateOptions): AsyncIterable<StreamChunk>;
|
|
202
206
|
private run;
|
|
203
207
|
private rememberSticky;
|
|
208
|
+
/** Await pending sticky persistence writes (tests and shutdown). */
|
|
209
|
+
flushSticky(): Promise<void>;
|
|
210
|
+
private loadSticky;
|
|
211
|
+
private pruneSticky;
|
|
212
|
+
private persistSticky;
|
|
204
213
|
private pushTrace;
|
|
205
214
|
private parse;
|
|
206
215
|
private send;
|
package/lib/index.js
CHANGED
|
@@ -17,7 +17,7 @@ const DEFAULT_GO_API_BASE = "https://opencode.ai/inference/go";
|
|
|
17
17
|
const DEFAULT_GO_ZEN_BASE = "https://opencode.ai/zen/go/v1";
|
|
18
18
|
const DEFAULT_USAGE_URL = "https://opencode.ai/zen/go/v1/usage";
|
|
19
19
|
const DEFAULT_AUTH_ENTRY = "opencode-go";
|
|
20
|
-
const DEFAULT_USER_AGENT = "dsh-opencode-provider/0.5.
|
|
20
|
+
const DEFAULT_USER_AGENT = "dsh-opencode-provider/0.5.6";
|
|
21
21
|
const DEFAULT_CONTEXT_WINDOW = 262144;
|
|
22
22
|
const DEFAULT_MAX_TOKENS = 32768;
|
|
23
23
|
const DEFAULT_GENERATE_MAX_TOKENS = 32768;
|
|
@@ -3076,11 +3076,17 @@ function defaultCooldownPath() {
|
|
|
3076
3076
|
const home = process.env.DSH_HOME?.trim() || join(homedir(), ".dsh");
|
|
3077
3077
|
return join(home, "storages", "dsh-opencode", "cooldowns.json");
|
|
3078
3078
|
}
|
|
3079
|
+
function defaultStickyPath() {
|
|
3080
|
+
const home = process.env.DSH_HOME?.trim() || join(homedir(), ".dsh");
|
|
3081
|
+
return join(home, "storages", "dsh-opencode", "sticky.json");
|
|
3082
|
+
}
|
|
3079
3083
|
var OpenCodeAdapter = class extends LlmAdapter {
|
|
3080
3084
|
deps;
|
|
3081
3085
|
fetchImpl;
|
|
3082
3086
|
cooldownPath;
|
|
3087
|
+
stickyPath;
|
|
3083
3088
|
cooldownWrites = Promise.resolve();
|
|
3089
|
+
stickyWrites = Promise.resolve();
|
|
3084
3090
|
/** key fingerprint → cooldown deadline and the rejection that caused it. */
|
|
3085
3091
|
cooldowns = /* @__PURE__ */ new Map();
|
|
3086
3092
|
sticky = /* @__PURE__ */ new Map();
|
|
@@ -3091,7 +3097,9 @@ var OpenCodeAdapter = class extends LlmAdapter {
|
|
|
3091
3097
|
this.deps = deps;
|
|
3092
3098
|
this.fetchImpl = deps.fetchImpl ?? fetch;
|
|
3093
3099
|
this.cooldownPath = deps.cooldownPath ?? defaultCooldownPath();
|
|
3100
|
+
this.stickyPath = deps.stickyPath ?? defaultStickyPath();
|
|
3094
3101
|
this.loadCooldowns();
|
|
3102
|
+
this.loadSticky();
|
|
3095
3103
|
}
|
|
3096
3104
|
providerInfo(provider) {
|
|
3097
3105
|
return {
|
|
@@ -3315,6 +3323,55 @@ var OpenCodeAdapter = class extends LlmAdapter {
|
|
|
3315
3323
|
model: options.model,
|
|
3316
3324
|
at: Date.now()
|
|
3317
3325
|
});
|
|
3326
|
+
this.persistSticky();
|
|
3327
|
+
}
|
|
3328
|
+
/** Await pending sticky persistence writes (tests and shutdown). */
|
|
3329
|
+
async flushSticky() {
|
|
3330
|
+
await this.stickyWrites;
|
|
3331
|
+
}
|
|
3332
|
+
loadSticky() {
|
|
3333
|
+
try {
|
|
3334
|
+
const entries = JSON.parse(readFileSync(this.stickyPath, "utf8")).sticky;
|
|
3335
|
+
if (typeof entries !== "object" || entries === null || Array.isArray(entries)) return;
|
|
3336
|
+
for (const [sessionId, value] of Object.entries(entries)) {
|
|
3337
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) continue;
|
|
3338
|
+
const record = value;
|
|
3339
|
+
const fingerprint = record.fingerprint;
|
|
3340
|
+
const label = record.label;
|
|
3341
|
+
const provider = record.provider;
|
|
3342
|
+
const model = record.model;
|
|
3343
|
+
const at = record.at;
|
|
3344
|
+
if (typeof fingerprint !== "string" || !/^[0-9a-f]{16}$/.test(fingerprint)) continue;
|
|
3345
|
+
if (typeof label !== "string" || label === "") continue;
|
|
3346
|
+
if (typeof provider !== "string" || provider === "") continue;
|
|
3347
|
+
if (typeof model !== "string" || model === "") continue;
|
|
3348
|
+
if (typeof at !== "number" || !Number.isFinite(at)) continue;
|
|
3349
|
+
this.sticky.set(sessionId, {
|
|
3350
|
+
fingerprint,
|
|
3351
|
+
label,
|
|
3352
|
+
provider,
|
|
3353
|
+
model,
|
|
3354
|
+
at
|
|
3355
|
+
});
|
|
3356
|
+
}
|
|
3357
|
+
this.pruneSticky();
|
|
3358
|
+
} catch {}
|
|
3359
|
+
}
|
|
3360
|
+
pruneSticky() {
|
|
3361
|
+
const cutoff = Date.now() - 12096e5;
|
|
3362
|
+
for (const [sessionId, entry] of this.sticky) if (entry.at < cutoff) this.sticky.delete(sessionId);
|
|
3363
|
+
const overflow = this.sticky.size - 500;
|
|
3364
|
+
if (overflow <= 0) return;
|
|
3365
|
+
const oldest = [...this.sticky.entries()].sort((a, b) => a[1].at - b[1].at).slice(0, overflow);
|
|
3366
|
+
for (const [sessionId] of oldest) this.sticky.delete(sessionId);
|
|
3367
|
+
}
|
|
3368
|
+
persistSticky() {
|
|
3369
|
+
this.pruneSticky();
|
|
3370
|
+
const payload = JSON.stringify({ sticky: Object.fromEntries(this.sticky) });
|
|
3371
|
+
this.stickyWrites = this.stickyWrites.then(async () => {
|
|
3372
|
+
await mkdir(dirname(this.stickyPath), { recursive: true });
|
|
3373
|
+
await writeFile(this.stickyPath, payload, "utf8");
|
|
3374
|
+
}).catch(() => {});
|
|
3318
3375
|
}
|
|
3319
3376
|
pushTrace(entry) {
|
|
3320
3377
|
this.routing.unshift(entry);
|