@dan-ai-studio/dshopencodego 0.1.47 → 0.1.49
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 +1 -1
- package/README.md +1 -1
- package/lib/client.js +8 -0
- package/lib/index.js +231 -16
- package/lib/types/adapter.d.ts +7 -0
- package/lib/types/catalog/constants.d.ts +21 -1
- package/lib/types/catalog/go-doc-discovery.d.ts +78 -0
- package/lib/types/catalog/go-doc.d.ts +25 -1
- package/lib/types/catalog/index.d.ts +52 -1
- package/lib/types/catalog/metadata-cache.d.ts +41 -0
- package/lib/types/catalog/reading.d.ts +9 -0
- package/package.json +1 -1
package/README.en.md
CHANGED
|
@@ -247,7 +247,7 @@ npm test # contract tests against a real local HTTP mock gateway
|
|
|
247
247
|
|
|
248
248
|
**Order matters**: `npm run build` must come before `npm test` — `tests/pack-contents.spec.ts` walks every `exports` target, and whether `lib/` exists decides whether that check runs or is silently skipped (it does not fail, it just stops protecting you). `package.json` no longer carries `prepare`, so `npm ci` will not build for you; all three CI workflows run `npm run build` explicitly.
|
|
249
249
|
|
|
250
|
-
The suite covers configuration validation, the protocol ladder, gateway and online metadata, the catalog projection, the adapter on the wire (all three protocols), Remote contracts and services, session-header invariants, usage windows and the meter, the client controller and usage pill, both halves' mounting and registration, the error mapping for stream idle timeouts and aborts, the exit-code contract of the verification scripts, and several **structural invariants** (inject declarations match the source's reads, peer coverage holds both ways, the READMEs match the manifest, and the READMEs' claims about the upstream match the upstream actually installed). That is **
|
|
250
|
+
The suite covers configuration validation, the protocol ladder, gateway and online metadata, the catalog projection, the adapter on the wire (all three protocols), Remote contracts and services, session-header invariants, usage windows and the meter, the client controller and usage pill, both halves' mounting and registration, the error mapping for stream idle timeouts and aborts, the exit-code contract of the verification scripts, and several **structural invariants** (inject declarations match the source's reads, peer coverage holds both ways, the READMEs match the manifest, and the READMEs' claims about the upstream match the upstream actually installed). That is **47 test files** — a number `tests/readme-consistency.spec.ts` checks against the `tests/` directory, so it cannot drift.
|
|
251
251
|
|
|
252
252
|
## Releasing (maintainers)
|
|
253
253
|
|
package/README.md
CHANGED
|
@@ -247,7 +247,7 @@ npm test # 契约测试:真实 HTTP mock 网关,断言请求头/
|
|
|
247
247
|
|
|
248
248
|
**顺序有讲究**:`npm run build` 要排在 `npm test` 之前——`tests/pack-contents.spec.ts` 会遍历 `exports` 的每个目标,`lib/` 存在与否决定这条检查是执行还是被跳过(不报错,但会少一层保护)。`package.json` 已移除 `prepare`,因此 `npm ci` 不会再替你隐式构建;三个 CI workflow 都显式跑了 `npm run build`。
|
|
249
249
|
|
|
250
|
-
测试覆盖配置校验、协议阶梯、网关与在线元数据、目录投影、适配器(三种协议的线上请求)、Remote 契约与服务、会话头不变量、用量窗口与计量、客户端控制器与用量组件、两半的挂载与登记、流空闲超时与中止的错误映射、验证脚本的退出码契约、以及几条**结构性不变量**(inject 声明与源码读取一致、peer 覆盖双向、README 与 manifest 一致、README 对上游的断言与实装的上游一致)。共 **
|
|
250
|
+
测试覆盖配置校验、协议阶梯、网关与在线元数据、目录投影、适配器(三种协议的线上请求)、Remote 契约与服务、会话头不变量、用量窗口与计量、客户端控制器与用量组件、两半的挂载与登记、流空闲超时与中止的错误映射、验证脚本的退出码契约、以及几条**结构性不变量**(inject 声明与源码读取一致、peer 覆盖双向、README 与 manifest 一致、README 对上游的断言与实装的上游一致)。共 **47 个测试文件**——这个数字由 `tests/readme-consistency.spec.ts` 对着 `tests/` 目录校验,不会漂移。
|
|
251
251
|
|
|
252
252
|
## 发布(维护者)
|
|
253
253
|
|
package/lib/client.js
CHANGED
|
@@ -128,6 +128,7 @@ function parseCatalogReading(value) {
|
|
|
128
128
|
stale: row["stale"],
|
|
129
129
|
...typeof row["error"] === "string" ? { error: row["error"] } : {},
|
|
130
130
|
degradations: parseDegradations(row["degradations"]),
|
|
131
|
+
...typeof row["factsCachedAt"] === "number" ? { factsCachedAt: row["factsCachedAt"] } : {},
|
|
131
132
|
fetchedAtMs: row["fetchedAtMs"],
|
|
132
133
|
quotaSource: quotaSource ?? "seed",
|
|
133
134
|
counts: {
|
|
@@ -335,6 +336,7 @@ var en = {
|
|
|
335
336
|
catalogDeprecated: "deprecated",
|
|
336
337
|
catalogUnconfigured: "not configurable",
|
|
337
338
|
quotaSeed: "Allowances are the built-in snapshot, transcribed",
|
|
339
|
+
factsCached: "Model metadata is from the last successful reading,",
|
|
338
340
|
refreshLabel: "Catalog refresh (minutes)",
|
|
339
341
|
refreshHint: "How long requests and the model picker reuse one listing.",
|
|
340
342
|
writeRejected: "The settings write was rejected (the namespace was reloaded) \u2014 try again",
|
|
@@ -434,6 +436,7 @@ var zh = {
|
|
|
434
436
|
catalogDeprecated: "\u5DF2\u5F03\u7528",
|
|
435
437
|
catalogUnconfigured: "\u65E0\u6CD5\u914D\u7F6E",
|
|
436
438
|
quotaSeed: "\u989D\u5EA6\u6765\u81EA\u5185\u7F6E\u5FEB\u7167\uFF0C\u8F6C\u5199\u4E8E",
|
|
439
|
+
factsCached: "\u6A21\u578B\u5143\u6570\u636E\u6765\u81EA\u4E0A\u6B21\u6210\u529F\u83B7\u53D6\uFF0C",
|
|
437
440
|
refreshLabel: "\u76EE\u5F55\u5237\u65B0\uFF08\u5206\u949F\uFF09",
|
|
438
441
|
refreshHint: "\u8BF7\u6C42\u4E0E\u6A21\u578B\u9009\u62E9\u5668\u590D\u7528\u540C\u4E00\u4EFD\u5217\u8868\u7684\u65F6\u957F\u3002",
|
|
439
442
|
writeRejected: "\u8BBE\u7F6E\u5199\u5165\u88AB\u62D2\u7EDD\uFF08\u5DF2\u91CD\u65B0\u8F7D\u5165\uFF09\uFF0C\u8BF7\u91CD\u8BD5",
|
|
@@ -1244,6 +1247,11 @@ function Section({ controller, t, getLocale }) {
|
|
|
1244
1247
|
" ",
|
|
1245
1248
|
SEED_TRANSCRIBED
|
|
1246
1249
|
] }),
|
|
1250
|
+
reading !== void 0 && reading.factsCachedAt !== void 0 && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("p", { className: section_default.hint, children: [
|
|
1251
|
+
t("factsCached"),
|
|
1252
|
+
" ",
|
|
1253
|
+
new Date(reading.factsCachedAt).toISOString().slice(0, 10)
|
|
1254
|
+
] }),
|
|
1247
1255
|
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: section_default.row, children: [
|
|
1248
1256
|
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("label", { className: section_default.hint, htmlFor: "dshopencodego-refresh", children: t("refreshLabel") }),
|
|
1249
1257
|
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
package/lib/index.js
CHANGED
|
@@ -17788,8 +17788,8 @@ function getBunSandboxEnvValue(name2) {
|
|
|
17788
17788
|
if (procEnvCache === null) {
|
|
17789
17789
|
procEnvCache = /* @__PURE__ */ new Map();
|
|
17790
17790
|
try {
|
|
17791
|
-
const { readFileSync } = __require("node:fs");
|
|
17792
|
-
const data =
|
|
17791
|
+
const { readFileSync: readFileSync3 } = __require("node:fs");
|
|
17792
|
+
const data = readFileSync3("/proc/self/environ", "utf-8");
|
|
17793
17793
|
for (const entry of data.split("\0")) {
|
|
17794
17794
|
const idx = entry.indexOf("=");
|
|
17795
17795
|
if (idx > 0) {
|
|
@@ -42462,6 +42462,8 @@ Check your ChatGPT usage: ${CHATGPT_USAGE_URL}` : errorMessage;
|
|
|
42462
42462
|
});
|
|
42463
42463
|
|
|
42464
42464
|
// src/index.ts
|
|
42465
|
+
import { homedir } from "node:os";
|
|
42466
|
+
import { join as join4 } from "node:path";
|
|
42465
42467
|
import { credentialRef } from "@deepseek-ai/dsh-credentials";
|
|
42466
42468
|
import { launchEnvironmentOf } from "@deepseek-ai/dsh-launch-environment";
|
|
42467
42469
|
import * as llm2 from "@deepseek-ai/dsh-llm";
|
|
@@ -42528,6 +42530,8 @@ var CONSOLE_BASE_URL = "https://opencode.ai/inference/v1";
|
|
|
42528
42530
|
var MODEL_METADATA_URL = "https://models.dev/api.json";
|
|
42529
42531
|
var MODEL_METADATA_PROVIDER = "opencode-go";
|
|
42530
42532
|
var METADATA_FETCH_TIMEOUT_MS = 1e4;
|
|
42533
|
+
var MODEL_METADATA_TIMEOUT_MS = 3e4;
|
|
42534
|
+
var METADATA_RETRY_DELAY_MS = 1500;
|
|
42531
42535
|
var MODEL_LISTING_MAX_BYTES = 1024 * 1024;
|
|
42532
42536
|
var MODEL_METADATA_MAX_BYTES = 16 * 1024 * 1024;
|
|
42533
42537
|
var UNKNOWN_MODEL_PROBE_COOLDOWN_MS = 5 * 6e4;
|
|
@@ -42549,7 +42553,8 @@ async function readBoundedJson(response, url, maxBytes) {
|
|
|
42549
42553
|
text = await readBoundedText(response, maxBytes);
|
|
42550
42554
|
} catch (error) {
|
|
42551
42555
|
if (error instanceof LlmError) throw error;
|
|
42552
|
-
|
|
42556
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
42557
|
+
throw new LlmError(`could not read ${url}: ${detail}`, "DISCOVERY_FAILED", { cause: error });
|
|
42553
42558
|
}
|
|
42554
42559
|
try {
|
|
42555
42560
|
return JSON.parse(text);
|
|
@@ -42626,6 +42631,78 @@ async function fetchModelIds(baseURL, signal) {
|
|
|
42626
42631
|
|
|
42627
42632
|
// src/catalog/go-doc.ts
|
|
42628
42633
|
import { attributionHeaders as attributionHeaders2, LlmError as LlmError3 } from "@deepseek-ai/dsh-llm";
|
|
42634
|
+
|
|
42635
|
+
// src/catalog/go-doc-discovery.ts
|
|
42636
|
+
import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
42637
|
+
import { join as join2 } from "node:path";
|
|
42638
|
+
var DISCOVERY_URL = "https://api.github.com/repos/anomalyco/opencode/git/trees/dev?recursive=1";
|
|
42639
|
+
var DOCUMENT_NAME = /^go[^/]*\.mdx$/i;
|
|
42640
|
+
var DISCOVERY_MAX_BYTES = 8 * 1024 * 1024;
|
|
42641
|
+
var DISCOVERY_TIMEOUT_MS = 1e4;
|
|
42642
|
+
var CACHE_FILE = "go-doc-paths.json";
|
|
42643
|
+
var CACHE_VERSION = 1;
|
|
42644
|
+
function selectDocumentPaths(paths) {
|
|
42645
|
+
const selected = [];
|
|
42646
|
+
for (const path5 of paths) {
|
|
42647
|
+
const name2 = path5.slice(path5.lastIndexOf("/") + 1);
|
|
42648
|
+
if (!DOCUMENT_NAME.test(name2)) continue;
|
|
42649
|
+
if (!/(^|\/)(docs?|content|console)\//i.test(path5) && !/^packages\/web\/src\/content\/docs\//i.test(path5)) continue;
|
|
42650
|
+
selected.push(path5);
|
|
42651
|
+
}
|
|
42652
|
+
return selected.sort((left, right) => right.length - left.length);
|
|
42653
|
+
}
|
|
42654
|
+
function mirrorsOf(path5) {
|
|
42655
|
+
const encoded = path5.split("/").map((segment) => encodeURIComponent(segment)).join("/");
|
|
42656
|
+
return [
|
|
42657
|
+
`https://raw.githubusercontent.com/anomalyco/opencode/dev/${encoded}`,
|
|
42658
|
+
`https://cdn.jsdelivr.net/gh/anomalyco/opencode@dev/${encoded}`
|
|
42659
|
+
];
|
|
42660
|
+
}
|
|
42661
|
+
async function discoverDocumentPaths(signal, fetchImpl = fetch) {
|
|
42662
|
+
const timeout = AbortSignal.timeout(DISCOVERY_TIMEOUT_MS);
|
|
42663
|
+
try {
|
|
42664
|
+
const response = await fetchImpl(DISCOVERY_URL, {
|
|
42665
|
+
headers: { accept: "application/vnd.github+json", "user-agent": "dshopencodego" },
|
|
42666
|
+
signal: signal === void 0 ? timeout : AbortSignal.any([signal, timeout])
|
|
42667
|
+
});
|
|
42668
|
+
if (!response.ok) return [];
|
|
42669
|
+
const body = await response.text();
|
|
42670
|
+
if (body.length > DISCOVERY_MAX_BYTES) return [];
|
|
42671
|
+
const listing = JSON.parse(body);
|
|
42672
|
+
const paths = (listing.tree ?? []).map((entry) => entry.path).filter((path5) => typeof path5 === "string");
|
|
42673
|
+
return selectDocumentPaths(paths);
|
|
42674
|
+
} catch {
|
|
42675
|
+
return [];
|
|
42676
|
+
}
|
|
42677
|
+
}
|
|
42678
|
+
function readCachedPaths(cacheDir) {
|
|
42679
|
+
if (cacheDir === void 0) return [];
|
|
42680
|
+
try {
|
|
42681
|
+
const parsed = JSON.parse(readFileSync(join2(cacheDir, CACHE_FILE), "utf8"));
|
|
42682
|
+
if (parsed.version !== CACHE_VERSION || !Array.isArray(parsed.paths)) return [];
|
|
42683
|
+
return parsed.paths.filter((path5) => typeof path5 === "string");
|
|
42684
|
+
} catch {
|
|
42685
|
+
return [];
|
|
42686
|
+
}
|
|
42687
|
+
}
|
|
42688
|
+
function rememberPaths(cacheDir, discovered) {
|
|
42689
|
+
const known = readCachedPaths(cacheDir);
|
|
42690
|
+
const merged = [];
|
|
42691
|
+
for (const path5 of [...discovered, ...known]) {
|
|
42692
|
+
if (!merged.includes(path5)) merged.push(path5);
|
|
42693
|
+
}
|
|
42694
|
+
if (cacheDir === void 0 || discovered.length === 0) return merged;
|
|
42695
|
+
try {
|
|
42696
|
+
mkdirSync(cacheDir, { recursive: true });
|
|
42697
|
+
const file = { version: CACHE_VERSION, paths: merged, updatedAt: Date.now() };
|
|
42698
|
+
writeFileSync(join2(cacheDir, CACHE_FILE), `${JSON.stringify(file, null, 2)}
|
|
42699
|
+
`, "utf8");
|
|
42700
|
+
} catch {
|
|
42701
|
+
}
|
|
42702
|
+
return merged;
|
|
42703
|
+
}
|
|
42704
|
+
|
|
42705
|
+
// src/catalog/go-doc.ts
|
|
42629
42706
|
var GO_DOC_URLS = [
|
|
42630
42707
|
"https://raw.githubusercontent.com/anomalyco/opencode/dev/services/www/src/docs/content/console/go.mdx",
|
|
42631
42708
|
"https://cdn.jsdelivr.net/gh/anomalyco/opencode@dev/services/www/src/docs/content/console/go.mdx"
|
|
@@ -42725,18 +42802,46 @@ async function readSource(url, signal) {
|
|
|
42725
42802
|
return { failure: `${url}: ${error instanceof Error ? error.message : String(error)}` };
|
|
42726
42803
|
}
|
|
42727
42804
|
}
|
|
42728
|
-
async function fetchGoDocument(signal) {
|
|
42729
|
-
const attempts = await Promise.all(GO_DOC_URLS.map((url) => readSource(url, signal)));
|
|
42805
|
+
async function fetchGoDocument(signal, options = {}) {
|
|
42730
42806
|
const failures = [];
|
|
42731
|
-
|
|
42732
|
-
|
|
42733
|
-
|
|
42807
|
+
const known = await readSources(GO_DOC_URLS, signal);
|
|
42808
|
+
if (known.document !== void 0) return known.document;
|
|
42809
|
+
failures.push(...known.failures);
|
|
42810
|
+
const remembered = readCachedPaths(options.cacheDir).filter((path5) => !GO_DOC_URLS.some((url) => url.endsWith(path5)));
|
|
42811
|
+
if (remembered.length > 0) {
|
|
42812
|
+
const cached = await readSources(remembered.flatMap(mirrorsOf), signal);
|
|
42813
|
+
if (cached.document !== void 0) return cached.document;
|
|
42814
|
+
failures.push(...cached.failures);
|
|
42815
|
+
}
|
|
42816
|
+
const discover = options.discover ?? discoverDocumentPaths;
|
|
42817
|
+
const discovered = await discover(signal);
|
|
42818
|
+
if (discovered.length === 0) {
|
|
42819
|
+
failures.push("the provider repository named no document");
|
|
42820
|
+
} else {
|
|
42821
|
+
const merged = rememberPaths(options.cacheDir, discovered);
|
|
42822
|
+
const fresh = merged.filter((path5) => !GO_DOC_URLS.some((url) => url.endsWith(path5))).flatMap(mirrorsOf);
|
|
42823
|
+
if (fresh.length === 0) {
|
|
42824
|
+
failures.push("the provider repository named the path that was already tried");
|
|
42825
|
+
} else {
|
|
42826
|
+
const result = await readSources(fresh, signal);
|
|
42827
|
+
if (result.document !== void 0) return result.document;
|
|
42828
|
+
failures.push(...result.failures);
|
|
42829
|
+
}
|
|
42734
42830
|
}
|
|
42735
42831
|
throw new LlmError3(
|
|
42736
42832
|
`could not read the Go documentation (${failures.join("; ")})`,
|
|
42737
42833
|
"DOCUMENT_UNAVAILABLE"
|
|
42738
42834
|
);
|
|
42739
42835
|
}
|
|
42836
|
+
async function readSources(urls, signal) {
|
|
42837
|
+
const attempts = await Promise.all(urls.map((url) => readSource(url, signal)));
|
|
42838
|
+
const failures = [];
|
|
42839
|
+
for (const attempt of attempts) {
|
|
42840
|
+
if ("document" in attempt) return { document: attempt.document, failures };
|
|
42841
|
+
failures.push(attempt.failure);
|
|
42842
|
+
}
|
|
42843
|
+
return { failures };
|
|
42844
|
+
}
|
|
42740
42845
|
|
|
42741
42846
|
// src/catalog/protocol.ts
|
|
42742
42847
|
var WIRE_PROTOCOLS = [
|
|
@@ -42951,6 +43056,43 @@ function toPiModel(facts, baseURL) {
|
|
|
42951
43056
|
};
|
|
42952
43057
|
}
|
|
42953
43058
|
|
|
43059
|
+
// src/catalog/metadata-cache.ts
|
|
43060
|
+
import { mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "node:fs";
|
|
43061
|
+
import { join as join3 } from "node:path";
|
|
43062
|
+
var CACHE_FILE2 = "models-dev-facts.json";
|
|
43063
|
+
var CACHE_VERSION2 = 1;
|
|
43064
|
+
function readFactsCache(cacheDir) {
|
|
43065
|
+
if (cacheDir === void 0) return void 0;
|
|
43066
|
+
try {
|
|
43067
|
+
const parsed = JSON.parse(readFileSync2(join3(cacheDir, CACHE_FILE2), "utf8"));
|
|
43068
|
+
if (parsed.version !== CACHE_VERSION2 || parsed.facts === null || typeof parsed.facts !== "object") return void 0;
|
|
43069
|
+
const facts = /* @__PURE__ */ new Map();
|
|
43070
|
+
for (const [id, fact] of Object.entries(parsed.facts)) {
|
|
43071
|
+
if (fact === null || typeof fact !== "object") continue;
|
|
43072
|
+
if (typeof fact.id !== "string" || typeof fact.api !== "string") continue;
|
|
43073
|
+
facts.set(id, fact);
|
|
43074
|
+
}
|
|
43075
|
+
if (facts.size === 0) return void 0;
|
|
43076
|
+
return { facts, updatedAt: typeof parsed.updatedAt === "number" ? parsed.updatedAt : 0 };
|
|
43077
|
+
} catch {
|
|
43078
|
+
return void 0;
|
|
43079
|
+
}
|
|
43080
|
+
}
|
|
43081
|
+
function writeFactsCache(cacheDir, facts) {
|
|
43082
|
+
if (cacheDir === void 0 || facts.size === 0) return;
|
|
43083
|
+
try {
|
|
43084
|
+
mkdirSync2(cacheDir, { recursive: true });
|
|
43085
|
+
const file = {
|
|
43086
|
+
version: CACHE_VERSION2,
|
|
43087
|
+
facts: Object.fromEntries(facts),
|
|
43088
|
+
updatedAt: Date.now()
|
|
43089
|
+
};
|
|
43090
|
+
writeFileSync2(join3(cacheDir, CACHE_FILE2), `${JSON.stringify(file)}
|
|
43091
|
+
`, "utf8");
|
|
43092
|
+
} catch {
|
|
43093
|
+
}
|
|
43094
|
+
}
|
|
43095
|
+
|
|
42954
43096
|
// src/catalog/index.ts
|
|
42955
43097
|
function reasonText(reason) {
|
|
42956
43098
|
if (reason instanceof Error) return reason.message;
|
|
@@ -42999,12 +43141,27 @@ var OpencodeGoCatalog = class {
|
|
|
42999
43141
|
metadataDocument;
|
|
43000
43142
|
metadataETag;
|
|
43001
43143
|
lastMetadata;
|
|
43144
|
+
/**
|
|
43145
|
+
* The facts a previous successful read produced, read from disk.
|
|
43146
|
+
*
|
|
43147
|
+
* This is what a cold process falls back on when the online metadata cannot
|
|
43148
|
+
* be read. Without it a degraded catalog declares every model unable to
|
|
43149
|
+
* think, and the host refuses the effort the session is already using.
|
|
43150
|
+
*/
|
|
43151
|
+
cachedMetadata;
|
|
43152
|
+
/** When the cached facts were written, for the page to show. */
|
|
43153
|
+
cachedAt;
|
|
43002
43154
|
/** The last parsed documentation; retained across failed fetches. */
|
|
43003
43155
|
document;
|
|
43004
43156
|
/** Ids that already forced a revalidation, with the time they did. */
|
|
43005
43157
|
probedAt = /* @__PURE__ */ new Map();
|
|
43006
43158
|
constructor(options) {
|
|
43007
43159
|
this.options = options;
|
|
43160
|
+
const cached = readFactsCache(options.cacheDir);
|
|
43161
|
+
if (cached !== void 0) {
|
|
43162
|
+
this.cachedMetadata = { models: cached.facts, errors: /* @__PURE__ */ new Map() };
|
|
43163
|
+
this.cachedAt = cached.updatedAt;
|
|
43164
|
+
}
|
|
43008
43165
|
}
|
|
43009
43166
|
/**
|
|
43010
43167
|
* The cached snapshot, refreshed when it is older than the configured TTL.
|
|
@@ -43074,7 +43231,7 @@ var OpencodeGoCatalog = class {
|
|
|
43074
43231
|
*/
|
|
43075
43232
|
async build(signal) {
|
|
43076
43233
|
const builtin = builtinModels(this.options.baseURL);
|
|
43077
|
-
const readDocument = this.options.readDocument ?? fetchGoDocument;
|
|
43234
|
+
const readDocument = this.options.readDocument ?? ((signal2) => fetchGoDocument(signal2, { cacheDir: this.options.cacheDir }));
|
|
43078
43235
|
const [listing, metadata, documentation] = await Promise.allSettled([
|
|
43079
43236
|
fetchModelIds(this.options.baseURL),
|
|
43080
43237
|
this.refreshMetadata(builtin),
|
|
@@ -43118,15 +43275,18 @@ var OpencodeGoCatalog = class {
|
|
|
43118
43275
|
...this.documentQuotas()
|
|
43119
43276
|
};
|
|
43120
43277
|
}
|
|
43278
|
+
const fromCache = metadata.status !== "fulfilled" && this.lastMetadata === void 0;
|
|
43279
|
+
const online = metadata.status === "fulfilled" ? metadata.value : this.lastMetadata ?? this.cachedMetadata;
|
|
43121
43280
|
return this.assemble(
|
|
43122
43281
|
listing.value,
|
|
43123
|
-
|
|
43282
|
+
online,
|
|
43124
43283
|
builtin,
|
|
43125
|
-
degradations
|
|
43284
|
+
degradations,
|
|
43285
|
+
fromCache ? this.cachedAt : void 0
|
|
43126
43286
|
);
|
|
43127
43287
|
}
|
|
43128
43288
|
/** Merge the listing with the metadata document and the installed entries. */
|
|
43129
|
-
assemble(ids, online, builtin, degradations = []) {
|
|
43289
|
+
assemble(ids, online, builtin, degradations = [], factsCachedAt) {
|
|
43130
43290
|
const facts = /* @__PURE__ */ new Map();
|
|
43131
43291
|
const unavailable = /* @__PURE__ */ new Map();
|
|
43132
43292
|
for (const id of ids) {
|
|
@@ -43158,6 +43318,7 @@ var OpencodeGoCatalog = class {
|
|
|
43158
43318
|
provider: buildProvider2(this.options.baseURL, [...facts.values()].map((fact) => toPiModel(fact, this.options.baseURL))),
|
|
43159
43319
|
live: true,
|
|
43160
43320
|
...degradations.length === 0 ? {} : { degradations },
|
|
43321
|
+
...factsCachedAt === void 0 ? {} : { factsCachedAt },
|
|
43161
43322
|
fetchedAtMs: Date.now(),
|
|
43162
43323
|
...this.documentQuotas()
|
|
43163
43324
|
};
|
|
@@ -43214,8 +43375,18 @@ var OpencodeGoCatalog = class {
|
|
|
43214
43375
|
releaseDate: void 0
|
|
43215
43376
|
};
|
|
43216
43377
|
}
|
|
43217
|
-
/**
|
|
43218
|
-
|
|
43378
|
+
/**
|
|
43379
|
+
* Read the model metadata document, once.
|
|
43380
|
+
*
|
|
43381
|
+
* The document is megabytes, so its budget is longer than the other two
|
|
43382
|
+
* reads' — and the host that runs this is not always a plain Node process:
|
|
43383
|
+
* the desktop build fetches over the browser's network stack, where the
|
|
43384
|
+
* same document that a shell reads in half a second can take long enough
|
|
43385
|
+
* to outrun a budget set for kilobyte-sized documents.
|
|
43386
|
+
* @param builtin - the installed catalog, for the parse.
|
|
43387
|
+
* @returns the parsed metadata.
|
|
43388
|
+
*/
|
|
43389
|
+
async readMetadataDocument(builtin) {
|
|
43219
43390
|
const response = await fetch(MODEL_METADATA_URL, {
|
|
43220
43391
|
redirect: "error",
|
|
43221
43392
|
headers: {
|
|
@@ -43224,7 +43395,7 @@ var OpencodeGoCatalog = class {
|
|
|
43224
43395
|
"cache-control": "no-cache",
|
|
43225
43396
|
...this.metadataETag === void 0 ? {} : { "if-none-match": this.metadataETag }
|
|
43226
43397
|
},
|
|
43227
|
-
signal: AbortSignal.timeout(
|
|
43398
|
+
signal: AbortSignal.timeout(MODEL_METADATA_TIMEOUT_MS)
|
|
43228
43399
|
});
|
|
43229
43400
|
if (response.status === 304 && this.metadataDocument !== void 0) {
|
|
43230
43401
|
await response.body?.cancel().catch(() => {
|
|
@@ -43251,8 +43422,36 @@ var OpencodeGoCatalog = class {
|
|
|
43251
43422
|
overrides: this.options.overrides
|
|
43252
43423
|
});
|
|
43253
43424
|
this.lastMetadata = parsed;
|
|
43425
|
+
writeFactsCache(this.options.cacheDir, parsed.models);
|
|
43254
43426
|
return parsed;
|
|
43255
43427
|
}
|
|
43428
|
+
/**
|
|
43429
|
+
* Read the model metadata document, retrying once.
|
|
43430
|
+
*
|
|
43431
|
+
* The failure this answers is not a broken network: the same document is
|
|
43432
|
+
* reachable from a browser on the same machine while the host cannot read
|
|
43433
|
+
* it. It is the host being busy — a plugin reload rewrites modules, unpacks
|
|
43434
|
+
* packages, and rebuilds the runtime while the catalog asks for the
|
|
43435
|
+
* document, and the request that loses that race is a timeout, not a
|
|
43436
|
+
* refusal. A second attempt, a moment later, is the same request on a host
|
|
43437
|
+
* that has finished what it was doing.
|
|
43438
|
+
* @param builtin - the installed catalog, for the parse.
|
|
43439
|
+
* @returns the parsed metadata.
|
|
43440
|
+
*/
|
|
43441
|
+
async refreshMetadata(builtin) {
|
|
43442
|
+
try {
|
|
43443
|
+
return await this.readMetadataDocument(builtin);
|
|
43444
|
+
} catch (first) {
|
|
43445
|
+
await new Promise((resolve3) => {
|
|
43446
|
+
setTimeout(resolve3, METADATA_RETRY_DELAY_MS);
|
|
43447
|
+
});
|
|
43448
|
+
try {
|
|
43449
|
+
return await this.readMetadataDocument(builtin);
|
|
43450
|
+
} catch (second) {
|
|
43451
|
+
throw new Error(`${reasonText(first)}; retry: ${reasonText(second)}`);
|
|
43452
|
+
}
|
|
43453
|
+
}
|
|
43454
|
+
}
|
|
43256
43455
|
};
|
|
43257
43456
|
function inferFamily(id) {
|
|
43258
43457
|
const lower = id.toLowerCase();
|
|
@@ -45028,6 +45227,7 @@ var OpencodeGoAdapter = class extends LlmAdapter {
|
|
|
45028
45227
|
},
|
|
45029
45228
|
overrides: config.modelProtocols,
|
|
45030
45229
|
...this.options.readDocument === void 0 ? {} : { readDocument: this.options.readDocument },
|
|
45230
|
+
...this.options.cacheDir === void 0 ? {} : { cacheDir: this.options.cacheDir },
|
|
45031
45231
|
observers: {
|
|
45032
45232
|
...this.options.onFallback === void 0 ? {} : { onFallback: this.options.onFallback },
|
|
45033
45233
|
...this.options.onUnconfigured === void 0 ? {} : { onUnconfigured: this.options.onUnconfigured }
|
|
@@ -45355,6 +45555,7 @@ function parseCatalogReading(value) {
|
|
|
45355
45555
|
stale: row["stale"],
|
|
45356
45556
|
...typeof row["error"] === "string" ? { error: row["error"] } : {},
|
|
45357
45557
|
degradations: parseDegradations(row["degradations"]),
|
|
45558
|
+
...typeof row["factsCachedAt"] === "number" ? { factsCachedAt: row["factsCachedAt"] } : {},
|
|
45358
45559
|
fetchedAtMs: row["fetchedAtMs"],
|
|
45359
45560
|
quotaSource: quotaSource ?? "seed",
|
|
45360
45561
|
counts: {
|
|
@@ -45596,6 +45797,7 @@ function catalogReading(snapshot2, visibility, listingFailure, now) {
|
|
|
45596
45797
|
stale: !snapshot2.live,
|
|
45597
45798
|
...!snapshot2.live && listingFailure !== void 0 ? { error: listingFailure } : {},
|
|
45598
45799
|
degradations: snapshot2.degradations ?? [],
|
|
45800
|
+
...snapshot2.factsCachedAt === void 0 ? {} : { factsCachedAt: snapshot2.factsCachedAt },
|
|
45599
45801
|
fetchedAtMs: snapshot2.fetchedAtMs,
|
|
45600
45802
|
quotaSource: documentQuotas === void 0 ? "seed" : "document",
|
|
45601
45803
|
counts: {
|
|
@@ -45764,7 +45966,7 @@ function createUpdateChecker(deps) {
|
|
|
45764
45966
|
}
|
|
45765
45967
|
|
|
45766
45968
|
// src/update/version.ts
|
|
45767
|
-
var PLUGIN_VERSION = true ? "0.1.
|
|
45969
|
+
var PLUGIN_VERSION = true ? "0.1.49" : "0.0.0-dev";
|
|
45768
45970
|
|
|
45769
45971
|
// src/update/service.ts
|
|
45770
45972
|
import { TypertRemoteService as TypertRemoteService2 } from "@deepseek-ai/dsh-typert-protocol";
|
|
@@ -45901,6 +46103,18 @@ var inject = ["llm"];
|
|
|
45901
46103
|
function isLiveConfig(raw) {
|
|
45902
46104
|
return typeof raw === "object" && raw !== null && typeof raw.enabled === "object" && typeof raw.enabled?.get === "function";
|
|
45903
46105
|
}
|
|
46106
|
+
function resolveCacheDir(ctx) {
|
|
46107
|
+
const home = ctx.dshHomePath;
|
|
46108
|
+
if (typeof home === "function") {
|
|
46109
|
+
try {
|
|
46110
|
+
return home("cache", "@dan-ai-studio/dshopencodego");
|
|
46111
|
+
} catch {
|
|
46112
|
+
}
|
|
46113
|
+
}
|
|
46114
|
+
const configured = process.env.DSH_HOME?.trim();
|
|
46115
|
+
const root = configured !== void 0 && configured.length > 0 ? configured : join4(homedir(), ".dsh");
|
|
46116
|
+
return join4(root, "cache", "@dan-ai-studio/dshopencodego");
|
|
46117
|
+
}
|
|
45904
46118
|
function apply(ctx, raw) {
|
|
45905
46119
|
const live = isLiveConfig(raw) ? raw : void 0;
|
|
45906
46120
|
const constant = live === void 0 ? PlainConfig(raw ?? {}) : void 0;
|
|
@@ -45921,6 +46135,7 @@ function apply(ctx, raw) {
|
|
|
45921
46135
|
const adapter = new OpencodeGoAdapter({
|
|
45922
46136
|
config: current,
|
|
45923
46137
|
resolveApiKey,
|
|
46138
|
+
cacheDir: resolveCacheDir(ctx),
|
|
45924
46139
|
imageAccess: {
|
|
45925
46140
|
resolveAttachments: () => ctx.get("attachments"),
|
|
45926
46141
|
resolveImageAccess: (attachments, ref) => llm2.resolveImageAttachmentAccess(
|
package/lib/types/adapter.d.ts
CHANGED
|
@@ -52,6 +52,13 @@ export interface OpencodeGoAdapterOptions {
|
|
|
52
52
|
* a fixed document so the suite stays offline. Production omits this.
|
|
53
53
|
*/
|
|
54
54
|
readonly readDocument?: () => Promise<GoDoc>;
|
|
55
|
+
/**
|
|
56
|
+
* Where the discovered provider-document paths are remembered.
|
|
57
|
+
*
|
|
58
|
+
* Absent means the plugin keeps them in memory only, which costs one
|
|
59
|
+
* discovery per start and nothing else.
|
|
60
|
+
*/
|
|
61
|
+
readonly cacheDir?: string;
|
|
55
62
|
/**
|
|
56
63
|
* Observe the provider's own usage for one completed call. This is the only
|
|
57
64
|
* honest source of token counts: the gateway's `/usage` endpoint reports
|
|
@@ -11,8 +11,28 @@ export declare const CONSOLE_BASE_URL = "https://opencode.ai/inference/v1";
|
|
|
11
11
|
export declare const MODEL_METADATA_URL = "https://models.dev/api.json";
|
|
12
12
|
/** The models.dev provider key this plugin reads. */
|
|
13
13
|
export declare const MODEL_METADATA_PROVIDER = "opencode-go";
|
|
14
|
-
/** Timeout for the two metadata GETs. */
|
|
14
|
+
/** Timeout for the two small metadata GETs. */
|
|
15
15
|
export declare const METADATA_FETCH_TIMEOUT_MS = 10000;
|
|
16
|
+
/**
|
|
17
|
+
* Timeout for the model metadata document.
|
|
18
|
+
*
|
|
19
|
+
* It is the one read measured in megabytes — 5.9 MB for the whole of
|
|
20
|
+
* models.dev, of which this plugin uses one provider — and the host that
|
|
21
|
+
* fetches it is not always a plain Node process. The desktop build fetches
|
|
22
|
+
* over the browser's network stack, where the same document a shell reads in
|
|
23
|
+
* half a second can outrun a budget set for kilobyte-sized documents. The
|
|
24
|
+
* longer budget costs nothing when the read succeeds; it is the difference
|
|
25
|
+
* between a catalog and a degraded one.
|
|
26
|
+
*/
|
|
27
|
+
export declare const MODEL_METADATA_TIMEOUT_MS = 30000;
|
|
28
|
+
/**
|
|
29
|
+
* How long to wait before the one retry of the metadata read.
|
|
30
|
+
*
|
|
31
|
+
* Long enough for the host to finish whatever it was doing — a plugin
|
|
32
|
+
* reload, a package unpack — and short enough that a genuinely unreachable
|
|
33
|
+
* document still fails inside the catalog build.
|
|
34
|
+
*/
|
|
35
|
+
export declare const METADATA_RETRY_DELAY_MS = 1500;
|
|
16
36
|
/** Response caps: the listing is tiny, the metadata document is not. */
|
|
17
37
|
export declare const MODEL_LISTING_MAX_BYTES: number;
|
|
18
38
|
export declare const MODEL_METADATA_MAX_BYTES: number;
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Finding the provider's document without being told where it is.
|
|
3
|
+
*
|
|
4
|
+
* The document is the provider's own statement of each model's protocol, and
|
|
5
|
+
* the provider moves it: `go.mdx` lived at
|
|
6
|
+
* `packages/web/src/content/docs/go.mdx` until the `packages/web` tree was
|
|
7
|
+
* restructured, and the plugin then read 404 from both mirrors for as long as
|
|
8
|
+
* the old path was hardcoded. A hardcoded path is a promise the provider did
|
|
9
|
+
* not make.
|
|
10
|
+
*
|
|
11
|
+
* So the path is discovered instead of declared. The discovery is a read-only
|
|
12
|
+
* listing of the provider's own repository, and it runs only when every known
|
|
13
|
+
* path has already failed — the common case costs one request to a file that
|
|
14
|
+
* is there, and the rare case costs one listing.
|
|
15
|
+
*
|
|
16
|
+
* The paths that discovery finds are remembered on disk, as a *set of paths
|
|
17
|
+
* known to have existed*, not as one current path. That distinction matters:
|
|
18
|
+
* a cache holding a single path would hand back a stale path the moment the
|
|
19
|
+
* provider moved the file again, which is exactly the moment the cache is
|
|
20
|
+
* consulted. A set is only ever a list of candidates to try, so a stale entry
|
|
21
|
+
* costs one failed request and nothing else.
|
|
22
|
+
*
|
|
23
|
+
* @module @dan-ai-studio/dshopencodego/catalog/go-doc-discovery
|
|
24
|
+
*/
|
|
25
|
+
/** A repository listing is large; the cap keeps a hostile response bounded. */
|
|
26
|
+
export declare const DISCOVERY_MAX_BYTES: number;
|
|
27
|
+
/**
|
|
28
|
+
* The repository paths a discovery run found.
|
|
29
|
+
*
|
|
30
|
+
* A path is only useful if it names the document this plugin parses, and the
|
|
31
|
+
* listing also contains unrelated `go.mdx` files in other packages' fixtures.
|
|
32
|
+
* The path must therefore sit under a documentation tree.
|
|
33
|
+
* @param paths - every repository path the listing mentioned.
|
|
34
|
+
* @returns the paths that name the provider's Go document.
|
|
35
|
+
*/
|
|
36
|
+
export declare function selectDocumentPaths(paths: readonly string[]): readonly string[];
|
|
37
|
+
/**
|
|
38
|
+
* The mirrors a repository path can be read from.
|
|
39
|
+
*
|
|
40
|
+
* Two hosts, one file: the repository itself and a CDN that caches it. They
|
|
41
|
+
* fail independently, which is why both are tried.
|
|
42
|
+
* @param path - a repository-relative path.
|
|
43
|
+
* @returns the URLs to read that path from.
|
|
44
|
+
*/
|
|
45
|
+
export declare function mirrorsOf(path: string): readonly string[];
|
|
46
|
+
/**
|
|
47
|
+
* Ask the provider's repository where its document is.
|
|
48
|
+
*
|
|
49
|
+
* The listing is unauthenticated, so it is rate-limited. That is why this runs
|
|
50
|
+
* only after every known path has failed, and why the paths it finds are
|
|
51
|
+
* remembered: the next process starts with the answer and never asks.
|
|
52
|
+
* @param signal - caller cancellation.
|
|
53
|
+
* @param fetchImpl - the fetch to use; injected for tests.
|
|
54
|
+
* @returns the repository paths that name the document; empty when the
|
|
55
|
+
* listing could not be read or named nothing.
|
|
56
|
+
*/
|
|
57
|
+
export declare function discoverDocumentPaths(signal?: AbortSignal, fetchImpl?: typeof fetch): Promise<readonly string[]>;
|
|
58
|
+
/**
|
|
59
|
+
* The paths the cache remembers, most recently discovered first.
|
|
60
|
+
*
|
|
61
|
+
* A cache that cannot be read is an empty cache: the caller then tries the
|
|
62
|
+
* paths it knows and discovers again.
|
|
63
|
+
* @param cacheDir - the plugin's cache directory.
|
|
64
|
+
* @returns the remembered repository paths.
|
|
65
|
+
*/
|
|
66
|
+
export declare function readCachedPaths(cacheDir: string | undefined): readonly string[];
|
|
67
|
+
/**
|
|
68
|
+
* Remember the paths a discovery run found.
|
|
69
|
+
*
|
|
70
|
+
* The known paths are merged rather than replaced, newest first, so a cache
|
|
71
|
+
* that a concurrent process also wrote does not lose the other process's
|
|
72
|
+
* entry. A cache that cannot be written is not an error: the paths stay in
|
|
73
|
+
* memory for this process and are rediscovered next time.
|
|
74
|
+
* @param cacheDir - the plugin's cache directory.
|
|
75
|
+
* @param discovered - the paths a discovery run found.
|
|
76
|
+
* @returns the merged set, in the order it should be tried.
|
|
77
|
+
*/
|
|
78
|
+
export declare function rememberPaths(cacheDir: string | undefined, discovered: readonly string[]): readonly string[];
|
|
@@ -55,6 +55,23 @@ export interface GoDoc {
|
|
|
55
55
|
* document does not carry the expected tables.
|
|
56
56
|
*/
|
|
57
57
|
export declare function parseGoDocument(markdown: string): GoDoc;
|
|
58
|
+
/**
|
|
59
|
+
* How the document is found and where the answer is kept.
|
|
60
|
+
*
|
|
61
|
+
* The discovery is injectable so a test can decide what the provider's
|
|
62
|
+
* repository says without reaching the network — which is the only way this
|
|
63
|
+
* module can be exercised offline at all.
|
|
64
|
+
*/
|
|
65
|
+
export interface GoDocOptions {
|
|
66
|
+
/** Where the discovered document paths are remembered. */
|
|
67
|
+
readonly cacheDir?: string | undefined;
|
|
68
|
+
/**
|
|
69
|
+
* Ask the provider's repository where its document is.
|
|
70
|
+
*
|
|
71
|
+
* Defaults to the real listing. A test injects a fixed answer.
|
|
72
|
+
*/
|
|
73
|
+
readonly discover?: (signal?: AbortSignal) => Promise<readonly string[]>;
|
|
74
|
+
}
|
|
58
75
|
/**
|
|
59
76
|
* Fetch and parse the documentation, asking every source concurrently.
|
|
60
77
|
*
|
|
@@ -64,9 +81,16 @@ export declare function parseGoDocument(markdown: string): GoDoc;
|
|
|
64
81
|
* quota badge and the protocol fallback. `GO_DOC_URLS` order still decides:
|
|
65
82
|
* the repository file is examined before its cache mirror, and a source that
|
|
66
83
|
* fails is reported with its own reason.
|
|
84
|
+
*
|
|
85
|
+
* The known paths are tried first, then the paths a previous discovery
|
|
86
|
+
* remembered, and only then is the provider's repository asked where its
|
|
87
|
+
* document is. That order is what keeps the cost at one request in the common
|
|
88
|
+
* case while still surviving the provider moving the file: a path that is
|
|
89
|
+
* there is used, and a path that is not is replaced by a discovered one.
|
|
67
90
|
* @param signal - caller cancellation; it reaches every source's fetch and
|
|
68
91
|
* aborts all of them, so a cancelled discovery stops waiting on the network.
|
|
92
|
+
* @param options - where the discovered paths are remembered.
|
|
69
93
|
* @returns the parsed document from the first source that carried one.
|
|
70
94
|
* @throws {LlmError} `DOCUMENT_UNAVAILABLE` when every source failed.
|
|
71
95
|
*/
|
|
72
|
-
export declare function fetchGoDocument(signal?: AbortSignal): Promise<GoDoc>;
|
|
96
|
+
export declare function fetchGoDocument(signal?: AbortSignal, options?: GoDocOptions): Promise<GoDoc>;
|
|
@@ -73,6 +73,15 @@ export interface CatalogSnapshot {
|
|
|
73
73
|
* a user cannot act on.
|
|
74
74
|
*/
|
|
75
75
|
readonly degradations?: readonly CatalogDegradation[];
|
|
76
|
+
/**
|
|
77
|
+
* When the facts in this snapshot were written, when they came from disk
|
|
78
|
+
* rather than from a live read.
|
|
79
|
+
*
|
|
80
|
+
* The listing is always live; the metadata behind it may be older. The page
|
|
81
|
+
* says so, because a capacity that was true last week reads exactly like
|
|
82
|
+
* one that is true now.
|
|
83
|
+
*/
|
|
84
|
+
readonly factsCachedAt?: number;
|
|
76
85
|
readonly fetchedAtMs: number;
|
|
77
86
|
}
|
|
78
87
|
/** Observers the plugin logs through. */
|
|
@@ -106,6 +115,14 @@ export interface CatalogOptions {
|
|
|
106
115
|
* cancellation, forwarded to every documentation request.
|
|
107
116
|
*/
|
|
108
117
|
readonly readDocument?: (signal?: AbortSignal) => Promise<GoDoc>;
|
|
118
|
+
/**
|
|
119
|
+
* Where the discovered document paths are remembered.
|
|
120
|
+
*
|
|
121
|
+
* Absent means the plugin cannot write a cache, and the paths are then only
|
|
122
|
+
* known to the running process. That costs one discovery per start, and
|
|
123
|
+
* nothing else.
|
|
124
|
+
*/
|
|
125
|
+
readonly cacheDir?: string;
|
|
109
126
|
}
|
|
110
127
|
/**
|
|
111
128
|
* One gateway's catalog. Requests reuse a cached snapshot; explicit discovery
|
|
@@ -118,6 +135,16 @@ export declare class OpencodeGoCatalog {
|
|
|
118
135
|
private metadataDocument;
|
|
119
136
|
private metadataETag;
|
|
120
137
|
private lastMetadata;
|
|
138
|
+
/**
|
|
139
|
+
* The facts a previous successful read produced, read from disk.
|
|
140
|
+
*
|
|
141
|
+
* This is what a cold process falls back on when the online metadata cannot
|
|
142
|
+
* be read. Without it a degraded catalog declares every model unable to
|
|
143
|
+
* think, and the host refuses the effort the session is already using.
|
|
144
|
+
*/
|
|
145
|
+
private cachedMetadata;
|
|
146
|
+
/** When the cached facts were written, for the page to show. */
|
|
147
|
+
private readonly cachedAt;
|
|
121
148
|
/** The last parsed documentation; retained across failed fetches. */
|
|
122
149
|
private document;
|
|
123
150
|
/** Ids that already forced a revalidation, with the time they did. */
|
|
@@ -185,7 +212,31 @@ export declare class OpencodeGoCatalog {
|
|
|
185
212
|
private withDocumentedProtocol;
|
|
186
213
|
/** Facts for an id no source describes, from the ladder and the route defaults. */
|
|
187
214
|
private inferredFacts;
|
|
188
|
-
/**
|
|
215
|
+
/**
|
|
216
|
+
* Read the model metadata document, once.
|
|
217
|
+
*
|
|
218
|
+
* The document is megabytes, so its budget is longer than the other two
|
|
219
|
+
* reads' — and the host that runs this is not always a plain Node process:
|
|
220
|
+
* the desktop build fetches over the browser's network stack, where the
|
|
221
|
+
* same document that a shell reads in half a second can take long enough
|
|
222
|
+
* to outrun a budget set for kilobyte-sized documents.
|
|
223
|
+
* @param builtin - the installed catalog, for the parse.
|
|
224
|
+
* @returns the parsed metadata.
|
|
225
|
+
*/
|
|
226
|
+
private readMetadataDocument;
|
|
227
|
+
/**
|
|
228
|
+
* Read the model metadata document, retrying once.
|
|
229
|
+
*
|
|
230
|
+
* The failure this answers is not a broken network: the same document is
|
|
231
|
+
* reachable from a browser on the same machine while the host cannot read
|
|
232
|
+
* it. It is the host being busy — a plugin reload rewrites modules, unpacks
|
|
233
|
+
* packages, and rebuilds the runtime while the catalog asks for the
|
|
234
|
+
* document, and the request that loses that race is a timeout, not a
|
|
235
|
+
* refusal. A second attempt, a moment later, is the same request on a host
|
|
236
|
+
* that has finished what it was doing.
|
|
237
|
+
* @param builtin - the installed catalog, for the parse.
|
|
238
|
+
* @returns the parsed metadata.
|
|
239
|
+
*/
|
|
189
240
|
private refreshMetadata;
|
|
190
241
|
}
|
|
191
242
|
/**
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Keeping what the last successful metadata read said.
|
|
3
|
+
*
|
|
4
|
+
* The online metadata is the only source that states which models can think
|
|
5
|
+
* and at which levels. When it cannot be read, the plugin still knows some of
|
|
6
|
+
* that — from the last time it could — and throwing that away turns a
|
|
7
|
+
* degraded catalog into a broken one: a model that does think is declared
|
|
8
|
+
* unable to think, the host's own check refuses the effort the session is
|
|
9
|
+
* already using, and the run fails before it starts.
|
|
10
|
+
*
|
|
11
|
+
* So the parsed facts are written down. What is written is deliberately
|
|
12
|
+
* small: the capabilities, the capacities, the prices — not the document.
|
|
13
|
+
* The listing stays the authority on which models exist, so a stale file can
|
|
14
|
+
* never introduce a model the gateway does not serve; it can only describe
|
|
15
|
+
* the ones it does, with what was true when the file was written.
|
|
16
|
+
*
|
|
17
|
+
* @module @dan-ai-studio/dshopencodego/catalog/metadata-cache
|
|
18
|
+
*/
|
|
19
|
+
import type { ModelFacts } from './metadata.ts';
|
|
20
|
+
/**
|
|
21
|
+
* The facts a previous successful read produced.
|
|
22
|
+
*
|
|
23
|
+
* A cache that cannot be read is no cache: the caller then has nothing to
|
|
24
|
+
* fall back on and says so, rather than pretending the models have no
|
|
25
|
+
* capabilities.
|
|
26
|
+
* @param cacheDir - the plugin's cache directory.
|
|
27
|
+
* @returns the remembered facts and when they were written, or undefined.
|
|
28
|
+
*/
|
|
29
|
+
export declare function readFactsCache(cacheDir: string | undefined): {
|
|
30
|
+
readonly facts: ReadonlyMap<string, ModelFacts>;
|
|
31
|
+
readonly updatedAt: number;
|
|
32
|
+
} | undefined;
|
|
33
|
+
/**
|
|
34
|
+
* Remember the facts a successful read produced.
|
|
35
|
+
*
|
|
36
|
+
* A cache directory that cannot be written costs a degraded catalog after the
|
|
37
|
+
* next restart, and nothing else: the run continues with what it has.
|
|
38
|
+
* @param cacheDir - the plugin's cache directory.
|
|
39
|
+
* @param facts - the parsed facts to remember.
|
|
40
|
+
*/
|
|
41
|
+
export declare function writeFactsCache(cacheDir: string | undefined, facts: ReadonlyMap<string, ModelFacts>): void;
|
|
@@ -29,6 +29,15 @@ export interface CatalogReading {
|
|
|
29
29
|
* difference between a user who can act and a user who cannot.
|
|
30
30
|
*/
|
|
31
31
|
readonly degradations: readonly CatalogDegradation[];
|
|
32
|
+
/**
|
|
33
|
+
* When the model metadata behind this reading was written, when it came
|
|
34
|
+
* from disk rather than from a live read.
|
|
35
|
+
*
|
|
36
|
+
* The listing is live; the capacities, prices, and thinking levels behind
|
|
37
|
+
* it may not be. The page shows this, because a stale number reads exactly
|
|
38
|
+
* like a current one.
|
|
39
|
+
*/
|
|
40
|
+
readonly factsCachedAt?: number;
|
|
32
41
|
/** When the underlying snapshot was built. */
|
|
33
42
|
readonly fetchedAtMs: number;
|
|
34
43
|
/**
|
package/package.json
CHANGED