@cortexkit/opencode-magic-context 0.22.1 → 0.22.2
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/config/agent-disable.d.ts +0 -9
- package/dist/config/agent-disable.d.ts.map +1 -1
- package/dist/config/schema/agent-overrides.d.ts +0 -3
- package/dist/config/schema/agent-overrides.d.ts.map +1 -1
- package/dist/features/builtin-commands/types.d.ts +0 -2
- package/dist/features/builtin-commands/types.d.ts.map +1 -1
- package/dist/features/magic-context/dreamer/scheduler.d.ts +0 -4
- package/dist/features/magic-context/dreamer/scheduler.d.ts.map +1 -1
- package/dist/features/magic-context/git-commits/index.d.ts +1 -0
- package/dist/features/magic-context/git-commits/index.d.ts.map +1 -1
- package/dist/features/magic-context/git-commits/storage-git-commits.d.ts.map +1 -1
- package/dist/features/magic-context/git-commits/sweep-coordinator.d.ts +48 -0
- package/dist/features/magic-context/git-commits/sweep-coordinator.d.ts.map +1 -0
- package/dist/features/magic-context/key-files/storage-key-files.d.ts +0 -5
- package/dist/features/magic-context/key-files/storage-key-files.d.ts.map +1 -1
- package/dist/features/magic-context/literal-probes.d.ts +24 -0
- package/dist/features/magic-context/literal-probes.d.ts.map +1 -0
- package/dist/features/magic-context/migrations.d.ts.map +1 -1
- package/dist/features/magic-context/project-embedding-registry.d.ts.map +1 -1
- package/dist/features/magic-context/search.d.ts +7 -0
- package/dist/features/magic-context/search.d.ts.map +1 -1
- package/dist/features/magic-context/storage-db.d.ts +1 -1
- package/dist/features/magic-context/storage-db.d.ts.map +1 -1
- package/dist/features/magic-context/storage-notes.d.ts +8 -0
- package/dist/features/magic-context/storage-notes.d.ts.map +1 -1
- package/dist/hooks/magic-context/compartment-runner-types.d.ts +14 -1
- package/dist/hooks/magic-context/compartment-runner-types.d.ts.map +1 -1
- package/dist/hooks/magic-context/derive-budgets.d.ts +3 -3
- package/dist/hooks/magic-context/event-resolvers.d.ts +1 -0
- package/dist/hooks/magic-context/event-resolvers.d.ts.map +1 -1
- package/dist/hooks/magic-context/recomp-orchestrator.d.ts +7 -2
- package/dist/hooks/magic-context/recomp-orchestrator.d.ts.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +478 -168
- package/dist/plugin/dream-timer.d.ts.map +1 -1
- package/dist/plugin/rpc-handlers.d.ts.map +1 -1
- package/dist/shared/models-dev-cache.d.ts +54 -27
- package/dist/shared/models-dev-cache.d.ts.map +1 -1
- package/dist/shared/rpc-types.d.ts +3 -1
- package/dist/shared/rpc-types.d.ts.map +1 -1
- package/dist/tools/ctx-note/tools.d.ts.map +1 -1
- package/dist/tools/ctx-search/tools.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/shared/models-dev-cache.test.ts +192 -360
- package/src/shared/models-dev-cache.ts +162 -193
- package/src/shared/rpc-types.ts +3 -1
- package/src/tui/index.tsx +17 -8
- package/src/tui/slots/sidebar-content.tsx +20 -10
|
@@ -1,44 +1,40 @@
|
|
|
1
1
|
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
|
|
2
|
-
import {
|
|
2
|
+
import { mkdtempSync, rmSync } from "node:fs";
|
|
3
3
|
import { tmpdir } from "node:os";
|
|
4
4
|
import { join } from "node:path";
|
|
5
5
|
import {
|
|
6
6
|
clearModelsDevCache,
|
|
7
7
|
getModelsDevCacheState,
|
|
8
|
-
|
|
8
|
+
getSdkContextLimit,
|
|
9
9
|
refreshModelLimitsFromApi,
|
|
10
10
|
} from "./models-dev-cache";
|
|
11
11
|
|
|
12
|
-
|
|
12
|
+
/**
|
|
13
|
+
* Model context limits resolve from OpenCode's SDK only (`config.providers()`),
|
|
14
|
+
* bounded to a sane [20k, 3M] range, with a persisted last-known-good cache for
|
|
15
|
+
* cold start. We no longer read OpenCode's `models.json` file ourselves (a torn
|
|
16
|
+
* read produced impossible limits and a stale copy out-voted the live cap).
|
|
17
|
+
*/
|
|
18
|
+
describe("models-dev-cache (SDK-only)", () => {
|
|
13
19
|
let tempDir: string;
|
|
14
|
-
let
|
|
20
|
+
let originalXdgData: string | undefined;
|
|
21
|
+
|
|
22
|
+
function makeClient(providers: Array<unknown>) {
|
|
23
|
+
return { config: { providers: async () => ({ data: { providers } }) } };
|
|
24
|
+
}
|
|
15
25
|
|
|
16
26
|
beforeEach(() => {
|
|
17
27
|
tempDir = mkdtempSync(join(tmpdir(), "mc-models-dev-"));
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
OPENCODE_CONFIG_DIR: process.env.OPENCODE_CONFIG_DIR,
|
|
23
|
-
};
|
|
24
|
-
// Isolate from user environment — including user's ~/.config/opencode/opencode.jsonc
|
|
25
|
-
// which may have custom provider limits that would override models.json entries.
|
|
26
|
-
delete process.env.OPENCODE_MODELS_PATH;
|
|
27
|
-
delete process.env.OPENCODE_MODELS_URL;
|
|
28
|
-
process.env.XDG_CACHE_HOME = tempDir;
|
|
29
|
-
// Point at an empty directory so no opencode.json{c} is read unless the test writes one.
|
|
30
|
-
const emptyConfigDir = join(tempDir, "config", "opencode");
|
|
31
|
-
mkdirSync(emptyConfigDir, { recursive: true });
|
|
32
|
-
process.env.OPENCODE_CONFIG_DIR = emptyConfigDir;
|
|
28
|
+
// Isolate the persisted-cache file under a temp data dir so tests never
|
|
29
|
+
// touch the real ~/.local/share/cortexkit/magic-context cache.
|
|
30
|
+
originalXdgData = process.env.XDG_DATA_HOME;
|
|
31
|
+
process.env.XDG_DATA_HOME = tempDir;
|
|
33
32
|
clearModelsDevCache();
|
|
34
33
|
});
|
|
35
34
|
|
|
36
35
|
afterEach(() => {
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
if (v === undefined) delete process.env[k];
|
|
40
|
-
else process.env[k] = v;
|
|
41
|
-
}
|
|
36
|
+
if (originalXdgData === undefined) delete process.env.XDG_DATA_HOME;
|
|
37
|
+
else process.env.XDG_DATA_HOME = originalXdgData;
|
|
42
38
|
try {
|
|
43
39
|
rmSync(tempDir, { recursive: true, force: true, maxRetries: 10, retryDelay: 100 });
|
|
44
40
|
} catch {
|
|
@@ -47,66 +43,46 @@ describe("models-dev-cache", () => {
|
|
|
47
43
|
clearModelsDevCache();
|
|
48
44
|
});
|
|
49
45
|
|
|
50
|
-
test("
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
46
|
+
test("resolves from the SDK and prefers limit.input over limit.context", async () => {
|
|
47
|
+
// github-copilot / codex shape: input is the max prompt, context the total
|
|
48
|
+
// window. Pressure math must use the input cap (OpenCode's own overflow.ts
|
|
49
|
+
// does the same), so a 400k-context / 272k-input model resolves to 272k.
|
|
50
|
+
await refreshModelLimitsFromApi(
|
|
51
|
+
makeClient([
|
|
52
|
+
{
|
|
53
|
+
id: "github-copilot",
|
|
57
54
|
models: {
|
|
58
|
-
"
|
|
59
|
-
|
|
60
|
-
},
|
|
61
|
-
"github-copilot": {
|
|
62
|
-
models: {
|
|
63
|
-
"gpt-5.3-codex": { limit: { context: 400000 } },
|
|
55
|
+
"gpt-5.3-codex": { limit: { context: 400000, input: 272000 } },
|
|
56
|
+
"legacy-only-context": { limit: { context: 100000 } },
|
|
64
57
|
},
|
|
65
58
|
},
|
|
66
|
-
|
|
59
|
+
]),
|
|
67
60
|
);
|
|
68
61
|
|
|
69
|
-
expect(
|
|
70
|
-
expect(
|
|
71
|
-
expect(
|
|
62
|
+
expect(getSdkContextLimit("github-copilot", "gpt-5.3-codex")).toBe(272000);
|
|
63
|
+
expect(getSdkContextLimit("github-copilot", "legacy-only-context")).toBe(100000);
|
|
64
|
+
expect(getSdkContextLimit("unknown", "unknown")).toBeUndefined();
|
|
72
65
|
});
|
|
73
66
|
|
|
74
|
-
test("
|
|
75
|
-
|
|
76
|
-
//
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
writeFileSync(
|
|
83
|
-
join(opencodeDir, "models.json"),
|
|
84
|
-
JSON.stringify({
|
|
85
|
-
"github-copilot": {
|
|
86
|
-
models: {
|
|
87
|
-
"gpt-5.3-codex": { limit: { context: 400000, input: 272000 } },
|
|
88
|
-
"claude-opus-4.6": { limit: { context: 144000, input: 128000 } },
|
|
89
|
-
// Context-only model (no input) falls back to context.
|
|
90
|
-
"legacy-only-context": { limit: { context: 100000 } },
|
|
91
|
-
},
|
|
67
|
+
test("Codex-OAuth cap is honored: a 400k/272k gpt-5.5 resolves to 272k (not the stale 922k)", async () => {
|
|
68
|
+
// The bug we're fixing: the SDK reports the auth-resolved cap; nothing may
|
|
69
|
+
// out-vote it with a larger stale value.
|
|
70
|
+
await refreshModelLimitsFromApi(
|
|
71
|
+
makeClient([
|
|
72
|
+
{
|
|
73
|
+
id: "openai",
|
|
74
|
+
models: { "gpt-5.5": { limit: { context: 400000, input: 272000 } } },
|
|
92
75
|
},
|
|
93
|
-
|
|
76
|
+
]),
|
|
94
77
|
);
|
|
95
|
-
|
|
96
|
-
//#then
|
|
97
|
-
expect(getModelsDevContextLimit("github-copilot", "gpt-5.3-codex")).toBe(272000);
|
|
98
|
-
expect(getModelsDevContextLimit("github-copilot", "claude-opus-4.6")).toBe(128000);
|
|
99
|
-
expect(getModelsDevContextLimit("github-copilot", "legacy-only-context")).toBe(100000);
|
|
78
|
+
expect(getSdkContextLimit("openai", "gpt-5.5")).toBe(272000);
|
|
100
79
|
});
|
|
101
80
|
|
|
102
|
-
test("derived experimental.modes inherit the effective (input) limit", () => {
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
join(opencodeDir, "models.json"),
|
|
108
|
-
JSON.stringify({
|
|
109
|
-
openai: {
|
|
81
|
+
test("derived experimental.modes inherit the effective (input) limit", async () => {
|
|
82
|
+
await refreshModelLimitsFromApi(
|
|
83
|
+
makeClient([
|
|
84
|
+
{
|
|
85
|
+
id: "openai",
|
|
110
86
|
models: {
|
|
111
87
|
"gpt-5.4": {
|
|
112
88
|
limit: { context: 1050000, input: 922000 },
|
|
@@ -114,249 +90,169 @@ describe("models-dev-cache", () => {
|
|
|
114
90
|
},
|
|
115
91
|
},
|
|
116
92
|
},
|
|
117
|
-
|
|
93
|
+
]),
|
|
118
94
|
);
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
expect(
|
|
122
|
-
expect(getModelsDevContextLimit("openai", "gpt-5.4-fast")).toBe(922000);
|
|
123
|
-
expect(getModelsDevContextLimit("openai", "gpt-5.4-mini")).toBe(922000);
|
|
95
|
+
expect(getSdkContextLimit("openai", "gpt-5.4")).toBe(922000);
|
|
96
|
+
expect(getSdkContextLimit("openai", "gpt-5.4-fast")).toBe(922000);
|
|
97
|
+
expect(getSdkContextLimit("openai", "gpt-5.4-mini")).toBe(922000);
|
|
124
98
|
});
|
|
125
99
|
|
|
126
|
-
test("
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
JSON.stringify({
|
|
135
|
-
provider: {
|
|
136
|
-
"my-proxy": {
|
|
137
|
-
models: {
|
|
138
|
-
"split-model": { limit: { context: 400000, input: 200000 } },
|
|
139
|
-
},
|
|
100
|
+
test("matches a tagged ollama model against its tag-less SDK entry", async () => {
|
|
101
|
+
await refreshModelLimitsFromApi(
|
|
102
|
+
makeClient([
|
|
103
|
+
{
|
|
104
|
+
id: "ollama-cloud",
|
|
105
|
+
models: {
|
|
106
|
+
"deepseek-v4-pro": { limit: { context: 1048576 } },
|
|
107
|
+
"gemma3:27b": { limit: { context: 131072 } },
|
|
140
108
|
},
|
|
141
109
|
},
|
|
142
|
-
|
|
110
|
+
]),
|
|
143
111
|
);
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
// Cleanup: restore env (afterEach also handles this, but we added a new var)
|
|
151
|
-
delete process.env.OPENCODE_CONFIG_DIR;
|
|
112
|
+
// Tagged invocation falls back to the tag-less entry.
|
|
113
|
+
expect(getSdkContextLimit("ollama-cloud", "deepseek-v4-pro:cloud")).toBe(1048576);
|
|
114
|
+
// Exact tagged match still wins (no wrongful collapse).
|
|
115
|
+
expect(getSdkContextLimit("ollama-cloud", "gemma3:27b")).toBe(131072);
|
|
116
|
+
// Unknown tagged model with no tag-less base stays undefined.
|
|
117
|
+
expect(getSdkContextLimit("ollama-cloud", "nonexistent:cloud")).toBeUndefined();
|
|
152
118
|
});
|
|
153
119
|
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
models: {
|
|
164
|
-
"gpt-5.3-codex": {
|
|
165
|
-
limit: { context: 400000, input: 272000 },
|
|
166
|
-
},
|
|
167
|
-
},
|
|
168
|
-
},
|
|
169
|
-
],
|
|
120
|
+
describe("sanity bounds [20k, 3M]", () => {
|
|
121
|
+
test("rejects an implausibly small limit (torn-read garbage like 6748)", async () => {
|
|
122
|
+
await refreshModelLimitsFromApi(
|
|
123
|
+
makeClient([
|
|
124
|
+
{
|
|
125
|
+
id: "ollama-cloud",
|
|
126
|
+
// 6748 is smaller than a single system prompt — impossible
|
|
127
|
+
// as a real limit; must be rejected, not trusted.
|
|
128
|
+
models: { "deepseek-v4-pro": { limit: { context: 6748 } } },
|
|
170
129
|
},
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
130
|
+
]),
|
|
131
|
+
);
|
|
132
|
+
expect(getSdkContextLimit("ollama-cloud", "deepseek-v4-pro")).toBeUndefined();
|
|
133
|
+
});
|
|
175
134
|
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
135
|
+
test("rejects a below-floor 8192 num_ctx default", async () => {
|
|
136
|
+
await refreshModelLimitsFromApi(
|
|
137
|
+
makeClient([{ id: "p", models: { m: { limit: { context: 8192 } } } }]),
|
|
138
|
+
);
|
|
139
|
+
expect(getSdkContextLimit("p", "m")).toBeUndefined();
|
|
140
|
+
});
|
|
179
141
|
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
142
|
+
test("rejects an impossibly large limit (> 3M)", async () => {
|
|
143
|
+
await refreshModelLimitsFromApi(
|
|
144
|
+
makeClient([{ id: "p", models: { m: { limit: { context: 5_000_000 } } } }]),
|
|
145
|
+
);
|
|
146
|
+
expect(getSdkContextLimit("p", "m")).toBeUndefined();
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
test("accepts values exactly on the bounds", async () => {
|
|
150
|
+
await refreshModelLimitsFromApi(
|
|
151
|
+
makeClient([
|
|
152
|
+
{
|
|
153
|
+
id: "p",
|
|
154
|
+
models: {
|
|
155
|
+
lo: { limit: { context: 20000 } },
|
|
156
|
+
hi: { limit: { context: 3000000 } },
|
|
191
157
|
},
|
|
192
158
|
},
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
expect(getModelsDevContextLimit("github-copilot", "gpt-5.4")).toBe(400000);
|
|
199
|
-
// Derived mode IDs inherit parent context.
|
|
200
|
-
expect(getModelsDevContextLimit("github-copilot", "gpt-5.4-fast")).toBe(400000);
|
|
201
|
-
expect(getModelsDevContextLimit("github-copilot", "gpt-5.4-high")).toBe(400000);
|
|
202
|
-
});
|
|
203
|
-
|
|
204
|
-
test("OPENCODE_MODELS_PATH env overrides default path", () => {
|
|
205
|
-
// Write real file somewhere unexpected.
|
|
206
|
-
const customPath = join(tempDir, "elsewhere", "my-models.json");
|
|
207
|
-
mkdirSync(join(tempDir, "elsewhere"), { recursive: true });
|
|
208
|
-
writeFileSync(
|
|
209
|
-
customPath,
|
|
210
|
-
JSON.stringify({
|
|
211
|
-
anthropic: { models: { "claude-4": { limit: { context: 1000000 } } } },
|
|
212
|
-
}),
|
|
213
|
-
);
|
|
214
|
-
process.env.OPENCODE_MODELS_PATH = customPath;
|
|
215
|
-
clearModelsDevCache();
|
|
216
|
-
|
|
217
|
-
expect(getModelsDevContextLimit("anthropic", "claude-4")).toBe(1000000);
|
|
159
|
+
]),
|
|
160
|
+
);
|
|
161
|
+
expect(getSdkContextLimit("p", "lo")).toBe(20000);
|
|
162
|
+
expect(getSdkContextLimit("p", "hi")).toBe(3000000);
|
|
163
|
+
});
|
|
218
164
|
});
|
|
219
165
|
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
166
|
+
describe("persisted cache (cold start)", () => {
|
|
167
|
+
test("seeds from the persisted file after a clear (restart simulation)", async () => {
|
|
168
|
+
// First run: warm + persist.
|
|
169
|
+
await refreshModelLimitsFromApi(
|
|
170
|
+
makeClient([{ id: "openai", models: { "gpt-5.5": { limit: { input: 272000 } } } }]),
|
|
171
|
+
);
|
|
172
|
+
expect(getSdkContextLimit("openai", "gpt-5.5")).toBe(272000);
|
|
173
|
+
|
|
174
|
+
// Simulate a restart: in-memory cache gone, but the persisted file
|
|
175
|
+
// remains under XDG_DATA_HOME. The next lookup seeds from disk.
|
|
176
|
+
clearModelsDevCache();
|
|
177
|
+
expect(getModelsDevCacheState().apiLoaded).toBe(false);
|
|
178
|
+
expect(getSdkContextLimit("openai", "gpt-5.5")).toBe(272000);
|
|
179
|
+
// Seeding populated the in-memory cache.
|
|
180
|
+
expect(getModelsDevCacheState().apiLoaded).toBe(true);
|
|
181
|
+
});
|
|
235
182
|
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
183
|
+
test("does not persist or seed insane values", async () => {
|
|
184
|
+
await refreshModelLimitsFromApi(
|
|
185
|
+
makeClient([
|
|
186
|
+
{
|
|
187
|
+
id: "p",
|
|
188
|
+
models: {
|
|
189
|
+
good: { limit: { context: 200000 } },
|
|
190
|
+
bad: { limit: { context: 6748 } },
|
|
191
|
+
},
|
|
192
|
+
},
|
|
193
|
+
]),
|
|
194
|
+
);
|
|
195
|
+
clearModelsDevCache();
|
|
196
|
+
expect(getSdkContextLimit("p", "good")).toBe(200000);
|
|
197
|
+
expect(getSdkContextLimit("p", "bad")).toBeUndefined();
|
|
198
|
+
});
|
|
239
199
|
});
|
|
240
200
|
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
// Mock client providing a LARGER value via API.
|
|
256
|
-
const mockClient = {
|
|
257
|
-
config: {
|
|
258
|
-
providers: async () => ({
|
|
259
|
-
data: {
|
|
260
|
-
providers: [
|
|
261
|
-
{
|
|
262
|
-
id: "anthropic",
|
|
263
|
-
models: {
|
|
264
|
-
"claude-4": { limit: { context: 1000000 } },
|
|
265
|
-
},
|
|
201
|
+
describe("startup retry", () => {
|
|
202
|
+
test("retries when the provider payload is empty, then succeeds", async () => {
|
|
203
|
+
let calls = 0;
|
|
204
|
+
const client = {
|
|
205
|
+
config: {
|
|
206
|
+
providers: async () => {
|
|
207
|
+
calls++;
|
|
208
|
+
if (calls === 1) return { data: { providers: [] } };
|
|
209
|
+
return {
|
|
210
|
+
data: {
|
|
211
|
+
providers: [
|
|
212
|
+
{ id: "p", models: { m: { limit: { context: 200000 } } } },
|
|
213
|
+
],
|
|
266
214
|
},
|
|
267
|
-
|
|
215
|
+
};
|
|
268
216
|
},
|
|
269
|
-
}),
|
|
270
|
-
},
|
|
271
|
-
};
|
|
272
|
-
await refreshModelLimitsFromApi(mockClient);
|
|
273
|
-
|
|
274
|
-
// Larger (API) value wins.
|
|
275
|
-
expect(getModelsDevContextLimit("anthropic", "claude-4")).toBe(1000000);
|
|
276
|
-
|
|
277
|
-
const state = getModelsDevCacheState();
|
|
278
|
-
expect(state.apiLoaded).toBe(true);
|
|
279
|
-
expect(state.apiCount).toBe(1);
|
|
280
|
-
});
|
|
281
|
-
|
|
282
|
-
test("file value wins when the live API reports a smaller (wrong) limit (issue #117)", async () => {
|
|
283
|
-
// The ollama-cloud scenario: models.dev has the correct large window, but
|
|
284
|
-
// ollama reports its tiny default num_ctx via the live /config/providers
|
|
285
|
-
// API. The larger, correct file value must win so pressure isn't bogus.
|
|
286
|
-
const opencodeDir = join(tempDir, "opencode");
|
|
287
|
-
mkdirSync(opencodeDir, { recursive: true });
|
|
288
|
-
writeFileSync(
|
|
289
|
-
join(opencodeDir, "models.json"),
|
|
290
|
-
JSON.stringify({
|
|
291
|
-
"ollama-cloud": {
|
|
292
|
-
models: { "deepseek-v4-pro": { limit: { context: 1048576 } } },
|
|
293
217
|
},
|
|
294
|
-
}
|
|
295
|
-
|
|
218
|
+
};
|
|
219
|
+
await refreshModelLimitsFromApi(client, { retries: 2, retryDelayMs: 1 });
|
|
220
|
+
expect(calls).toBe(2);
|
|
221
|
+
expect(getSdkContextLimit("p", "m")).toBe(200000);
|
|
222
|
+
});
|
|
296
223
|
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
"
|
|
307
|
-
|
|
224
|
+
test("stops early on first successful load (no wasted retries)", async () => {
|
|
225
|
+
let calls = 0;
|
|
226
|
+
const client = {
|
|
227
|
+
config: {
|
|
228
|
+
providers: async () => {
|
|
229
|
+
calls++;
|
|
230
|
+
return {
|
|
231
|
+
data: {
|
|
232
|
+
providers: [
|
|
233
|
+
{ id: "p", models: { m: { limit: { context: 200000 } } } },
|
|
234
|
+
],
|
|
308
235
|
},
|
|
309
|
-
|
|
310
|
-
},
|
|
311
|
-
}),
|
|
312
|
-
},
|
|
313
|
-
};
|
|
314
|
-
await refreshModelLimitsFromApi(mockClient);
|
|
315
|
-
|
|
316
|
-
// Larger (file/models.dev) value wins, not the tiny live-API value.
|
|
317
|
-
expect(getModelsDevContextLimit("ollama-cloud", "deepseek-v4-pro")).toBe(1048576);
|
|
318
|
-
});
|
|
319
|
-
|
|
320
|
-
test("matches a tagged ollama model against its tag-less models.dev entry (issue #117)", () => {
|
|
321
|
-
// ollama invokes cloud models with a tag (deepseek-v4-pro:cloud) while
|
|
322
|
-
// models.dev stores them tag-less (deepseek-v4-pro).
|
|
323
|
-
const opencodeDir = join(tempDir, "opencode");
|
|
324
|
-
mkdirSync(opencodeDir, { recursive: true });
|
|
325
|
-
writeFileSync(
|
|
326
|
-
join(opencodeDir, "models.json"),
|
|
327
|
-
JSON.stringify({
|
|
328
|
-
"ollama-cloud": {
|
|
329
|
-
models: {
|
|
330
|
-
"deepseek-v4-pro": { limit: { context: 1048576 } },
|
|
331
|
-
// A legitimately-tagged model must still match exactly.
|
|
332
|
-
"gemma3:27b": { limit: { context: 131072 } },
|
|
236
|
+
};
|
|
333
237
|
},
|
|
334
238
|
},
|
|
335
|
-
}
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
expect(getModelsDevContextLimit("ollama-cloud", "deepseek-v4-pro:cloud")).toBe(1048576);
|
|
340
|
-
// Exact tagged match still wins (no wrongful collapse).
|
|
341
|
-
expect(getModelsDevContextLimit("ollama-cloud", "gemma3:27b")).toBe(131072);
|
|
342
|
-
// Unknown tagged model with no tag-less base stays undefined.
|
|
343
|
-
expect(getModelsDevContextLimit("ollama-cloud", "nonexistent:cloud")).toBeUndefined();
|
|
239
|
+
};
|
|
240
|
+
await refreshModelLimitsFromApi(client, { retries: 3, retryDelayMs: 1 });
|
|
241
|
+
expect(calls).toBe(1);
|
|
242
|
+
});
|
|
344
243
|
});
|
|
345
244
|
|
|
346
|
-
test("
|
|
347
|
-
// Undefined data.
|
|
245
|
+
test("tolerates empty / malformed / thrown responses without populating", async () => {
|
|
348
246
|
await refreshModelLimitsFromApi({
|
|
349
247
|
config: { providers: async () => ({ data: undefined }) },
|
|
350
248
|
});
|
|
351
249
|
expect(getModelsDevCacheState().apiLoaded).toBe(false);
|
|
352
250
|
|
|
353
|
-
// Non-array providers.
|
|
354
251
|
await refreshModelLimitsFromApi({
|
|
355
252
|
config: { providers: async () => ({ data: { providers: "not an array" } }) },
|
|
356
253
|
});
|
|
357
254
|
expect(getModelsDevCacheState().apiLoaded).toBe(false);
|
|
358
255
|
|
|
359
|
-
// Thrown error.
|
|
360
256
|
await refreshModelLimitsFromApi({
|
|
361
257
|
config: {
|
|
362
258
|
providers: async () => {
|
|
@@ -367,96 +263,32 @@ describe("models-dev-cache", () => {
|
|
|
367
263
|
expect(getModelsDevCacheState().apiLoaded).toBe(false);
|
|
368
264
|
});
|
|
369
265
|
|
|
370
|
-
test("repeated
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
id: "p",
|
|
380
|
-
models: {
|
|
381
|
-
m1: { limit: { context: 100 } },
|
|
382
|
-
m2: { limit: { context: 100 } },
|
|
383
|
-
m3: { limit: { context: 100 } },
|
|
384
|
-
},
|
|
385
|
-
},
|
|
386
|
-
],
|
|
266
|
+
test("repeated refreshes replace cache state without corruption", async () => {
|
|
267
|
+
const clientA = makeClient([
|
|
268
|
+
{
|
|
269
|
+
id: "p",
|
|
270
|
+
models: {
|
|
271
|
+
m1: { limit: { context: 200000 } },
|
|
272
|
+
m2: { limit: { context: 200000 } },
|
|
273
|
+
m3: { limit: { context: 200000 } },
|
|
274
|
+
},
|
|
387
275
|
},
|
|
388
|
-
|
|
389
|
-
const
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
id: "p",
|
|
394
|
-
models: {
|
|
395
|
-
m1: { limit: { context: 100 } },
|
|
396
|
-
m2: { limit: { context: 100 } },
|
|
397
|
-
},
|
|
398
|
-
},
|
|
399
|
-
],
|
|
276
|
+
]);
|
|
277
|
+
const clientB = makeClient([
|
|
278
|
+
{
|
|
279
|
+
id: "p",
|
|
280
|
+
models: { m1: { limit: { context: 200000 } }, m2: { limit: { context: 200000 } } },
|
|
400
281
|
},
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
const clientA = { config: { providers: async () => sizeA } };
|
|
404
|
-
const clientB = { config: { providers: async () => sizeB } };
|
|
405
|
-
|
|
406
|
-
await refreshModelLimitsFromApi(clientA);
|
|
407
|
-
expect(getModelsDevCacheState().apiCount).toBe(3);
|
|
408
|
-
|
|
409
|
-
await refreshModelLimitsFromApi(clientB);
|
|
410
|
-
expect(getModelsDevCacheState().apiCount).toBe(2);
|
|
282
|
+
]);
|
|
411
283
|
|
|
412
284
|
await refreshModelLimitsFromApi(clientA);
|
|
413
285
|
expect(getModelsDevCacheState().apiCount).toBe(3);
|
|
414
|
-
|
|
415
286
|
await refreshModelLimitsFromApi(clientB);
|
|
416
287
|
expect(getModelsDevCacheState().apiCount).toBe(2);
|
|
417
|
-
|
|
418
288
|
await refreshModelLimitsFromApi(clientA);
|
|
419
289
|
expect(getModelsDevCacheState().apiCount).toBe(3);
|
|
420
290
|
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
// a logging concern. Last call was clientA → all three models present.
|
|
424
|
-
expect(getModelsDevContextLimit("p", "m1")).toBe(100);
|
|
425
|
-
expect(getModelsDevContextLimit("p", "m2")).toBe(100);
|
|
426
|
-
expect(getModelsDevContextLimit("p", "m3")).toBe(100);
|
|
427
|
-
});
|
|
428
|
-
|
|
429
|
-
test("falls back to file layer when API provider/model key is missing", async () => {
|
|
430
|
-
const opencodeDir = join(tempDir, "opencode");
|
|
431
|
-
mkdirSync(opencodeDir, { recursive: true });
|
|
432
|
-
writeFileSync(
|
|
433
|
-
join(opencodeDir, "models.json"),
|
|
434
|
-
JSON.stringify({
|
|
435
|
-
anthropic: { models: { "claude-only-in-file": { limit: { context: 777777 } } } },
|
|
436
|
-
}),
|
|
437
|
-
);
|
|
438
|
-
|
|
439
|
-
const mockClient = {
|
|
440
|
-
config: {
|
|
441
|
-
providers: async () => ({
|
|
442
|
-
data: {
|
|
443
|
-
providers: [
|
|
444
|
-
{
|
|
445
|
-
id: "anthropic",
|
|
446
|
-
models: {
|
|
447
|
-
"claude-only-in-api": { limit: { context: 888888 } },
|
|
448
|
-
},
|
|
449
|
-
},
|
|
450
|
-
],
|
|
451
|
-
},
|
|
452
|
-
}),
|
|
453
|
-
},
|
|
454
|
-
};
|
|
455
|
-
await refreshModelLimitsFromApi(mockClient);
|
|
456
|
-
|
|
457
|
-
// API-only key comes from API.
|
|
458
|
-
expect(getModelsDevContextLimit("anthropic", "claude-only-in-api")).toBe(888888);
|
|
459
|
-
// File-only key falls through to file layer.
|
|
460
|
-
expect(getModelsDevContextLimit("anthropic", "claude-only-in-file")).toBe(777777);
|
|
291
|
+
expect(getSdkContextLimit("p", "m1")).toBe(200000);
|
|
292
|
+
expect(getSdkContextLimit("p", "m3")).toBe(200000);
|
|
461
293
|
});
|
|
462
294
|
});
|