@golba98/codexa 1.0.2 → 1.0.4

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.
Files changed (206) hide show
  1. package/README.md +396 -100
  2. package/bin/codexa.js +62 -144
  3. package/package.json +14 -8
  4. package/src/app.tsx +596 -306
  5. package/src/commands/handler.ts +6 -6
  6. package/src/config/buildInfo.ts +2 -2
  7. package/src/config/layeredConfig.ts +1 -1
  8. package/src/config/persistence.ts +10 -0
  9. package/src/config/runtimeConfig.ts +1 -1
  10. package/src/config/settings.ts +8 -16
  11. package/src/config/trustStore.ts +1 -1
  12. package/src/config/updateCheckCache.ts +19 -1
  13. package/src/core/README.md +52 -0
  14. package/src/core/agent/loop.ts +282 -0
  15. package/src/core/agent/protocol.ts +211 -0
  16. package/src/core/agent/tools.ts +414 -0
  17. package/src/core/{codexExecArgs.ts → codex/codexExecArgs.ts} +2 -2
  18. package/src/core/{codexLaunch.ts → codex/codexLaunch.ts} +3 -3
  19. package/src/core/{codexPrompt.ts → codex/codexPrompt.ts} +3 -3
  20. package/src/core/debug/modelStateDebug.ts +34 -0
  21. package/src/core/executables/antigravityExecutable.ts +48 -0
  22. package/src/core/executables/codexExecutable.ts +1 -0
  23. package/src/core/executables/executableResolver.ts +65 -43
  24. package/src/core/perf/renderDebug.ts +10 -6
  25. package/src/core/process/processValidation.ts +9 -5
  26. package/src/core/providerLauncher/launcher.ts +59 -42
  27. package/src/core/providerLauncher/registry.ts +30 -14
  28. package/src/core/providerLauncher/types.ts +11 -9
  29. package/src/core/providerLauncher/workspaceConfig.ts +41 -26
  30. package/src/core/providerRuntime/anthropic.ts +7 -1
  31. package/src/core/providerRuntime/antigravity.ts +305 -0
  32. package/src/core/providerRuntime/claudeCodeDiscovery.ts +268 -22
  33. package/src/core/providerRuntime/claudeCodeDiscoveryDebug.ts +55 -0
  34. package/src/core/providerRuntime/contextMetadata.ts +12 -24
  35. package/src/core/providerRuntime/local.ts +129 -51
  36. package/src/core/providerRuntime/models.ts +22 -11
  37. package/src/core/providerRuntime/registry.ts +58 -31
  38. package/src/core/providerRuntime/types.ts +19 -14
  39. package/src/core/providers/codexSubprocess.ts +2 -2
  40. package/src/core/providers/types.ts +1 -1
  41. package/src/core/{attachments.ts → shared/attachments.ts} +27 -4
  42. package/src/core/{cleanupFastFail.ts → shared/cleanupFastFail.ts} +1 -1
  43. package/src/core/{hollowResponseFormat.ts → shared/hollowResponseFormat.ts} +1 -1
  44. package/src/core/terminal/clearFrameBoundary.ts +814 -0
  45. package/src/core/terminal/frameLock.ts +109 -0
  46. package/src/core/terminal/inkRenderReset.ts +123 -0
  47. package/src/core/terminal/terminalControl.ts +22 -0
  48. package/src/core/terminal/terminalTitle.ts +16 -102
  49. package/src/core/version/channel.ts +23 -0
  50. package/src/core/version/updateCheck.ts +193 -0
  51. package/src/core/{launchContext.ts → workspace/launchContext.ts} +32 -39
  52. package/src/core/{planStorage.ts → workspace/planStorage.ts} +2 -2
  53. package/src/core/{workspaceGuard.ts → workspace/workspaceGuard.ts} +97 -13
  54. package/src/headless/execRunner.ts +2 -2
  55. package/src/index.tsx +43 -89
  56. package/src/session/appSession.ts +10 -7
  57. package/src/session/chatLifecycle.ts +1 -1
  58. package/src/session/liveRenderScheduler.ts +1 -1
  59. package/src/session/types.ts +3 -2
  60. package/src/ui/ActionRequiredBlock.tsx +5 -5
  61. package/src/ui/ActivityBars.tsx +3 -3
  62. package/src/ui/ActivityIndicator.tsx +6 -6
  63. package/src/ui/AgentBlock.tsx +6 -6
  64. package/src/ui/AnimatedStatusText.tsx +1 -1
  65. package/src/ui/AppShell.tsx +670 -719
  66. package/src/ui/AttachmentImportPanel.tsx +8 -8
  67. package/src/ui/AuthPanel.tsx +20 -20
  68. package/src/ui/BottomComposer.tsx +158 -118
  69. package/src/ui/DashCard.tsx +3 -3
  70. package/src/ui/Markdown.tsx +17 -17
  71. package/src/ui/ModelPickerScreen.tsx +222 -42
  72. package/src/ui/ModelReasoningPicker.tsx +15 -15
  73. package/src/ui/Panel.tsx +3 -3
  74. package/src/ui/PlanActionPicker.tsx +6 -6
  75. package/src/ui/PlanReviewPanel.tsx +9 -9
  76. package/src/ui/ProviderPicker.tsx +735 -321
  77. package/src/ui/RunFooter.tsx +3 -3
  78. package/src/ui/RuntimeStatusBar.tsx +108 -0
  79. package/src/ui/SelectionPanel.tsx +8 -4
  80. package/src/ui/SettingsPanel.tsx +9 -9
  81. package/src/ui/Spinner.tsx +1 -1
  82. package/src/ui/TextEntryPanel.tsx +11 -11
  83. package/src/ui/ThinkingBlock.tsx +8 -8
  84. package/src/ui/Timeline.tsx +1625 -1472
  85. package/src/ui/TopHeader.tsx +437 -293
  86. package/src/ui/TranscriptShell.tsx +322 -0
  87. package/src/ui/TurnGroup.tsx +33 -33
  88. package/src/ui/UpdateAvailableCard.tsx +41 -0
  89. package/src/ui/UpdatePromptPanel.tsx +197 -0
  90. package/src/ui/focus.ts +3 -0
  91. package/src/ui/layout.ts +299 -25
  92. package/src/ui/layoutListWindow.ts +145 -0
  93. package/src/ui/logoVariants.ts +103 -0
  94. package/src/ui/modeDisplay.ts +12 -12
  95. package/src/ui/runtimeDisplay.ts +112 -0
  96. package/src/ui/textLayout.ts +15 -4
  97. package/src/ui/theme.tsx +274 -395
  98. package/src/ui/timelineMeasure.ts +218 -136
  99. package/scripts/audit-codexa-capabilities.mjs +0 -466
  100. package/scripts/gen-build-info.mjs +0 -33
  101. package/scripts/smoke-terminal-bench.mjs +0 -35
  102. package/src/appRenderStability.test.ts +0 -131
  103. package/src/commands/handler.test.ts +0 -655
  104. package/src/config/launchArgs.test.ts +0 -189
  105. package/src/config/layeredConfig.test.ts +0 -143
  106. package/src/config/persistence.test.ts +0 -114
  107. package/src/config/runtimeConfig.test.ts +0 -218
  108. package/src/config/settings.test.ts +0 -155
  109. package/src/config/trustStore.test.ts +0 -29
  110. package/src/core/attachments.test.ts +0 -155
  111. package/src/core/auth/codexAuth.test.ts +0 -68
  112. package/src/core/cleanupFastFail.test.ts +0 -76
  113. package/src/core/codex.ts +0 -124
  114. package/src/core/codexExecArgs.test.ts +0 -195
  115. package/src/core/codexLaunch.test.ts +0 -205
  116. package/src/core/codexPrompt.test.ts +0 -252
  117. package/src/core/executables/codexExecutable.test.ts +0 -212
  118. package/src/core/executables/executableResolver.test.ts +0 -129
  119. package/src/core/executables/geminiExecutable.test.ts +0 -116
  120. package/src/core/executables/pathSanityScan.test.ts +0 -47
  121. package/src/core/githubDiagnostics.test.ts +0 -92
  122. package/src/core/hollowResponseFormat.test.ts +0 -58
  123. package/src/core/launchContext.test.ts +0 -157
  124. package/src/core/models/codexCapabilities.test.ts +0 -45
  125. package/src/core/models/codexModelCapabilities.test.ts +0 -246
  126. package/src/core/models/modelSpecs.test.ts +0 -283
  127. package/src/core/perf/renderDebug.test.ts +0 -230
  128. package/src/core/planStorage.test.ts +0 -143
  129. package/src/core/process/CommandRunner.test.ts +0 -105
  130. package/src/core/projectInstructions.test.ts +0 -50
  131. package/src/core/providerLauncher/launcher.test.ts +0 -238
  132. package/src/core/providerLauncher/registry.test.ts +0 -324
  133. package/src/core/providerLauncher/workspaceConfig.test.ts +0 -638
  134. package/src/core/providerRuntime/anthropic.test.ts +0 -1120
  135. package/src/core/providerRuntime/capabilityProfile.test.ts +0 -311
  136. package/src/core/providerRuntime/contextMetadata.test.ts +0 -468
  137. package/src/core/providerRuntime/gemini.test.ts +0 -437
  138. package/src/core/providerRuntime/lmstudio.test.ts +0 -168
  139. package/src/core/providerRuntime/local.test.ts +0 -787
  140. package/src/core/providerRuntime/registry.test.ts +0 -233
  141. package/src/core/providers/codexJsonStream.test.ts +0 -148
  142. package/src/core/providers/codexSubprocess.test.ts +0 -68
  143. package/src/core/providers/codexTranscript.test.ts +0 -284
  144. package/src/core/terminal/startupClear.test.ts +0 -55
  145. package/src/core/terminal/terminalCapabilities.test.ts +0 -93
  146. package/src/core/terminal/terminalControl.test.ts +0 -75
  147. package/src/core/terminal/terminalSanitize.test.ts +0 -22
  148. package/src/core/terminal/terminalSelection.test.ts +0 -42
  149. package/src/core/terminal/terminalTitle.test.ts +0 -328
  150. package/src/core/updateCheck.test.ts +0 -194
  151. package/src/core/updateCheck.ts +0 -172
  152. package/src/core/workspaceActivity.test.ts +0 -163
  153. package/src/core/workspaceGuard.test.ts +0 -151
  154. package/src/core/workspaceRoot.test.ts +0 -23
  155. package/src/exec.test.ts +0 -13
  156. package/src/headless/execArgs.test.ts +0 -147
  157. package/src/headless/execRunner.test.ts +0 -436
  158. package/src/index.test.tsx +0 -620
  159. package/src/session/appSession.test.ts +0 -897
  160. package/src/session/chatLifecycle.test.ts +0 -64
  161. package/src/session/liveRenderScheduler.test.ts +0 -201
  162. package/src/session/planFlow.test.ts +0 -103
  163. package/src/session/planTranscript.test.ts +0 -65
  164. package/src/session/promptRunSchedule.test.ts +0 -36
  165. package/src/ui/ActivityIndicator.test.tsx +0 -58
  166. package/src/ui/AgentBlock.test.ts +0 -6
  167. package/src/ui/AnimatedStatusText.test.ts +0 -16
  168. package/src/ui/AppShell.test.tsx +0 -1776
  169. package/src/ui/AttachmentImportPanel.test.tsx +0 -204
  170. package/src/ui/BottomComposer.test.ts +0 -674
  171. package/src/ui/CodexLogo.tsx +0 -55
  172. package/src/ui/Markdown.test.ts +0 -157
  173. package/src/ui/ModelPickerProviderScope.test.tsx +0 -411
  174. package/src/ui/ModelPickerScreen.test.tsx +0 -99
  175. package/src/ui/ModelPickerState.test.tsx +0 -151
  176. package/src/ui/ModelReasoningPicker.test.tsx +0 -447
  177. package/src/ui/PlanReviewPanel.test.tsx +0 -267
  178. package/src/ui/PromptCardBorder.test.tsx +0 -161
  179. package/src/ui/ProviderPicker.test.tsx +0 -289
  180. package/src/ui/ProviderShortcut.test.tsx +0 -143
  181. package/src/ui/SettingsPanel.test.tsx +0 -233
  182. package/src/ui/StaticTranscriptItem.tsx +0 -56
  183. package/src/ui/Timeline.test.ts +0 -2067
  184. package/src/ui/TimelineNavigation.test.tsx +0 -201
  185. package/src/ui/TopHeader.test.tsx +0 -254
  186. package/src/ui/TurnGroup.test.tsx +0 -365
  187. package/src/ui/busyStatusAnimation.test.ts +0 -30
  188. package/src/ui/commandNormalize.test.ts +0 -142
  189. package/src/ui/diffRenderer.test.ts +0 -102
  190. package/src/ui/focusFlow.test.tsx +0 -1098
  191. package/src/ui/inputBuffer.test.ts +0 -151
  192. package/src/ui/layout.test.ts +0 -146
  193. package/src/ui/modeDisplay.test.ts +0 -42
  194. package/src/ui/runActivityView.test.ts +0 -89
  195. package/src/ui/runLifecycleView.test.tsx +0 -237
  196. package/src/ui/statusRenderIsolation.test.tsx +0 -654
  197. package/src/ui/terminalAnswerFormat.test.ts +0 -19
  198. package/src/ui/textLayout.test.ts +0 -18
  199. package/src/ui/themeFlow.test.ts +0 -53
  200. package/src/ui/timelineMeasureCache.test.ts +0 -986
  201. /package/src/core/{inputDebug.ts → debug/inputDebug.ts} +0 -0
  202. /package/src/core/{clipboard.ts → shared/clipboard.ts} +0 -0
  203. /package/src/core/{githubDiagnostics.ts → shared/githubDiagnostics.ts} +0 -0
  204. /package/src/core/{projectInstructions.ts → workspace/projectInstructions.ts} +0 -0
  205. /package/src/core/{workspaceActivity.ts → workspace/workspaceActivity.ts} +0 -0
  206. /package/src/core/{workspaceRoot.ts → workspace/workspaceRoot.ts} +0 -0
@@ -1,787 +0,0 @@
1
- import assert from "node:assert/strict";
2
- import test from "node:test";
3
- import { normalizeRuntimeConfig, resolveRuntimeConfig } from "../../config/runtimeConfig.js";
4
- import {
5
- checkLocalProvider,
6
- discoverLocalModels,
7
- resetLocalProviderStateForTests,
8
- resolveLocalProviderConfig,
9
- runLocalDiagnostics,
10
- runLocalOpenAiCompatible,
11
- } from "./local.js";
12
- import type { ProviderChatRequest } from "./types.js";
13
-
14
- const QWEN_LM_STUDIO_LIST_FIXTURE = {
15
- data: [
16
- {
17
- id: "qwen/qwen3.6-27b",
18
- object: "model",
19
- type: "vlm",
20
- publisher: "qwen",
21
- arch: "qwen35",
22
- compatibility_type: "gguf",
23
- quantization: "Q4_K_M",
24
- state: "loaded",
25
- max_context_length: 262144,
26
- loaded_context_length: 32000,
27
- capabilities: [
28
- "tool_use",
29
- ],
30
- },
31
- ],
32
- object: "list",
33
- };
34
-
35
- function jsonResponse(body: unknown, status = 200): Response {
36
- return new Response(JSON.stringify(body), {
37
- status,
38
- headers: { "Content-Type": "application/json" },
39
- });
40
- }
41
-
42
- function streamResponse(chunks: readonly string[]): Response {
43
- const encoder = new TextEncoder();
44
- return new Response(new ReadableStream({
45
- start(controller) {
46
- for (const chunk of chunks) {
47
- controller.enqueue(encoder.encode(chunk));
48
- }
49
- controller.close();
50
- },
51
- }), {
52
- status: 200,
53
- headers: { "Content-Type": "text/event-stream" },
54
- });
55
- }
56
-
57
- function buildRequest(overrides: Partial<ProviderChatRequest> = {}): ProviderChatRequest {
58
- return {
59
- prompt: "hi",
60
- route: {
61
- providerId: "local",
62
- modelId: "google/gemma-4-26b-a4b",
63
- backendKind: "local-openai-compatible",
64
- },
65
- runtime: resolveRuntimeConfig(normalizeRuntimeConfig({})),
66
- workspaceRoot: process.cwd(),
67
- ...overrides,
68
- };
69
- }
70
-
71
- async function withLocalEnv<T>(
72
- env: Partial<NodeJS.ProcessEnv>,
73
- callback: () => T | Promise<T>,
74
- ): Promise<T> {
75
- const original = {
76
- CODEXA_LOCAL_BASE_URL: process.env.CODEXA_LOCAL_BASE_URL,
77
- CODEXA_LOCAL_API_KEY: process.env.CODEXA_LOCAL_API_KEY,
78
- CODEXA_LOCAL_MODEL: process.env.CODEXA_LOCAL_MODEL,
79
- OPENAI_BASE_URL: process.env.OPENAI_BASE_URL,
80
- OPENAI_API_BASE: process.env.OPENAI_API_BASE,
81
- OPENAI_API_KEY: process.env.OPENAI_API_KEY,
82
- };
83
-
84
- try {
85
- for (const key of Object.keys(original) as Array<keyof typeof original>) {
86
- if (key in env) {
87
- process.env[key] = env[key];
88
- } else {
89
- delete process.env[key];
90
- }
91
- }
92
- resetLocalProviderStateForTests();
93
- return await callback();
94
- } finally {
95
- for (const [key, value] of Object.entries(original)) {
96
- if (value === undefined) {
97
- delete process.env[key];
98
- } else {
99
- process.env[key] = value;
100
- }
101
- }
102
- resetLocalProviderStateForTests();
103
- }
104
- }
105
-
106
- test("Local provider is unavailable when endpoint is unreachable", async () => {
107
- await withLocalEnv({}, async () => {
108
- const result = await checkLocalProvider({
109
- fetchImpl: (async () => {
110
- throw new Error("ECONNREFUSED");
111
- }) as typeof fetch,
112
- });
113
-
114
- assert.equal(result.status, "not-configured");
115
- assert.equal(result.backendKind, "unavailable");
116
- assert.match(result.message ?? "", /Could not reach http:\/\/localhost:1234\/v1/);
117
- assert.equal(result.diagnostics?.baseUrl, "http://localhost:1234/v1");
118
- assert.equal(result.diagnostics?.endpointCheckResult, "unavailable");
119
- });
120
- });
121
-
122
- test("Local provider is available when /v1/models returns model list", async () => {
123
- await withLocalEnv({}, async () => {
124
- const result = await checkLocalProvider({
125
- fetchImpl: (async (input) => {
126
- if (String(input).includes("/api/v0/")) {
127
- return new Response(null, { status: 404 });
128
- }
129
- return jsonResponse({ data: [{ id: "google/gemma-4-26b-a4b" }] });
130
- }) as typeof fetch,
131
- });
132
-
133
- assert.equal(result.status, "ready");
134
- assert.equal(result.backendKind, "local-openai-compatible");
135
- const discovery = discoverLocalModels();
136
- assert.equal(discovery.models[0]?.modelId, "google/gemma-4-26b-a4b");
137
- assert.equal(discovery.diagnostics?.selectedModel, "google/gemma-4-26b-a4b");
138
- });
139
- });
140
-
141
- test("Local provider uses configured defaultModel only as fallback when LM Studio native metadata is unavailable", async () => {
142
- await withLocalEnv({}, async () => {
143
- const result = await checkLocalProvider({
144
- override: {
145
- defaultModel: "second-model",
146
- baseUrl: "http://local.test/v1/",
147
- },
148
- fetchImpl: (async (input) => {
149
- if (String(input).includes("/api/v0/")) {
150
- return new Response(null, { status: 404 });
151
- }
152
- return jsonResponse({ data: [{ id: "first-model" }, { id: "second-model" }] });
153
- }) as typeof fetch,
154
- });
155
-
156
- assert.equal(result.status, "ready");
157
- assert.equal(result.diagnostics?.baseUrl, "http://local.test/v1");
158
- assert.equal(result.diagnostics?.selectedModel, "second-model");
159
- });
160
- });
161
-
162
- test("Local provider falls back to first returned model when no default configured", async () => {
163
- await withLocalEnv({}, async () => {
164
- const result = await checkLocalProvider({
165
- fetchImpl: (async (input) => {
166
- if (String(input).includes("/api/v0/")) {
167
- return new Response(null, { status: 404 });
168
- }
169
- return jsonResponse({ data: [{ id: "first-model" }, { id: "second-model" }] });
170
- }) as typeof fetch,
171
- });
172
-
173
- assert.equal(result.status, "ready");
174
- assert.equal(result.diagnostics?.selectedModel, "first-model");
175
- });
176
- });
177
-
178
- test("LM Studio /api/v0/models loaded model overrides stale Local currentModel and defaultModel", async () => {
179
- await withLocalEnv({}, async () => {
180
- const result = await checkLocalProvider({
181
- override: {
182
- currentModel: "google/gemma-4-26b-a4b",
183
- defaultModel: "google/gemma-4-26b-a4b",
184
- baseUrl: "http://localhost:1234/v1",
185
- },
186
- fetchImpl: (async (input) => {
187
- if (String(input) === "http://localhost:1234/api/v0/models") {
188
- return jsonResponse(QWEN_LM_STUDIO_LIST_FIXTURE);
189
- }
190
- return jsonResponse({ data: [{ id: "qwen/qwen3.6-27b" }, { id: "google/gemma-4-26b-a4b" }] });
191
- }) as typeof fetch,
192
- });
193
-
194
- assert.equal(result.status, "ready");
195
- assert.equal(result.diagnostics?.selectedModel, "qwen/qwen3.6-27b");
196
- assert.equal(result.diagnostics?.selectionReason, "single-loaded");
197
- assert.equal(result.diagnostics?.contextSource, "lmstudio-api");
198
- assert.equal(result.diagnostics?.contextRawField, "loaded_context_length");
199
-
200
- const discovery = discoverLocalModels({
201
- baseUrl: "http://localhost:1234/v1",
202
- currentModel: "google/gemma-4-26b-a4b",
203
- defaultModel: "google/gemma-4-26b-a4b",
204
- });
205
- const active = discovery.models.find((model) => model.modelId === "qwen/qwen3.6-27b");
206
- assert.ok(active, "Qwen model should be discovered");
207
- assert.equal((active.raw as Record<string, unknown>).loaded_context_length, 32000);
208
- assert.equal((active.raw as Record<string, unknown>).max_context_length, 262144);
209
- assert.deepEqual((active.raw as Record<string, unknown>).capabilities, ["tool_use"]);
210
- assert.equal(discovery.diagnostics?.selectedModel, "qwen/qwen3.6-27b");
211
- });
212
- });
213
-
214
- test("zero loaded LM Studio models produces a clear not-ready Local status", async () => {
215
- await withLocalEnv({}, async () => {
216
- const result = await checkLocalProvider({
217
- override: { baseUrl: "http://localhost:1234/v1" },
218
- fetchImpl: (async (input) => {
219
- if (String(input).includes("/api/v0/")) {
220
- return jsonResponse({
221
- data: [{ id: "available-model", state: "not-loaded" }],
222
- object: "list",
223
- });
224
- }
225
- return jsonResponse({ data: [{ id: "available-model" }] });
226
- }) as typeof fetch,
227
- });
228
-
229
- assert.equal(result.status, "not-configured");
230
- assert.equal(result.backendKind, "unavailable");
231
- assert.equal(result.message, "LM Studio is running, but no model is loaded.");
232
- assert.equal(result.diagnostics?.endpointCheckResult, "no-models");
233
- assert.equal(result.diagnostics?.selectedModel, null);
234
- });
235
- });
236
-
237
- test("multiple loaded LM Studio models are handled deterministically", async () => {
238
- await withLocalEnv({}, async () => {
239
- const fetchImpl = (async (input: RequestInfo | URL) => {
240
- if (String(input).includes("/api/v0/")) {
241
- return jsonResponse({
242
- data: [
243
- { id: "first-loaded", state: "loaded", loaded_context_length: 8192 },
244
- { id: "second-loaded", state: "loaded", loaded_context_length: 16384 },
245
- ],
246
- object: "list",
247
- });
248
- }
249
- return jsonResponse({ data: [{ id: "first-loaded" }, { id: "second-loaded" }] });
250
- }) as typeof fetch;
251
-
252
- const first = await checkLocalProvider({ override: { baseUrl: "http://localhost:1234/v1" }, fetchImpl });
253
- assert.equal(first.status, "ready");
254
- assert.equal(first.diagnostics?.selectedModel, "first-loaded");
255
- assert.equal(first.diagnostics?.selectionReason, "first-loaded");
256
-
257
- const second = await checkLocalProvider({ override: { baseUrl: "http://localhost:1234/v1" }, fetchImpl });
258
- assert.equal(second.diagnostics?.selectedModel, "first-loaded");
259
- assert.equal(second.diagnostics?.selectionReason, "previous-loaded");
260
- });
261
- });
262
-
263
- test("pinnedModel wins only when it matches a loaded model", async () => {
264
- await withLocalEnv({}, async () => {
265
- const fetchImpl = (async (input: RequestInfo | URL) => {
266
- if (String(input).includes("/api/v0/")) {
267
- return jsonResponse({
268
- data: [
269
- { id: "first-loaded", state: "loaded" },
270
- { id: "pinned-loaded", state: "loaded" },
271
- { id: "pinned-unloaded", state: "not-loaded" },
272
- ],
273
- object: "list",
274
- });
275
- }
276
- return jsonResponse({ data: [{ id: "first-loaded" }, { id: "pinned-loaded" }, { id: "pinned-unloaded" }] });
277
- }) as typeof fetch;
278
-
279
- const loaded = await checkLocalProvider({
280
- override: { baseUrl: "http://localhost:1234/v1", pinnedModel: "pinned-loaded" },
281
- fetchImpl,
282
- });
283
- assert.equal(loaded.diagnostics?.selectedModel, "pinned-loaded");
284
- assert.equal(loaded.diagnostics?.selectionReason, "pinned-loaded");
285
-
286
- resetLocalProviderStateForTests();
287
- const unloaded = await checkLocalProvider({
288
- override: { baseUrl: "http://localhost:1234/v1", pinnedModel: "pinned-unloaded" },
289
- fetchImpl,
290
- });
291
- assert.equal(unloaded.diagnostics?.selectedModel, "first-loaded");
292
- assert.equal(unloaded.diagnostics?.selectionReason, "first-loaded");
293
- });
294
- });
295
-
296
- test("Local provider reports no models when endpoint returns an empty list", async () => {
297
- await withLocalEnv({}, async () => {
298
- const result = await checkLocalProvider({
299
- fetchImpl: (async (input) => {
300
- if (String(input).includes("/api/v0/")) {
301
- return new Response(null, { status: 404 });
302
- }
303
- return jsonResponse({ data: [] });
304
- }) as typeof fetch,
305
- });
306
-
307
- assert.equal(result.status, "not-configured");
308
- assert.match(result.message ?? "", /no models were returned/i);
309
- assert.equal(result.diagnostics?.endpointCheckResult, "no-models");
310
- });
311
- });
312
-
313
- test("Local provider sends prompt to configured OpenAI-compatible base URL", async () => {
314
- await withLocalEnv({}, async () => {
315
- const calls: Array<{ url: string; body: unknown }> = [];
316
- const deltas: string[] = [];
317
- const fetchImpl = (async (input, init) => {
318
- if (String(input).includes("/api/v0/")) {
319
- return new Response(null, { status: 404 });
320
- }
321
- calls.push({
322
- url: String(input),
323
- body: init?.body ? JSON.parse(String(init.body)) as unknown : null,
324
- });
325
- if (String(input).endsWith("/models")) {
326
- return jsonResponse({ data: [{ id: "google/gemma-4-26b-a4b" }] });
327
- }
328
- return streamResponse([
329
- "data: {\"choices\":[{\"delta\":{\"content\":\"Hello\"}}]}\n\n",
330
- "data: {\"choices\":[{\"delta\":{\"content\":\" there\"}}]}\n\n",
331
- "data: [DONE]\n\n",
332
- ]);
333
- }) as typeof fetch;
334
-
335
- await checkLocalProvider({
336
- override: { baseUrl: "http://lmstudio.test/v1", apiKey: "dummy" },
337
- fetchImpl,
338
- });
339
-
340
- const text = await runLocalOpenAiCompatible(
341
- buildRequest({ localConfig: { baseUrl: "http://lmstudio.test/v1", apiKey: "dummy" } }),
342
- {
343
- onResponse: () => undefined,
344
- onError: assert.fail,
345
- onAssistantDelta: (chunk) => deltas.push(chunk),
346
- },
347
- { fetchImpl },
348
- );
349
-
350
- assert.equal(text, "Hello there");
351
- assert.equal(calls[1]?.url, "http://lmstudio.test/v1/chat/completions");
352
- assert.deepEqual(calls[1]?.body, {
353
- model: "google/gemma-4-26b-a4b",
354
- messages: [{ role: "user", content: "hi" }],
355
- stream: true,
356
- });
357
- assert.deepEqual(deltas, ["Hello", " there"]);
358
- });
359
- });
360
-
361
- test("Local request payload uses refreshed LM Studio active loaded model", async () => {
362
- await withLocalEnv({}, async () => {
363
- const payloads: Array<Record<string, unknown>> = [];
364
- const fetchImpl = (async (input, init) => {
365
- const url = String(input);
366
- if (url.includes("/api/v0/")) {
367
- return jsonResponse(QWEN_LM_STUDIO_LIST_FIXTURE);
368
- }
369
- if (url.endsWith("/models")) {
370
- return jsonResponse({ data: [{ id: "google/gemma-4-26b-a4b" }, { id: "qwen/qwen3.6-27b" }] });
371
- }
372
- if (init?.body) {
373
- payloads.push(JSON.parse(String(init.body)) as Record<string, unknown>);
374
- }
375
- return jsonResponse({ choices: [{ message: { content: "ok" } }] });
376
- }) as typeof fetch;
377
-
378
- await checkLocalProvider({
379
- override: {
380
- baseUrl: "http://localhost:1234/v1",
381
- currentModel: "google/gemma-4-26b-a4b",
382
- defaultModel: "google/gemma-4-26b-a4b",
383
- },
384
- fetchImpl,
385
- });
386
- await runLocalOpenAiCompatible(
387
- buildRequest({
388
- route: { providerId: "local", modelId: "google/gemma-4-26b-a4b", backendKind: "local-openai-compatible" },
389
- localConfig: {
390
- baseUrl: "http://localhost:1234/v1",
391
- currentModel: "google/gemma-4-26b-a4b",
392
- defaultModel: "google/gemma-4-26b-a4b",
393
- },
394
- }),
395
- { onResponse: () => undefined, onError: assert.fail },
396
- { fetchImpl },
397
- );
398
-
399
- assert.equal(payloads.at(-1)?.model, "qwen/qwen3.6-27b");
400
- assert.notEqual(payloads.at(-1)?.model, "google/gemma-4-26b-a4b");
401
- });
402
- });
403
-
404
- test("Local provider falls back to non-streaming chat completion", async () => {
405
- await withLocalEnv({}, async () => {
406
- const streamValues: boolean[] = [];
407
- const fetchImpl = (async (_input, init) => {
408
- const body = init?.body ? JSON.parse(String(init.body)) as { stream?: boolean } : {};
409
- streamValues.push(Boolean(body.stream));
410
- if (body.stream) {
411
- return new Response("stream unavailable", { status: 400 });
412
- }
413
- return jsonResponse({ choices: [{ message: { content: "Fallback response" } }] });
414
- }) as typeof fetch;
415
-
416
- const text = await runLocalOpenAiCompatible(
417
- buildRequest({ localConfig: { baseUrl: "http://lmstudio.test/v1" } }),
418
- {
419
- onResponse: () => undefined,
420
- onError: assert.fail,
421
- },
422
- { fetchImpl },
423
- );
424
-
425
- assert.equal(text, "Fallback response");
426
- assert.deepEqual(streamValues, [true, false]);
427
- });
428
- });
429
-
430
- test("Local provider run path does not use CLI startup labels", async () => {
431
- const progress: string[] = [];
432
- const fetchImpl = (async () => jsonResponse({ choices: [{ message: { content: "Local response" } }] })) as typeof fetch;
433
- const text = await runLocalOpenAiCompatible(
434
- buildRequest({ localConfig: { baseUrl: "http://local.test/v1" } }),
435
- {
436
- onResponse: () => undefined,
437
- onError: assert.fail,
438
- onProgress: (update) => progress.push(update.text),
439
- },
440
- { fetchImpl },
441
- );
442
-
443
- assert.equal(text, "Local response");
444
- assert.equal(progress.some((line) => /Gemini|Claude|Codex CLI/i.test(line)), false);
445
- });
446
-
447
- test("Local diagnostics show base URL, selected model, and discovered models", async () => {
448
- await withLocalEnv({}, async () => {
449
- const diagnostics = await runLocalDiagnostics({
450
- localConfig: { baseUrl: "http://diag.test/v1", defaultModel: "model-b" },
451
- fetchImpl: (async (input) => {
452
- if (String(input).includes("/api/v0/")) {
453
- return new Response(null, { status: 404 });
454
- }
455
- return jsonResponse({ data: [{ id: "model-a" }, { id: "model-b" }] });
456
- }) as typeof fetch,
457
- });
458
-
459
- assert.match(diagnostics, /Local: available/);
460
- assert.match(diagnostics, /Base URL: http:\/\/diag\.test\/v1/);
461
- assert.match(diagnostics, /Models: model-a, model-b/);
462
- assert.match(diagnostics, /Selected: model-b/);
463
- });
464
- });
465
-
466
- test("Local config uses CODEXA env first and common OpenAI-compatible env second", async () => {
467
- await withLocalEnv({
468
- OPENAI_BASE_URL: "http://openai-compatible.test/v1",
469
- OPENAI_API_KEY: "openai-env-key",
470
- CODEXA_LOCAL_BASE_URL: "http://codexa-local.test/v1",
471
- CODEXA_LOCAL_API_KEY: "local-env-key",
472
- CODEXA_LOCAL_MODEL: "local-env-model",
473
- }, async () => {
474
- const config = resolveLocalProviderConfig(null);
475
- assert.equal(config.baseUrl, "http://codexa-local.test/v1");
476
- assert.equal(config.apiKey, "local-env-key");
477
- assert.equal(config.defaultModel, "local-env-model");
478
- });
479
- });
480
-
481
- test("Local provider omits system prompt when supports_system_prompt: false in API metadata", async () => {
482
- await withLocalEnv({}, async () => {
483
- const calls: Array<{ body: unknown }> = [];
484
- const fetchImpl = (async (input, init) => {
485
- if (String(input).includes("/api/v0/")) {
486
- return new Response(null, { status: 404 });
487
- }
488
- if (String(input).endsWith("/models")) {
489
- return jsonResponse({ data: [{ id: "no-sys-model", supports_system_prompt: false }] });
490
- }
491
- calls.push({ body: init?.body ? JSON.parse(String(init.body)) as unknown : null });
492
- return streamResponse([
493
- "data: {\"choices\":[{\"delta\":{\"content\":\"ok\"}}]}\n\n",
494
- "data: [DONE]\n\n",
495
- ]);
496
- }) as typeof fetch;
497
-
498
- await checkLocalProvider({ fetchImpl });
499
- await runLocalOpenAiCompatible(
500
- buildRequest({
501
- route: { providerId: "local", modelId: "no-sys-model", backendKind: "local-openai-compatible" },
502
- projectInstructions: { content: "You are a helpful assistant.", path: "AGENTS.md" },
503
- }),
504
- { onResponse: () => undefined, onError: assert.fail },
505
- { fetchImpl },
506
- );
507
-
508
- const body = calls[0]?.body as { messages?: Array<{ role: string }> };
509
- const roles = body?.messages?.map((m) => m.role) ?? [];
510
- assert.deepEqual(roles, ["user"], "system message must be excluded when supportsSystemPrompt is false");
511
- });
512
- });
513
-
514
- test("Local provider includes system prompt when supportsSystemPrompt is unknown (absent from API metadata)", async () => {
515
- await withLocalEnv({}, async () => {
516
- const calls: Array<{ body: unknown }> = [];
517
- const fetchImpl = (async (input, init) => {
518
- if (String(input).includes("/api/v0/")) {
519
- return new Response(null, { status: 404 });
520
- }
521
- if (String(input).endsWith("/models")) {
522
- return jsonResponse({ data: [{ id: "unknown-caps-model" }] });
523
- }
524
- calls.push({ body: init?.body ? JSON.parse(String(init.body)) as unknown : null });
525
- return streamResponse([
526
- "data: {\"choices\":[{\"delta\":{\"content\":\"ok\"}}]}\n\n",
527
- "data: [DONE]\n\n",
528
- ]);
529
- }) as typeof fetch;
530
-
531
- await checkLocalProvider({ fetchImpl });
532
- await runLocalOpenAiCompatible(
533
- buildRequest({
534
- route: { providerId: "local", modelId: "unknown-caps-model", backendKind: "local-openai-compatible" },
535
- projectInstructions: { content: "You are a helpful assistant.", path: "AGENTS.md" },
536
- }),
537
- { onResponse: () => undefined, onError: assert.fail },
538
- { fetchImpl },
539
- );
540
-
541
- const body = calls[0]?.body as { messages?: Array<{ role: string }> };
542
- const roles = body?.messages?.map((m) => m.role) ?? [];
543
- assert.deepEqual(roles, ["system", "user"], "system message must be included when supportsSystemPrompt is unknown");
544
- });
545
- });
546
-
547
- test("Local provider skips streaming when supports_streaming: false in API metadata", async () => {
548
- await withLocalEnv({}, async () => {
549
- const streamValues: boolean[] = [];
550
- const fetchImpl = (async (input, init) => {
551
- if (String(input).includes("/api/v0/")) {
552
- return new Response(null, { status: 404 });
553
- }
554
- if (String(input).endsWith("/models")) {
555
- return jsonResponse({ data: [{ id: "no-stream-model", supports_streaming: false }] });
556
- }
557
- const body = init?.body ? JSON.parse(String(init.body)) as { stream?: boolean } : {};
558
- streamValues.push(Boolean(body.stream));
559
- return jsonResponse({ choices: [{ message: { content: "Non-streaming response" } }] });
560
- }) as typeof fetch;
561
-
562
- await checkLocalProvider({ fetchImpl });
563
- const text = await runLocalOpenAiCompatible(
564
- buildRequest({
565
- route: { providerId: "local", modelId: "no-stream-model", backendKind: "local-openai-compatible" },
566
- }),
567
- { onResponse: () => undefined, onError: assert.fail },
568
- { fetchImpl },
569
- );
570
-
571
- assert.equal(text, "Non-streaming response");
572
- assert.equal(streamValues.length, 1, "exactly one request should be made when streaming is unsupported");
573
- assert.equal(streamValues[0], false, "streaming must not be requested");
574
- });
575
- });
576
-
577
- test("Local config override supportsSystemPrompt: false omits system prompt without API metadata", async () => {
578
- await withLocalEnv({}, async () => {
579
- const calls: Array<{ body: unknown }> = [];
580
- const fetchImpl = (async (_input, init) => {
581
- calls.push({ body: init?.body ? JSON.parse(String(init.body)) as unknown : null });
582
- return streamResponse([
583
- "data: {\"choices\":[{\"delta\":{\"content\":\"ok\"}}]}\n\n",
584
- "data: [DONE]\n\n",
585
- ]);
586
- }) as typeof fetch;
587
-
588
- await runLocalOpenAiCompatible(
589
- buildRequest({
590
- route: { providerId: "local", modelId: "config-gate-model", backendKind: "local-openai-compatible" },
591
- localConfig: {
592
- baseUrl: "http://localhost:1234/v1",
593
- models: {
594
- "config-gate-model": {
595
- supportsSystemPrompt: false,
596
- },
597
- },
598
- },
599
- projectInstructions: { content: "You are a helpful assistant.", path: "AGENTS.md" },
600
- }),
601
- { onResponse: () => undefined, onError: assert.fail },
602
- { fetchImpl },
603
- );
604
-
605
- const body = calls[0]?.body as { messages?: Array<{ role: string }> };
606
- const roles = body?.messages?.map((m) => m.role) ?? [];
607
- assert.deepEqual(roles, ["user"], "system message must be excluded when config overrides supportsSystemPrompt: false");
608
- });
609
- });
610
-
611
- test("resetLocalProviderStateForTests clears capability profile cache for test isolation", async () => {
612
- await withLocalEnv({}, async () => {
613
- // Seed the capability cache via discovery
614
- const fetchImpl = (async (input: RequestInfo | URL) => {
615
- if (String(input).includes("/api/v0/")) {
616
- return new Response(null, { status: 404 });
617
- }
618
- if (String(input).endsWith("/models")) {
619
- return jsonResponse({ data: [{ id: "isolation-model", supports_streaming: false }] });
620
- }
621
- return jsonResponse({ choices: [{ message: { content: "ok" } }] });
622
- }) as typeof fetch;
623
- await checkLocalProvider({ fetchImpl });
624
-
625
- // Verify the cache has the capability
626
- const calls: boolean[] = [];
627
- const trackFetch = (async (input: RequestInfo | URL, init?: RequestInit) => {
628
- if (String(input).includes("/api/v0/")) {
629
- return new Response(null, { status: 404 });
630
- }
631
- if (!String(input).endsWith("/models")) {
632
- const body = init?.body ? JSON.parse(String(init.body)) as { stream?: boolean } : {};
633
- calls.push(Boolean(body.stream));
634
- return jsonResponse({ choices: [{ message: { content: "ok" } }] });
635
- }
636
- return jsonResponse({ data: [{ id: "isolation-model", supports_streaming: false }] });
637
- }) as typeof fetch;
638
-
639
- await runLocalOpenAiCompatible(
640
- buildRequest({ route: { providerId: "local", modelId: "isolation-model", backendKind: "local-openai-compatible" } }),
641
- { onResponse: () => undefined, onError: assert.fail },
642
- { fetchImpl: trackFetch },
643
- );
644
- assert.equal(calls[0], false, "streaming should be disabled from cache");
645
- });
646
-
647
- // After withLocalEnv resets state, the capability cache should be cleared.
648
- // A fresh env with the same model should now have unknown capabilities.
649
- await withLocalEnv({}, async () => {
650
- const calls: boolean[] = [];
651
- const fetchImpl = (async (input: RequestInfo | URL, init?: RequestInit) => {
652
- if (String(input).includes("/api/v0/")) {
653
- return new Response(null, { status: 404 });
654
- }
655
- if (String(input).endsWith("/models")) {
656
- // Return model WITHOUT supports_streaming field (unknown)
657
- return jsonResponse({ data: [{ id: "isolation-model" }] });
658
- }
659
- const body = init?.body ? JSON.parse(String(init.body)) as { stream?: boolean } : {};
660
- calls.push(Boolean(body.stream));
661
- return streamResponse([
662
- "data: {\"choices\":[{\"delta\":{\"content\":\"ok\"}}]}\n\n",
663
- "data: [DONE]\n\n",
664
- ]);
665
- }) as typeof fetch;
666
-
667
- await checkLocalProvider({ fetchImpl });
668
- await runLocalOpenAiCompatible(
669
- buildRequest({ route: { providerId: "local", modelId: "isolation-model", backendKind: "local-openai-compatible" } }),
670
- { onResponse: () => undefined, onError: assert.fail },
671
- { fetchImpl },
672
- );
673
- assert.equal(calls[0], true, "streaming should be attempted after cache was cleared (unknown defaults to try)");
674
- });
675
- });
676
-
677
- test("checkLocalProvider enriches selected model raw with LM Studio native API metadata", async () => {
678
- await withLocalEnv({}, async () => {
679
- const lmStudioFixture = {
680
- data: [{
681
- id: "google/gemma-4-26b-a4b",
682
- object: "model",
683
- type: "vlm",
684
- publisher: "google",
685
- arch: "gemma4",
686
- compatibility_type: "gguf",
687
- quantization: "Q4_K_M",
688
- state: "loaded",
689
- max_context_length: 262144,
690
- loaded_context_length: 64000,
691
- capabilities: ["tool_use"],
692
- }],
693
- object: "list",
694
- };
695
- const fetchImpl = (async (input: RequestInfo | URL) => {
696
- if (String(input).includes("/api/v0/")) {
697
- return jsonResponse(lmStudioFixture);
698
- }
699
- if (String(input).endsWith("/models")) {
700
- return jsonResponse({ data: [{ id: "google/gemma-4-26b-a4b", context_length: 8192 }] });
701
- }
702
- return jsonResponse({ choices: [{ message: { content: "ok" } }] });
703
- }) as typeof fetch;
704
-
705
- await checkLocalProvider({
706
- override: { baseUrl: "http://localhost:1234/v1", apiKey: "lm-studio" },
707
- fetchImpl,
708
- });
709
-
710
- const discovered = discoverLocalModels({ baseUrl: "http://localhost:1234/v1", apiKey: "lm-studio" });
711
- const model = discovered.models.find((m) => m.modelId === "google/gemma-4-26b-a4b");
712
- assert.ok(model, "model should be in discovered list");
713
- const raw = model?.raw as Record<string, unknown>;
714
- assert.equal(raw?.loaded_context_length, 64000, "loaded_context_length should be merged from LM Studio native API");
715
- assert.deepEqual(raw?.capabilities, ["tool_use"], "capabilities array should be present");
716
- assert.equal(raw?.arch, "gemma4", "arch should be present");
717
- });
718
- });
719
-
720
- test("checkLocalProvider LM Studio API failure leaves raw metadata from /v1/models intact", async () => {
721
- await withLocalEnv({}, async () => {
722
- const fetchImpl = (async (input: RequestInfo | URL) => {
723
- if (String(input).includes("/api/v0/")) {
724
- throw new Error("ECONNREFUSED");
725
- }
726
- if (String(input).endsWith("/models")) {
727
- return jsonResponse({ data: [{ id: "fallback-model", context_length: 4096 }] });
728
- }
729
- throw new Error("unexpected request");
730
- }) as typeof fetch;
731
-
732
- const result = await checkLocalProvider({
733
- override: { baseUrl: "http://localhost:1234/v1" },
734
- fetchImpl,
735
- });
736
-
737
- assert.equal(result.status, "ready");
738
- const discovered = discoverLocalModels({ baseUrl: "http://localhost:1234/v1" });
739
- const model = discovered.models.find((m) => m.modelId === "fallback-model");
740
- assert.ok(model, "model should still be discoverable after LM Studio API failure");
741
- const raw = model?.raw as Record<string, unknown>;
742
- assert.equal(raw?.context_length, 4096, "original /v1/models raw data preserved on LM Studio API failure");
743
- });
744
- });
745
-
746
- test("runLocalDiagnostics shows LM Studio metadata fields when available", async () => {
747
- await withLocalEnv({}, async () => {
748
- const lmStudioFixture = {
749
- data: [{
750
- id: "google/gemma-4-26b-a4b",
751
- type: "vlm",
752
- arch: "gemma4",
753
- quantization: "Q4_K_M",
754
- state: "loaded",
755
- max_context_length: 262144,
756
- loaded_context_length: 64000,
757
- capabilities: ["tool_use"],
758
- }],
759
- object: "list",
760
- };
761
- const fetchImpl = (async (input: RequestInfo | URL) => {
762
- if (String(input).includes("/api/v0/")) {
763
- return jsonResponse(lmStudioFixture);
764
- }
765
- if (String(input).endsWith("/models")) {
766
- return jsonResponse({ data: [{ id: "google/gemma-4-26b-a4b" }] });
767
- }
768
- return new Response(null, { status: 404 });
769
- }) as typeof fetch;
770
-
771
- const diagnostics = await runLocalDiagnostics({
772
- localConfig: { baseUrl: "http://localhost:1234/v1" },
773
- fetchImpl,
774
- });
775
-
776
- assert.match(diagnostics, /State: loaded/);
777
- assert.match(diagnostics, /Type: vlm/);
778
- assert.match(diagnostics, /Architecture: gemma4/);
779
- assert.match(diagnostics, /Quantization: Q4_K_M/);
780
- assert.match(diagnostics, /Loaded context: 64,000/);
781
- assert.match(diagnostics, /Max context: 262,144/);
782
- assert.match(diagnostics, /Capabilities: tool_use/);
783
- assert.match(diagnostics, /Active context limit: 64,000/);
784
- assert.match(diagnostics, /Source: lmstudio-api/);
785
- assert.match(diagnostics, /Field: loaded_context_length/);
786
- });
787
- });