@cortexkit/opencode-magic-context 0.48.0 → 0.49.0
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/auto-search-worker.js +3 -3
- package/dist/{conflict-warning-hook-w8cykrnf.js → conflict-warning-hook-kzx4dmk1.js} +7 -7
- package/dist/{context-m0cy262m.js → context-nj5f293n.js} +96 -43
- package/dist/{derive-budgets-sg0xsq7s.js → derive-budgets-qz5rrr08.js} +2 -2
- package/dist/embedding-backfill-aetxv1vt.js +21 -0
- package/dist/embedding-worker.js +4 -4
- package/dist/features/magic-context/git-commits/search-git-commits.d.ts +2 -0
- package/dist/features/magic-context/git-commits/search-git-commits.d.ts.map +1 -1
- package/dist/features/magic-context/message-index.d.ts.map +1 -1
- package/dist/features/magic-context/pi-content-decisions.d.ts +1 -1
- package/dist/features/magic-context/pi-content-decisions.d.ts.map +1 -1
- package/dist/features/magic-context/search-rerank.d.ts +35 -0
- package/dist/features/magic-context/search-rerank.d.ts.map +1 -0
- package/dist/features/magic-context/search.d.ts +11 -0
- package/dist/features/magic-context/search.d.ts.map +1 -1
- package/dist/features/magic-context/storage-meta-persisted.d.ts +13 -2
- package/dist/features/magic-context/storage-meta-persisted.d.ts.map +1 -1
- package/dist/features/magic-context/storage-meta-session.d.ts +1 -1
- package/dist/features/magic-context/storage-meta-session.d.ts.map +1 -1
- package/dist/features/magic-context/storage-meta.d.ts +1 -1
- package/dist/features/magic-context/storage-meta.d.ts.map +1 -1
- package/dist/features/magic-context/storage-session-tables.d.ts +2 -0
- package/dist/features/magic-context/storage-session-tables.d.ts.map +1 -1
- package/dist/features/magic-context/storage-tag-identity-revision.d.ts +29 -9
- package/dist/features/magic-context/storage-tag-identity-revision.d.ts.map +1 -1
- package/dist/features/magic-context/storage-tags.d.ts +9 -0
- package/dist/features/magic-context/storage-tags.d.ts.map +1 -1
- package/dist/features/magic-context/storage.d.ts +1 -1
- package/dist/features/magic-context/storage.d.ts.map +1 -1
- package/dist/features/magic-context/tagger.d.ts +2 -0
- package/dist/features/magic-context/tagger.d.ts.map +1 -1
- package/dist/features/magic-context/transform-decision-log.d.ts +3 -0
- package/dist/features/magic-context/transform-decision-log.d.ts.map +1 -1
- package/dist/hooks/magic-context/auto-search-deadline.d.ts.map +1 -1
- package/dist/hooks/magic-context/auto-search-replay.fixture.d.ts +24 -0
- package/dist/hooks/magic-context/auto-search-replay.fixture.d.ts.map +1 -0
- package/dist/hooks/magic-context/auto-search-runner.d.ts +1 -1
- package/dist/hooks/magic-context/auto-search-runner.d.ts.map +1 -1
- package/dist/hooks/magic-context/auto-search-worker-client.d.ts +2 -0
- package/dist/hooks/magic-context/auto-search-worker-client.d.ts.map +1 -1
- package/dist/hooks/magic-context/command-handler.d.ts +2 -0
- package/dist/hooks/magic-context/command-handler.d.ts.map +1 -1
- package/dist/hooks/magic-context/emergency-fail-closed.d.ts +28 -0
- package/dist/hooks/magic-context/emergency-fail-closed.d.ts.map +1 -1
- package/dist/hooks/magic-context/event-handler.d.ts.map +1 -1
- package/dist/hooks/magic-context/historian-host-runner.d.ts +8 -0
- package/dist/hooks/magic-context/historian-host-runner.d.ts.map +1 -1
- package/dist/hooks/magic-context/hook.d.ts +2 -1
- package/dist/hooks/magic-context/hook.d.ts.map +1 -1
- package/dist/hooks/magic-context/inject-compartments.d.ts.map +1 -1
- package/dist/hooks/magic-context/latest-thinking-recovery.d.ts +7 -0
- package/dist/hooks/magic-context/latest-thinking-recovery.d.ts.map +1 -1
- package/dist/hooks/magic-context/lkg-slot.d.ts.map +1 -1
- package/dist/hooks/magic-context/pass-outcome.d.ts +1 -0
- package/dist/hooks/magic-context/pass-outcome.d.ts.map +1 -1
- package/dist/hooks/magic-context/read-session-chunk.d.ts +7 -1
- package/dist/hooks/magic-context/read-session-chunk.d.ts.map +1 -1
- package/dist/hooks/magic-context/read-session-raw.d.ts +35 -0
- package/dist/hooks/magic-context/read-session-raw.d.ts.map +1 -1
- package/dist/hooks/magic-context/rust-mode-transform.d.ts +36 -0
- package/dist/hooks/magic-context/rust-mode-transform.d.ts.map +1 -1
- package/dist/hooks/magic-context/shared-compartment-boundaries.d.ts +8 -1
- package/dist/hooks/magic-context/shared-compartment-boundaries.d.ts.map +1 -1
- package/dist/hooks/magic-context/strict-thinking-prefix.d.ts +8 -0
- package/dist/hooks/magic-context/strict-thinking-prefix.d.ts.map +1 -0
- package/dist/hooks/magic-context/tag-messages.d.ts.map +1 -1
- package/dist/hooks/magic-context/transform-postprocess-phase.d.ts +13 -0
- package/dist/hooks/magic-context/transform-postprocess-phase.d.ts.map +1 -1
- package/dist/hooks/magic-context/transform.d.ts +1 -0
- package/dist/hooks/magic-context/transform.d.ts.map +1 -1
- package/dist/{index-db7zwa3v.js → index-1bt88epp.js} +93 -9
- package/dist/{index-f0h6xwqm.js → index-1g30axtz.js} +229 -67
- package/dist/{index-k1xb52kd.js → index-1r0k9js3.js} +5 -5
- package/dist/{index-692jzy0p.js → index-2jcbj7r1.js} +122 -371
- package/dist/{index-bdckkfw6.js → index-2jgwttpt.js} +16 -8
- package/dist/{index-2y0s2hep.js → index-4mjhqadz.js} +1 -1
- package/dist/{index-pdecwzrd.js → index-57mrqed8.js} +1 -1
- package/dist/{index-ty64eqcn.js → index-9qjqq45k.js} +3 -3
- package/dist/{index-yq8n9qcb.js → index-9rt53q79.js} +4 -4
- package/dist/{index-pg50n748.js → index-b7mv1jx3.js} +4 -4
- package/dist/{index-mmyh6d8f.js → index-ezmszwbd.js} +6 -4
- package/dist/{index-fbv1dn35.js → index-gzcmtmar.js} +1 -1
- package/dist/{index-0an7742d.js → index-h2py7207.js} +549 -973
- package/dist/{index-0qhce371.js → index-h2wc01qk.js} +2 -2
- package/dist/{index-dhwzyd54.js → index-h5zfkjyk.js} +160 -25
- package/dist/{index-q2tqeqde.js → index-ha87j7a8.js} +1229 -78
- package/dist/{index-14cjxt20.js → index-hj2q50k0.js} +45 -8
- package/dist/{index-cb7emxgj.js → index-kmpvemph.js} +336 -6
- package/dist/{index-c43kr12f.js → index-kp5qe690.js} +7 -7
- package/dist/{index-bhvjj0ys.js → index-kq57tdf0.js} +2 -2
- package/dist/{index-7mx8v0pw.js → index-nm6a8xpd.js} +4 -4
- package/dist/{index-8j7gqj40.js → index-nsjyysj0.js} +6 -2
- package/dist/{index-k9nnwad5.js → index-nydv6g8r.js} +44 -7
- package/dist/{index-5zygecde.js → index-peshmj6h.js} +4 -4
- package/dist/{index-knh4y330.js → index-pxwp2hjk.js} +7 -2
- package/dist/{index-1qb0z7wa.js → index-qx53vezw.js} +1 -1
- package/dist/{index-q7h7csab.js → index-s4j7t92z.js} +9 -9
- package/dist/{index-z8e0psj8.js → index-stbr4ce6.js} +3 -3
- package/dist/{index-jcbd3znx.js → index-tqf7yg27.js} +1 -1
- package/dist/{index-72nhv486.js → index-vkbqdnkj.js} +2 -2
- package/dist/{index-eeezr5zz.js → index-z1tjp257.js} +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +61 -44
- package/dist/{logger-d3wex3dz.js → logger-ewak5q1s.js} +1 -1
- package/dist/{message-index-bounded-7ezbgddp.js → message-index-bounded-h0mrd4pr.js} +13 -13
- package/dist/{message-time-backfill-brbd63hv.js → message-time-backfill-8x06r9kx.js} +2 -2
- package/dist/migration-worker.js +9 -9
- package/dist/plugin/hooks/create-session-hooks.d.ts +2 -0
- package/dist/plugin/hooks/create-session-hooks.d.ts.map +1 -1
- package/dist/plugin/tool-registry.d.ts.map +1 -1
- package/dist/{project-embedding-registry-7qzptcw7.js → project-embedding-registry-rtt0djyb.js} +9 -9
- package/dist/raw-ordinal-warmup-worker.js +4 -4
- package/dist/{read-session-chunk-6k83m4fj.js → read-session-chunk-ty89v75e.js} +10 -8
- package/dist/{read-session-raw-3g9qy1xa.js → read-session-raw-8sjbc9h0.js} +5 -3
- package/dist/{recomp-orchestrator-d3fp1cy0.js → recomp-orchestrator-4f1yyd1q.js} +25 -25
- package/dist/{safe-notification-target-5ffqq32m.js → safe-notification-target-q0cyrx3x.js} +2 -2
- package/dist/{search-epax1ecv.js → search-pz4f4wge.js} +20 -20
- package/dist/{send-session-notification-erecz4y4.js → send-session-notification-38cd5dqt.js} +5 -5
- package/dist/shared/auto-search-lifecycle.d.ts +14 -0
- package/dist/shared/auto-search-lifecycle.d.ts.map +1 -1
- package/dist/shared/logger.d.ts.map +1 -1
- package/dist/shared/models-dev-temperature.d.ts +18 -0
- package/dist/shared/models-dev-temperature.d.ts.map +1 -0
- package/dist/shared/project-directory-key.d.ts +16 -0
- package/dist/shared/project-directory-key.d.ts.map +1 -1
- package/dist/shared/subagent-temperature.d.ts +46 -0
- package/dist/shared/subagent-temperature.d.ts.map +1 -0
- package/dist/{storage-tags-d3b0v2ss.js → storage-tags-k0pda32s.js} +6 -4
- package/dist/tools/ctx-reduce/tools.d.ts.map +1 -1
- package/dist/tools/ctx-search/tools.d.ts.map +1 -1
- package/dist/tools/ctx-search/types.d.ts +1 -0
- package/dist/tools/ctx-search/types.d.ts.map +1 -1
- package/dist/{transform-postprocess-phase-spcy2qre.js → transform-postprocess-phase-tck4g20s.js} +25 -25
- package/dist/v2/auto-search-worker.js +3 -3
- package/dist/v2/{context-sf98kstd.js → context-crbt6p1y.js} +809 -1052
- package/dist/v2/{derive-budgets-3vc0y84t.js → derive-budgets-fgej9j46.js} +3 -3
- package/dist/v2/embedding-backfill-ph1qfyn3.js +21 -0
- package/dist/v2/embedding-worker.js +4 -4
- package/dist/v2/hidden-child-native.d.ts.map +1 -1
- package/dist/v2/hidden-completion.d.ts +7 -0
- package/dist/v2/hidden-completion.d.ts.map +1 -1
- package/dist/v2/hooks/context.d.ts.map +1 -1
- package/dist/v2/hooks/dream-trigger.d.ts +4 -1
- package/dist/v2/hooks/dream-trigger.d.ts.map +1 -1
- package/dist/v2/hooks/generate.d.ts +9 -2
- package/dist/v2/hooks/generate.d.ts.map +1 -1
- package/dist/v2/{logger-mxd0ym09.js → logger-pj02s7sr.js} +1 -1
- package/dist/v2/{message-index-bounded-dy3s2k1e.js → message-index-bounded-xwqdywxg.js} +13 -13
- package/dist/v2/{message-time-backfill-gtx1pwk1.js → message-time-backfill-cje68egj.js} +2 -2
- package/dist/v2/migration-worker.js +9 -9
- package/dist/v2/{project-embedding-registry-1tgba5dv.js → project-embedding-registry-d6m7tyxr.js} +9 -9
- package/dist/v2/raw-ordinal-warmup-worker.js +4 -4
- package/dist/v2/{read-session-chunk-rh4khdkv.js → read-session-chunk-8p5mdryx.js} +10 -8
- package/dist/v2/{read-session-raw-bsbmf0vj.js → read-session-raw-j8dvp7f4.js} +5 -3
- package/dist/v2/{recomp-orchestrator-az61rfcg.js → recomp-orchestrator-ckfgt6wq.js} +26 -26
- package/dist/v2/{safe-notification-target-73ax1p5v.js → safe-notification-target-y3p1hce9.js} +2 -2
- package/dist/v2/{search-nzkwrc6n.js → search-ym8rehhz.js} +20 -20
- package/dist/v2/{send-session-notification-6km439kn.js → send-session-notification-5m0yapqx.js} +5 -5
- package/dist/v2/{server-x2g40a71.js → server-19zjmn02.js} +2 -2
- package/dist/v2/{server-sggvvzhb.js → server-1tk82krw.js} +45 -8
- package/dist/v2/{server-vqpf3167.js → server-2s6ayq3t.js} +2 -2
- package/dist/v2/{server-z2037m6z.js → server-2s9qz6n7.js} +2 -2
- package/dist/v2/{server-k228xhjg.js → server-3j4d00kf.js} +1 -1
- package/dist/v2/{server-aan4q8mx.js → server-3t0q2cqx.js} +1 -1
- package/dist/v2/{server-2fbhp79z.js → server-4ez8mntn.js} +16 -8
- package/dist/v2/{server-zxhgvsf9.js → server-676xw7at.js} +1 -1
- package/dist/v2/{server-52awr2mw.js → server-6byqfbqf.js} +93 -9
- package/dist/v2/{server-qye6ykf7.js → server-7ekna9zs.js} +2 -2
- package/dist/v2/{server-g5r0n3b6.js → server-7h861yms.js} +160 -25
- package/dist/v2/{server-v06jcv0w.js → server-8k2agx29.js} +1 -1
- package/dist/v2/{server-686jg6eb.js → server-c5rrgrcd.js} +44 -7
- package/dist/v2/{server-4k748ke6.js → server-hb6xhh9q.js} +2 -2
- package/dist/v2/{server-7wwc2nga.js → server-jvxzm1jy.js} +7 -7
- package/dist/v2/{server-1r6pmz4f.js → server-kzns2hbz.js} +1 -1
- package/dist/v2/{server-xdfwt6kh.js → server-pj4pjv02.js} +2 -2
- package/dist/v2/{server-psqc7e9n.js → server-q4ph9bwn.js} +1227 -78
- package/dist/v2/{server-8pyt7hev.js → server-sa8jvrfc.js} +9 -9
- package/dist/v2/{server-ezhze64f.js → server-ss95ky04.js} +4 -4
- package/dist/v2/{server-5m3dt5r1.js → server-t4k4qexw.js} +6 -2
- package/dist/v2/{server-e0vdaae2.js → server-v4mewb78.js} +4 -4
- package/dist/v2/{server-gjcr4w98.js → server-w32kmdez.js} +4 -4
- package/dist/v2/{server-k8x2rbxc.js → server-x4hscabv.js} +119 -365
- package/dist/v2/{server-yczc713m.js → server-xwz4dqh9.js} +7 -2
- package/dist/v2/{server-xxm59eww.js → server-ycw9qqcy.js} +4 -4
- package/dist/v2/{server-8tqanswc.js → server-zp53y8js.js} +331 -4
- package/dist/v2/server.js +4 -4
- package/dist/v2/{storage-tags-w4tv77sn.js → storage-tags-8vw924zn.js} +6 -4
- package/dist/v2/store-reader.d.ts +8 -2
- package/dist/v2/store-reader.d.ts.map +1 -1
- package/dist/v2/{wrapup-orchestrator-mz7efs7w.js → wrapup-orchestrator-a7efxaax.js} +26 -26
- package/dist/wrapup-orchestrator-barbc6h9.js +44 -0
- package/package.json +1 -1
- package/src/shared/auto-search-lifecycle.ts +43 -0
- package/src/shared/logger.test.ts +38 -0
- package/src/shared/logger.ts +10 -2
- package/src/shared/models-dev-temperature.ts +107 -0
- package/src/shared/project-directory-key.test.ts +59 -0
- package/src/shared/project-directory-key.ts +20 -0
- package/src/shared/subagent-temperature.test.ts +270 -0
- package/src/shared/subagent-temperature.ts +110 -0
- package/dist/embedding-backfill-9rcm1mfy.js +0 -21
- package/dist/v2/embedding-backfill-vn8g1nh1.js +0 -21
- package/dist/wrapup-orchestrator-amzta1t4.js +0 -44
|
@@ -1,4 +1,47 @@
|
|
|
1
|
+
import type { AutoSearchHintNoHintReason } from "../features/magic-context/storage-meta-persisted";
|
|
2
|
+
|
|
1
3
|
const pending = new Map<string, Set<AbortController>>();
|
|
4
|
+
const unpublishedNoHints = new Map<string, Map<string, AutoSearchHintNoHintReason>>();
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* A failed save can still send the user message without a search hint. Remember
|
|
8
|
+
* that no-hint decision until a later pass saves it to SQLite, including across
|
|
9
|
+
* closing and reopening the database handle. Switching or shutting down a session
|
|
10
|
+
* does not prove deletion and must not discard it. If all compare-and-swap (CAS)
|
|
11
|
+
* save retries fail and the process restarts before repair, this unsaved record
|
|
12
|
+
* is lost: that compound failure is the remaining durability gap.
|
|
13
|
+
*/
|
|
14
|
+
export function rememberUnpublishedAutoSearchNoHint(
|
|
15
|
+
sessionId: string,
|
|
16
|
+
messageId: string,
|
|
17
|
+
reason: AutoSearchHintNoHintReason,
|
|
18
|
+
): void {
|
|
19
|
+
let messages = unpublishedNoHints.get(sessionId);
|
|
20
|
+
if (!messages) {
|
|
21
|
+
messages = new Map();
|
|
22
|
+
unpublishedNoHints.set(sessionId, messages);
|
|
23
|
+
}
|
|
24
|
+
if (!messages.has(messageId)) messages.set(messageId, reason);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function getUnpublishedAutoSearchNoHint(
|
|
28
|
+
sessionId: string,
|
|
29
|
+
messageId: string,
|
|
30
|
+
): AutoSearchHintNoHintReason | undefined {
|
|
31
|
+
return unpublishedNoHints.get(sessionId)?.get(messageId);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function forgetUnpublishedAutoSearchNoHint(sessionId: string, messageId: string): void {
|
|
35
|
+
const messages = unpublishedNoHints.get(sessionId);
|
|
36
|
+
messages?.delete(messageId);
|
|
37
|
+
if (messages?.size === 0) unpublishedNoHints.delete(sessionId);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Deleting a session releases its unsaved no-hint records; switching away does not. */
|
|
41
|
+
export function clearUnpublishedAutoSearchNoHints(sessionId?: string): void {
|
|
42
|
+
if (sessionId === undefined) unpublishedNoHints.clear();
|
|
43
|
+
else unpublishedNoHints.delete(sessionId);
|
|
44
|
+
}
|
|
2
45
|
|
|
3
46
|
/** Cleanup cancels optional searches and backfill, not durable served decisions. */
|
|
4
47
|
export function registerAutoSearchWork(sessionId: string): {
|
|
@@ -129,6 +129,28 @@ if (scenario === "retry") {
|
|
|
129
129
|
predecessorMode: fs.statSync(predecessorPath).mode & 0o777,
|
|
130
130
|
files: fs.readdirSync(path.dirname(logPath)).filter((name) => name.startsWith("magic-context.log")).sort(),
|
|
131
131
|
}));
|
|
132
|
+
} else if (scenario === "stale-rotation") {
|
|
133
|
+
const maxLogBytes = 32 * 1024 * 1024;
|
|
134
|
+
fs.mkdirSync(path.dirname(logPath), { recursive: true });
|
|
135
|
+
fs.writeFileSync(logPath, Buffer.alloc(maxLogBytes - 200, "o"));
|
|
136
|
+
fs.chmodSync(logPath, 0o600);
|
|
137
|
+
// This process now caches a size just under the bound.
|
|
138
|
+
logger.log("cached size");
|
|
139
|
+
logger.flushLogger();
|
|
140
|
+
|
|
141
|
+
// Another process sharing the file rotates it: the full log becomes the
|
|
142
|
+
// predecessor and the current file starts over.
|
|
143
|
+
fs.copyFileSync(logPath, logPath + ".1");
|
|
144
|
+
fs.writeFileSync(logPath, "written by the other process\\n");
|
|
145
|
+
|
|
146
|
+
// A line that crosses the bound only by this process's stale cached size.
|
|
147
|
+
logger.log("x".repeat(400));
|
|
148
|
+
logger.flushLogger();
|
|
149
|
+
console.log(JSON.stringify({
|
|
150
|
+
current: fs.readFileSync(logPath, "utf8"),
|
|
151
|
+
predecessorFirstByte: fs.readFileSync(logPath + ".1")[0],
|
|
152
|
+
predecessorSize: fs.statSync(logPath + ".1").size,
|
|
153
|
+
}));
|
|
132
154
|
} else if (scenario === "hot-path") {
|
|
133
155
|
for (let index = 0; index < 20; index++) {
|
|
134
156
|
logger.log("hot-path-" + index);
|
|
@@ -182,6 +204,7 @@ async function runLoggerScenario<T>(
|
|
|
182
204
|
| "recovery"
|
|
183
205
|
| "diagnostics"
|
|
184
206
|
| "rotation"
|
|
207
|
+
| "stale-rotation"
|
|
185
208
|
| "hot-path"
|
|
186
209
|
| "retry"
|
|
187
210
|
| "bounded"
|
|
@@ -269,6 +292,21 @@ describe("logger", () => {
|
|
|
269
292
|
}
|
|
270
293
|
});
|
|
271
294
|
|
|
295
|
+
test("a stale cached size does not rotate a file another process already rotated", async () => {
|
|
296
|
+
const result = await runLoggerScenario<{
|
|
297
|
+
current: string;
|
|
298
|
+
predecessorFirstByte: number;
|
|
299
|
+
predecessorSize: number;
|
|
300
|
+
}>("stale-rotation");
|
|
301
|
+
|
|
302
|
+
// The other process's full predecessor survives, and the new line joins
|
|
303
|
+
// the restarted current file instead of replacing that predecessor.
|
|
304
|
+
expect(result.predecessorFirstByte).toBe("o".charCodeAt(0));
|
|
305
|
+
expect(result.predecessorSize).toBeGreaterThan(32 * 1024 * 1024 - 1024);
|
|
306
|
+
expect(result.current).toContain("written by the other process");
|
|
307
|
+
expect(result.current).toContain("x".repeat(400));
|
|
308
|
+
});
|
|
309
|
+
|
|
272
310
|
test("uses cached size between periodic rotation checks", async () => {
|
|
273
311
|
const result = await runLoggerScenario<HotPathScenarioResult>("hot-path");
|
|
274
312
|
|
package/src/shared/logger.ts
CHANGED
|
@@ -163,8 +163,16 @@ function flush(): void {
|
|
|
163
163
|
let currentSize = getCurrentLogSize(logFile);
|
|
164
164
|
const dataSize = Buffer.byteLength(data);
|
|
165
165
|
if (currentSize > 0 && currentSize + dataSize > MAX_LOG_FILE_BYTES) {
|
|
166
|
-
|
|
167
|
-
|
|
166
|
+
// Several processes share this file and each caches its size. A stale
|
|
167
|
+
// cache must not rotate a file another process already rotated: that
|
|
168
|
+
// second rotation would overwrite the full predecessor with the few
|
|
169
|
+
// lines written since. Decide on the file's real size instead.
|
|
170
|
+
flushesSinceSizeCheck = SIZE_CHECK_INTERVAL_FLUSHES;
|
|
171
|
+
currentSize = getCurrentLogSize(logFile);
|
|
172
|
+
if (currentSize > 0 && currentSize + dataSize > MAX_LOG_FILE_BYTES) {
|
|
173
|
+
rotateLogFile(logFile, currentSize);
|
|
174
|
+
currentSize = 0;
|
|
175
|
+
}
|
|
168
176
|
}
|
|
169
177
|
fs.appendFileSync(logFile, data, { encoding: "utf8", mode: 0o600 });
|
|
170
178
|
buffer = [];
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Temperature capability from the models.dev catalog that OpenCode caches on disk.
|
|
3
|
+
*
|
|
4
|
+
* OpenCode 1 copies models.dev's `temperature` flag onto each model, but OpenCode 2's
|
|
5
|
+
* model rows, Pi's model registry and Oh My Pi's Gemini models carry no equivalent
|
|
6
|
+
* field. For those hosts the cached catalog is the only positive evidence on the
|
|
7
|
+
* machine, so a missing file, an unparsable file or a missing entry all mean "unknown"
|
|
8
|
+
* and the caller omits temperature.
|
|
9
|
+
*
|
|
10
|
+
* This module is bundled into the Pi/OMP historian child extension, so it must only
|
|
11
|
+
* depend on Node built-ins.
|
|
12
|
+
*/
|
|
13
|
+
import { readFileSync, statSync } from "node:fs";
|
|
14
|
+
import { homedir } from "node:os";
|
|
15
|
+
import { join } from "node:path";
|
|
16
|
+
|
|
17
|
+
export interface CatalogTemperature {
|
|
18
|
+
temperature: boolean;
|
|
19
|
+
/** The catalog entry that decided, as `provider/model`. */
|
|
20
|
+
entry: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Harness provider ids that name the same models.dev provider. Codex (`openai-codex`)
|
|
25
|
+
* is deliberately absent: the ChatGPT backend is a different transport from the
|
|
26
|
+
* OpenAI API that models.dev describes, so it gets no catalog answer and no temperature.
|
|
27
|
+
*/
|
|
28
|
+
const CATALOG_PROVIDER_ALIASES: Readonly<Record<string, string>> = {
|
|
29
|
+
"google-antigravity": "google",
|
|
30
|
+
"opencode-zen": "opencode",
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
/** The Antigravity auth plugins register Gemini models as `google/antigravity-<model>`. */
|
|
34
|
+
const ANTIGRAVITY_MODEL_PREFIX = "antigravity-";
|
|
35
|
+
|
|
36
|
+
type CatalogIndex = Map<string, boolean>;
|
|
37
|
+
|
|
38
|
+
let cached: { path: string; mtimeMs: number; size: number; index: CatalogIndex } | undefined;
|
|
39
|
+
|
|
40
|
+
/** OpenCode reads `OPENCODE_MODELS_PATH` instead of its cache when it is set. */
|
|
41
|
+
export function modelsDevCatalogPath(env: NodeJS.ProcessEnv = process.env): string {
|
|
42
|
+
if (env.OPENCODE_MODELS_PATH) return env.OPENCODE_MODELS_PATH;
|
|
43
|
+
return join(env.XDG_CACHE_HOME || join(homedir(), ".cache"), "opencode", "models.json");
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function buildIndex(raw: unknown): CatalogIndex {
|
|
47
|
+
const index: CatalogIndex = new Map();
|
|
48
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return index;
|
|
49
|
+
for (const [providerID, provider] of Object.entries(raw as Record<string, unknown>)) {
|
|
50
|
+
const models = (provider as { models?: unknown } | null)?.models;
|
|
51
|
+
if (!models || typeof models !== "object") continue;
|
|
52
|
+
for (const [modelID, model] of Object.entries(models as Record<string, unknown>)) {
|
|
53
|
+
const temperature = (model as { temperature?: unknown } | null)?.temperature;
|
|
54
|
+
if (typeof temperature === "boolean")
|
|
55
|
+
index.set(`${providerID}/${modelID}`, temperature);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return index;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function loadIndex(): CatalogIndex | undefined {
|
|
62
|
+
const path = modelsDevCatalogPath();
|
|
63
|
+
try {
|
|
64
|
+
const stat = statSync(path);
|
|
65
|
+
if (cached?.path === path && cached.mtimeMs === stat.mtimeMs && cached.size === stat.size)
|
|
66
|
+
return cached.index;
|
|
67
|
+
const index = buildIndex(JSON.parse(readFileSync(path, "utf8")));
|
|
68
|
+
cached = { path, mtimeMs: stat.mtimeMs, size: stat.size, index };
|
|
69
|
+
return index;
|
|
70
|
+
} catch {
|
|
71
|
+
// Absent or partially written catalog: no evidence either way.
|
|
72
|
+
return undefined;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** Spellings to try for a harness `provider/model` reference, most specific first. */
|
|
77
|
+
export function catalogCandidates(providerID: string, modelID: string): string[] {
|
|
78
|
+
const provider = Object.hasOwn(CATALOG_PROVIDER_ALIASES, providerID)
|
|
79
|
+
? CATALOG_PROVIDER_ALIASES[providerID]
|
|
80
|
+
: providerID;
|
|
81
|
+
const candidates = [`${provider}/${modelID}`];
|
|
82
|
+
if (provider === "google" && modelID.startsWith(ANTIGRAVITY_MODEL_PREFIX))
|
|
83
|
+
candidates.push(`google/${modelID.slice(ANTIGRAVITY_MODEL_PREFIX.length)}`);
|
|
84
|
+
return candidates;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Look `provider/model` up in the cached catalogue, trying only the exact spellings from
|
|
89
|
+
* `catalogCandidates`. A malformed reference, a missing catalogue or no entry returns undefined.
|
|
90
|
+
*/
|
|
91
|
+
export function catalogTemperature(model: string): CatalogTemperature | undefined {
|
|
92
|
+
const slash = model.indexOf("/");
|
|
93
|
+
if (slash <= 0 || slash === model.length - 1) return undefined;
|
|
94
|
+
const index = loadIndex();
|
|
95
|
+
if (!index) return undefined;
|
|
96
|
+
for (const entry of catalogCandidates(model.slice(0, slash), model.slice(slash + 1))) {
|
|
97
|
+
const temperature = index.get(entry);
|
|
98
|
+
if (temperature !== undefined) return { temperature, entry };
|
|
99
|
+
}
|
|
100
|
+
return undefined;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export const __catalogTest = {
|
|
104
|
+
reset(): void {
|
|
105
|
+
cached = undefined;
|
|
106
|
+
},
|
|
107
|
+
};
|
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
__setCanonicalDirectoryRealpathForTests,
|
|
14
14
|
canonicalProjectDirectory,
|
|
15
15
|
projectDirectoryKey,
|
|
16
|
+
sameProjectDirectory,
|
|
16
17
|
} from "./project-directory-key";
|
|
17
18
|
import { projectHash, rpcPortDirsForLookup } from "./rpc-utils";
|
|
18
19
|
import { createTestTempDirFromPath } from "./test-temp-dir";
|
|
@@ -109,3 +110,61 @@ describe("one discovery directory per project directory", () => {
|
|
|
109
110
|
expect(dirs[0]).toBe(join("/storage", "rpc", projectHash("C:\\Users\\A\\Proj\\")));
|
|
110
111
|
});
|
|
111
112
|
});
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Background-session code compares a session's recorded directory with the
|
|
116
|
+
* plugin's own. OpenCode 2 on Windows records `D:/coding/app` while the plugin
|
|
117
|
+
* knows `D:\coding\app`; both must count as one directory, and a different
|
|
118
|
+
* directory must never be taken for this one.
|
|
119
|
+
*/
|
|
120
|
+
describe("sameProjectDirectory", () => {
|
|
121
|
+
for (const [name, left, right] of WINDOWS_PAIRS) {
|
|
122
|
+
test(`Windows ${name} is the same directory`, () => {
|
|
123
|
+
expect(sameProjectDirectory(left, right)).toBe(true);
|
|
124
|
+
expect(sameProjectDirectory(right, left)).toBe(true);
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
test("Windows forward slashes against backslashes, as OpenCode 2 stores them", () => {
|
|
129
|
+
expect(
|
|
130
|
+
sameProjectDirectory(
|
|
131
|
+
"D:/coding/siac/wt/migracion-asincrona",
|
|
132
|
+
"D:\\coding\\siac\\wt\\migracion-asincrona",
|
|
133
|
+
),
|
|
134
|
+
).toBe(true);
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
test("Windows `.` and `..` segments and a trailing forward slash", () => {
|
|
138
|
+
expect(
|
|
139
|
+
sameProjectDirectory("C:\\Users\\dev\\.\\work\\tmp\\..\\app", "C:/Users/dev/work/app/"),
|
|
140
|
+
).toBe(true);
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
test("POSIX trailing slash and `..` segments", () => {
|
|
144
|
+
expect(sameProjectDirectory("/work/app/", "/work/app")).toBe(true);
|
|
145
|
+
expect(sameProjectDirectory("/work/tmp/../app", "/work/app")).toBe(true);
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
test("different directories are refused", () => {
|
|
149
|
+
expect(
|
|
150
|
+
sameProjectDirectory(
|
|
151
|
+
"D:/coding/siac/wt/notificaciones-pui",
|
|
152
|
+
"D:\\coding\\siac\\wt\\migracion-asincrona",
|
|
153
|
+
),
|
|
154
|
+
).toBe(false);
|
|
155
|
+
// A sibling whose name extends this one, a parent, and another drive.
|
|
156
|
+
expect(sameProjectDirectory("C:/Users/dev/app-worktree", "C:\\Users\\dev\\app")).toBe(
|
|
157
|
+
false,
|
|
158
|
+
);
|
|
159
|
+
expect(sameProjectDirectory("C:/Users/dev", "C:\\Users\\dev\\app")).toBe(false);
|
|
160
|
+
expect(sameProjectDirectory("D:/Users/dev/app", "C:\\Users\\dev\\app")).toBe(false);
|
|
161
|
+
// POSIX paths are case-sensitive.
|
|
162
|
+
expect(sameProjectDirectory("/work/App", "/work/app")).toBe(false);
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
test("an empty directory matches nothing, not even the process's working directory", () => {
|
|
166
|
+
expect(sameProjectDirectory("", process.cwd())).toBe(false);
|
|
167
|
+
expect(sameProjectDirectory(process.cwd(), "")).toBe(false);
|
|
168
|
+
expect(sameProjectDirectory("", "")).toBe(false);
|
|
169
|
+
});
|
|
170
|
+
});
|
|
@@ -20,6 +20,26 @@ export function projectDirectoryKey(directory: string): string {
|
|
|
20
20
|
return path.resolve(directory);
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
+
/**
|
|
24
|
+
* Whether two spellings name the same directory, judged by spelling alone.
|
|
25
|
+
*
|
|
26
|
+
* Hosts and the plugin do not agree on how to write a directory: OpenCode 2 on
|
|
27
|
+
* Windows stores a session's directory as `D:/work/app` while the plugin's own
|
|
28
|
+
* location is `D:\work\app`, and a drive letter can arrive in either case.
|
|
29
|
+
* Both sides go through `projectDirectoryKey`, so separators, Windows case,
|
|
30
|
+
* `\\?\` prefixes, trailing separators and `.`/`..` segments cannot make one
|
|
31
|
+
* directory look like two. POSIX paths keep their case. Symlinks are not
|
|
32
|
+
* resolved here; callers that must follow them use `canonicalProjectDirectory`.
|
|
33
|
+
*
|
|
34
|
+
* An empty spelling never matches anything: `path.resolve("")` is the process's
|
|
35
|
+
* working directory, and a session with no recorded directory must not be
|
|
36
|
+
* taken for one in the directory the process happens to run in.
|
|
37
|
+
*/
|
|
38
|
+
export function sameProjectDirectory(a: string, b: string): boolean {
|
|
39
|
+
if (a.length === 0 || b.length === 0) return false;
|
|
40
|
+
return projectDirectoryKey(a) === projectDirectoryKey(b);
|
|
41
|
+
}
|
|
42
|
+
|
|
23
43
|
let realpathForCanonicalDirectory: (directory: string) => string = (directory) =>
|
|
24
44
|
realpathSync.native(directory);
|
|
25
45
|
|
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
import { afterAll, beforeEach, describe, expect, test } from "bun:test";
|
|
2
|
+
import { mkdirSync, rmSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { modelsDevCatalogPath } from "./models-dev-temperature";
|
|
6
|
+
import {
|
|
7
|
+
__temperatureTest,
|
|
8
|
+
applyHiddenSubagentTemperature,
|
|
9
|
+
resolveTemperatureCapability,
|
|
10
|
+
subagentTemperature,
|
|
11
|
+
} from "./subagent-temperature";
|
|
12
|
+
import { cleanupTestTempDir, createTestTempDirFromPath } from "./test-temp-dir";
|
|
13
|
+
|
|
14
|
+
// Every test reads a catalog it wrote itself, never the machine's OpenCode cache.
|
|
15
|
+
const cacheRoot = createTestTempDirFromPath(join(tmpdir(), "mc-temperature-catalog-"));
|
|
16
|
+
const savedEnv = {
|
|
17
|
+
XDG_CACHE_HOME: process.env.XDG_CACHE_HOME,
|
|
18
|
+
OPENCODE_MODELS_PATH: process.env.OPENCODE_MODELS_PATH,
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
/** Shape of OpenCode's cached models.dev api.json, trimmed to the fields that matter. */
|
|
22
|
+
const CATALOG = {
|
|
23
|
+
google: {
|
|
24
|
+
id: "google",
|
|
25
|
+
models: {
|
|
26
|
+
"gemini-3.8-flash": { id: "gemini-3.8-flash", temperature: true, reasoning: true },
|
|
27
|
+
},
|
|
28
|
+
},
|
|
29
|
+
openrouter: {
|
|
30
|
+
id: "openrouter",
|
|
31
|
+
models: {
|
|
32
|
+
"google/gemini-3.8-flash": { temperature: true, reasoning: true },
|
|
33
|
+
"openai/gpt-6.1-sol": { temperature: false, reasoning: true },
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
openai: { id: "openai", models: { "gpt-6.1-sol": { temperature: false, reasoning: true } } },
|
|
37
|
+
opencode: { id: "opencode", models: { "gemini-3.8-flash": { temperature: true } } },
|
|
38
|
+
broken: { id: "broken", models: { "no-flag": { reasoning: true } } },
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
function writeCatalog(contents: string = JSON.stringify(CATALOG)): void {
|
|
42
|
+
const path = join(cacheRoot, "opencode", "models.json");
|
|
43
|
+
mkdirSync(join(cacheRoot, "opencode"), { recursive: true });
|
|
44
|
+
writeFileSync(path, contents);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function removeCatalog(): void {
|
|
48
|
+
rmSync(join(cacheRoot, "opencode"), { recursive: true, force: true });
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
beforeEach(() => {
|
|
52
|
+
process.env.XDG_CACHE_HOME = cacheRoot;
|
|
53
|
+
delete process.env.OPENCODE_MODELS_PATH;
|
|
54
|
+
removeCatalog();
|
|
55
|
+
__temperatureTest.reset();
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
afterAll(() => {
|
|
59
|
+
for (const [key, value] of Object.entries(savedEnv)) {
|
|
60
|
+
if (value === undefined) delete process.env[key];
|
|
61
|
+
else process.env[key] = value;
|
|
62
|
+
}
|
|
63
|
+
cleanupTestTempDir(cacheRoot);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
describe("subagent sampling metadata", () => {
|
|
67
|
+
for (const [host, yes, no] of [
|
|
68
|
+
[
|
|
69
|
+
"OpenCode 1",
|
|
70
|
+
{ capabilities: { temperature: true } },
|
|
71
|
+
{ capabilities: { temperature: false } },
|
|
72
|
+
],
|
|
73
|
+
["OpenCode 2", { temperature: true }, { temperature: false }],
|
|
74
|
+
[
|
|
75
|
+
"Pi",
|
|
76
|
+
{ compat: { supportsTemperature: true } },
|
|
77
|
+
{ compat: { supportsTemperature: false } },
|
|
78
|
+
],
|
|
79
|
+
[
|
|
80
|
+
"OMP",
|
|
81
|
+
{ compat: { supportsSamplingParams: true } },
|
|
82
|
+
{ compat: { supportsSamplingParams: false } },
|
|
83
|
+
],
|
|
84
|
+
] as const) {
|
|
85
|
+
test(`${host} requires affirmative metadata and preserves explicit zero`, () => {
|
|
86
|
+
const logs: string[] = [];
|
|
87
|
+
const note = (line: string) => logs.push(line);
|
|
88
|
+
expect(subagentTemperature("provider/yes", yes, 0.1, note)).toBe(0.1);
|
|
89
|
+
expect(subagentTemperature("provider/yes", yes, 0, note)).toBe(0);
|
|
90
|
+
expect(subagentTemperature("provider/no", no, 0.1, note)).toBeUndefined();
|
|
91
|
+
expect(
|
|
92
|
+
subagentTemperature("provider/missing", { reasoning: false }, 0.1, note),
|
|
93
|
+
).toBeUndefined();
|
|
94
|
+
expect(
|
|
95
|
+
subagentTemperature("provider/unknown", { temperature: "yes" }, 0.1, note),
|
|
96
|
+
).toBeUndefined();
|
|
97
|
+
expect(subagentTemperature("provider/unset", yes, undefined, note)).toBeUndefined();
|
|
98
|
+
expect(logs).toEqual([
|
|
99
|
+
"Subagent temperature sent for provider/yes: host model metadata says yes",
|
|
100
|
+
"Subagent temperature omitted for provider/no: host model metadata says no",
|
|
101
|
+
"Subagent temperature omitted for provider/missing: no host or models.dev metadata",
|
|
102
|
+
"Subagent temperature omitted for provider/unknown: no host or models.dev metadata",
|
|
103
|
+
]);
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
test("decision logging is once per model and decision, and does not remember a denial as a capability", () => {
|
|
108
|
+
const logs: string[] = [];
|
|
109
|
+
const note = (line: string) => logs.push(line);
|
|
110
|
+
expect(subagentTemperature("p/m", undefined, 0.1, note)).toBeUndefined();
|
|
111
|
+
expect(subagentTemperature("p/m", undefined, 0.1, note)).toBeUndefined();
|
|
112
|
+
expect(subagentTemperature("p/m", { temperature: true }, 0.1, note)).toBe(0.1);
|
|
113
|
+
expect(subagentTemperature("p/m", { temperature: true }, 0.1, note)).toBe(0.1);
|
|
114
|
+
expect(logs).toEqual([
|
|
115
|
+
"Subagent temperature omitted for p/m: no host or models.dev metadata",
|
|
116
|
+
"Subagent temperature sent for p/m: host model metadata says yes",
|
|
117
|
+
]);
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
test("OpenCode 1 guards historian, dreamer and compiler per resolved model, not unrelated agents", () => {
|
|
121
|
+
const yes = { providerID: "p", id: "flash", capabilities: { temperature: true } };
|
|
122
|
+
const no = { providerID: "p", id: "fallback", capabilities: { temperature: false } };
|
|
123
|
+
for (const agent of [
|
|
124
|
+
"historian",
|
|
125
|
+
"historian-editor",
|
|
126
|
+
"historian-recomp",
|
|
127
|
+
"dreamer",
|
|
128
|
+
"dreamer-classifier",
|
|
129
|
+
"smart-note-compiler",
|
|
130
|
+
]) {
|
|
131
|
+
const output: { temperature?: number } = { temperature: 0 };
|
|
132
|
+
applyHiddenSubagentTemperature({ agent, model: yes }, output, () => {});
|
|
133
|
+
expect(output).toEqual({ temperature: 0 });
|
|
134
|
+
applyHiddenSubagentTemperature({ agent, model: no }, output, () => {});
|
|
135
|
+
expect(output).toEqual({});
|
|
136
|
+
output.temperature = 0.1;
|
|
137
|
+
applyHiddenSubagentTemperature({ agent }, output, () => {});
|
|
138
|
+
expect(output).toEqual({});
|
|
139
|
+
}
|
|
140
|
+
const output = { temperature: 0.4 };
|
|
141
|
+
applyHiddenSubagentTemperature({ agent: "build", model: no }, output, () => {});
|
|
142
|
+
expect(output.temperature).toBe(0.4);
|
|
143
|
+
});
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
describe("models.dev catalog fallback when the host is silent", () => {
|
|
147
|
+
test("resolves the cached catalog by provider and exact model id", () => {
|
|
148
|
+
writeCatalog();
|
|
149
|
+
const logs: string[] = [];
|
|
150
|
+
const note = (line: string) => logs.push(line);
|
|
151
|
+
// Pi and OpenCode 2 rows carry no temperature field.
|
|
152
|
+
const silent = { reasoning: true, compat: { thinkingFormat: "openrouter" } };
|
|
153
|
+
expect(subagentTemperature("openrouter/google/gemini-3.8-flash", silent, 0.1, note)).toBe(
|
|
154
|
+
0.1,
|
|
155
|
+
);
|
|
156
|
+
expect(subagentTemperature("google/gemini-3.8-flash", silent, 0, note)).toBe(0);
|
|
157
|
+
expect(subagentTemperature("opencode-zen/gemini-3.8-flash", silent, 0.1, note)).toBe(0.1);
|
|
158
|
+
expect(subagentTemperature("openai/gpt-6.1-sol", silent, 0.1, note)).toBeUndefined();
|
|
159
|
+
expect(
|
|
160
|
+
subagentTemperature("openrouter/openai/gpt-6.1-sol", silent, 0.1, note),
|
|
161
|
+
).toBeUndefined();
|
|
162
|
+
expect(logs).toEqual([
|
|
163
|
+
"Subagent temperature sent for openrouter/google/gemini-3.8-flash: models.dev cache entry openrouter/google/gemini-3.8-flash says yes",
|
|
164
|
+
"Subagent temperature sent for google/gemini-3.8-flash: models.dev cache entry google/gemini-3.8-flash says yes",
|
|
165
|
+
"Subagent temperature sent for opencode-zen/gemini-3.8-flash: models.dev cache entry opencode/gemini-3.8-flash says yes",
|
|
166
|
+
"Subagent temperature omitted for openai/gpt-6.1-sol: models.dev cache entry openai/gpt-6.1-sol says no",
|
|
167
|
+
"Subagent temperature omitted for openrouter/openai/gpt-6.1-sol: models.dev cache entry openrouter/openai/gpt-6.1-sol says no",
|
|
168
|
+
]);
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
test("both Antigravity spellings map to the underlying Gemini entry", () => {
|
|
172
|
+
writeCatalog();
|
|
173
|
+
for (const model of [
|
|
174
|
+
"google/antigravity-gemini-3.8-flash",
|
|
175
|
+
"google-antigravity/gemini-3.8-flash",
|
|
176
|
+
"google-antigravity/antigravity-gemini-3.8-flash",
|
|
177
|
+
]) {
|
|
178
|
+
expect(resolveTemperatureCapability(model, {})).toEqual({
|
|
179
|
+
supported: true,
|
|
180
|
+
source: "catalog",
|
|
181
|
+
entry: "google/gemini-3.8-flash",
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
// The prefix is only an Antigravity spelling on Google, not a general rewrite.
|
|
185
|
+
expect(resolveTemperatureCapability("openrouter/antigravity-gemini-3.8-flash", {})).toEqual(
|
|
186
|
+
{
|
|
187
|
+
supported: undefined,
|
|
188
|
+
source: "none",
|
|
189
|
+
},
|
|
190
|
+
);
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
test("Codex and unmatched models stay unknown, so temperature is omitted", () => {
|
|
194
|
+
writeCatalog();
|
|
195
|
+
for (const model of [
|
|
196
|
+
"openai-codex/gpt-6.1-sol",
|
|
197
|
+
"google/gemini-3.8-flash-preview",
|
|
198
|
+
"google/claude-opus-4-6-thinking",
|
|
199
|
+
"broken/no-flag",
|
|
200
|
+
"unknown/unknown",
|
|
201
|
+
"no-slash",
|
|
202
|
+
]) {
|
|
203
|
+
expect(resolveTemperatureCapability(model, undefined)).toEqual({
|
|
204
|
+
supported: undefined,
|
|
205
|
+
source: "none",
|
|
206
|
+
});
|
|
207
|
+
expect(subagentTemperature(model, undefined, 0.1, () => {})).toBeUndefined();
|
|
208
|
+
}
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
test("host metadata outranks the catalog in both directions", () => {
|
|
212
|
+
writeCatalog();
|
|
213
|
+
expect(
|
|
214
|
+
subagentTemperature(
|
|
215
|
+
"google/gemini-3.8-flash",
|
|
216
|
+
{ capabilities: { temperature: false } },
|
|
217
|
+
0.1,
|
|
218
|
+
() => {},
|
|
219
|
+
),
|
|
220
|
+
).toBeUndefined();
|
|
221
|
+
expect(
|
|
222
|
+
subagentTemperature(
|
|
223
|
+
"openai/gpt-6.1-sol",
|
|
224
|
+
{ compat: { supportsSamplingParams: true } },
|
|
225
|
+
0.1,
|
|
226
|
+
() => {},
|
|
227
|
+
),
|
|
228
|
+
).toBe(0.1);
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
test("a missing, empty or partially written catalog is not evidence", () => {
|
|
232
|
+
const silent = {};
|
|
233
|
+
expect(
|
|
234
|
+
subagentTemperature("google/gemini-3.8-flash", silent, 0.1, () => {}),
|
|
235
|
+
).toBeUndefined();
|
|
236
|
+
writeCatalog(JSON.stringify(CATALOG).slice(0, 200));
|
|
237
|
+
expect(
|
|
238
|
+
subagentTemperature("google/gemini-3.8-flash", silent, 0.1, () => {}),
|
|
239
|
+
).toBeUndefined();
|
|
240
|
+
writeCatalog("[]");
|
|
241
|
+
expect(
|
|
242
|
+
subagentTemperature("google/gemini-3.8-flash", silent, 0.1, () => {}),
|
|
243
|
+
).toBeUndefined();
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
test("a rewritten catalog is re-read rather than served from memory", () => {
|
|
247
|
+
writeCatalog();
|
|
248
|
+
expect(resolveTemperatureCapability("google/gemini-3.8-flash", {}).supported).toBe(true);
|
|
249
|
+
writeCatalog(
|
|
250
|
+
JSON.stringify({
|
|
251
|
+
google: { models: { "gemini-3.8-flash": { temperature: false, note: "changed" } } },
|
|
252
|
+
}),
|
|
253
|
+
);
|
|
254
|
+
expect(resolveTemperatureCapability("google/gemini-3.8-flash", {}).supported).toBe(false);
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
test("OPENCODE_MODELS_PATH takes precedence over the cache, as it does for OpenCode", () => {
|
|
258
|
+
writeCatalog();
|
|
259
|
+
const override = join(cacheRoot, "override.json");
|
|
260
|
+
writeFileSync(
|
|
261
|
+
override,
|
|
262
|
+
JSON.stringify({ google: { models: { "gemini-3.8-flash": { temperature: false } } } }),
|
|
263
|
+
);
|
|
264
|
+
process.env.OPENCODE_MODELS_PATH = override;
|
|
265
|
+
expect(modelsDevCatalogPath()).toBe(override);
|
|
266
|
+
expect(resolveTemperatureCapability("google/gemini-3.8-flash", {}).supported).toBe(false);
|
|
267
|
+
delete process.env.OPENCODE_MODELS_PATH;
|
|
268
|
+
expect(modelsDevCatalogPath()).toBe(join(cacheRoot, "opencode", "models.json"));
|
|
269
|
+
});
|
|
270
|
+
});
|