@cortexkit/opencode-magic-context 0.16.3 → 0.17.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/features/magic-context/message-index-async.d.ts +12 -0
- package/dist/features/magic-context/message-index-async.d.ts.map +1 -0
- package/dist/features/magic-context/message-index.d.ts +4 -0
- package/dist/features/magic-context/message-index.d.ts.map +1 -1
- package/dist/features/magic-context/migrations.d.ts +7 -0
- package/dist/features/magic-context/migrations.d.ts.map +1 -1
- package/dist/features/magic-context/search.d.ts +2 -2
- package/dist/features/magic-context/search.d.ts.map +1 -1
- package/dist/features/magic-context/storage-db.d.ts.map +1 -1
- package/dist/features/magic-context/storage-meta-persisted.d.ts +3 -6
- package/dist/features/magic-context/storage-meta-persisted.d.ts.map +1 -1
- package/dist/features/magic-context/storage-tags.d.ts +163 -1
- 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 +52 -2
- package/dist/features/magic-context/tagger.d.ts.map +1 -1
- package/dist/features/magic-context/tool-definition-tokens.d.ts +26 -3
- package/dist/features/magic-context/tool-definition-tokens.d.ts.map +1 -1
- package/dist/features/magic-context/tool-owner-backfill.d.ts +90 -0
- package/dist/features/magic-context/tool-owner-backfill.d.ts.map +1 -0
- package/dist/features/magic-context/types.d.ts +17 -0
- package/dist/features/magic-context/types.d.ts.map +1 -1
- package/dist/hooks/magic-context/auto-search-runner.d.ts.map +1 -1
- package/dist/hooks/magic-context/compartment-runner-drop-queue.d.ts +23 -0
- package/dist/hooks/magic-context/compartment-runner-drop-queue.d.ts.map +1 -1
- package/dist/hooks/magic-context/event-handler.d.ts.map +1 -1
- package/dist/hooks/magic-context/event-payloads.d.ts +8 -0
- package/dist/hooks/magic-context/event-payloads.d.ts.map +1 -1
- package/dist/hooks/magic-context/heuristic-cleanup.d.ts.map +1 -1
- package/dist/hooks/magic-context/hook-handlers.d.ts.map +1 -1
- package/dist/hooks/magic-context/hook.d.ts.map +1 -1
- package/dist/hooks/magic-context/inject-compartments.d.ts +16 -0
- package/dist/hooks/magic-context/inject-compartments.d.ts.map +1 -1
- package/dist/hooks/magic-context/nudger.d.ts.map +1 -1
- package/dist/hooks/magic-context/read-session-chunk.d.ts +24 -1
- package/dist/hooks/magic-context/read-session-chunk.d.ts.map +1 -1
- package/dist/hooks/magic-context/read-session-db.d.ts +1 -0
- package/dist/hooks/magic-context/read-session-db.d.ts.map +1 -1
- package/dist/hooks/magic-context/read-session-raw.d.ts +1 -0
- package/dist/hooks/magic-context/read-session-raw.d.ts.map +1 -1
- package/dist/hooks/magic-context/strip-content.d.ts +9 -6
- package/dist/hooks/magic-context/strip-content.d.ts.map +1 -1
- package/dist/hooks/magic-context/tag-messages.d.ts +1 -1
- package/dist/hooks/magic-context/tag-messages.d.ts.map +1 -1
- package/dist/hooks/magic-context/tool-drop-target.d.ts +16 -1
- package/dist/hooks/magic-context/tool-drop-target.d.ts.map +1 -1
- package/dist/hooks/magic-context/transform-postprocess-phase.d.ts +0 -11
- package/dist/hooks/magic-context/transform-postprocess-phase.d.ts.map +1 -1
- package/dist/hooks/magic-context/transform.d.ts.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1731 -758
- package/dist/plugin/hooks/create-session-hooks.d.ts.map +1 -1
- package/dist/plugin/rpc-handlers.d.ts +3 -0
- package/dist/plugin/rpc-handlers.d.ts.map +1 -1
- package/dist/shared/models-dev-cache.d.ts +3 -10
- package/dist/shared/models-dev-cache.d.ts.map +1 -1
- package/dist/shared/tag-transcript.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/shared/models-dev-cache.test.ts +64 -57
- package/src/shared/models-dev-cache.ts +49 -68
- package/src/shared/tag-transcript.ts +137 -126
- package/dist/hooks/magic-context/reasoning-capability.d.ts +0 -23
- package/dist/hooks/magic-context/reasoning-capability.d.ts.map +0 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"create-session-hooks.d.ts","sourceRoot":"","sources":["../../../src/plugin/hooks/create-session-hooks.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,wBAAwB,EAAE,MAAM,cAAc,CAAC;AAU7D,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,8CAA8C,CAAC;AACrF,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAE9C,wBAAgB,kBAAkB,CAAC,IAAI,EAAE;IACrC,GAAG,EAAE,aAAa,CAAC;IACnB,YAAY,EAAE,wBAAwB,CAAC;IACvC,gBAAgB,EAAE,gBAAgB,CAAC;CACtC;;;;;;qBAqDyvJ,CAAC;;;;;;;;;;;;
|
|
1
|
+
{"version":3,"file":"create-session-hooks.d.ts","sourceRoot":"","sources":["../../../src/plugin/hooks/create-session-hooks.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,wBAAwB,EAAE,MAAM,cAAc,CAAC;AAU7D,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,8CAA8C,CAAC;AACrF,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAE9C,wBAAgB,kBAAkB,CAAC,IAAI,EAAE;IACrC,GAAG,EAAE,aAAa,CAAC;IACnB,YAAY,EAAE,wBAAwB,CAAC;IACvC,gBAAgB,EAAE,gBAAgB,CAAC;CACtC;;;;;;qBAqDyvJ,CAAC;;;;;;;;;;;;qBAA/1D,CAAC;mBAAyB,CAAC;iBAAuB,CAAC;iBAAuB,CAAC;0BAAc,CAAC;uBAAiB,CAAC;;;;;;0BAAulpB,CAAC;;;;;;EAD/lvB"}
|
|
@@ -3,8 +3,11 @@
|
|
|
3
3
|
* and returns typed responses for TUI consumption.
|
|
4
4
|
*/
|
|
5
5
|
import type { MagicContextConfig } from "../config/schema/magic-context";
|
|
6
|
+
import { type ContextDatabase as Database } from "../features/magic-context/storage";
|
|
6
7
|
import type { LiveSessionState } from "../hooks/magic-context/live-session-state";
|
|
7
8
|
import type { MagicContextRpcServer } from "../shared/rpc-server";
|
|
9
|
+
import type { SidebarSnapshot } from "../shared/rpc-types";
|
|
10
|
+
export declare function buildSidebarSnapshot(db: Database, sessionId: string, directory: string, liveSessionState?: LiveSessionState, injectionBudgetTokens?: number): SidebarSnapshot;
|
|
8
11
|
/**
|
|
9
12
|
* Register all RPC handlers on the server.
|
|
10
13
|
*/
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"rpc-handlers.d.ts","sourceRoot":"","sources":["../../src/plugin/rpc-handlers.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,gCAAgC,CAAC;
|
|
1
|
+
{"version":3,"file":"rpc-handlers.d.ts","sourceRoot":"","sources":["../../src/plugin/rpc-handlers.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,gCAAgC,CAAC;AAGzE,OAAO,EAAE,KAAK,eAAe,IAAI,QAAQ,EAAgB,MAAM,mCAAmC,CAAC;AAQnG,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,2CAA2C,CAAC;AASlF,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,sBAAsB,CAAC;AAClE,OAAO,KAAK,EAAE,eAAe,EAAgB,MAAM,qBAAqB,CAAC;AAmDzE,wBAAgB,oBAAoB,CAChC,EAAE,EAAE,QAAQ,EACZ,SAAS,EAAE,MAAM,EACjB,SAAS,EAAE,MAAM,EACjB,gBAAgB,CAAC,EAAE,gBAAgB,EACnC,qBAAqB,CAAC,EAAE,MAAM,GAC/B,eAAe,CAySjB;AAoMD;;GAEG;AACH,wBAAgB,mBAAmB,CAC/B,SAAS,EAAE,qBAAqB,EAChC,IAAI,EAAE;IACF,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,kBAAkB,CAAC;IAC3B,MAAM,EAAE,OAAO,CAAC;IAChB,gBAAgB,EAAE,gBAAgB,CAAC;CACtC,GACF,IAAI,CAoIN"}
|
|
@@ -14,10 +14,9 @@
|
|
|
14
14
|
* Used during cold starts before the API cache warms up and in any
|
|
15
15
|
* code path that cannot reach the SDK client.
|
|
16
16
|
*
|
|
17
|
-
* The public
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
* from `src/index.ts` at startup and on a timer.
|
|
17
|
+
* The public getter (`getModelsDevContextLimit()`) is synchronous: it checks
|
|
18
|
+
* the API cache first, then the file cache. The plugin warms and refreshes
|
|
19
|
+
* the API cache from `src/index.ts` at startup and on a timer.
|
|
21
20
|
*/
|
|
22
21
|
interface OpencodeClientLike {
|
|
23
22
|
config: {
|
|
@@ -52,12 +51,6 @@ export declare function refreshModelLimitsFromApi(client: OpencodeClientLike): P
|
|
|
52
51
|
* Returns `undefined` if neither layer knows the model.
|
|
53
52
|
*/
|
|
54
53
|
export declare function getModelsDevContextLimit(providerID: string, modelID: string): number | undefined;
|
|
55
|
-
/**
|
|
56
|
-
* Returns the provider-specific interleaved reasoning field when the model
|
|
57
|
-
* requires one (for example `reasoning_content` for Moonshot/Kimi style
|
|
58
|
-
* providers). Undefined means the cache has no such capability recorded.
|
|
59
|
-
*/
|
|
60
|
-
export declare function getModelsDevInterleavedField(providerID: string, modelID: string): string | undefined;
|
|
61
54
|
/** Clear in-memory caches (for testing). */
|
|
62
55
|
export declare function clearModelsDevCache(): void;
|
|
63
56
|
/** Inspection helpers (for logging / debugging). */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"models-dev-cache.d.ts","sourceRoot":"","sources":["../../src/shared/models-dev-cache.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"models-dev-cache.d.ts","sourceRoot":"","sources":["../../src/shared/models-dev-cache.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AASH,UAAU,kBAAkB;IACxB,MAAM,EAAE;QACJ,SAAS,EAAE,MAAM,OAAO,CAAC;YAAE,IAAI,CAAC,EAAE;gBAAE,SAAS,CAAC,EAAE,OAAO,CAAA;aAAE,CAAA;SAAE,CAAC,CAAC;KAChE,CAAC;CACL;AA4MD;;;;;;;;;;GAUG;AACH,wBAAsB,yBAAyB,CAAC,MAAM,EAAE,kBAAkB,GAAG,OAAO,CAAC,IAAI,CAAC,CAmEzF;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,wBAAwB,CAAC,UAAU,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAchG;AAED,4CAA4C;AAC5C,wBAAgB,mBAAmB,IAAI,IAAI,CAO1C;AAED,oDAAoD;AACpD,wBAAgB,sBAAsB,IAAI;IACtC,SAAS,EAAE,OAAO,CAAC;IACnB,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;CACrB,CAOA"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tag-transcript.d.ts","sourceRoot":"","sources":["../../src/shared/tag-transcript.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6CG;AAEH,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,mCAAmC,CAAC;AAGzE,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,kCAAkC,CAAC;AAM/D,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,qCAAqC,CAAC;AACrE,OAAO,KAAK,EAAE,UAAU,EAAkB,MAAM,cAAc,CAAC;AAE/D,MAAM,WAAW,oBAAoB;IACjC;;;;;;;OAOG;IACH,mBAAmB,CAAC,EAAE,OAAO,CAAC;CACjC;AAED,MAAM,WAAW,mBAAmB;IAChC,OAAO,EAAE,GAAG,CAAC,MAAM,EAAE,SAAS,CAAC,CAAC;CACnC;AAyDD,wBAAgB,aAAa,CACzB,SAAS,EAAE,MAAM,EACjB,UAAU,EAAE,UAAU,EACtB,MAAM,EAAE,MAAM,EACd,EAAE,EAAE,eAAe,EACnB,OAAO,GAAE,oBAAyB,GACnC,mBAAmB,
|
|
1
|
+
{"version":3,"file":"tag-transcript.d.ts","sourceRoot":"","sources":["../../src/shared/tag-transcript.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6CG;AAEH,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,mCAAmC,CAAC;AAGzE,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,kCAAkC,CAAC;AAM/D,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,qCAAqC,CAAC;AACrE,OAAO,KAAK,EAAE,UAAU,EAAkB,MAAM,cAAc,CAAC;AAE/D,MAAM,WAAW,oBAAoB;IACjC;;;;;;;OAOG;IACH,mBAAmB,CAAC,EAAE,OAAO,CAAC;CACjC;AAED,MAAM,WAAW,mBAAmB;IAChC,OAAO,EAAE,GAAG,CAAC,MAAM,EAAE,SAAS,CAAC,CAAC;CACnC;AAyDD,wBAAgB,aAAa,CACzB,SAAS,EAAE,MAAM,EACjB,UAAU,EAAE,UAAU,EACtB,MAAM,EAAE,MAAM,EACd,EAAE,EAAE,eAAe,EACnB,OAAO,GAAE,oBAAyB,GACnC,mBAAmB,CAoKrB"}
|
package/package.json
CHANGED
|
@@ -6,7 +6,6 @@ import {
|
|
|
6
6
|
clearModelsDevCache,
|
|
7
7
|
getModelsDevCacheState,
|
|
8
8
|
getModelsDevContextLimit,
|
|
9
|
-
getModelsDevInterleavedField,
|
|
10
9
|
refreshModelLimitsFromApi,
|
|
11
10
|
} from "./models-dev-cache";
|
|
12
11
|
|
|
@@ -276,62 +275,6 @@ describe("models-dev-cache", () => {
|
|
|
276
275
|
expect(state.apiCount).toBe(1);
|
|
277
276
|
});
|
|
278
277
|
|
|
279
|
-
test("reads interleaved reasoning field metadata from models.json", () => {
|
|
280
|
-
const opencodeDir = join(tempDir, "opencode");
|
|
281
|
-
mkdirSync(opencodeDir, { recursive: true });
|
|
282
|
-
writeFileSync(
|
|
283
|
-
join(opencodeDir, "models.json"),
|
|
284
|
-
JSON.stringify({
|
|
285
|
-
"opencode-go": {
|
|
286
|
-
models: {
|
|
287
|
-
"kimi-k2.6": {
|
|
288
|
-
limit: { context: 262144 },
|
|
289
|
-
interleaved: { field: "reasoning_content" },
|
|
290
|
-
},
|
|
291
|
-
"plain-model": {
|
|
292
|
-
limit: { context: 262144 },
|
|
293
|
-
},
|
|
294
|
-
},
|
|
295
|
-
},
|
|
296
|
-
}),
|
|
297
|
-
);
|
|
298
|
-
|
|
299
|
-
expect(getModelsDevInterleavedField("opencode-go", "kimi-k2.6")).toBe("reasoning_content");
|
|
300
|
-
expect(getModelsDevInterleavedField("opencode-go", "plain-model")).toBeUndefined();
|
|
301
|
-
});
|
|
302
|
-
|
|
303
|
-
test("API cache exposes interleaved reasoning metadata", async () => {
|
|
304
|
-
const mockClient = {
|
|
305
|
-
config: {
|
|
306
|
-
providers: async () => ({
|
|
307
|
-
data: {
|
|
308
|
-
providers: [
|
|
309
|
-
{
|
|
310
|
-
id: "opencode-go",
|
|
311
|
-
models: {
|
|
312
|
-
"kimi-k2.6": {
|
|
313
|
-
limit: { context: 262144 },
|
|
314
|
-
capabilities: {
|
|
315
|
-
interleaved: { field: "reasoning_content" },
|
|
316
|
-
},
|
|
317
|
-
},
|
|
318
|
-
"plain-model": {
|
|
319
|
-
limit: { context: 262144 },
|
|
320
|
-
capabilities: { interleaved: false },
|
|
321
|
-
},
|
|
322
|
-
},
|
|
323
|
-
},
|
|
324
|
-
],
|
|
325
|
-
},
|
|
326
|
-
}),
|
|
327
|
-
},
|
|
328
|
-
};
|
|
329
|
-
await refreshModelLimitsFromApi(mockClient);
|
|
330
|
-
|
|
331
|
-
expect(getModelsDevInterleavedField("opencode-go", "kimi-k2.6")).toBe("reasoning_content");
|
|
332
|
-
expect(getModelsDevInterleavedField("opencode-go", "plain-model")).toBeUndefined();
|
|
333
|
-
});
|
|
334
|
-
|
|
335
278
|
test("refreshModelLimitsFromApi tolerates empty/malformed responses", async () => {
|
|
336
279
|
// Undefined data.
|
|
337
280
|
await refreshModelLimitsFromApi({
|
|
@@ -356,6 +299,70 @@ describe("models-dev-cache", () => {
|
|
|
356
299
|
expect(getModelsDevCacheState().apiLoaded).toBe(false);
|
|
357
300
|
});
|
|
358
301
|
|
|
302
|
+
test("suppresses repeated logs when API count oscillates between known sizes", async () => {
|
|
303
|
+
// Simulates github-copilot's /models endpoint returning different model sets
|
|
304
|
+
// between calls. We want first sighting of each new size to log, but once a
|
|
305
|
+
// size has been seen before, further flips between known sizes should be
|
|
306
|
+
// silent (with one "oscillating" notice).
|
|
307
|
+
const sizeA = {
|
|
308
|
+
data: {
|
|
309
|
+
providers: [
|
|
310
|
+
{
|
|
311
|
+
id: "p",
|
|
312
|
+
models: {
|
|
313
|
+
m1: { limit: { context: 100 } },
|
|
314
|
+
m2: { limit: { context: 100 } },
|
|
315
|
+
m3: { limit: { context: 100 } },
|
|
316
|
+
},
|
|
317
|
+
},
|
|
318
|
+
],
|
|
319
|
+
},
|
|
320
|
+
};
|
|
321
|
+
const sizeB = {
|
|
322
|
+
data: {
|
|
323
|
+
providers: [
|
|
324
|
+
{
|
|
325
|
+
id: "p",
|
|
326
|
+
models: {
|
|
327
|
+
m1: { limit: { context: 100 } },
|
|
328
|
+
m2: { limit: { context: 100 } },
|
|
329
|
+
},
|
|
330
|
+
},
|
|
331
|
+
],
|
|
332
|
+
},
|
|
333
|
+
};
|
|
334
|
+
|
|
335
|
+
const clientA = { config: { providers: async () => sizeA } };
|
|
336
|
+
const clientB = { config: { providers: async () => sizeB } };
|
|
337
|
+
|
|
338
|
+
// First sighting of size 3 → logs "loaded 3 entries".
|
|
339
|
+
await refreshModelLimitsFromApi(clientA);
|
|
340
|
+
expect(getModelsDevCacheState().apiCount).toBe(3);
|
|
341
|
+
|
|
342
|
+
// First sighting of size 2 → logs "loaded 2 entries (was 3)".
|
|
343
|
+
await refreshModelLimitsFromApi(clientB);
|
|
344
|
+
expect(getModelsDevCacheState().apiCount).toBe(2);
|
|
345
|
+
|
|
346
|
+
// Second sighting of size 3 → logs the "oscillating" notice once.
|
|
347
|
+
await refreshModelLimitsFromApi(clientA);
|
|
348
|
+
expect(getModelsDevCacheState().apiCount).toBe(3);
|
|
349
|
+
|
|
350
|
+
// Second sighting of size 2 → silent (no new log expected).
|
|
351
|
+
await refreshModelLimitsFromApi(clientB);
|
|
352
|
+
expect(getModelsDevCacheState().apiCount).toBe(2);
|
|
353
|
+
|
|
354
|
+
// Third sighting of size 3 → still silent.
|
|
355
|
+
await refreshModelLimitsFromApi(clientA);
|
|
356
|
+
expect(getModelsDevCacheState().apiCount).toBe(3);
|
|
357
|
+
|
|
358
|
+
// The cache itself still updates on every call (model contents are correct
|
|
359
|
+
// for whichever provider response just arrived). The suppression is purely
|
|
360
|
+
// a logging concern. Last call was clientA → all three models present.
|
|
361
|
+
expect(getModelsDevContextLimit("p", "m1")).toBe(100);
|
|
362
|
+
expect(getModelsDevContextLimit("p", "m2")).toBe(100);
|
|
363
|
+
expect(getModelsDevContextLimit("p", "m3")).toBe(100);
|
|
364
|
+
});
|
|
365
|
+
|
|
359
366
|
test("falls back to file layer when API provider/model key is missing", async () => {
|
|
360
367
|
const opencodeDir = join(tempDir, "opencode");
|
|
361
368
|
mkdirSync(opencodeDir, { recursive: true });
|
|
@@ -14,10 +14,9 @@
|
|
|
14
14
|
* Used during cold starts before the API cache warms up and in any
|
|
15
15
|
* code path that cannot reach the SDK client.
|
|
16
16
|
*
|
|
17
|
-
* The public
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
* from `src/index.ts` at startup and on a timer.
|
|
17
|
+
* The public getter (`getModelsDevContextLimit()`) is synchronous: it checks
|
|
18
|
+
* the API cache first, then the file cache. The plugin warms and refreshes
|
|
19
|
+
* the API cache from `src/index.ts` at startup and on a timer.
|
|
21
20
|
*/
|
|
22
21
|
|
|
23
22
|
import { createHash } from "node:crypto";
|
|
@@ -33,18 +32,30 @@ interface OpencodeClientLike {
|
|
|
33
32
|
};
|
|
34
33
|
}
|
|
35
34
|
|
|
36
|
-
|
|
35
|
+
// File-cache fallback only. The primary `models.json` API refresh is driven
|
|
36
|
+
// by `setInterval(refreshModelLimitsFromApi, ...)` in `index.ts` at a 1-hour
|
|
37
|
+
// cadence; this 5-minute interval governs the on-disk-cache fallback path
|
|
38
|
+
// when the API loader hasn't run yet (e.g. during plugin warmup).
|
|
39
|
+
const RELOAD_INTERVAL_MS = 5 * 60 * 1000;
|
|
37
40
|
|
|
38
41
|
interface CachedModelMetadata {
|
|
39
42
|
limit?: number;
|
|
40
|
-
interleavedField?: string;
|
|
41
43
|
}
|
|
42
44
|
|
|
43
|
-
type InterleavedConfig = boolean | { field?: string } | undefined;
|
|
44
|
-
|
|
45
45
|
/** Populated async from OpenCode SDK. Primary source of truth when available. */
|
|
46
46
|
let apiCache: Map<string, CachedModelMetadata> | null = null;
|
|
47
47
|
let apiLoadedAt = 0;
|
|
48
|
+
/**
|
|
49
|
+
* Recently-seen API cache sizes, used to detect oscillation between two
|
|
50
|
+
* stable values (typically caused by upstream provider plugins like
|
|
51
|
+
* github-copilot whose `/models` endpoint returns slightly different model
|
|
52
|
+
* sets between calls based on `model_picker_enabled` toggles). Once the
|
|
53
|
+
* same size has been observed before, we stop logging count changes —
|
|
54
|
+
* the count is a function of upstream behavior we can't control, and
|
|
55
|
+
* repeated logs only add noise.
|
|
56
|
+
*/
|
|
57
|
+
const recentlySeenApiSizes = new Set<number>();
|
|
58
|
+
let oscillationLogged = false;
|
|
48
59
|
|
|
49
60
|
/** Populated sync from disk as fallback. */
|
|
50
61
|
let fileCache: Map<string, CachedModelMetadata> | null = null;
|
|
@@ -110,47 +121,28 @@ function resolveLimit(limit: { context?: number; input?: number } | undefined):
|
|
|
110
121
|
return undefined;
|
|
111
122
|
}
|
|
112
123
|
|
|
113
|
-
function resolveInterleavedField(interleaved: InterleavedConfig): string | undefined {
|
|
114
|
-
if (
|
|
115
|
-
interleaved &&
|
|
116
|
-
typeof interleaved === "object" &&
|
|
117
|
-
typeof interleaved.field === "string" &&
|
|
118
|
-
interleaved.field.length > 0
|
|
119
|
-
) {
|
|
120
|
-
return interleaved.field;
|
|
121
|
-
}
|
|
122
|
-
return undefined;
|
|
123
|
-
}
|
|
124
|
-
|
|
125
124
|
function setCachedModelMetadata(
|
|
126
125
|
cache: Map<string, CachedModelMetadata>,
|
|
127
126
|
key: string,
|
|
128
127
|
model:
|
|
129
128
|
| {
|
|
130
129
|
limit?: { context?: number; input?: number };
|
|
131
|
-
capabilities?: { interleaved?: InterleavedConfig };
|
|
132
|
-
interleaved?: InterleavedConfig;
|
|
133
130
|
experimental?: { modes?: Record<string, unknown> };
|
|
134
131
|
}
|
|
135
132
|
| undefined,
|
|
136
133
|
): void {
|
|
137
134
|
const limit = resolveLimit(model?.limit);
|
|
138
|
-
const interleavedField =
|
|
139
|
-
resolveInterleavedField(model?.capabilities?.interleaved) ??
|
|
140
|
-
resolveInterleavedField(model?.interleaved);
|
|
141
135
|
|
|
142
|
-
if (limit === undefined
|
|
136
|
+
if (limit === undefined) {
|
|
143
137
|
return;
|
|
144
138
|
}
|
|
145
139
|
|
|
146
|
-
const value: CachedModelMetadata = {};
|
|
147
|
-
if (limit !== undefined) value.limit = limit;
|
|
148
|
-
if (interleavedField !== undefined) value.interleavedField = interleavedField;
|
|
140
|
+
const value: CachedModelMetadata = { limit };
|
|
149
141
|
cache.set(key, value);
|
|
150
142
|
|
|
151
143
|
// OpenCode creates derived model IDs from experimental.modes
|
|
152
144
|
// e.g. gpt-5.4 + modes.fast → gpt-5.4-fast. These inherit the same
|
|
153
|
-
// context limit
|
|
145
|
+
// context limit as the parent model.
|
|
154
146
|
const modes = model?.experimental?.modes;
|
|
155
147
|
if (modes && typeof modes === "object") {
|
|
156
148
|
for (const mode of Object.keys(modes)) {
|
|
@@ -176,8 +168,6 @@ function loadModelsDevMetadataFromFile(): Map<string, CachedModelMetadata> {
|
|
|
176
168
|
string,
|
|
177
169
|
{
|
|
178
170
|
limit?: { context?: number; input?: number };
|
|
179
|
-
capabilities?: { interleaved?: InterleavedConfig };
|
|
180
|
-
interleaved?: InterleavedConfig;
|
|
181
171
|
experimental?: { modes?: Record<string, unknown> };
|
|
182
172
|
}
|
|
183
173
|
>;
|
|
@@ -273,8 +263,6 @@ export async function refreshModelLimitsFromApi(client: OpencodeClientLike): Pro
|
|
|
273
263
|
string,
|
|
274
264
|
{
|
|
275
265
|
limit?: { context?: number; input?: number };
|
|
276
|
-
capabilities?: { interleaved?: InterleavedConfig };
|
|
277
|
-
interleaved?: InterleavedConfig;
|
|
278
266
|
experimental?: { modes?: Record<string, unknown> };
|
|
279
267
|
}
|
|
280
268
|
>;
|
|
@@ -288,15 +276,34 @@ export async function refreshModelLimitsFromApi(client: OpencodeClientLike): Pro
|
|
|
288
276
|
const previousSize = apiCache?.size ?? null;
|
|
289
277
|
apiCache = map;
|
|
290
278
|
apiLoadedAt = Date.now();
|
|
291
|
-
|
|
292
|
-
//
|
|
293
|
-
|
|
279
|
+
|
|
280
|
+
// Log policy:
|
|
281
|
+
// - Always log the first successful load.
|
|
282
|
+
// - Log a count change once per new size we haven't seen before.
|
|
283
|
+
// - When the count returns to a previously-seen size, log an
|
|
284
|
+
// "oscillation" message exactly once explaining the cause, then
|
|
285
|
+
// stay silent on further flips between known sizes.
|
|
286
|
+
if (previousSize === null) {
|
|
287
|
+
recentlySeenApiSizes.add(map.size);
|
|
294
288
|
sessionLog(
|
|
295
289
|
"global",
|
|
296
|
-
`models-dev-cache: API layer loaded ${map.size} model metadata entries
|
|
297
|
-
previousSize !== null ? ` (was ${previousSize})` : ""
|
|
298
|
-
}`,
|
|
290
|
+
`models-dev-cache: API layer loaded ${map.size} model metadata entries`,
|
|
299
291
|
);
|
|
292
|
+
} else if (previousSize !== map.size) {
|
|
293
|
+
const sizeAlreadySeen = recentlySeenApiSizes.has(map.size);
|
|
294
|
+
recentlySeenApiSizes.add(map.size);
|
|
295
|
+
if (!sizeAlreadySeen) {
|
|
296
|
+
sessionLog(
|
|
297
|
+
"global",
|
|
298
|
+
`models-dev-cache: API layer loaded ${map.size} model metadata entries (was ${previousSize})`,
|
|
299
|
+
);
|
|
300
|
+
} else if (!oscillationLogged) {
|
|
301
|
+
oscillationLogged = true;
|
|
302
|
+
sessionLog(
|
|
303
|
+
"global",
|
|
304
|
+
`models-dev-cache: API count oscillating between ${[...recentlySeenApiSizes].sort((a, b) => a - b).join(" ↔ ")} — likely upstream provider plugin returning slightly different model sets between calls (e.g. github-copilot's /models endpoint toggling model_picker_enabled). Suppressing further size-change logs.`,
|
|
305
|
+
);
|
|
306
|
+
}
|
|
300
307
|
}
|
|
301
308
|
} catch (error) {
|
|
302
309
|
sessionLog(
|
|
@@ -334,38 +341,12 @@ export function getModelsDevContextLimit(providerID: string, modelID: string): n
|
|
|
334
341
|
return fileCache.get(key)?.limit;
|
|
335
342
|
}
|
|
336
343
|
|
|
337
|
-
/**
|
|
338
|
-
* Returns the provider-specific interleaved reasoning field when the model
|
|
339
|
-
* requires one (for example `reasoning_content` for Moonshot/Kimi style
|
|
340
|
-
* providers). Undefined means the cache has no such capability recorded.
|
|
341
|
-
*/
|
|
342
|
-
export function getModelsDevInterleavedField(
|
|
343
|
-
providerID: string,
|
|
344
|
-
modelID: string,
|
|
345
|
-
): string | undefined {
|
|
346
|
-
const key = `${providerID}/${modelID}`;
|
|
347
|
-
|
|
348
|
-
if (apiCache) {
|
|
349
|
-
const fromApi = apiCache.get(key)?.interleavedField;
|
|
350
|
-
if (typeof fromApi === "string" && fromApi.length > 0) {
|
|
351
|
-
return fromApi;
|
|
352
|
-
}
|
|
353
|
-
}
|
|
354
|
-
|
|
355
|
-
const now = Date.now();
|
|
356
|
-
if (!fileCache || now - fileLastAttempt > RELOAD_INTERVAL_MS) {
|
|
357
|
-
fileLastAttempt = now;
|
|
358
|
-
fileCache = loadModelsDevMetadataFromFile();
|
|
359
|
-
}
|
|
360
|
-
|
|
361
|
-
const fromFile = fileCache.get(key)?.interleavedField;
|
|
362
|
-
return typeof fromFile === "string" && fromFile.length > 0 ? fromFile : undefined;
|
|
363
|
-
}
|
|
364
|
-
|
|
365
344
|
/** Clear in-memory caches (for testing). */
|
|
366
345
|
export function clearModelsDevCache(): void {
|
|
367
346
|
apiCache = null;
|
|
368
347
|
apiLoadedAt = 0;
|
|
348
|
+
recentlySeenApiSizes.clear();
|
|
349
|
+
oscillationLogged = false;
|
|
369
350
|
fileCache = null;
|
|
370
351
|
fileLastAttempt = 0;
|
|
371
352
|
}
|