dsh-opencode-go 0.1.19 → 0.1.21
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.en.md +51 -4
- package/README.md +43 -5
- package/docs/development.md +4 -2
- package/docs/verification.md +105 -0
- package/lib/client.js +1390 -184
- package/lib/index.js +351 -23
- package/lib/types/accounts.d.ts +26 -0
- package/lib/types/adapter.d.ts +14 -0
- package/lib/types/catalog.d.ts +2 -1
- package/lib/types/client/AccountsCard.d.ts +19 -0
- package/lib/types/client/Section.d.ts +3 -6
- package/lib/types/client/UsagePill.d.ts +4 -2
- package/lib/types/client/accounts-controller.d.ts +80 -0
- package/lib/types/client/locales.d.ts +46 -1
- package/lib/types/client/section-controller.d.ts +13 -2
- package/lib/types/client/staged-form.d.ts +8 -0
- package/lib/types/config.d.ts +6 -0
- package/lib/types/index.d.ts +1 -0
- package/lib/types/json-response.d.ts +1 -1
- package/lib/types/proxy-url.d.ts +2 -0
- package/lib/types/proxy.d.ts +11 -0
- package/lib/types/usage-contract.d.ts +5 -0
- package/lib/types/usage.d.ts +14 -2
- package/package.json +4 -2
package/lib/index.js
CHANGED
|
@@ -988,7 +988,7 @@ function diagnosticURL(raw) {
|
|
|
988
988
|
url.hash = "";
|
|
989
989
|
return url.href;
|
|
990
990
|
}
|
|
991
|
-
async function fetchJsonResponse(url, init, maxBytes, encoding = "identity") {
|
|
991
|
+
async function fetchJsonResponse(url, init, maxBytes, encoding = "identity", fetcher = globalThis.fetch) {
|
|
992
992
|
if ((init.method ?? "GET").toUpperCase() !== "GET") throw new TypeError("JSON requests must use GET");
|
|
993
993
|
const headers = new Headers(init.headers);
|
|
994
994
|
headers.set("accept-encoding", encoding);
|
|
@@ -996,7 +996,7 @@ async function fetchJsonResponse(url, init, maxBytes, encoding = "identity") {
|
|
|
996
996
|
for (let attempt = 0; ; attempt += 1) {
|
|
997
997
|
init.signal?.throwIfAborted();
|
|
998
998
|
try {
|
|
999
|
-
const response = await
|
|
999
|
+
const response = await fetcher(url, options);
|
|
1000
1000
|
if (!response.ok) {
|
|
1001
1001
|
await response.body?.cancel().catch(() => {
|
|
1002
1002
|
});
|
|
@@ -1167,7 +1167,7 @@ function readLiveModelIds(body) {
|
|
|
1167
1167
|
}
|
|
1168
1168
|
return [...new Set(ids)];
|
|
1169
1169
|
}
|
|
1170
|
-
async function fetchLiveModelIds(baseURL) {
|
|
1170
|
+
async function fetchLiveModelIds(baseURL, fetcher) {
|
|
1171
1171
|
const url = `${baseURL.replace(/\/+$/, "")}/models`;
|
|
1172
1172
|
const endpoint = diagnosticURL(url);
|
|
1173
1173
|
let result;
|
|
@@ -1177,7 +1177,7 @@ async function fetchLiveModelIds(baseURL) {
|
|
|
1177
1177
|
cache: "no-cache",
|
|
1178
1178
|
headers: { ...attributionHeaders(), accept: "application/json" },
|
|
1179
1179
|
signal: AbortSignal.timeout(MODELS_FETCH_TIMEOUT_MS)
|
|
1180
|
-
}, MODEL_LISTING_MAX_BYTES);
|
|
1180
|
+
}, MODEL_LISTING_MAX_BYTES, "identity", fetcher);
|
|
1181
1181
|
} catch (error) {
|
|
1182
1182
|
const detail = error instanceof SyntaxError ? "invalid JSON response" : error instanceof RangeError ? `response exceeds the ${MODEL_LISTING_MAX_BYTES} byte limit` : transportFailure(error);
|
|
1183
1183
|
const action = error instanceof SyntaxError || error instanceof RangeError ? "read" : "reach";
|
|
@@ -1213,12 +1213,13 @@ function buildProvider(baseURL, models) {
|
|
|
1213
1213
|
}
|
|
1214
1214
|
var OpencodeGoCatalog = class {
|
|
1215
1215
|
constructor(baseURL, refreshMs, onFallback, onOmitted, onRefresh = () => {
|
|
1216
|
-
}) {
|
|
1216
|
+
}, fetcher) {
|
|
1217
1217
|
this.baseURL = baseURL;
|
|
1218
1218
|
this.refreshMs = refreshMs;
|
|
1219
1219
|
this.onFallback = onFallback;
|
|
1220
1220
|
this.onOmitted = onOmitted;
|
|
1221
1221
|
this.onRefresh = onRefresh;
|
|
1222
|
+
this.fetcher = fetcher;
|
|
1222
1223
|
}
|
|
1223
1224
|
served;
|
|
1224
1225
|
pending;
|
|
@@ -1260,7 +1261,7 @@ var OpencodeGoCatalog = class {
|
|
|
1260
1261
|
}
|
|
1261
1262
|
startRefresh(force) {
|
|
1262
1263
|
const builtin = builtinModels(this.baseURL);
|
|
1263
|
-
const listing = settled(fetchLiveModelIds(this.baseURL).then((ids) => ({ ids, updatedAtMs: Date.now() })));
|
|
1264
|
+
const listing = settled(fetchLiveModelIds(this.baseURL, this.fetcher).then((ids) => ({ ids, updatedAtMs: Date.now() })));
|
|
1264
1265
|
const restored = this.metadata;
|
|
1265
1266
|
if (!force && this.served === void 0 && restored !== void 0) {
|
|
1266
1267
|
const status = { metadataLive: false, metadataUpdatedAtMs: this.metadataUpdatedAtMs };
|
|
@@ -1304,7 +1305,7 @@ var OpencodeGoCatalog = class {
|
|
|
1304
1305
|
},
|
|
1305
1306
|
cache: "no-cache",
|
|
1306
1307
|
signal: AbortSignal.timeout(METADATA_FETCH_TIMEOUT_MS)
|
|
1307
|
-
}, MODEL_METADATA_MAX_BYTES, "gzip");
|
|
1308
|
+
}, MODEL_METADATA_MAX_BYTES, "gzip", this.fetcher);
|
|
1308
1309
|
if (response.status === 304 && this.metadata !== void 0) {
|
|
1309
1310
|
this.metadataUpdatedAtMs = Date.now();
|
|
1310
1311
|
this.metadataETag = metadataETag(response.headers.get("etag")) ?? this.metadataETag;
|
|
@@ -1464,6 +1465,35 @@ import z from "@deepseek-ai/schemastery";
|
|
|
1464
1465
|
var USAGE_DISPLAY_MODES = ["auto", "always", "off"];
|
|
1465
1466
|
var DEFAULT_USAGE_DISPLAY = "auto";
|
|
1466
1467
|
|
|
1468
|
+
// src/accounts.ts
|
|
1469
|
+
var MAX_ACCOUNTS = 20;
|
|
1470
|
+
var DEFAULT_ACCOUNT_REF = "OPENCODE_API_KEY";
|
|
1471
|
+
function accountRefOf(settings) {
|
|
1472
|
+
return settings.apiKeyEnv || DEFAULT_ACCOUNT_REF;
|
|
1473
|
+
}
|
|
1474
|
+
function accountsOf(settings) {
|
|
1475
|
+
const ref = accountRefOf(settings);
|
|
1476
|
+
if (settings.accounts?.length === 0) return [];
|
|
1477
|
+
const accounts = settings.accounts ?? [];
|
|
1478
|
+
return accounts.some((account) => account.apiKeyEnv === ref) ? accounts : [
|
|
1479
|
+
{ id: `legacy:${ref}`, name: "", apiKeyEnv: ref },
|
|
1480
|
+
...accounts
|
|
1481
|
+
];
|
|
1482
|
+
}
|
|
1483
|
+
function assertAccounts(accounts, selectedRef) {
|
|
1484
|
+
if (accounts == null) return;
|
|
1485
|
+
if (!Array.isArray(accounts) || accounts.length > MAX_ACCOUNTS || accounts.length === MAX_ACCOUNTS && selectedRef && !accounts.some((account) => account.apiKeyEnv === selectedRef)) {
|
|
1486
|
+
throw new Error(`OpenCode Go supports at most ${MAX_ACCOUNTS} accounts, including the selected reference`);
|
|
1487
|
+
}
|
|
1488
|
+
const ids = /* @__PURE__ */ new Set();
|
|
1489
|
+
const refs = /* @__PURE__ */ new Set();
|
|
1490
|
+
for (const account of accounts) {
|
|
1491
|
+
if (!account || typeof account.id !== "string" || !account.id || account.id.length > 128 || account.id.startsWith("legacy:") && account.id !== `legacy:${account.apiKeyEnv}` || typeof account.name !== "string" || !account.name.trim() && account.id !== `legacy:${account.apiKeyEnv}` || account.name.length > 80 || typeof account.apiKeyEnv !== "string" || !/^[A-Za-z_][A-Za-z0-9_]*$/.test(account.apiKeyEnv) || ids.has(account.id) || refs.has(account.apiKeyEnv)) throw new Error("Invalid or duplicate OpenCode Go account");
|
|
1492
|
+
ids.add(account.id);
|
|
1493
|
+
refs.add(account.apiKeyEnv);
|
|
1494
|
+
}
|
|
1495
|
+
}
|
|
1496
|
+
|
|
1467
1497
|
// src/config.ts
|
|
1468
1498
|
var DEFAULT_API_KEY_ENV = "OPENCODE_API_KEY";
|
|
1469
1499
|
var DEFAULT_REFRESH_MINUTES = 60;
|
|
@@ -1473,7 +1503,14 @@ var fields = {
|
|
|
1473
1503
|
usageDisplay: z.union(USAGE_DISPLAY_MODES.map((mode) => z.const(mode))).default(DEFAULT_USAGE_DISPLAY),
|
|
1474
1504
|
modelVisibility: z.dict(z.boolean().required()).default({}),
|
|
1475
1505
|
apiKeyEnv: z.string().role("credential-ref").default(DEFAULT_API_KEY_ENV),
|
|
1506
|
+
accounts: z.union([z.const(null), z.array(z.object({
|
|
1507
|
+
id: z.string().required(),
|
|
1508
|
+
name: z.string().required(),
|
|
1509
|
+
apiKeyEnv: z.string().role("credential-ref").required()
|
|
1510
|
+
})).max(MAX_ACCOUNTS)]).default(null),
|
|
1511
|
+
autoSwitch: z.boolean().default(false),
|
|
1476
1512
|
baseURL: z.string().default(DEFAULT_BASE_URL),
|
|
1513
|
+
proxyURL: z.string().default(""),
|
|
1477
1514
|
refreshMinutes: z.number().step(1).min(1).max(7 * 24 * 60).default(DEFAULT_REFRESH_MINUTES),
|
|
1478
1515
|
streamIdleTimeoutMs: z.number().min(Number.MIN_VALUE).max(MAX_TIMER_DELAY_MS).default(DEFAULT_STREAM_IDLE_TIMEOUT_MS),
|
|
1479
1516
|
maxImages: z.number().step(1).min(1).max(Number.MAX_SAFE_INTEGER),
|
|
@@ -1511,7 +1548,87 @@ function assertBaseURL(raw) {
|
|
|
1511
1548
|
return url.toString().replace(/\/+$/, "");
|
|
1512
1549
|
}
|
|
1513
1550
|
|
|
1551
|
+
// src/proxy-url.ts
|
|
1552
|
+
function assertProxyURL(raw) {
|
|
1553
|
+
const value = raw?.trim() ?? "";
|
|
1554
|
+
if (!value) return "";
|
|
1555
|
+
let url;
|
|
1556
|
+
try {
|
|
1557
|
+
url = new URL(value);
|
|
1558
|
+
} catch {
|
|
1559
|
+
throw new Error("OpenCode Go: proxy address must be a valid URL");
|
|
1560
|
+
}
|
|
1561
|
+
if (!["http:", "https:", "socks5:"].includes(url.protocol)) {
|
|
1562
|
+
throw new Error("OpenCode Go: proxy address must use http://, https:// or socks5://");
|
|
1563
|
+
}
|
|
1564
|
+
if (!url.hostname || url.port === "0" || url.pathname !== "" && url.pathname !== "/" || url.search || url.hash) {
|
|
1565
|
+
throw new Error("OpenCode Go: proxy address must contain a host and optional port, without a path, query or fragment");
|
|
1566
|
+
}
|
|
1567
|
+
try {
|
|
1568
|
+
decodeURIComponent(url.username);
|
|
1569
|
+
decodeURIComponent(url.password);
|
|
1570
|
+
} catch {
|
|
1571
|
+
throw new Error("OpenCode Go: proxy credentials contain invalid URL encoding");
|
|
1572
|
+
}
|
|
1573
|
+
return url.href;
|
|
1574
|
+
}
|
|
1575
|
+
|
|
1576
|
+
// src/proxy.ts
|
|
1577
|
+
import { ProxyAgent, Socks5ProxyAgent } from "undici";
|
|
1578
|
+
var ProxyTransport = class {
|
|
1579
|
+
current;
|
|
1580
|
+
retired = /* @__PURE__ */ new Set();
|
|
1581
|
+
disposed = false;
|
|
1582
|
+
forProxy(raw) {
|
|
1583
|
+
const address = assertProxyURL(raw);
|
|
1584
|
+
if (!address) {
|
|
1585
|
+
this.retire();
|
|
1586
|
+
return void 0;
|
|
1587
|
+
}
|
|
1588
|
+
return (input, init) => {
|
|
1589
|
+
if (this.disposed) return Promise.reject(new Error("OpenCode Go proxy transport is disposed"));
|
|
1590
|
+
if (this.current?.address !== address) {
|
|
1591
|
+
const url = new URL(address);
|
|
1592
|
+
const dispatcher = url.protocol === "socks5:" ? new Socks5ProxyAgent(url) : new ProxyAgent(address);
|
|
1593
|
+
this.retire();
|
|
1594
|
+
this.current = { address, dispatcher };
|
|
1595
|
+
}
|
|
1596
|
+
const options = { ...init, dispatcher: this.current.dispatcher };
|
|
1597
|
+
return globalThis.fetch(input, options);
|
|
1598
|
+
};
|
|
1599
|
+
}
|
|
1600
|
+
/** Close replaced pools after their in-flight response bodies have finished. */
|
|
1601
|
+
retire() {
|
|
1602
|
+
if (!this.current) return;
|
|
1603
|
+
const { dispatcher } = this.current;
|
|
1604
|
+
this.current = void 0;
|
|
1605
|
+
this.retired.add(dispatcher);
|
|
1606
|
+
void dispatcher.close().catch(() => dispatcher.destroy()).catch(() => {
|
|
1607
|
+
}).finally(() => {
|
|
1608
|
+
this.retired.delete(dispatcher);
|
|
1609
|
+
});
|
|
1610
|
+
}
|
|
1611
|
+
/** Unloading the mount cancels remaining requests and releases every pool. */
|
|
1612
|
+
async dispose() {
|
|
1613
|
+
this.disposed = true;
|
|
1614
|
+
const dispatchers = [...this.retired, ...this.current ? [this.current.dispatcher] : []];
|
|
1615
|
+
this.current = void 0;
|
|
1616
|
+
this.retired.clear();
|
|
1617
|
+
await Promise.all(dispatchers.map((dispatcher) => dispatcher.destroy().catch(() => {
|
|
1618
|
+
})));
|
|
1619
|
+
}
|
|
1620
|
+
};
|
|
1621
|
+
|
|
1514
1622
|
// src/adapter.ts
|
|
1623
|
+
function accountFailureReason(failure) {
|
|
1624
|
+
if (failure.code === "QUOTA") return "quota";
|
|
1625
|
+
if (failure.code === "MISSING_CREDENTIAL" || failure.code === "INVALID_CREDENTIAL" || failure.code === "AUTH" && (/\b401\b/.test(failure.message) || /invalid[ _-]?(?:api[ _-]?)?key|incorrect[ _-]?(?:api[ _-]?)?key|expired[ _-]?(?:api[ _-]?)?key/i.test(failure.message))) return "credential";
|
|
1626
|
+
return void 0;
|
|
1627
|
+
}
|
|
1628
|
+
var REJECTED_KEY_TTL_MS = 5 * 6e4;
|
|
1629
|
+
function isGatewayKeyRejection(failure) {
|
|
1630
|
+
return failure.code === "INVALID_CREDENTIAL" || failure.code === "AUTH";
|
|
1631
|
+
}
|
|
1515
1632
|
function withModelLimit(model, limits) {
|
|
1516
1633
|
const limit = limits[model.id];
|
|
1517
1634
|
if (limit == null) return model;
|
|
@@ -1528,6 +1645,7 @@ var OpencodeGoAdapter = class extends LlmAdapter {
|
|
|
1528
1645
|
constructor(options) {
|
|
1529
1646
|
super();
|
|
1530
1647
|
this.options = options;
|
|
1648
|
+
this.transport = options.transport ?? new ProxyTransport();
|
|
1531
1649
|
}
|
|
1532
1650
|
/**
|
|
1533
1651
|
* One catalog instance per endpoint/refresh pair. A settings write that
|
|
@@ -1536,6 +1654,12 @@ var OpencodeGoAdapter = class extends LlmAdapter {
|
|
|
1536
1654
|
* for the whole refresh interval.
|
|
1537
1655
|
*/
|
|
1538
1656
|
catalogCache;
|
|
1657
|
+
transport;
|
|
1658
|
+
/** Per-reference rejection deadlines, isolated by the gateway that rejected it. */
|
|
1659
|
+
rejectedKeys = /* @__PURE__ */ new Map();
|
|
1660
|
+
dispose() {
|
|
1661
|
+
return this.transport.dispose();
|
|
1662
|
+
}
|
|
1539
1663
|
/**
|
|
1540
1664
|
* The catalog resolver for one configuration, rebuilding on the facts it
|
|
1541
1665
|
* owns. Public for the plugin's discovery registration, which resolves the
|
|
@@ -1544,7 +1668,7 @@ var OpencodeGoAdapter = class extends LlmAdapter {
|
|
|
1544
1668
|
* @returns the resolver caching catalog values, independent of deployment limits.
|
|
1545
1669
|
*/
|
|
1546
1670
|
catalogOf(config) {
|
|
1547
|
-
const key =
|
|
1671
|
+
const key = JSON.stringify([config.baseURL, config.refreshMinutes, assertProxyURL(config.proxyURL)]);
|
|
1548
1672
|
if (this.catalogCache?.key !== key) {
|
|
1549
1673
|
const catalog = new OpencodeGoCatalog(
|
|
1550
1674
|
assertBaseURL(config.baseURL),
|
|
@@ -1557,7 +1681,8 @@ var OpencodeGoAdapter = class extends LlmAdapter {
|
|
|
1557
1681
|
}),
|
|
1558
1682
|
() => {
|
|
1559
1683
|
if (this.catalogCache?.catalog === catalog) this.options.onCatalogRefresh?.();
|
|
1560
|
-
}
|
|
1684
|
+
},
|
|
1685
|
+
this.transport.forProxy(config.proxyURL)
|
|
1561
1686
|
);
|
|
1562
1687
|
this.catalogCache = { key, catalog };
|
|
1563
1688
|
}
|
|
@@ -1649,6 +1774,93 @@ var OpencodeGoAdapter = class extends LlmAdapter {
|
|
|
1649
1774
|
yield* this.streamWithSnapshot(options, snapshot);
|
|
1650
1775
|
}
|
|
1651
1776
|
async *streamWithSnapshot(options, snapshot) {
|
|
1777
|
+
const accounts = accountsOf(snapshot.config);
|
|
1778
|
+
if (accounts.length === 0) throw new LlmError6("OpenCode Go has no accounts", "MISSING_CREDENTIAL");
|
|
1779
|
+
const ordered = snapshot.config.autoSwitch ? [.../* @__PURE__ */ new Set([snapshot.config.apiKeyEnv, ...accounts.map((account) => account.apiKeyEnv)])] : [snapshot.config.apiKeyEnv];
|
|
1780
|
+
const now = Date.now();
|
|
1781
|
+
const gateway = assertBaseURL(snapshot.config.baseURL);
|
|
1782
|
+
const usable = ordered.filter((ref) => {
|
|
1783
|
+
const rejected = this.rejectedKeys.get(ref);
|
|
1784
|
+
const until = rejected?.get(gateway);
|
|
1785
|
+
if (until === void 0) return true;
|
|
1786
|
+
if (now >= until) {
|
|
1787
|
+
rejected.delete(gateway);
|
|
1788
|
+
if (rejected.size === 0) this.rejectedKeys.delete(ref);
|
|
1789
|
+
return true;
|
|
1790
|
+
}
|
|
1791
|
+
return false;
|
|
1792
|
+
});
|
|
1793
|
+
const refs = usable.length > 0 ? usable : ordered;
|
|
1794
|
+
const skippedRef = ordered.find((ref) => !refs.includes(ref));
|
|
1795
|
+
let switchReason;
|
|
1796
|
+
let failedRef;
|
|
1797
|
+
for (let attempt = 0; attempt < refs.length; attempt++) {
|
|
1798
|
+
let emitted = false;
|
|
1799
|
+
let usage;
|
|
1800
|
+
let retry = false;
|
|
1801
|
+
const announce = () => {
|
|
1802
|
+
if (emitted) return;
|
|
1803
|
+
const serving = refs[attempt];
|
|
1804
|
+
if (serving === snapshot.config.apiKeyEnv) this.options.onAccountSwitch?.(void 0, snapshot.config);
|
|
1805
|
+
else this.options.onAccountSwitch?.({
|
|
1806
|
+
// The notice names the account the journey left: the first one that
|
|
1807
|
+
// failed this request, else the first one skipped for a remembered
|
|
1808
|
+
// rejection.
|
|
1809
|
+
fromRef: failedRef ?? skippedRef ?? snapshot.config.apiKeyEnv,
|
|
1810
|
+
toRef: serving,
|
|
1811
|
+
reason: switchReason ?? "credential",
|
|
1812
|
+
at: Date.now()
|
|
1813
|
+
}, snapshot.config);
|
|
1814
|
+
};
|
|
1815
|
+
try {
|
|
1816
|
+
for await (const chunk of this.streamAttempt(options, {
|
|
1817
|
+
...snapshot,
|
|
1818
|
+
config: { ...snapshot.config, apiKeyEnv: refs[attempt] }
|
|
1819
|
+
})) {
|
|
1820
|
+
if (chunk.type === "usage") {
|
|
1821
|
+
usage = chunk;
|
|
1822
|
+
continue;
|
|
1823
|
+
}
|
|
1824
|
+
if (chunk.type === "finish") {
|
|
1825
|
+
const failure = chunk.reason.kind === "error" ? chunk.reason.failure : void 0;
|
|
1826
|
+
const reason = failure === void 0 ? void 0 : accountFailureReason(failure);
|
|
1827
|
+
if (reason && failure !== void 0 && !emitted && !options.signal?.aborted && !((usage?.usage.totalTokens ?? 0) > 0) && attempt + 1 < refs.length) {
|
|
1828
|
+
if (isGatewayKeyRejection(failure)) this.rememberRejectedKey(refs[attempt], gateway);
|
|
1829
|
+
switchReason ??= reason;
|
|
1830
|
+
failedRef ??= refs[attempt];
|
|
1831
|
+
retry = true;
|
|
1832
|
+
break;
|
|
1833
|
+
}
|
|
1834
|
+
if (chunk.reason.kind === "stop" || chunk.reason.kind === "tool-calls" || chunk.reason.kind === "max-tokens") announce();
|
|
1835
|
+
if (usage) yield usage;
|
|
1836
|
+
yield chunk;
|
|
1837
|
+
return;
|
|
1838
|
+
}
|
|
1839
|
+
announce();
|
|
1840
|
+
emitted = true;
|
|
1841
|
+
yield chunk;
|
|
1842
|
+
}
|
|
1843
|
+
if (!retry) return;
|
|
1844
|
+
} catch (error) {
|
|
1845
|
+
const reason = error instanceof LlmError6 ? accountFailureReason(error) : void 0;
|
|
1846
|
+
if (!reason || emitted || options.signal?.aborted || attempt + 1 >= refs.length) throw error;
|
|
1847
|
+
if (error instanceof LlmError6 && isGatewayKeyRejection(error)) this.rememberRejectedKey(refs[attempt], gateway);
|
|
1848
|
+
switchReason ??= reason;
|
|
1849
|
+
failedRef ??= refs[attempt];
|
|
1850
|
+
}
|
|
1851
|
+
}
|
|
1852
|
+
}
|
|
1853
|
+
rememberRejectedKey(ref, gateway) {
|
|
1854
|
+
const rejected = this.rejectedKeys.get(ref) ?? /* @__PURE__ */ new Map();
|
|
1855
|
+
rejected.set(gateway, Date.now() + REJECTED_KEY_TTL_MS);
|
|
1856
|
+
this.rejectedKeys.set(ref, rejected);
|
|
1857
|
+
}
|
|
1858
|
+
/** A stored change to a reference outranks the gateway's last rejection of it. */
|
|
1859
|
+
forgetRejectedKey(ref) {
|
|
1860
|
+
this.rejectedKeys.delete(ref);
|
|
1861
|
+
}
|
|
1862
|
+
/** One account, one SDK attempt; host recovery still owns failures after output. */
|
|
1863
|
+
async *streamAttempt(options, snapshot) {
|
|
1652
1864
|
var _stack = [];
|
|
1653
1865
|
try {
|
|
1654
1866
|
if (options.stop !== void 0) {
|
|
@@ -1691,6 +1903,7 @@ var OpencodeGoAdapter = class extends LlmAdapter {
|
|
|
1691
1903
|
const context = imageRequest === void 0 ? toPiContext(options, void 0, this.options.onReplayDegrade) : await toPiContext({ ...options, signal: watchdog.signal }, imageRequest, this.options.onReplayDegrade);
|
|
1692
1904
|
const events = catalog.provider.streamSimple(withRequestReasoning(model, reasoning), normalizeContext(context), {
|
|
1693
1905
|
apiKey,
|
|
1906
|
+
fetch: this.transport.forProxy(config.proxyURL),
|
|
1694
1907
|
...reasoning === void 0 || reasoning === "off" ? {} : { reasoning },
|
|
1695
1908
|
...options.temperature === void 0 ? {} : { temperature: options.temperature },
|
|
1696
1909
|
...maxTokens === void 0 ? {} : { maxTokens },
|
|
@@ -1753,6 +1966,11 @@ import { RemoteError, TypertRemoteService } from "@deepseek-ai/dsh-typert-protoc
|
|
|
1753
1966
|
import { attributionHeaders as attributionHeaders3 } from "@deepseek-ai/dsh-llm";
|
|
1754
1967
|
|
|
1755
1968
|
// src/usage-contract.ts
|
|
1969
|
+
function parseAccountSwitch(value) {
|
|
1970
|
+
const notice = value;
|
|
1971
|
+
if (!notice || typeof notice.fromRef !== "string" || typeof notice.toRef !== "string" || !/^[A-Za-z_][A-Za-z0-9_]*$/.test(notice.fromRef) || !/^[A-Za-z_][A-Za-z0-9_]*$/.test(notice.toRef) || notice.reason !== "quota" && notice.reason !== "credential" || typeof notice.at !== "number" || !Number.isSafeInteger(notice.at) || notice.at < 0 || notice.at > 864e13) throw new Error("Invalid account switch notice");
|
|
1972
|
+
return { fromRef: notice.fromRef, toRef: notice.toRef, reason: notice.reason, at: notice.at };
|
|
1973
|
+
}
|
|
1756
1974
|
function parseGoUsage(value) {
|
|
1757
1975
|
if (!value || typeof value !== "object") throw new Error("Invalid OpenCode Go usage response");
|
|
1758
1976
|
const source = value;
|
|
@@ -1763,6 +1981,9 @@ function parseGoUsage(value) {
|
|
|
1763
1981
|
}
|
|
1764
1982
|
result.source = source.source;
|
|
1765
1983
|
}
|
|
1984
|
+
if (source.lastSwitch !== void 0) {
|
|
1985
|
+
result.lastSwitch = parseAccountSwitch(source.lastSwitch);
|
|
1986
|
+
}
|
|
1766
1987
|
for (const key of ["rolling", "weekly", "monthly"]) {
|
|
1767
1988
|
const row = source[key];
|
|
1768
1989
|
if (!row || row.status !== "ok" && row.status !== "rate-limited" || typeof row.percent !== "number" || !Number.isFinite(row.percent) || row.percent < 0 || typeof row.resetsAt !== "string" || !Number.isFinite(Date.parse(row.resetsAt))) {
|
|
@@ -1778,6 +1999,16 @@ var usageCodec = {
|
|
|
1778
1999
|
schema: { parse: parseGoUsage },
|
|
1779
2000
|
create: () => ({ parse: parseGoUsage })
|
|
1780
2001
|
};
|
|
2002
|
+
var parseRef = (value) => {
|
|
2003
|
+
if (typeof value !== "string" || !/^[A-Za-z_][A-Za-z0-9_]*$/.test(value)) throw new Error("Invalid account reference");
|
|
2004
|
+
return value;
|
|
2005
|
+
};
|
|
2006
|
+
var refCodec = {
|
|
2007
|
+
mode: "strict",
|
|
2008
|
+
typeSymbol: "dsh-opencode-go#AccountRef",
|
|
2009
|
+
schema: { parse: parseRef },
|
|
2010
|
+
create: () => ({ parse: parseRef })
|
|
2011
|
+
};
|
|
1781
2012
|
var usageRemote = {
|
|
1782
2013
|
package: "dsh-opencode-go",
|
|
1783
2014
|
descriptors: [{
|
|
@@ -1788,6 +2019,14 @@ var usageRemote = {
|
|
|
1788
2019
|
invocation: { kind: "direct" },
|
|
1789
2020
|
parameters: [],
|
|
1790
2021
|
result: usageCodec
|
|
2022
|
+
}, {
|
|
2023
|
+
id: "dsh-opencode-go#opencodeGoUsage/readAccount",
|
|
2024
|
+
service: "opencodeGoUsage",
|
|
2025
|
+
namespace: "opencodeGoUsage",
|
|
2026
|
+
method: "readAccount",
|
|
2027
|
+
invocation: { kind: "direct" },
|
|
2028
|
+
parameters: [{ name: "ref", wire: "ref", source: "json", codec: refCodec }],
|
|
2029
|
+
result: usageCodec
|
|
1791
2030
|
}]
|
|
1792
2031
|
};
|
|
1793
2032
|
|
|
@@ -1797,15 +2036,48 @@ var GoUsageService = class extends TypertRemoteService {
|
|
|
1797
2036
|
constructor(ctx, options) {
|
|
1798
2037
|
super(ctx, "opencodeGoUsage");
|
|
1799
2038
|
this.options = options;
|
|
2039
|
+
this.transport = options.transport ?? new ProxyTransport();
|
|
2040
|
+
if (!options.transport) ctx.effect(() => () => this.transport.dispose());
|
|
1800
2041
|
}
|
|
1801
|
-
|
|
2042
|
+
identities = /* @__PURE__ */ new Map();
|
|
2043
|
+
pending = /* @__PURE__ */ new Map();
|
|
2044
|
+
transport;
|
|
1802
2045
|
async read() {
|
|
2046
|
+
try {
|
|
2047
|
+
const usage = await this.readRef(this.options.activeRef?.());
|
|
2048
|
+
const lastSwitch = this.options.lastSwitch?.();
|
|
2049
|
+
return lastSwitch ? { ...usage, lastSwitch } : usage;
|
|
2050
|
+
} catch (error) {
|
|
2051
|
+
const lastSwitch = this.options.lastSwitch?.();
|
|
2052
|
+
if (lastSwitch && error instanceof RemoteError && error.code === "opencode-go/usage-unavailable") {
|
|
2053
|
+
throw new RemoteError(
|
|
2054
|
+
"opencode-go/usage-unavailable",
|
|
2055
|
+
error.message,
|
|
2056
|
+
{ ...error.details, lastSwitch },
|
|
2057
|
+
{ cause: error }
|
|
2058
|
+
);
|
|
2059
|
+
}
|
|
2060
|
+
throw error;
|
|
2061
|
+
}
|
|
2062
|
+
}
|
|
2063
|
+
async readAccount(ref) {
|
|
2064
|
+
if (!this.options.accountRefs?.().includes(ref)) {
|
|
2065
|
+
throw new RemoteError("gateway/bad-request", "Unknown OpenCode Go account", {});
|
|
2066
|
+
}
|
|
2067
|
+
return this.readRef(ref);
|
|
2068
|
+
}
|
|
2069
|
+
async readRef(ref) {
|
|
2070
|
+
const identityKey = ref ?? "";
|
|
2071
|
+
if (this.options.accountRefs) for (const saved of this.identities.keys()) {
|
|
2072
|
+
if (saved && !this.options.accountRefs().includes(saved)) this.identities.delete(saved);
|
|
2073
|
+
}
|
|
1803
2074
|
const baseURL = assertBaseURL(this.options.baseURL()).replace(/\/$/, "");
|
|
2075
|
+
const proxyURL = assertProxyURL(this.options.proxyURL?.());
|
|
1804
2076
|
let key;
|
|
1805
2077
|
try {
|
|
1806
|
-
key = await this.options.resolveApiKey();
|
|
2078
|
+
key = await this.options.resolveApiKey(ref);
|
|
1807
2079
|
} catch (error) {
|
|
1808
|
-
this.
|
|
2080
|
+
this.identities.delete(identityKey);
|
|
1809
2081
|
const missing = error instanceof Error && "code" in error && error.code === "MISSING_CREDENTIAL";
|
|
1810
2082
|
throw new RemoteError("opencode-go/usage-unavailable", missing ? "OpenCode Go API key is not configured" : "Could not resolve the OpenCode Go API key", {
|
|
1811
2083
|
retryable: !missing,
|
|
@@ -1813,16 +2085,29 @@ var GoUsageService = class extends TypertRemoteService {
|
|
|
1813
2085
|
}, { cause: error });
|
|
1814
2086
|
}
|
|
1815
2087
|
if (!key) {
|
|
1816
|
-
this.
|
|
2088
|
+
this.identities.delete(identityKey);
|
|
1817
2089
|
throw new RemoteError("opencode-go/usage-unavailable", "OpenCode Go API key is not configured", {
|
|
1818
2090
|
retryable: false,
|
|
1819
2091
|
retainPrevious: false
|
|
1820
2092
|
});
|
|
1821
2093
|
}
|
|
1822
|
-
|
|
1823
|
-
|
|
2094
|
+
let identity = this.identities.get(identityKey);
|
|
2095
|
+
if (identity?.baseURL !== baseURL || identity.proxyURL !== proxyURL || identity.key !== key) {
|
|
2096
|
+
identity = { baseURL, proxyURL, key, source: randomUUID3() };
|
|
2097
|
+
this.identities.set(identityKey, identity);
|
|
2098
|
+
}
|
|
2099
|
+
const { source } = identity;
|
|
2100
|
+
const shared = this.pending.get(source);
|
|
2101
|
+
if (shared) return shared;
|
|
2102
|
+
const pending = this.fetchUsage(baseURL, key, source, this.transport.forProxy(proxyURL));
|
|
2103
|
+
this.pending.set(source, pending);
|
|
2104
|
+
try {
|
|
2105
|
+
return await pending;
|
|
2106
|
+
} finally {
|
|
2107
|
+
this.pending.delete(source);
|
|
1824
2108
|
}
|
|
1825
|
-
|
|
2109
|
+
}
|
|
2110
|
+
async fetchUsage(baseURL, key, source, fetcher) {
|
|
1826
2111
|
const endpoint = diagnosticURL(`${baseURL}/usage`);
|
|
1827
2112
|
let result;
|
|
1828
2113
|
try {
|
|
@@ -1830,7 +2115,7 @@ var GoUsageService = class extends TypertRemoteService {
|
|
|
1830
2115
|
headers: { ...attributionHeaders3(), Authorization: `Bearer ${key}`, Accept: "application/json" },
|
|
1831
2116
|
signal: AbortSignal.timeout(1e4),
|
|
1832
2117
|
redirect: "error"
|
|
1833
|
-
}, USAGE_MAX_BYTES);
|
|
2118
|
+
}, USAGE_MAX_BYTES, "identity", fetcher);
|
|
1834
2119
|
} catch (error) {
|
|
1835
2120
|
const invalid = error instanceof SyntaxError || error instanceof RangeError;
|
|
1836
2121
|
const detail = error instanceof SyntaxError ? "invalid JSON response" : error instanceof RangeError ? `response exceeds the ${USAGE_MAX_BYTES} byte limit` : transportFailure(error);
|
|
@@ -1905,9 +2190,14 @@ function apply(ctx, raw) {
|
|
|
1905
2190
|
const config = raw && typeof raw.enabled === "object" ? raw : Config(raw);
|
|
1906
2191
|
const entry = readConfig(config);
|
|
1907
2192
|
assertBaseURL(entry.baseURL);
|
|
2193
|
+
assertProxyURL(entry.proxyURL);
|
|
2194
|
+
assertAccounts(entry.accounts, entry.apiKeyEnv);
|
|
1908
2195
|
let current = () => readConfig(config);
|
|
1909
2196
|
const resolveApiKey = async (config2 = current()) => {
|
|
1910
2197
|
const ref = config2.apiKeyEnv;
|
|
2198
|
+
if (!accountsOf(config2).some((account) => account.apiKeyEnv === ref)) {
|
|
2199
|
+
throw new LlmError7("OpenCode Go has no selected account", "MISSING_CREDENTIAL");
|
|
2200
|
+
}
|
|
1911
2201
|
const credentials = ctx.get("credentials");
|
|
1912
2202
|
const hit = credentials !== void 0 ? (await credentials.resolve(credentialRef(ref)))?.value : launchEnvironmentOf(ctx).get(ref)?.value;
|
|
1913
2203
|
if (hit !== void 0 && hit.length > 0) return assertUsableApiKey(hit, name, ref);
|
|
@@ -1917,7 +2207,18 @@ function apply(ctx, raw) {
|
|
|
1917
2207
|
);
|
|
1918
2208
|
};
|
|
1919
2209
|
registerGoRemotes(ctx);
|
|
1920
|
-
|
|
2210
|
+
let lastSwitch;
|
|
2211
|
+
const transport = new ProxyTransport();
|
|
2212
|
+
ctx.effect(() => () => transport.dispose());
|
|
2213
|
+
ctx.plugin(GoUsageService, {
|
|
2214
|
+
baseURL: () => current().baseURL,
|
|
2215
|
+
proxyURL: () => current().proxyURL,
|
|
2216
|
+
transport,
|
|
2217
|
+
resolveApiKey: (ref) => resolveApiKey({ ...current(), apiKeyEnv: ref ?? current().apiKeyEnv }),
|
|
2218
|
+
activeRef: () => accountRefOf(current()),
|
|
2219
|
+
accountRefs: () => accountsOf(current()).map((account) => account.apiKeyEnv),
|
|
2220
|
+
lastSwitch: () => lastSwitch
|
|
2221
|
+
});
|
|
1921
2222
|
const logger = {
|
|
1922
2223
|
fallback: ({ url, error }) => {
|
|
1923
2224
|
ctx.logger.warn(`llm-opencode-go: could not refresh ${url}; using last-known model data (${String(error)})`);
|
|
@@ -1928,8 +2229,13 @@ function apply(ctx, raw) {
|
|
|
1928
2229
|
};
|
|
1929
2230
|
let registration;
|
|
1930
2231
|
const adapter = new OpencodeGoAdapter({
|
|
2232
|
+
transport,
|
|
1931
2233
|
config: () => current(),
|
|
1932
2234
|
resolveApiKey,
|
|
2235
|
+
onAccountSwitch: (notice, captured) => {
|
|
2236
|
+
const config2 = current();
|
|
2237
|
+
if (config2.baseURL === captured.baseURL && config2.proxyURL === captured.proxyURL && config2.apiKeyEnv === captured.apiKeyEnv && JSON.stringify(config2.accounts) === JSON.stringify(captured.accounts)) lastSwitch = notice;
|
|
2238
|
+
},
|
|
1933
2239
|
imageAccess: {
|
|
1934
2240
|
resolveAttachments: () => ctx.get("attachments"),
|
|
1935
2241
|
resolveImageAccess: (attachments, ref) => resolveImageAttachmentAccess(
|
|
@@ -1971,19 +2277,29 @@ function apply(ctx, raw) {
|
|
|
1971
2277
|
}
|
|
1972
2278
|
}
|
|
1973
2279
|
};
|
|
2280
|
+
let routeCheck = 0;
|
|
2281
|
+
let accountIdentity = "";
|
|
1974
2282
|
const syncRoute = () => {
|
|
2283
|
+
const check = ++routeCheck;
|
|
2284
|
+
const config2 = current();
|
|
2285
|
+
const identity = JSON.stringify([config2.apiKeyEnv, config2.accounts, config2.baseURL, config2.proxyURL, config2.autoSwitch]);
|
|
2286
|
+
if (identity !== accountIdentity) {
|
|
2287
|
+
accountIdentity = identity;
|
|
2288
|
+
lastSwitch = void 0;
|
|
2289
|
+
}
|
|
1975
2290
|
const visibility = pickerVisibilityOf();
|
|
1976
2291
|
if (pickerVisibility !== visibility) {
|
|
1977
2292
|
pickerVisibility = visibility;
|
|
1978
2293
|
registration?.replace([PROVIDER_ID]);
|
|
1979
2294
|
}
|
|
1980
2295
|
const credentials = ctx.get("credentials");
|
|
2296
|
+
const refs = accountsOf(config2).filter((account) => config2.autoSwitch || account.apiKeyEnv === config2.apiKeyEnv).map((account) => account.apiKeyEnv);
|
|
1981
2297
|
if (credentials === void 0) {
|
|
1982
|
-
applyRoute(launchEnvironmentOf(ctx).get(
|
|
2298
|
+
applyRoute(refs.some((ref) => Boolean(launchEnvironmentOf(ctx).get(ref)?.value)));
|
|
1983
2299
|
return;
|
|
1984
2300
|
}
|
|
1985
|
-
void credentials.describe(credentialRef(
|
|
1986
|
-
applyRoute(info.configured);
|
|
2301
|
+
void Promise.all(refs.map((ref) => credentials.describe(credentialRef(ref)))).then((infos) => {
|
|
2302
|
+
if (check === routeCheck) applyRoute(infos.some((info) => info.configured));
|
|
1987
2303
|
}).catch((error) => {
|
|
1988
2304
|
ctx.logger.error(`llm-opencode-go: credential describe failed; keeping the previous route state (${String(error)})`);
|
|
1989
2305
|
});
|
|
@@ -2012,6 +2328,8 @@ function apply(ctx, raw) {
|
|
|
2012
2328
|
settingsCtx.settings.installSection(ctx, NS, PlainConfig, entry, {
|
|
2013
2329
|
validate: (value) => {
|
|
2014
2330
|
assertBaseURL(value.baseURL);
|
|
2331
|
+
assertProxyURL(value.proxyURL);
|
|
2332
|
+
assertAccounts(value.accounts, value.apiKeyEnv);
|
|
2015
2333
|
},
|
|
2016
2334
|
setSource: (source) => {
|
|
2017
2335
|
current = source;
|
|
@@ -2023,13 +2341,22 @@ function apply(ctx, raw) {
|
|
|
2023
2341
|
});
|
|
2024
2342
|
ctx.on("internal/config", function(_raw, next) {
|
|
2025
2343
|
const value = next();
|
|
2026
|
-
if (this === ctx.fiber)
|
|
2344
|
+
if (this === ctx.fiber) {
|
|
2345
|
+
const config2 = PlainConfig(value);
|
|
2346
|
+
assertBaseURL(config2.baseURL);
|
|
2347
|
+
assertProxyURL(config2.proxyURL);
|
|
2348
|
+
assertAccounts(config2.accounts, config2.apiKeyEnv);
|
|
2349
|
+
}
|
|
2027
2350
|
return value;
|
|
2028
2351
|
});
|
|
2029
2352
|
ctx.on("loader/volatile-update", syncRoute);
|
|
2030
2353
|
ctx.inject(["credentials"], (credentialsCtx) => {
|
|
2031
2354
|
credentialsCtx.on("credentials/reference-updated", (ref) => {
|
|
2032
|
-
|
|
2355
|
+
adapter.forgetRejectedKey(ref);
|
|
2356
|
+
if (accountsOf(current()).some((account) => account.apiKeyEnv === ref)) {
|
|
2357
|
+
lastSwitch = void 0;
|
|
2358
|
+
syncRoute();
|
|
2359
|
+
}
|
|
2033
2360
|
});
|
|
2034
2361
|
syncRoute();
|
|
2035
2362
|
});
|
|
@@ -2045,6 +2372,7 @@ export {
|
|
|
2045
2372
|
PlainConfig,
|
|
2046
2373
|
apply,
|
|
2047
2374
|
assertBaseURL,
|
|
2375
|
+
assertProxyURL,
|
|
2048
2376
|
discoverCatalogModels,
|
|
2049
2377
|
inject,
|
|
2050
2378
|
name,
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/** Client-safe account metadata. Secrets stay in the host credential service. */
|
|
2
|
+
export type GoAccount = {
|
|
3
|
+
id: string;
|
|
4
|
+
name: string;
|
|
5
|
+
apiKeyEnv: string;
|
|
6
|
+
};
|
|
7
|
+
export declare const MAX_ACCOUNTS = 20;
|
|
8
|
+
export declare const ACCOUNT_REF_PREFIX = "DSH_OPENCODE_GO_ACCOUNT_";
|
|
9
|
+
export declare const DEFAULT_ACCOUNT_REF = "OPENCODE_API_KEY";
|
|
10
|
+
export interface AccountSettings {
|
|
11
|
+
apiKeyEnv?: string;
|
|
12
|
+
/** Omission/null preserves legacy credentials; an explicit empty list removes all accounts. */
|
|
13
|
+
accounts?: readonly GoAccount[] | null;
|
|
14
|
+
autoSwitch?: boolean;
|
|
15
|
+
}
|
|
16
|
+
export declare function accountRefOf(settings: AccountSettings): string;
|
|
17
|
+
/** Include an externally selected reference without rewriting existing configuration. */
|
|
18
|
+
export declare function accountsOf(settings: AccountSettings): readonly GoAccount[];
|
|
19
|
+
export declare function assertAccounts(accounts: readonly GoAccount[] | null | undefined, selectedRef?: string): void;
|
|
20
|
+
/** Safe UI notice for a successful request using a fallback account. */
|
|
21
|
+
export interface GoAccountSwitch {
|
|
22
|
+
fromRef: string;
|
|
23
|
+
toRef: string;
|
|
24
|
+
reason: 'quota' | 'credential';
|
|
25
|
+
at: number;
|
|
26
|
+
}
|