openmausbot 0.1.91 → 0.1.93
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/dist/assets/DesktopViewer-CKNR3SEW.js +5 -0
- package/dist/assets/{abnfDiagram-O67JEVCF-yP-w8pRq.js → abnfDiagram-O67JEVCF-Ckrm1I1X.js} +1 -1
- package/dist/assets/{arc-DsivfZEh.js → arc-BBdBuCgB.js} +1 -1
- package/dist/assets/{architectureDiagram-NJMV4G6O-CILw7lzT.js → architectureDiagram-NJMV4G6O-CDdA3Hjs.js} +1 -1
- package/dist/assets/{blockDiagram-BEXU5L5S-DEQUV3HF.js → blockDiagram-BEXU5L5S-AUrsx1YR.js} +1 -1
- package/dist/assets/{c4Diagram-YGBWAQC7-lSXCZoKA.js → c4Diagram-YGBWAQC7-BEqQ0Unh.js} +1 -1
- package/dist/assets/channel-BU94VpxE.js +1 -0
- package/dist/assets/{chunk-2Q5K7J3B-QCD5bgWP.js → chunk-2Q5K7J3B-CEEuvd4C.js} +1 -1
- package/dist/assets/{chunk-3FUC2YCW-C5-iZJuL.js → chunk-3FUC2YCW-DyANCiHl.js} +2 -2
- package/dist/assets/{chunk-5VM5RSS4-cxsawMpY.js → chunk-5VM5RSS4-Dx_Slpw9.js} +1 -1
- package/dist/assets/{chunk-GTNCS2PH-DV8tV8H7.js → chunk-GTNCS2PH-Dl7h7iiD.js} +1 -1
- package/dist/assets/{chunk-GWA4HPMP-CJFZyfbW.js → chunk-GWA4HPMP-BLx07SPb.js} +1 -1
- package/dist/assets/{chunk-JWPE2WC7-BigbMXco.js → chunk-JWPE2WC7-BMxDj3n7.js} +1 -1
- package/dist/assets/{chunk-NETBCI7D-oVxTfpDy.js → chunk-NETBCI7D-CN66PT4H.js} +1 -1
- package/dist/assets/{chunk-WEXAMYUT-CH4BiroW.js → chunk-WEXAMYUT-6HwhsDKw.js} +1 -1
- package/dist/assets/{chunk-XXDRQBXY-KEDa6gax.js → chunk-XXDRQBXY-gWnLt7SN.js} +1 -1
- package/dist/assets/{classDiagram-v2-NBCMYWYE-BiPjfqVv.js → classDiagram-v2-NBCMYWYE-CMS5l2ea.js} +1 -1
- package/dist/assets/{cose-bilkent-JH36ORCC-Caxew3OY.js → cose-bilkent-JH36ORCC-CV7sQwIG.js} +1 -1
- package/dist/assets/{cynefin-OW5HDTMX-B_Pui369.js → cynefin-OW5HDTMX-CSyl-7vl.js} +1 -1
- package/dist/assets/{cynefinDiagram-VND7K2PF-X64ydIhP.js → cynefinDiagram-VND7K2PF-BCLKL6az.js} +1 -1
- package/dist/assets/{dagre-6A5THRUB-TqYCI1E1.js → dagre-6A5THRUB-DiaZs6S1.js} +1 -1
- package/dist/assets/{diagram-22UHCM2B-TeOt3rAQ.js → diagram-22UHCM2B-BeTtmt3I.js} +1 -1
- package/dist/assets/{diagram-3UASUU5V-D70BLhDA.js → diagram-3UASUU5V-CsdBG-QQ.js} +1 -1
- package/dist/assets/{diagram-ATOU4E4O-BbQDLwUC.js → diagram-ATOU4E4O-BczvdbR1.js} +1 -1
- package/dist/assets/{diagram-CDSNMT55-ChNJZ1u-.js → diagram-CDSNMT55-DHTTTMhi.js} +1 -1
- package/dist/assets/{diagram-MLGK6HIB-DMBl2APa.js → diagram-MLGK6HIB-RTjHyd8z.js} +1 -1
- package/dist/assets/{diagram-MPIPVDR6-CEqDchOp.js → diagram-MPIPVDR6-BDenHEH_.js} +1 -1
- package/dist/assets/{ebnfDiagram-ZINNZB2B-BVDKRp_-.js → ebnfDiagram-ZINNZB2B-BSiRSbtL.js} +1 -1
- package/dist/assets/{elk-276RUBZZ-CRkofO9B.js → elk-276RUBZZ-BCAElss-.js} +1 -1
- package/dist/assets/{erDiagram-OPXOYQCR-DruOMh5e.js → erDiagram-OPXOYQCR-C4_1ZPJe.js} +1 -1
- package/dist/assets/{flowDiagram-KWPJA3E3-8GMQeB7t.js → flowDiagram-KWPJA3E3-Bcb4Z75S.js} +1 -1
- package/dist/assets/{ganttDiagram-FUAMR5RP-ORE9jcsM.js → ganttDiagram-FUAMR5RP-BiPOzxzb.js} +1 -1
- package/dist/assets/{gitGraphDiagram-X574FWY7-BmsedsWh.js → gitGraphDiagram-X574FWY7-B1wtF3ZS.js} +1 -1
- package/dist/assets/index-Biji-bfL.css +1 -0
- package/dist/assets/{index-DKNEM8Q6.js → index-BkYwilkG.js} +1 -1
- package/dist/assets/index-DZlhoES0.js +597 -0
- package/dist/assets/{infoDiagram-VRGFBTTK-E_p7AooJ.js → infoDiagram-VRGFBTTK-A19No71U.js} +1 -1
- package/dist/assets/{ishikawaDiagram-OU5B5YK6-Y-B2mKda.js → ishikawaDiagram-OU5B5YK6-BQsqJ_cV.js} +1 -1
- package/dist/assets/{journeyDiagram-ZHPQQLJL-eA8RELcK.js → journeyDiagram-ZHPQQLJL-DAO0pVan.js} +1 -1
- package/dist/assets/{kanban-definition-PNTS6WVX-DqIL2e8e.js → kanban-definition-PNTS6WVX-NepmKEe2.js} +1 -1
- package/dist/assets/{linear-yC_29R6e.js → linear-bq1PTDTW.js} +1 -1
- package/dist/assets/{mermaid.core-VhEMkSND.js → mermaid.core-s4LTbQle.js} +5 -5
- package/dist/assets/{mindmap-definition-NLK3R4M7-oJ7kyQZy.js → mindmap-definition-NLK3R4M7-LtdbgHFH.js} +1 -1
- package/dist/assets/{pegDiagram-GJSIUBJH-BmfYcY0O.js → pegDiagram-GJSIUBJH-tdbaCxiK.js} +1 -1
- package/dist/assets/{pieDiagram-5QR66LMP-DKvgP7w9.js → pieDiagram-5QR66LMP-DvkbpW8l.js} +1 -1
- package/dist/assets/{quadrantDiagram-O4NWA36T-BEKL3W0L.js → quadrantDiagram-O4NWA36T-Do4KaH-E.js} +1 -1
- package/dist/assets/{railroadDiagram-XR7U4H2S-BEsiqR5s.js → railroadDiagram-XR7U4H2S-BQWULOAs.js} +1 -1
- package/dist/assets/{requirementDiagram-PLB6GJNP-DWBcWvAP.js → requirementDiagram-PLB6GJNP-w9kei1FU.js} +1 -1
- package/dist/assets/{sankeyDiagram-IPEJSGJF-rpZ2_O4s.js → sankeyDiagram-IPEJSGJF-D2gRy0e8.js} +1 -1
- package/dist/assets/{sequenceDiagram-PO4LG4MO-B99HQdJR.js → sequenceDiagram-PO4LG4MO-a2sO5_Eo.js} +1 -1
- package/dist/assets/{sizeCapture-INFHLROL-BjN0qT5T.js → sizeCapture-INFHLROL-CiPJmFEB.js} +1 -1
- package/dist/assets/{stateDiagram-v2-GCMORJYK-BbsVgGsC.js → stateDiagram-v2-GCMORJYK-DAkUUlL7.js} +1 -1
- package/dist/assets/{swimlanes-2SLR337P-DG3gKkeR.js → swimlanes-2SLR337P-DANf2R8Y.js} +1 -1
- package/dist/assets/swimlanesDiagram-TC7HE7FX-CdHxG8aM.js +8 -0
- package/dist/assets/{timeline-definition-EJHVYXUP-BTHOdtF_.js → timeline-definition-EJHVYXUP-BKpQmzrT.js} +1 -1
- package/dist/assets/{usecaseDiagram-POWQR4AR-acLSLFwc.js → usecaseDiagram-POWQR4AR-BtI7qLN-.js} +1 -1
- package/dist/assets/{vennDiagram-UO4OBE2U-C5JzNX9G.js → vennDiagram-UO4OBE2U-gXJXrAG_.js} +1 -1
- package/dist/assets/{wardleyDiagram-VNRHLVJA-DL1-PrR_.js → wardleyDiagram-VNRHLVJA-GU8Po5-n.js} +1 -1
- package/dist/assets/{xychartDiagram-PMCCYNJV-Bvr1477p.js → xychartDiagram-PMCCYNJV-BF4J30g6.js} +1 -1
- package/dist/index.html +2 -2
- package/dist/novnc-NOTICE.txt +118 -0
- package/dist-server/cloud-home-start.js +112 -14
- package/dist-server/companion/src/routes.js +24 -1
- package/dist-server/container-mcp.js +201 -16
- package/dist-server/drivers/agents-proxy.js +7 -4
- package/dist-server/enterprise/server/index.js +5 -1
- package/dist-server/index.js +10698 -4737
- package/dist-server/local-computer-proxy.js +133 -8
- package/dist-server/mcp-gate.js +16 -6
- package/dist-server/model-catalog/models-dev.snapshot.json +7299 -0
- package/dist-server/openmausbot.js +4656 -1258
- package/dist-server/pair-cli.js +4656 -1258
- package/dist-server/server/boat.js +5 -5
- package/dist-server/server/browser-engine.js +10 -1
- package/dist-server/server/browser-live.js +46 -2
- package/dist-server/server/browser-runtime.js +74 -8
- package/dist-server/server/browser-tool-shape.js +6 -4
- package/dist-server/server/chief-of-staff.js +1 -1
- package/dist-server/server/cloud-home-start.js +118 -26
- package/dist-server/server/cloud-home.js +39 -3
- package/dist-server/server/cloud-lending.js +74 -10
- package/dist-server/server/cloud-owner.js +196 -0
- package/dist-server/server/cloud-secrets-boot.js +6 -0
- package/dist-server/server/cloud-secrets.js +65 -0
- package/dist-server/server/computer-control.js +6 -0
- package/dist-server/server/config.js +187 -14
- package/dist-server/server/container-computer.js +46 -34
- package/dist-server/server/desktop-viewer-targets.js +26 -0
- package/dist-server/server/digest.js +12 -2
- package/dist-server/server/drivers/acp/core.js +339 -33
- package/dist-server/server/drivers/acp/cursor.js +13 -1
- package/dist-server/server/drivers/acp/opencode-go.js +614 -85
- package/dist-server/server/drivers/acp/quiet-status.js +257 -0
- package/dist-server/server/drivers/acp/qwen.js +18 -0
- package/dist-server/server/drivers/agents-catalog.js +6 -6
- package/dist-server/server/drivers/antigravity-acp.js +69 -11
- package/dist-server/server/drivers/antigravity-temp.js +360 -0
- package/dist-server/server/drivers/antigravity.js +21 -0
- package/dist-server/server/drivers/builtIn.js +2 -0
- package/dist-server/server/drivers/cerebras.js +108 -0
- package/dist-server/server/drivers/chat-mcp-tools.js +20 -3
- package/dist-server/server/drivers/chatgpt-plan-auth.js +481 -0
- package/dist-server/server/drivers/claude.js +329 -37
- package/dist-server/server/drivers/codex.js +228 -18
- package/dist-server/server/drivers/grok.js +2 -2
- package/dist-server/server/drivers/mistral.js +4 -3
- package/dist-server/server/drivers/openai-chat-protocol.js +4 -3
- package/dist-server/server/drivers/openai-chat.js +66 -4
- package/dist-server/server/drivers/openai-compat.js +73 -23
- package/dist-server/server/drivers/prompt-split.js +2 -0
- package/dist-server/server/engine-install.js +46 -12
- package/dist-server/server/env-path.js +21 -5
- package/dist-server/server/harness/registry.js +2 -2
- package/dist-server/server/index.js +1545 -238
- package/dist-server/server/lending-memory.js +479 -0
- package/dist-server/server/local-vm-idle.js +24 -6
- package/dist-server/server/local-vm-stop-reason.js +26 -0
- package/dist-server/server/managed-desktop.js +3 -0
- package/dist-server/server/mcp-bridge.js +5 -3
- package/dist-server/server/mcp-http.js +6 -3
- package/dist-server/server/mcp-oauth-discovery.js +133 -0
- package/dist-server/server/mcp-oauth-store.js +70 -0
- package/dist-server/server/mcp-oauth.js +550 -0
- package/dist-server/server/mcp-probe.js +14 -1
- package/dist-server/server/mcp-registry.js +43 -0
- package/dist-server/server/memory-upkeep.js +94 -84
- package/dist-server/server/model-catalog/catalog.js +374 -0
- package/dist-server/server/model-catalog/presets.js +176 -0
- package/dist-server/server/model-catalog/trim.js +219 -0
- package/dist-server/server/provider-key-check.js +5 -2
- package/dist-server/server/recent-work.js +1 -1
- package/dist-server/server/request-auth.js +23 -0
- package/dist-server/server/routes/antigravity-leftovers.js +45 -0
- package/dist-server/server/routes/bot-memory.js +31 -6
- package/dist-server/server/routes/desktop-viewer.js +238 -0
- package/dist-server/server/routine-requests.js +1 -1
- package/dist-server/server/routines.js +6 -3
- package/dist-server/server/sessions.js +56 -9
- package/dist-server/server/skills.js +25 -10
- package/dist-server/server/store.js +116 -122
- package/dist-server/server/thread-events.js +2 -0
- package/dist-server/server/thread-starters.js +33 -0
- package/dist-server/server/thread-title.js +46 -0
- package/dist-server/server/tts/fish.js +4 -5
- package/dist-server/server/tts/index.js +5 -3
- package/dist-server/server/vps-computer.js +80 -13
- package/dist-server/server/webhooks.js +18 -1
- package/dist-server/server/workspace-backup-policy.js +3 -0
- package/dist-server/server/workspace-backup.js +25 -0
- package/dist-server/server/workspace.js +66 -16
- package/dist-server/shared/approval-mode.js +1 -1
- package/dist-server/shared/redact.js +1 -0
- package/dist-server/shared/tool-name.js +22 -0
- package/dist-server/shared/workspace-backup-client.js +1 -0
- package/dist-server/src/lib/citations.js +177 -0
- package/dist-server/src/lib/composer-attachments.js +7 -1
- package/dist-server/vps-container-mcp.js +202 -16
- package/dist-server/workspace-backup.worker.js +87 -9
- package/enterprise/server/index.js +5 -1
- package/package.json +1 -1
- package/dist/assets/channel-DrfGMu7X.js +0 -1
- package/dist/assets/index-BlnCEcU6.js +0 -588
- package/dist/assets/index-DVxjOvID.css +0 -1
- package/dist/assets/swimlanesDiagram-TC7HE7FX-BCZhxl7e.js +0 -8
|
@@ -25,7 +25,7 @@ import { mergeTopicText } from "./memory-topics.js";
|
|
|
25
25
|
import { applyMoves, MAX_MOVES, organizeCandidates, organizePrompt, parseMoves } from "./memory-organize.js";
|
|
26
26
|
import { recordMemoryChange } from "./memory-journal.js";
|
|
27
27
|
import { applyTidy, contradictionCandidates, contradictionPrompt, parseContradictions, planChanges, planTidy } from "./memory-tidy.js";
|
|
28
|
-
import { ensureWorkspace, listMemoryTopics, memoryDate, memoryEntry, memoryTopicIndex, updateMemory, workspaceDir, writeMemoryFile, writeMemoryTopic } from "./workspace.js";
|
|
28
|
+
import { ensureWorkspace, listMemoryTopics, memoryDate, memoryEntry, memoryTopicIndex, readMemoryText, updateMemory, workspaceDir, writeMemoryFile, writeMemoryTopic } from "./workspace.js";
|
|
29
29
|
export const CAPTURE_MAX_TURNS = 6;
|
|
30
30
|
export const MODEL_TIMEOUT_MS = 60_000;
|
|
31
31
|
export const TIDY_CHECK_MS = 10 * 60_000;
|
|
@@ -36,7 +36,7 @@ export const ARCHIVE_TOPIC = "archive.md";
|
|
|
36
36
|
export const TOPIC_MAX_BYTES = 64 * 1024;
|
|
37
37
|
/** On unless switched off: every bot keeps its memory in shape by default. */
|
|
38
38
|
export function upkeepEnabled(bot) {
|
|
39
|
-
return Boolean(bot) && bot.memoryUpkeep !== false;
|
|
39
|
+
return Boolean(bot) && bot.memoryUpkeep !== false && bot.memoryEnabled !== false;
|
|
40
40
|
}
|
|
41
41
|
/** How many "this entry is core" judgements are remembered per bot. */
|
|
42
42
|
const MAX_CORE = 500;
|
|
@@ -63,7 +63,7 @@ function saveState(state) {
|
|
|
63
63
|
}
|
|
64
64
|
function readRaw(botId, relative) {
|
|
65
65
|
try {
|
|
66
|
-
return
|
|
66
|
+
return readMemoryText(join(workspaceDir(botId), relative));
|
|
67
67
|
}
|
|
68
68
|
catch {
|
|
69
69
|
return null;
|
|
@@ -103,6 +103,7 @@ export function createMemoryUpkeep(deps) {
|
|
|
103
103
|
void promise.finally(() => inflight.delete(promise)).catch(() => undefined);
|
|
104
104
|
return promise;
|
|
105
105
|
};
|
|
106
|
+
const writing = deps.writing ?? ((_botId, write) => write());
|
|
106
107
|
async function capture(batch) {
|
|
107
108
|
const bot = deps.bot(batch.botId);
|
|
108
109
|
const at = now().getTime();
|
|
@@ -138,54 +139,57 @@ export function createMemoryUpkeep(deps) {
|
|
|
138
139
|
// From here to the journal rows there is no await, so no write interleaves.
|
|
139
140
|
if (!upkeepEnabled(deps.bot(bot.id)))
|
|
140
141
|
return off;
|
|
141
|
-
const
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
if (added)
|
|
160
|
-
recordMemoryChange(bot.id, { path: "MEMORY.md", actor: "upkeep", via: "capture", threadId: batch.threadId, before, after: readRaw(bot.id, "MEMORY.md") });
|
|
161
|
-
// Detail goes to a topic file the bot keeps for that subject, created
|
|
162
|
-
// with a header (title and other words for it) the first time.
|
|
163
|
-
const existing = new Map(listMemoryTopics(bot.id).map((topic) => [topic.name.toLowerCase(), topic.name]));
|
|
164
|
-
let topics = 0;
|
|
165
|
-
for (const [wanted, candidates] of byTopic) {
|
|
166
|
-
const name = existing.get(wanted) ?? wanted;
|
|
167
|
-
const path = `memory/${name}`;
|
|
168
|
-
const current = readRaw(bot.id, path);
|
|
169
|
-
if (current !== null && Buffer.byteLength(current, "utf8") > TOPIC_MAX_BYTES)
|
|
170
|
-
continue;
|
|
171
|
-
const seen = notebookIdentities(current ?? "");
|
|
172
|
-
const lines = candidates
|
|
173
|
-
.filter((candidate) => !seen.has(factIdentity(candidate.text)))
|
|
174
|
-
.map((candidate) => memoryEntry(candidate.text, { source, now: now(), ...(candidate.until ? { until: candidate.until } : {}) }));
|
|
175
|
-
if (!lines.length)
|
|
176
|
-
continue;
|
|
177
|
-
const title = name.replace(/\.md$/, "").replace(/-/g, " ");
|
|
178
|
-
const aliases = [...new Set(candidates.flatMap((candidate) => candidate.topicAliases ?? []))];
|
|
179
|
-
try {
|
|
180
|
-
writeMemoryTopic(bot.id, name, mergeTopicText(current, { title, aliases, lines }));
|
|
142
|
+
const { parsed, added, topics, full } = writing(bot.id, () => {
|
|
143
|
+
const before = readRaw(bot.id, "MEMORY.md");
|
|
144
|
+
const parsed = parseCandidates(answer, today);
|
|
145
|
+
const fresh = newCandidates(parsed, before ?? "");
|
|
146
|
+
const source = `${deps.sourceLabel(bot.id, batch.threadId)} (noticed)`;
|
|
147
|
+
let added = 0;
|
|
148
|
+
let full = false;
|
|
149
|
+
const byTopic = new Map();
|
|
150
|
+
for (const candidate of fresh) {
|
|
151
|
+
if (candidate.topic) {
|
|
152
|
+
byTopic.set(candidate.topic, [...(byTopic.get(candidate.topic) ?? []), candidate]);
|
|
153
|
+
continue;
|
|
154
|
+
}
|
|
155
|
+
const result = updateMemory(bot.id, { action: "append", text: candidate.text, ...(candidate.until ? { until: candidate.until } : {}) }, { source, now: now() });
|
|
156
|
+
if (result.ok)
|
|
157
|
+
added += 1;
|
|
158
|
+
else if (result.code === "over-budget")
|
|
159
|
+
full = true;
|
|
181
160
|
}
|
|
182
|
-
|
|
183
|
-
|
|
161
|
+
if (added)
|
|
162
|
+
recordMemoryChange(bot.id, { path: "MEMORY.md", actor: "upkeep", via: "capture", threadId: batch.threadId, before, after: readRaw(bot.id, "MEMORY.md") });
|
|
163
|
+
// Detail goes to a topic file the bot keeps for that subject, created
|
|
164
|
+
// with a header (title and other words for it) the first time.
|
|
165
|
+
const existing = new Map(listMemoryTopics(bot.id).map((topic) => [topic.name.toLowerCase(), topic.name]));
|
|
166
|
+
let topics = 0;
|
|
167
|
+
for (const [wanted, candidates] of byTopic) {
|
|
168
|
+
const name = existing.get(wanted) ?? wanted;
|
|
169
|
+
const path = `memory/${name}`;
|
|
170
|
+
const current = readRaw(bot.id, path);
|
|
171
|
+
if (current !== null && Buffer.byteLength(current, "utf8") > TOPIC_MAX_BYTES)
|
|
172
|
+
continue;
|
|
173
|
+
const seen = notebookIdentities(current ?? "");
|
|
174
|
+
const lines = candidates
|
|
175
|
+
.filter((candidate) => !seen.has(factIdentity(candidate.text)))
|
|
176
|
+
.map((candidate) => memoryEntry(candidate.text, { source, now: now(), ...(candidate.until ? { until: candidate.until } : {}) }));
|
|
177
|
+
if (!lines.length)
|
|
178
|
+
continue;
|
|
179
|
+
const title = name.replace(/\.md$/, "").replace(/-/g, " ");
|
|
180
|
+
const aliases = [...new Set(candidates.flatMap((candidate) => candidate.topicAliases ?? []))];
|
|
181
|
+
try {
|
|
182
|
+
writeMemoryTopic(bot.id, name, mergeTopicText(current, { title, aliases, lines }));
|
|
183
|
+
}
|
|
184
|
+
catch {
|
|
185
|
+
continue;
|
|
186
|
+
}
|
|
187
|
+
recordMemoryChange(bot.id, { path, actor: "upkeep", via: "capture", threadId: batch.threadId, before: current, after: readRaw(bot.id, path) });
|
|
188
|
+
existing.set(name.toLowerCase(), name);
|
|
189
|
+
topics += lines.length;
|
|
184
190
|
}
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
topics += lines.length;
|
|
188
|
-
}
|
|
191
|
+
return { parsed, added, topics, full };
|
|
192
|
+
});
|
|
189
193
|
// Only the owner's own words reach About me: on a shared workspace
|
|
190
194
|
// another person's facts are not the owner's profile. Every parsed fact
|
|
191
195
|
// counts, not only new ones — the bot may already have noted it in its
|
|
@@ -245,37 +249,39 @@ export function createMemoryUpkeep(deps) {
|
|
|
245
249
|
saveState(saved);
|
|
246
250
|
if (!moves.length)
|
|
247
251
|
return 0;
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
const
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
252
|
+
return writing(botId, () => {
|
|
253
|
+
const { byTopic } = applyMoves(before, moves);
|
|
254
|
+
const written = new Set();
|
|
255
|
+
const existing = new Map(listMemoryTopics(botId).map((topic) => [topic.name.toLowerCase(), topic.name]));
|
|
256
|
+
let count = 0;
|
|
257
|
+
for (const [wanted, group] of byTopic) {
|
|
258
|
+
const name = existing.get(wanted) ?? wanted;
|
|
259
|
+
const path = `memory/${name}`;
|
|
260
|
+
const current = readRaw(botId, path);
|
|
261
|
+
const seen = notebookIdentities(current ?? "");
|
|
262
|
+
const lines = group.lines.filter((line) => !seen.has(lineFactIdentity(line)));
|
|
263
|
+
if (lines.length) {
|
|
264
|
+
try {
|
|
265
|
+
writeMemoryTopic(botId, name, mergeTopicText(current, { title: name.replace(/\.md$/, "").replace(/-/g, " "), aliases: group.aliases, lines }));
|
|
266
|
+
}
|
|
267
|
+
catch (error) {
|
|
268
|
+
log(`memory upkeep: could not write ${path}; kept its entries in MEMORY.md: ${error.message}`);
|
|
269
|
+
continue;
|
|
270
|
+
}
|
|
271
|
+
recordMemoryChange(botId, { path, actor: "upkeep", via: "organize", before: current, after: readRaw(botId, path) });
|
|
261
272
|
}
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
}
|
|
266
|
-
recordMemoryChange(botId, { path, actor: "upkeep", via: "organize", before: current, after: readRaw(botId, path) });
|
|
273
|
+
existing.set(name.toLowerCase(), name);
|
|
274
|
+
written.add(wanted);
|
|
275
|
+
count += group.lines.length;
|
|
267
276
|
}
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
recordMemoryChange(botId, { path: "MEMORY.md", actor: "upkeep", via: "organize", before, after: readRaw(botId, "MEMORY.md") });
|
|
277
|
-
log(`memory upkeep: moved ${count} entr${count === 1 ? "y" : "ies"} from MEMORY.md into topic files for ${deps.bot(botId)?.name ?? botId}`);
|
|
278
|
-
return count;
|
|
277
|
+
if (!written.size)
|
|
278
|
+
return 0;
|
|
279
|
+
const { text } = applyMoves(before, moves.filter((move) => written.has(move.topic)));
|
|
280
|
+
writeMemoryFile(botId, text);
|
|
281
|
+
recordMemoryChange(botId, { path: "MEMORY.md", actor: "upkeep", via: "organize", before, after: readRaw(botId, "MEMORY.md") });
|
|
282
|
+
log(`memory upkeep: moved ${count} entr${count === 1 ? "y" : "ies"} from MEMORY.md into topic files for ${deps.bot(botId)?.name ?? botId}`);
|
|
283
|
+
return count;
|
|
284
|
+
});
|
|
279
285
|
}
|
|
280
286
|
function appendArchive(botId, archived) {
|
|
281
287
|
const archivePath = `memory/${ARCHIVE_TOPIC}`;
|
|
@@ -330,9 +336,11 @@ export function createMemoryUpkeep(deps) {
|
|
|
330
336
|
// The archive is written first: a line is never out of MEMORY.md
|
|
331
337
|
// before it is in the archive, and the newest journal row — the one a
|
|
332
338
|
// person undoes — is MEMORY.md, whose undo brings the line back.
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
339
|
+
writing(botId, () => {
|
|
340
|
+
if (archived.length)
|
|
341
|
+
appendArchive(botId, archived);
|
|
342
|
+
writeMemoryFile(botId, text);
|
|
343
|
+
});
|
|
336
344
|
recordMemoryChange(botId, { path: "MEMORY.md", actor: "upkeep", via: "tidy", before, after: readRaw(botId, "MEMORY.md") });
|
|
337
345
|
}
|
|
338
346
|
// Topic files get the two steps that need no model: expired lines to the
|
|
@@ -350,9 +358,11 @@ export function createMemoryUpkeep(deps) {
|
|
|
350
358
|
if (!planChanges(topicPlan))
|
|
351
359
|
continue;
|
|
352
360
|
const tidied = applyTidy(topicBefore, topicPlan, today);
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
361
|
+
writing(botId, () => {
|
|
362
|
+
if (tidied.archived.length)
|
|
363
|
+
appendArchive(botId, tidied.archived);
|
|
364
|
+
writeMemoryTopic(botId, topic.name, tidied.text);
|
|
365
|
+
});
|
|
356
366
|
recordMemoryChange(botId, { path, actor: "upkeep", via: "tidy", before: topicBefore, after: readRaw(botId, path) });
|
|
357
367
|
topicExpired += topicPlan.expired.length;
|
|
358
368
|
topicDuplicates += topicPlan.duplicates.length;
|
|
@@ -0,0 +1,374 @@
|
|
|
1
|
+
/*! Adapted from anomalyco/opencode@2fa3363c924c5c3e367b84a87ae478296a0ed59b
|
|
2
|
+
* packages/core/src/models-dev.ts:160-258, MIT; modified.
|
|
3
|
+
*
|
|
4
|
+
* MIT License
|
|
5
|
+
*
|
|
6
|
+
* Copyright (c) 2025 opencode
|
|
7
|
+
*
|
|
8
|
+
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
9
|
+
* of this software and associated documentation files (the "Software"), to deal
|
|
10
|
+
* in the Software without restriction, including without limitation the rights
|
|
11
|
+
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
12
|
+
* copies of the Software, and to permit persons to whom the Software is
|
|
13
|
+
* furnished to do so, subject to the following conditions:
|
|
14
|
+
*
|
|
15
|
+
* The above copyright notice and this permission notice shall be included in all
|
|
16
|
+
* copies or substantial portions of the Software.
|
|
17
|
+
*
|
|
18
|
+
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
19
|
+
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
20
|
+
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
21
|
+
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
22
|
+
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
23
|
+
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
24
|
+
* SOFTWARE.
|
|
25
|
+
*/
|
|
26
|
+
// The model provider catalog: load it, keep it fresh, merge in the presets.
|
|
27
|
+
//
|
|
28
|
+
// Kept from OpenCode's models-dev.ts: a cache file refreshed from
|
|
29
|
+
// models.dev/api.json with a 10 s timeout, written through a temp file and a
|
|
30
|
+
// rename; a corrupt cache is deleted; a snapshot bundled with the app; a
|
|
31
|
+
// forced refresh skips the freshness check; failures are logged and never
|
|
32
|
+
// surface; one switch turns fetching off.
|
|
33
|
+
//
|
|
34
|
+
// Changed for OpenMausBot:
|
|
35
|
+
// - plain Node instead of Effect; one server process owns DATA_DIR, so a
|
|
36
|
+
// single in-flight promise stands in for OpenCode's cross-process flock;
|
|
37
|
+
// - data counts as fresh for 24 hours, and a refresh runs only when asked
|
|
38
|
+
// (Settings or the model picker opening), not on a timer;
|
|
39
|
+
// - If-None-Match with the stored ETag, a 20 MB cap, redirects refused;
|
|
40
|
+
// - every source is trimmed and brace-filtered (trim.ts) before use;
|
|
41
|
+
// - the source is the newest of: our cache, OpenCode's own cache
|
|
42
|
+
// (~/.cache/opencode/models.json, read only), the bundled snapshot. On a tie
|
|
43
|
+
// that order decides, so a newer snapshot beats an older cache;
|
|
44
|
+
// - switched off by OMB_DISABLE_MODEL_CATALOG_FETCH=1, and by the caller on
|
|
45
|
+
// managed or hosted installs.
|
|
46
|
+
//
|
|
47
|
+
// This list is never the last word on what a key can use: the provider's own
|
|
48
|
+
// /models, called with the key, is. The catalog supplies names, key variable
|
|
49
|
+
// names, base URLs and the capability flags used to pick recommended models.
|
|
50
|
+
import { existsSync, mkdirSync, readFileSync, rmSync, statSync } from "node:fs";
|
|
51
|
+
import { homedir } from "node:os";
|
|
52
|
+
import { dirname, join } from "node:path";
|
|
53
|
+
import { fileURLToPath } from "node:url";
|
|
54
|
+
import { writeFileAtomic } from "../atomic.js";
|
|
55
|
+
import { OPENAI_COMPATIBLE_NPM, PROVIDER_PRESETS } from "./presets.js";
|
|
56
|
+
import { MODEL_CATALOG_SCHEMA, catalogStats, packCatalog, trimModelsDevCatalog, unpackCatalog, } from "./trim.js";
|
|
57
|
+
export const MODELS_DEV_URL = "https://models.dev/api.json";
|
|
58
|
+
export const CATALOG_MAX_AGE_MS = 24 * 60 * 60 * 1000;
|
|
59
|
+
export const CATALOG_FETCH_TIMEOUT_MS = 10_000;
|
|
60
|
+
export const CATALOG_MAX_BYTES = 20 * 1024 * 1024;
|
|
61
|
+
export const CATALOG_DISABLE_ENV = "OMB_DISABLE_MODEL_CATALOG_FETCH";
|
|
62
|
+
export const SNAPSHOT_FILE = "models-dev.snapshot.json";
|
|
63
|
+
/** Our cache, under providers/ so backups and Move to Cloud leave it out. */
|
|
64
|
+
export function modelCatalogCachePath(dataDir) {
|
|
65
|
+
return join(dataDir, "providers", "model-providers", "models-dev.json");
|
|
66
|
+
}
|
|
67
|
+
/** Where OpenCode keeps models.dev (xdg-basedir's cache dir, on every OS). */
|
|
68
|
+
export function openCodeCatalogCachePath(env = process.env) {
|
|
69
|
+
return join(env.XDG_CACHE_HOME || join(homedir(), ".cache"), "opencode", "models.json");
|
|
70
|
+
}
|
|
71
|
+
/** The snapshot sits beside this module in a checkout, and under
|
|
72
|
+
* model-catalog/ beside the bundled server (scripts/bundle-server.mjs). */
|
|
73
|
+
function readBundledSnapshot() {
|
|
74
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
75
|
+
for (const path of [join(here, SNAPSHOT_FILE), join(here, "model-catalog", SNAPSHOT_FILE)]) {
|
|
76
|
+
if (existsSync(path))
|
|
77
|
+
return readFileSync(path, "utf8");
|
|
78
|
+
}
|
|
79
|
+
return undefined;
|
|
80
|
+
}
|
|
81
|
+
// eslint-disable-next-line no-control-regex
|
|
82
|
+
const CONTROL = /[\u0000-\u001f\u007f]/u;
|
|
83
|
+
// An ETag is echoed back in a request header: keep only a plain, short one.
|
|
84
|
+
const safeEtag = (value) => typeof value === "string" && value.length <= 256 && !CONTROL.test(value) ? value : undefined;
|
|
85
|
+
const time = (value) => {
|
|
86
|
+
if (typeof value !== "string")
|
|
87
|
+
return undefined;
|
|
88
|
+
const parsed = Date.parse(value);
|
|
89
|
+
return Number.isNaN(parsed) ? undefined : parsed;
|
|
90
|
+
};
|
|
91
|
+
const message = (error) => error instanceof Error ? error.message : String(error);
|
|
92
|
+
export class ModelCatalogStore {
|
|
93
|
+
options;
|
|
94
|
+
cachePath;
|
|
95
|
+
memo;
|
|
96
|
+
inflight;
|
|
97
|
+
constructor(options) {
|
|
98
|
+
this.options = options;
|
|
99
|
+
this.cachePath = modelCatalogCachePath(options.dataDir);
|
|
100
|
+
}
|
|
101
|
+
get now() {
|
|
102
|
+
return (this.options.now ?? Date.now)();
|
|
103
|
+
}
|
|
104
|
+
log(text) {
|
|
105
|
+
(this.options.log ?? ((line) => console.warn(line)))(`[model-catalog] ${text}`);
|
|
106
|
+
}
|
|
107
|
+
get fetchDisabled() {
|
|
108
|
+
return Boolean(this.options.fetchDisabled) || (this.options.env ?? process.env)[CATALOG_DISABLE_ENV] === "1";
|
|
109
|
+
}
|
|
110
|
+
/** The newest usable catalog. Never throws; worst case an empty catalog. */
|
|
111
|
+
get() {
|
|
112
|
+
if (this.memo)
|
|
113
|
+
return this.memo;
|
|
114
|
+
const candidates = [this.ownCache(), this.openCodeCache(), this.snapshot()]
|
|
115
|
+
.filter((candidate) => candidate !== undefined);
|
|
116
|
+
// Stable sort: on equal times the listed order (cache, OpenCode, snapshot) wins.
|
|
117
|
+
candidates.sort((a, b) => b.updatedAt - a.updatedAt);
|
|
118
|
+
for (const candidate of candidates) {
|
|
119
|
+
let drops = 0;
|
|
120
|
+
const providers = trimModelsDevCatalog(candidate.providers(), { onDrop: () => { drops += 1; } });
|
|
121
|
+
if (!Object.keys(providers).length) {
|
|
122
|
+
this.log(`the ${candidate.origin} catalog held no usable providers; trying the next one`);
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
if (drops)
|
|
126
|
+
this.log(`left out ${drops} ${candidate.origin} catalog entries that were malformed or contained { or }`);
|
|
127
|
+
this.memo = { origin: candidate.origin, updatedAt: new Date(candidate.updatedAt).toISOString(), providers };
|
|
128
|
+
return this.memo;
|
|
129
|
+
}
|
|
130
|
+
this.memo = { origin: "empty", updatedAt: new Date(0).toISOString(), providers: trimModelsDevCatalog(undefined) };
|
|
131
|
+
return this.memo;
|
|
132
|
+
}
|
|
133
|
+
/** Refresh from models.dev unless the cache is under 24 hours old (or
|
|
134
|
+
* `force`). Concurrent calls share one request. Never throws. */
|
|
135
|
+
refresh(options = {}) {
|
|
136
|
+
if (this.fetchDisabled)
|
|
137
|
+
return Promise.resolve("disabled");
|
|
138
|
+
this.inflight ??= this.fetchAndStore(Boolean(options.force)).finally(() => {
|
|
139
|
+
this.inflight = undefined;
|
|
140
|
+
});
|
|
141
|
+
return this.inflight;
|
|
142
|
+
}
|
|
143
|
+
readDocument() {
|
|
144
|
+
if (!existsSync(this.cachePath))
|
|
145
|
+
return undefined;
|
|
146
|
+
try {
|
|
147
|
+
if (statSync(this.cachePath).size > (this.options.maxBytes ?? CATALOG_MAX_BYTES))
|
|
148
|
+
throw new Error("too large");
|
|
149
|
+
const doc = JSON.parse(readFileSync(this.cachePath, "utf8"));
|
|
150
|
+
const updatedAt = time(doc?.updatedAt);
|
|
151
|
+
if (doc?.schema !== MODEL_CATALOG_SCHEMA || updatedAt === undefined || !doc.providers)
|
|
152
|
+
throw new Error("unrecognised format");
|
|
153
|
+
return { doc, updatedAt };
|
|
154
|
+
}
|
|
155
|
+
catch (error) {
|
|
156
|
+
// As OpenCode does, drop a cache we cannot read; the next refresh rewrites it.
|
|
157
|
+
this.log(`removed an unreadable catalog cache (${message(error)})`);
|
|
158
|
+
try {
|
|
159
|
+
rmSync(this.cachePath, { force: true });
|
|
160
|
+
}
|
|
161
|
+
catch { /* best effort */ }
|
|
162
|
+
return undefined;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
ownCache() {
|
|
166
|
+
const cached = this.readDocument();
|
|
167
|
+
return cached && { origin: "cache", updatedAt: cached.updatedAt, providers: () => unpackCatalog(cached.doc.providers) };
|
|
168
|
+
}
|
|
169
|
+
openCodeCache() {
|
|
170
|
+
const path = this.options.openCodeCachePath === undefined
|
|
171
|
+
? openCodeCatalogCachePath(this.options.env ?? process.env)
|
|
172
|
+
: this.options.openCodeCachePath;
|
|
173
|
+
if (!path)
|
|
174
|
+
return undefined;
|
|
175
|
+
try {
|
|
176
|
+
const stat = statSync(path);
|
|
177
|
+
if (!stat.isFile() || stat.size > (this.options.maxBytes ?? CATALOG_MAX_BYTES))
|
|
178
|
+
return undefined;
|
|
179
|
+
// Read only if chosen: it is models.dev's full 5 MB catalog.
|
|
180
|
+
return { origin: "opencode", updatedAt: stat.mtimeMs, providers: () => {
|
|
181
|
+
try {
|
|
182
|
+
return JSON.parse(readFileSync(path, "utf8"));
|
|
183
|
+
}
|
|
184
|
+
catch {
|
|
185
|
+
return undefined;
|
|
186
|
+
}
|
|
187
|
+
} };
|
|
188
|
+
}
|
|
189
|
+
catch {
|
|
190
|
+
return undefined;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
snapshot() {
|
|
194
|
+
try {
|
|
195
|
+
const text = (this.options.readSnapshot ?? readBundledSnapshot)();
|
|
196
|
+
if (text === undefined) {
|
|
197
|
+
this.log("the bundled catalog snapshot is missing");
|
|
198
|
+
return undefined;
|
|
199
|
+
}
|
|
200
|
+
const doc = JSON.parse(text);
|
|
201
|
+
const updatedAt = time(doc?.updatedAt);
|
|
202
|
+
if (doc?.schema !== MODEL_CATALOG_SCHEMA || updatedAt === undefined)
|
|
203
|
+
throw new Error("unrecognised format");
|
|
204
|
+
return { origin: "snapshot", updatedAt, providers: () => unpackCatalog(doc.providers) };
|
|
205
|
+
}
|
|
206
|
+
catch (error) {
|
|
207
|
+
this.log(`could not read the bundled catalog snapshot (${message(error)})`);
|
|
208
|
+
return undefined;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
async fetchAndStore(force) {
|
|
212
|
+
const cached = this.readDocument();
|
|
213
|
+
const age = cached ? this.now - cached.updatedAt : Infinity;
|
|
214
|
+
// A cache stamped in the future (clock moved back) counts as stale.
|
|
215
|
+
if (!force && age >= 0 && age < CATALOG_MAX_AGE_MS)
|
|
216
|
+
return "fresh";
|
|
217
|
+
try {
|
|
218
|
+
const etag = cached ? safeEtag(cached.doc.source?.etag) : undefined;
|
|
219
|
+
const response = await this.download(etag);
|
|
220
|
+
if (response.status === 304) {
|
|
221
|
+
if (!cached)
|
|
222
|
+
throw new Error("models.dev answered 304 without a cached copy");
|
|
223
|
+
this.write({ ...cached.doc, updatedAt: new Date(this.now).toISOString() });
|
|
224
|
+
this.memo = undefined;
|
|
225
|
+
return "not-modified";
|
|
226
|
+
}
|
|
227
|
+
let raw;
|
|
228
|
+
try {
|
|
229
|
+
raw = JSON.parse(response.text);
|
|
230
|
+
}
|
|
231
|
+
catch {
|
|
232
|
+
throw new Error("models.dev returned invalid JSON");
|
|
233
|
+
}
|
|
234
|
+
let drops = 0;
|
|
235
|
+
const providers = trimModelsDevCatalog(raw, { onDrop: () => { drops += 1; } });
|
|
236
|
+
// Never replace a good cache with an empty one.
|
|
237
|
+
if (!Object.keys(providers).length)
|
|
238
|
+
throw new Error("models.dev returned no usable providers");
|
|
239
|
+
this.write({
|
|
240
|
+
schema: MODEL_CATALOG_SCHEMA,
|
|
241
|
+
updatedAt: new Date(this.now).toISOString(),
|
|
242
|
+
source: { name: "models.dev", url: MODELS_DEV_URL, license: "MIT", ...(response.etag ? { etag: response.etag } : {}) },
|
|
243
|
+
providers: packCatalog(providers),
|
|
244
|
+
});
|
|
245
|
+
this.memo = undefined;
|
|
246
|
+
const stats = catalogStats(providers);
|
|
247
|
+
this.log(`updated from models.dev: ${stats.providers} providers, ${stats.models} models` +
|
|
248
|
+
(drops ? `; left out ${drops} entries that were malformed or contained { or }` : ""));
|
|
249
|
+
return "updated";
|
|
250
|
+
}
|
|
251
|
+
catch (error) {
|
|
252
|
+
this.log(`could not refresh from models.dev (${message(error)}); keeping the current catalog`);
|
|
253
|
+
return "failed";
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
write(doc) {
|
|
257
|
+
mkdirSync(dirname(this.cachePath), { recursive: true });
|
|
258
|
+
writeFileAtomic(this.cachePath, JSON.stringify(doc));
|
|
259
|
+
}
|
|
260
|
+
async download(etag) {
|
|
261
|
+
const maxBytes = this.options.maxBytes ?? CATALOG_MAX_BYTES;
|
|
262
|
+
const controller = new AbortController();
|
|
263
|
+
const timer = setTimeout(() => controller.abort(), this.options.timeoutMs ?? CATALOG_FETCH_TIMEOUT_MS);
|
|
264
|
+
try {
|
|
265
|
+
const response = await (this.options.fetch ?? fetch)(MODELS_DEV_URL, {
|
|
266
|
+
headers: { accept: "application/json", "user-agent": "OpenMausBot", ...(etag ? { "if-none-match": etag } : {}) },
|
|
267
|
+
// Nothing secret is sent, but the catalog comes from models.dev only.
|
|
268
|
+
redirect: "error",
|
|
269
|
+
signal: controller.signal,
|
|
270
|
+
});
|
|
271
|
+
if (response.status === 304 && etag) {
|
|
272
|
+
void response.body?.cancel().catch(() => { });
|
|
273
|
+
return { status: 304 };
|
|
274
|
+
}
|
|
275
|
+
if (response.status !== 200) {
|
|
276
|
+
void response.body?.cancel().catch(() => { });
|
|
277
|
+
throw new Error(`models.dev returned HTTP ${response.status}`);
|
|
278
|
+
}
|
|
279
|
+
const declared = Number(response.headers.get("content-length"));
|
|
280
|
+
if (Number.isFinite(declared) && declared > maxBytes) {
|
|
281
|
+
void response.body?.cancel().catch(() => { });
|
|
282
|
+
throw new Error(`models.dev sent more than ${maxBytes} bytes`);
|
|
283
|
+
}
|
|
284
|
+
// The timer also covers reading the body.
|
|
285
|
+
const text = await readCapped(response, maxBytes);
|
|
286
|
+
return { status: 200, text, etag: safeEtag(response.headers.get("etag")) };
|
|
287
|
+
}
|
|
288
|
+
catch (error) {
|
|
289
|
+
if (controller.signal.aborted)
|
|
290
|
+
throw new Error("models.dev did not answer in time");
|
|
291
|
+
throw error;
|
|
292
|
+
}
|
|
293
|
+
finally {
|
|
294
|
+
clearTimeout(timer);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
async function readCapped(response, maxBytes) {
|
|
299
|
+
if (!response.body)
|
|
300
|
+
return "";
|
|
301
|
+
const reader = response.body.getReader();
|
|
302
|
+
const decoder = new TextDecoder();
|
|
303
|
+
let total = 0;
|
|
304
|
+
let text = "";
|
|
305
|
+
for (;;) {
|
|
306
|
+
const { done, value } = await reader.read();
|
|
307
|
+
if (done)
|
|
308
|
+
break;
|
|
309
|
+
total += value.byteLength;
|
|
310
|
+
if (total > maxBytes) {
|
|
311
|
+
await reader.cancel().catch(() => { });
|
|
312
|
+
throw new Error(`models.dev sent more than ${maxBytes} bytes`);
|
|
313
|
+
}
|
|
314
|
+
text += decoder.decode(value, { stream: true });
|
|
315
|
+
}
|
|
316
|
+
return text + decoder.decode();
|
|
317
|
+
}
|
|
318
|
+
/** The SDK package for a preset's pinned `api`. models.dev's `npm` describes
|
|
319
|
+
* models.dev's `api` (or, when it lists none, the SDK's own default address,
|
|
320
|
+
* which the preset pins too), so it is kept only when that is the address the
|
|
321
|
+
* preset pins. Otherwise the pinned address is OpenAI-compatible by definition. */
|
|
322
|
+
function presetNpm(preset, provider) {
|
|
323
|
+
if (preset.npm)
|
|
324
|
+
return preset.npm;
|
|
325
|
+
if (!provider?.npm)
|
|
326
|
+
return undefined;
|
|
327
|
+
return provider.api === undefined || provider.api === preset.api ? provider.npm : OPENAI_COMPATIBLE_NPM;
|
|
328
|
+
}
|
|
329
|
+
/** The catalog merged with the presets: recommended rows first, in their
|
|
330
|
+
* order, then every other provider by name. Presets whose provider the
|
|
331
|
+
* catalog lacks (or dropped) are still listed. */
|
|
332
|
+
export function catalogEntries(catalog, presets = PROVIDER_PRESETS) {
|
|
333
|
+
const entries = [];
|
|
334
|
+
const covered = new Set();
|
|
335
|
+
for (const preset of presets) {
|
|
336
|
+
const provider = preset.catalogId ? catalog.providers[preset.catalogId] : undefined;
|
|
337
|
+
if (provider)
|
|
338
|
+
covered.add(provider.id);
|
|
339
|
+
const entry = {
|
|
340
|
+
id: preset.id,
|
|
341
|
+
label: preset.label,
|
|
342
|
+
env: provider?.env ?? (preset.env ? [preset.env] : []),
|
|
343
|
+
api: preset.api,
|
|
344
|
+
modelCount: provider ? Object.keys(provider.models).length : 0,
|
|
345
|
+
preset,
|
|
346
|
+
};
|
|
347
|
+
if (provider)
|
|
348
|
+
entry.catalogId = provider.id;
|
|
349
|
+
const npm = presetNpm(preset, provider);
|
|
350
|
+
if (npm)
|
|
351
|
+
entry.npm = npm;
|
|
352
|
+
const doc = preset.doc ?? provider?.doc;
|
|
353
|
+
if (doc)
|
|
354
|
+
entry.doc = doc;
|
|
355
|
+
if (preset.recommended !== undefined)
|
|
356
|
+
entry.recommended = preset.recommended;
|
|
357
|
+
entries.push(entry);
|
|
358
|
+
}
|
|
359
|
+
for (const provider of Object.values(catalog.providers)) {
|
|
360
|
+
if (covered.has(provider.id) || presets.some((preset) => preset.id === provider.id))
|
|
361
|
+
continue;
|
|
362
|
+
const entry = { id: provider.id, catalogId: provider.id, label: provider.name, env: provider.env, modelCount: Object.keys(provider.models).length };
|
|
363
|
+
if (provider.npm)
|
|
364
|
+
entry.npm = provider.npm;
|
|
365
|
+
if (provider.api)
|
|
366
|
+
entry.api = provider.api;
|
|
367
|
+
if (provider.doc)
|
|
368
|
+
entry.doc = provider.doc;
|
|
369
|
+
entries.push(entry);
|
|
370
|
+
}
|
|
371
|
+
return entries.sort((a, b) => (a.recommended ?? Infinity) - (b.recommended ?? Infinity) ||
|
|
372
|
+
a.label.localeCompare(b.label, "en", { sensitivity: "base" }) ||
|
|
373
|
+
a.id.localeCompare(b.id));
|
|
374
|
+
}
|