@cortexkit/opencode-magic-context 0.22.0 → 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/README.md +10 -0
- package/dist/config/agent-disable.d.ts +0 -9
- package/dist/config/agent-disable.d.ts.map +1 -1
- package/dist/config/index.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/config/schema/magic-context.d.ts +15 -0
- package/dist/config/schema/magic-context.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/memory/embedding-identity.d.ts.map +1 -1
- package/dist/features/magic-context/memory/embedding-openai.d.ts +6 -0
- package/dist/features/magic-context/memory/embedding-openai.d.ts.map +1 -1
- package/dist/features/magic-context/memory/embedding-probe.d.ts +5 -0
- package/dist/features/magic-context/memory/embedding-probe.d.ts.map +1 -1
- package/dist/features/magic-context/memory/embedding.d.ts.map +1 -1
- 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/auto-update-checker/cache.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 +667 -266
- package/dist/plugin/dream-timer.d.ts.map +1 -1
- package/dist/plugin/event.d.ts +10 -0
- package/dist/plugin/event.d.ts.map +1 -1
- package/dist/plugin/rpc-handlers.d.ts.map +1 -1
- package/dist/shared/announcement.d.ts +16 -0
- package/dist/shared/announcement.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 +4 -4
- package/src/shared/announcement.test.ts +23 -7
- package/src/shared/announcement.ts +24 -1
- package/src/shared/conflict-detector.test.ts +15 -2
- package/src/shared/conflict-fixer.test.ts +5 -1
- package/src/shared/models-dev-cache.test.ts +200 -300
- package/src/shared/models-dev-cache.ts +184 -176
- package/src/shared/opencode-compaction-detector.test.ts +10 -2
- package/src/shared/rpc-client.test.ts +5 -1
- 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,108 +1,88 @@
|
|
|
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
|
-
|
|
40
|
-
|
|
36
|
+
if (originalXdgData === undefined) delete process.env.XDG_DATA_HOME;
|
|
37
|
+
else process.env.XDG_DATA_HOME = originalXdgData;
|
|
38
|
+
try {
|
|
39
|
+
rmSync(tempDir, { recursive: true, force: true, maxRetries: 10, retryDelay: 100 });
|
|
40
|
+
} catch {
|
|
41
|
+
/* Ignore EBUSY on Windows */
|
|
41
42
|
}
|
|
42
|
-
rmSync(tempDir, { recursive: true, force: true });
|
|
43
43
|
clearModelsDevCache();
|
|
44
44
|
});
|
|
45
45
|
|
|
46
|
-
test("
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
"claude-sonnet-4-6": { limit: { context: 200000 } },
|
|
55
|
-
},
|
|
56
|
-
},
|
|
57
|
-
"github-copilot": {
|
|
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",
|
|
58
54
|
models: {
|
|
59
|
-
"gpt-5.3-codex": { limit: { context: 400000 } },
|
|
55
|
+
"gpt-5.3-codex": { limit: { context: 400000, input: 272000 } },
|
|
56
|
+
"legacy-only-context": { limit: { context: 100000 } },
|
|
60
57
|
},
|
|
61
58
|
},
|
|
62
|
-
|
|
59
|
+
]),
|
|
63
60
|
);
|
|
64
61
|
|
|
65
|
-
expect(
|
|
66
|
-
expect(
|
|
67
|
-
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();
|
|
68
65
|
});
|
|
69
66
|
|
|
70
|
-
test("
|
|
71
|
-
|
|
72
|
-
//
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
writeFileSync(
|
|
79
|
-
join(opencodeDir, "models.json"),
|
|
80
|
-
JSON.stringify({
|
|
81
|
-
"github-copilot": {
|
|
82
|
-
models: {
|
|
83
|
-
"gpt-5.3-codex": { limit: { context: 400000, input: 272000 } },
|
|
84
|
-
"claude-opus-4.6": { limit: { context: 144000, input: 128000 } },
|
|
85
|
-
// Context-only model (no input) falls back to context.
|
|
86
|
-
"legacy-only-context": { limit: { context: 100000 } },
|
|
87
|
-
},
|
|
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 } } },
|
|
88
75
|
},
|
|
89
|
-
|
|
76
|
+
]),
|
|
90
77
|
);
|
|
91
|
-
|
|
92
|
-
//#then
|
|
93
|
-
expect(getModelsDevContextLimit("github-copilot", "gpt-5.3-codex")).toBe(272000);
|
|
94
|
-
expect(getModelsDevContextLimit("github-copilot", "claude-opus-4.6")).toBe(128000);
|
|
95
|
-
expect(getModelsDevContextLimit("github-copilot", "legacy-only-context")).toBe(100000);
|
|
78
|
+
expect(getSdkContextLimit("openai", "gpt-5.5")).toBe(272000);
|
|
96
79
|
});
|
|
97
80
|
|
|
98
|
-
test("derived experimental.modes inherit the effective (input) limit", () => {
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
join(opencodeDir, "models.json"),
|
|
104
|
-
JSON.stringify({
|
|
105
|
-
openai: {
|
|
81
|
+
test("derived experimental.modes inherit the effective (input) limit", async () => {
|
|
82
|
+
await refreshModelLimitsFromApi(
|
|
83
|
+
makeClient([
|
|
84
|
+
{
|
|
85
|
+
id: "openai",
|
|
106
86
|
models: {
|
|
107
87
|
"gpt-5.4": {
|
|
108
88
|
limit: { context: 1050000, input: 922000 },
|
|
@@ -110,185 +90,169 @@ describe("models-dev-cache", () => {
|
|
|
110
90
|
},
|
|
111
91
|
},
|
|
112
92
|
},
|
|
113
|
-
|
|
93
|
+
]),
|
|
114
94
|
);
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
expect(
|
|
118
|
-
expect(getModelsDevContextLimit("openai", "gpt-5.4-fast")).toBe(922000);
|
|
119
|
-
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);
|
|
120
98
|
});
|
|
121
99
|
|
|
122
|
-
test("
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
JSON.stringify({
|
|
131
|
-
provider: {
|
|
132
|
-
"my-proxy": {
|
|
133
|
-
models: {
|
|
134
|
-
"split-model": { limit: { context: 400000, input: 200000 } },
|
|
135
|
-
},
|
|
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 } },
|
|
136
108
|
},
|
|
137
109
|
},
|
|
138
|
-
|
|
110
|
+
]),
|
|
139
111
|
);
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
// Cleanup: restore env (afterEach also handles this, but we added a new var)
|
|
147
|
-
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();
|
|
148
118
|
});
|
|
149
119
|
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
models: {
|
|
160
|
-
"gpt-5.3-codex": {
|
|
161
|
-
limit: { context: 400000, input: 272000 },
|
|
162
|
-
},
|
|
163
|
-
},
|
|
164
|
-
},
|
|
165
|
-
],
|
|
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 } } },
|
|
166
129
|
},
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
130
|
+
]),
|
|
131
|
+
);
|
|
132
|
+
expect(getSdkContextLimit("ollama-cloud", "deepseek-v4-pro")).toBeUndefined();
|
|
133
|
+
});
|
|
171
134
|
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
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
|
+
});
|
|
175
141
|
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
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 } },
|
|
187
157
|
},
|
|
188
158
|
},
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
expect(getModelsDevContextLimit("github-copilot", "gpt-5.4")).toBe(400000);
|
|
195
|
-
// Derived mode IDs inherit parent context.
|
|
196
|
-
expect(getModelsDevContextLimit("github-copilot", "gpt-5.4-fast")).toBe(400000);
|
|
197
|
-
expect(getModelsDevContextLimit("github-copilot", "gpt-5.4-high")).toBe(400000);
|
|
198
|
-
});
|
|
199
|
-
|
|
200
|
-
test("OPENCODE_MODELS_PATH env overrides default path", () => {
|
|
201
|
-
// Write real file somewhere unexpected.
|
|
202
|
-
const customPath = join(tempDir, "elsewhere", "my-models.json");
|
|
203
|
-
mkdirSync(join(tempDir, "elsewhere"), { recursive: true });
|
|
204
|
-
writeFileSync(
|
|
205
|
-
customPath,
|
|
206
|
-
JSON.stringify({
|
|
207
|
-
anthropic: { models: { "claude-4": { limit: { context: 1000000 } } } },
|
|
208
|
-
}),
|
|
209
|
-
);
|
|
210
|
-
process.env.OPENCODE_MODELS_PATH = customPath;
|
|
211
|
-
clearModelsDevCache();
|
|
212
|
-
|
|
213
|
-
expect(getModelsDevContextLimit("anthropic", "claude-4")).toBe(1000000);
|
|
159
|
+
]),
|
|
160
|
+
);
|
|
161
|
+
expect(getSdkContextLimit("p", "lo")).toBe(20000);
|
|
162
|
+
expect(getSdkContextLimit("p", "hi")).toBe(3000000);
|
|
163
|
+
});
|
|
214
164
|
});
|
|
215
165
|
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
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
|
+
});
|
|
231
182
|
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
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
|
+
});
|
|
235
199
|
});
|
|
236
200
|
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
// Mock client providing DIFFERENT value via API.
|
|
252
|
-
const mockClient = {
|
|
253
|
-
config: {
|
|
254
|
-
providers: async () => ({
|
|
255
|
-
data: {
|
|
256
|
-
providers: [
|
|
257
|
-
{
|
|
258
|
-
id: "anthropic",
|
|
259
|
-
models: {
|
|
260
|
-
"claude-4": { limit: { context: 1000000 } },
|
|
261
|
-
},
|
|
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
|
+
],
|
|
262
214
|
},
|
|
263
|
-
|
|
215
|
+
};
|
|
264
216
|
},
|
|
265
|
-
}
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
expect(getModelsDevContextLimit("anthropic", "claude-4")).toBe(1000000);
|
|
217
|
+
},
|
|
218
|
+
};
|
|
219
|
+
await refreshModelLimitsFromApi(client, { retries: 2, retryDelayMs: 1 });
|
|
220
|
+
expect(calls).toBe(2);
|
|
221
|
+
expect(getSdkContextLimit("p", "m")).toBe(200000);
|
|
222
|
+
});
|
|
272
223
|
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
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
|
+
],
|
|
235
|
+
},
|
|
236
|
+
};
|
|
237
|
+
},
|
|
238
|
+
},
|
|
239
|
+
};
|
|
240
|
+
await refreshModelLimitsFromApi(client, { retries: 3, retryDelayMs: 1 });
|
|
241
|
+
expect(calls).toBe(1);
|
|
242
|
+
});
|
|
276
243
|
});
|
|
277
244
|
|
|
278
|
-
test("
|
|
279
|
-
// Undefined data.
|
|
245
|
+
test("tolerates empty / malformed / thrown responses without populating", async () => {
|
|
280
246
|
await refreshModelLimitsFromApi({
|
|
281
247
|
config: { providers: async () => ({ data: undefined }) },
|
|
282
248
|
});
|
|
283
249
|
expect(getModelsDevCacheState().apiLoaded).toBe(false);
|
|
284
250
|
|
|
285
|
-
// Non-array providers.
|
|
286
251
|
await refreshModelLimitsFromApi({
|
|
287
252
|
config: { providers: async () => ({ data: { providers: "not an array" } }) },
|
|
288
253
|
});
|
|
289
254
|
expect(getModelsDevCacheState().apiLoaded).toBe(false);
|
|
290
255
|
|
|
291
|
-
// Thrown error.
|
|
292
256
|
await refreshModelLimitsFromApi({
|
|
293
257
|
config: {
|
|
294
258
|
providers: async () => {
|
|
@@ -299,96 +263,32 @@ describe("models-dev-cache", () => {
|
|
|
299
263
|
expect(getModelsDevCacheState().apiLoaded).toBe(false);
|
|
300
264
|
});
|
|
301
265
|
|
|
302
|
-
test("repeated
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
id: "p",
|
|
312
|
-
models: {
|
|
313
|
-
m1: { limit: { context: 100 } },
|
|
314
|
-
m2: { limit: { context: 100 } },
|
|
315
|
-
m3: { limit: { context: 100 } },
|
|
316
|
-
},
|
|
317
|
-
},
|
|
318
|
-
],
|
|
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
|
+
},
|
|
319
275
|
},
|
|
320
|
-
|
|
321
|
-
const
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
id: "p",
|
|
326
|
-
models: {
|
|
327
|
-
m1: { limit: { context: 100 } },
|
|
328
|
-
m2: { limit: { context: 100 } },
|
|
329
|
-
},
|
|
330
|
-
},
|
|
331
|
-
],
|
|
276
|
+
]);
|
|
277
|
+
const clientB = makeClient([
|
|
278
|
+
{
|
|
279
|
+
id: "p",
|
|
280
|
+
models: { m1: { limit: { context: 200000 } }, m2: { limit: { context: 200000 } } },
|
|
332
281
|
},
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
const clientA = { config: { providers: async () => sizeA } };
|
|
336
|
-
const clientB = { config: { providers: async () => sizeB } };
|
|
282
|
+
]);
|
|
337
283
|
|
|
338
284
|
await refreshModelLimitsFromApi(clientA);
|
|
339
285
|
expect(getModelsDevCacheState().apiCount).toBe(3);
|
|
340
|
-
|
|
341
286
|
await refreshModelLimitsFromApi(clientB);
|
|
342
287
|
expect(getModelsDevCacheState().apiCount).toBe(2);
|
|
343
|
-
|
|
344
288
|
await refreshModelLimitsFromApi(clientA);
|
|
345
289
|
expect(getModelsDevCacheState().apiCount).toBe(3);
|
|
346
290
|
|
|
347
|
-
|
|
348
|
-
expect(
|
|
349
|
-
|
|
350
|
-
await refreshModelLimitsFromApi(clientA);
|
|
351
|
-
expect(getModelsDevCacheState().apiCount).toBe(3);
|
|
352
|
-
|
|
353
|
-
// The cache itself still updates on every call (model contents are correct
|
|
354
|
-
// for whichever provider response just arrived). The suppression is purely
|
|
355
|
-
// a logging concern. Last call was clientA → all three models present.
|
|
356
|
-
expect(getModelsDevContextLimit("p", "m1")).toBe(100);
|
|
357
|
-
expect(getModelsDevContextLimit("p", "m2")).toBe(100);
|
|
358
|
-
expect(getModelsDevContextLimit("p", "m3")).toBe(100);
|
|
359
|
-
});
|
|
360
|
-
|
|
361
|
-
test("falls back to file layer when API provider/model key is missing", async () => {
|
|
362
|
-
const opencodeDir = join(tempDir, "opencode");
|
|
363
|
-
mkdirSync(opencodeDir, { recursive: true });
|
|
364
|
-
writeFileSync(
|
|
365
|
-
join(opencodeDir, "models.json"),
|
|
366
|
-
JSON.stringify({
|
|
367
|
-
anthropic: { models: { "claude-only-in-file": { limit: { context: 777777 } } } },
|
|
368
|
-
}),
|
|
369
|
-
);
|
|
370
|
-
|
|
371
|
-
const mockClient = {
|
|
372
|
-
config: {
|
|
373
|
-
providers: async () => ({
|
|
374
|
-
data: {
|
|
375
|
-
providers: [
|
|
376
|
-
{
|
|
377
|
-
id: "anthropic",
|
|
378
|
-
models: {
|
|
379
|
-
"claude-only-in-api": { limit: { context: 888888 } },
|
|
380
|
-
},
|
|
381
|
-
},
|
|
382
|
-
],
|
|
383
|
-
},
|
|
384
|
-
}),
|
|
385
|
-
},
|
|
386
|
-
};
|
|
387
|
-
await refreshModelLimitsFromApi(mockClient);
|
|
388
|
-
|
|
389
|
-
// API-only key comes from API.
|
|
390
|
-
expect(getModelsDevContextLimit("anthropic", "claude-only-in-api")).toBe(888888);
|
|
391
|
-
// File-only key falls through to file layer.
|
|
392
|
-
expect(getModelsDevContextLimit("anthropic", "claude-only-in-file")).toBe(777777);
|
|
291
|
+
expect(getSdkContextLimit("p", "m1")).toBe(200000);
|
|
292
|
+
expect(getSdkContextLimit("p", "m3")).toBe(200000);
|
|
393
293
|
});
|
|
394
294
|
});
|