@golba98/codexa 1.0.1

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 (226) hide show
  1. package/README.md +320 -0
  2. package/bin/codexa.js +445 -0
  3. package/package.json +45 -0
  4. package/scripts/audit-codexa-capabilities.mjs +466 -0
  5. package/scripts/smoke-terminal-bench.mjs +35 -0
  6. package/src/app.tsx +4561 -0
  7. package/src/appRenderStability.test.ts +131 -0
  8. package/src/commands/handler.test.ts +643 -0
  9. package/src/commands/handler.ts +875 -0
  10. package/src/config/launchArgs.test.ts +158 -0
  11. package/src/config/launchArgs.ts +186 -0
  12. package/src/config/layeredConfig.test.ts +143 -0
  13. package/src/config/layeredConfig.ts +836 -0
  14. package/src/config/persistence.test.ts +110 -0
  15. package/src/config/persistence.ts +311 -0
  16. package/src/config/runtimeConfig.test.ts +218 -0
  17. package/src/config/runtimeConfig.ts +554 -0
  18. package/src/config/settings.test.ts +155 -0
  19. package/src/config/settings.ts +401 -0
  20. package/src/config/toml-serialize.ts +98 -0
  21. package/src/config/trustStore.test.ts +29 -0
  22. package/src/config/trustStore.ts +68 -0
  23. package/src/core/attachments.test.ts +155 -0
  24. package/src/core/attachments.ts +71 -0
  25. package/src/core/auth/codexAuth.test.ts +68 -0
  26. package/src/core/auth/codexAuth.ts +359 -0
  27. package/src/core/cleanupFastFail.test.ts +76 -0
  28. package/src/core/cleanupFastFail.ts +67 -0
  29. package/src/core/clipboard.ts +24 -0
  30. package/src/core/codex.ts +124 -0
  31. package/src/core/codexExecArgs.test.ts +195 -0
  32. package/src/core/codexExecArgs.ts +152 -0
  33. package/src/core/codexLaunch.test.ts +205 -0
  34. package/src/core/codexLaunch.ts +162 -0
  35. package/src/core/codexPrompt.test.ts +252 -0
  36. package/src/core/codexPrompt.ts +428 -0
  37. package/src/core/executables/claudeExecutable.ts +63 -0
  38. package/src/core/executables/codexExecutable.test.ts +212 -0
  39. package/src/core/executables/codexExecutable.ts +159 -0
  40. package/src/core/executables/executableResolver.test.ts +129 -0
  41. package/src/core/executables/executableResolver.ts +138 -0
  42. package/src/core/executables/geminiExecutable.test.ts +116 -0
  43. package/src/core/executables/geminiExecutable.ts +78 -0
  44. package/src/core/executables/pathSanityScan.test.ts +47 -0
  45. package/src/core/githubDiagnostics.test.ts +92 -0
  46. package/src/core/githubDiagnostics.ts +222 -0
  47. package/src/core/hollowResponseFormat.test.ts +58 -0
  48. package/src/core/hollowResponseFormat.ts +39 -0
  49. package/src/core/inputDebug.ts +51 -0
  50. package/src/core/launchContext.test.ts +157 -0
  51. package/src/core/launchContext.ts +266 -0
  52. package/src/core/models/codexCapabilities.test.ts +45 -0
  53. package/src/core/models/codexCapabilities.ts +95 -0
  54. package/src/core/models/codexModelCapabilities.test.ts +246 -0
  55. package/src/core/models/codexModelCapabilities.ts +571 -0
  56. package/src/core/models/modelSpecs.test.ts +283 -0
  57. package/src/core/models/modelSpecs.ts +300 -0
  58. package/src/core/perf/profiler.ts +125 -0
  59. package/src/core/perf/renderDebug.test.ts +230 -0
  60. package/src/core/perf/renderDebug.ts +373 -0
  61. package/src/core/planStorage.test.ts +143 -0
  62. package/src/core/planStorage.ts +141 -0
  63. package/src/core/process/CommandRunner.test.ts +105 -0
  64. package/src/core/process/CommandRunner.ts +269 -0
  65. package/src/core/process/processValidation.ts +101 -0
  66. package/src/core/projectInstructions.test.ts +50 -0
  67. package/src/core/projectInstructions.ts +54 -0
  68. package/src/core/providerLauncher/launcher.test.ts +238 -0
  69. package/src/core/providerLauncher/launcher.ts +203 -0
  70. package/src/core/providerLauncher/registry.test.ts +324 -0
  71. package/src/core/providerLauncher/registry.ts +253 -0
  72. package/src/core/providerLauncher/types.ts +84 -0
  73. package/src/core/providerLauncher/workspaceConfig.test.ts +638 -0
  74. package/src/core/providerLauncher/workspaceConfig.ts +407 -0
  75. package/src/core/providerRuntime/anthropic.test.ts +1120 -0
  76. package/src/core/providerRuntime/anthropic.ts +576 -0
  77. package/src/core/providerRuntime/capabilityProfile.test.ts +311 -0
  78. package/src/core/providerRuntime/capabilityProfile.ts +288 -0
  79. package/src/core/providerRuntime/claudeCodeDiscovery.ts +446 -0
  80. package/src/core/providerRuntime/contextMetadata.test.ts +468 -0
  81. package/src/core/providerRuntime/contextMetadata.ts +409 -0
  82. package/src/core/providerRuntime/gemini.test.ts +437 -0
  83. package/src/core/providerRuntime/gemini.ts +784 -0
  84. package/src/core/providerRuntime/lmstudio.test.ts +168 -0
  85. package/src/core/providerRuntime/lmstudio.ts +118 -0
  86. package/src/core/providerRuntime/local.test.ts +787 -0
  87. package/src/core/providerRuntime/local.ts +754 -0
  88. package/src/core/providerRuntime/models.ts +150 -0
  89. package/src/core/providerRuntime/reasoning.ts +17 -0
  90. package/src/core/providerRuntime/registry.test.ts +233 -0
  91. package/src/core/providerRuntime/registry.ts +203 -0
  92. package/src/core/providerRuntime/types.ts +103 -0
  93. package/src/core/providers/codexJsonStream.test.ts +148 -0
  94. package/src/core/providers/codexJsonStream.ts +305 -0
  95. package/src/core/providers/codexSubprocess.test.ts +68 -0
  96. package/src/core/providers/codexSubprocess.ts +372 -0
  97. package/src/core/providers/codexTranscript.test.ts +284 -0
  98. package/src/core/providers/codexTranscript.ts +695 -0
  99. package/src/core/providers/openaiNative.ts +13 -0
  100. package/src/core/providers/registry.ts +21 -0
  101. package/src/core/providers/types.ts +59 -0
  102. package/src/core/terminal/terminalCapabilities.test.ts +93 -0
  103. package/src/core/terminal/terminalCapabilities.ts +100 -0
  104. package/src/core/terminal/terminalControl.test.ts +75 -0
  105. package/src/core/terminal/terminalControl.ts +147 -0
  106. package/src/core/terminal/terminalSanitize.test.ts +22 -0
  107. package/src/core/terminal/terminalSanitize.ts +147 -0
  108. package/src/core/terminal/terminalSelection.test.ts +42 -0
  109. package/src/core/terminal/terminalSelection.ts +66 -0
  110. package/src/core/terminal/terminalTitle.test.ts +328 -0
  111. package/src/core/terminal/terminalTitle.ts +483 -0
  112. package/src/core/workspaceActivity.test.ts +163 -0
  113. package/src/core/workspaceActivity.ts +380 -0
  114. package/src/core/workspaceGuard.test.ts +151 -0
  115. package/src/core/workspaceGuard.ts +288 -0
  116. package/src/core/workspaceRoot.test.ts +23 -0
  117. package/src/core/workspaceRoot.ts +47 -0
  118. package/src/exec.test.ts +13 -0
  119. package/src/exec.ts +72 -0
  120. package/src/headless/execArgs.test.ts +147 -0
  121. package/src/headless/execArgs.ts +294 -0
  122. package/src/headless/execRunner.test.ts +434 -0
  123. package/src/headless/execRunner.ts +304 -0
  124. package/src/index.test.tsx +618 -0
  125. package/src/index.tsx +296 -0
  126. package/src/session/appSession.test.ts +897 -0
  127. package/src/session/appSession.ts +761 -0
  128. package/src/session/chatLifecycle.test.ts +64 -0
  129. package/src/session/chatLifecycle.ts +951 -0
  130. package/src/session/liveRenderScheduler.test.ts +201 -0
  131. package/src/session/liveRenderScheduler.ts +214 -0
  132. package/src/session/planFlow.test.ts +103 -0
  133. package/src/session/planFlow.ts +149 -0
  134. package/src/session/planTranscript.test.ts +65 -0
  135. package/src/session/planTranscript.ts +15 -0
  136. package/src/session/promptRunSchedule.test.ts +36 -0
  137. package/src/session/promptRunSchedule.ts +26 -0
  138. package/src/session/types.ts +228 -0
  139. package/src/test/runtimeTestUtils.ts +14 -0
  140. package/src/types/react-dom.d.ts +3 -0
  141. package/src/ui/ActionRequiredBlock.tsx +38 -0
  142. package/src/ui/ActivityBars.tsx +68 -0
  143. package/src/ui/ActivityIndicator.test.tsx +58 -0
  144. package/src/ui/ActivityIndicator.tsx +58 -0
  145. package/src/ui/AgentBlock.test.ts +6 -0
  146. package/src/ui/AgentBlock.tsx +130 -0
  147. package/src/ui/AnimatedStatusText.test.ts +16 -0
  148. package/src/ui/AnimatedStatusText.tsx +69 -0
  149. package/src/ui/AppShell.test.tsx +1739 -0
  150. package/src/ui/AppShell.tsx +698 -0
  151. package/src/ui/AttachmentImportPanel.test.tsx +204 -0
  152. package/src/ui/AttachmentImportPanel.tsx +98 -0
  153. package/src/ui/AuthPanel.tsx +113 -0
  154. package/src/ui/BackendPicker.tsx +28 -0
  155. package/src/ui/BottomComposer.test.ts +674 -0
  156. package/src/ui/BottomComposer.tsx +1028 -0
  157. package/src/ui/CodexLogo.tsx +55 -0
  158. package/src/ui/DashCard.tsx +82 -0
  159. package/src/ui/Markdown.test.ts +157 -0
  160. package/src/ui/Markdown.tsx +310 -0
  161. package/src/ui/ModePicker.tsx +27 -0
  162. package/src/ui/ModelPicker.tsx +31 -0
  163. package/src/ui/ModelPickerProviderScope.test.tsx +411 -0
  164. package/src/ui/ModelPickerScreen.test.tsx +99 -0
  165. package/src/ui/ModelPickerScreen.tsx +416 -0
  166. package/src/ui/ModelPickerState.test.tsx +151 -0
  167. package/src/ui/ModelReasoningPicker.test.tsx +447 -0
  168. package/src/ui/ModelReasoningPicker.tsx +458 -0
  169. package/src/ui/Panel.tsx +51 -0
  170. package/src/ui/PermissionsPanel.tsx +78 -0
  171. package/src/ui/PlanActionPicker.tsx +119 -0
  172. package/src/ui/PlanReviewPanel.test.tsx +267 -0
  173. package/src/ui/PlanReviewPanel.tsx +212 -0
  174. package/src/ui/PromptCardBorder.test.tsx +161 -0
  175. package/src/ui/ProviderPicker.test.tsx +289 -0
  176. package/src/ui/ProviderPicker.tsx +321 -0
  177. package/src/ui/ProviderShortcut.test.tsx +143 -0
  178. package/src/ui/ReasoningPicker.tsx +46 -0
  179. package/src/ui/RunFooter.tsx +65 -0
  180. package/src/ui/SelectionPanel.tsx +67 -0
  181. package/src/ui/SettingsPanel.test.tsx +233 -0
  182. package/src/ui/SettingsPanel.tsx +156 -0
  183. package/src/ui/Spinner.tsx +25 -0
  184. package/src/ui/StaticIntroItem.tsx +54 -0
  185. package/src/ui/StaticTranscriptItem.tsx +56 -0
  186. package/src/ui/TextEntryPanel.tsx +139 -0
  187. package/src/ui/ThemePicker.tsx +31 -0
  188. package/src/ui/ThinkingBlock.tsx +100 -0
  189. package/src/ui/Timeline.test.ts +2067 -0
  190. package/src/ui/Timeline.tsx +1472 -0
  191. package/src/ui/TimelineNavigation.test.tsx +201 -0
  192. package/src/ui/TopHeader.test.tsx +239 -0
  193. package/src/ui/TopHeader.tsx +257 -0
  194. package/src/ui/TurnGroup.test.tsx +365 -0
  195. package/src/ui/TurnGroup.tsx +657 -0
  196. package/src/ui/busyStatusAnimation.test.ts +30 -0
  197. package/src/ui/busyStatusAnimation.ts +11 -0
  198. package/src/ui/commandNormalize.test.ts +142 -0
  199. package/src/ui/commandNormalize.ts +66 -0
  200. package/src/ui/diffRenderer.test.ts +102 -0
  201. package/src/ui/diffRenderer.ts +116 -0
  202. package/src/ui/focus.ts +61 -0
  203. package/src/ui/focusFlow.test.tsx +1098 -0
  204. package/src/ui/inputBuffer.test.ts +151 -0
  205. package/src/ui/inputBuffer.ts +203 -0
  206. package/src/ui/layout.test.ts +145 -0
  207. package/src/ui/layout.ts +287 -0
  208. package/src/ui/modeDisplay.test.ts +42 -0
  209. package/src/ui/modeDisplay.ts +52 -0
  210. package/src/ui/outputPipeline.ts +64 -0
  211. package/src/ui/progressEntries.ts +156 -0
  212. package/src/ui/runActivityView.test.ts +89 -0
  213. package/src/ui/runActivityView.ts +37 -0
  214. package/src/ui/runLifecycleView.test.tsx +237 -0
  215. package/src/ui/slashCommands.ts +41 -0
  216. package/src/ui/statusRenderIsolation.test.tsx +654 -0
  217. package/src/ui/terminalAnswerFormat.test.ts +19 -0
  218. package/src/ui/terminalAnswerFormat.ts +128 -0
  219. package/src/ui/textLayout.test.ts +18 -0
  220. package/src/ui/textLayout.ts +338 -0
  221. package/src/ui/theme.tsx +395 -0
  222. package/src/ui/themeFlow.test.ts +53 -0
  223. package/src/ui/themeFlow.ts +41 -0
  224. package/src/ui/timelineMeasure.ts +3088 -0
  225. package/src/ui/timelineMeasureCache.test.ts +986 -0
  226. package/src/ui/useThrottledValue.ts +31 -0
@@ -0,0 +1,168 @@
1
+ import assert from "node:assert/strict";
2
+ import test from "node:test";
3
+ import { deriveLmStudioApiRoot, fetchLmStudioModelInfo, fetchLmStudioModels, parseLmStudioModelsResponse } from "./lmstudio.js";
4
+
5
+ const FIXTURE = {
6
+ id: "google/gemma-4-26b-a4b",
7
+ object: "model",
8
+ type: "vlm",
9
+ publisher: "google",
10
+ arch: "gemma4",
11
+ compatibility_type: "gguf",
12
+ quantization: "Q4_K_M",
13
+ state: "loaded",
14
+ max_context_length: 262144,
15
+ loaded_context_length: 64000,
16
+ capabilities: ["tool_use"],
17
+ };
18
+
19
+ const LIST_FIXTURE = {
20
+ data: [
21
+ {
22
+ id: "qwen/qwen3.6-27b",
23
+ object: "model",
24
+ type: "vlm",
25
+ publisher: "qwen",
26
+ arch: "qwen35",
27
+ compatibility_type: "gguf",
28
+ quantization: "Q4_K_M",
29
+ state: "loaded",
30
+ max_context_length: 262144,
31
+ loaded_context_length: 32000,
32
+ capabilities: [
33
+ "tool_use",
34
+ ],
35
+ },
36
+ ],
37
+ object: "list",
38
+ };
39
+
40
+ test("deriveLmStudioApiRoot strips /v1 path", () => {
41
+ assert.equal(deriveLmStudioApiRoot("http://localhost:1234/v1"), "http://localhost:1234/api/v0");
42
+ });
43
+
44
+ test("deriveLmStudioApiRoot strips trailing slash from /v1/", () => {
45
+ assert.equal(deriveLmStudioApiRoot("http://localhost:1234/v1/"), "http://localhost:1234/api/v0");
46
+ });
47
+
48
+ test("deriveLmStudioApiRoot works with arbitrary host and port", () => {
49
+ assert.equal(
50
+ deriveLmStudioApiRoot("http://my-server.local:8080/api/v1"),
51
+ "http://my-server.local:8080/api/v0",
52
+ );
53
+ });
54
+
55
+ test("deriveLmStudioApiRoot returns null for invalid URL", () => {
56
+ assert.equal(deriveLmStudioApiRoot("not-a-url"), null);
57
+ });
58
+
59
+ test("fetchLmStudioModelInfo URL-encodes model IDs containing slashes", async () => {
60
+ let capturedUrl = "";
61
+ const mockFetch = async (url: string) => {
62
+ capturedUrl = url;
63
+ return new Response(JSON.stringify(FIXTURE), { status: 200 });
64
+ };
65
+
66
+ await fetchLmStudioModelInfo({
67
+ apiRoot: "http://localhost:1234/api/v0",
68
+ modelId: "google/gemma-4-26b-a4b",
69
+ fetchImpl: mockFetch as typeof fetch,
70
+ });
71
+
72
+ assert.match(capturedUrl, /google%2Fgemma-4-26b-a4b/);
73
+ assert.doesNotMatch(capturedUrl, /google\/gemma/);
74
+ });
75
+
76
+ test("fetchLmStudioModelInfo returns fixture values on success", async () => {
77
+ const mockFetch = async () => new Response(JSON.stringify(FIXTURE), { status: 200 });
78
+
79
+ const result = await fetchLmStudioModelInfo({
80
+ apiRoot: "http://localhost:1234/api/v0",
81
+ modelId: "google/gemma-4-26b-a4b",
82
+ fetchImpl: mockFetch as typeof fetch,
83
+ });
84
+
85
+ assert.deepEqual(result, FIXTURE);
86
+ });
87
+
88
+ test("fetchLmStudioModelInfo returns null on non-2xx response", async () => {
89
+ const mockFetch = async () => new Response("Not Found", { status: 404 });
90
+
91
+ const result = await fetchLmStudioModelInfo({
92
+ apiRoot: "http://localhost:1234/api/v0",
93
+ modelId: "some-model",
94
+ fetchImpl: mockFetch as typeof fetch,
95
+ });
96
+
97
+ assert.equal(result, null);
98
+ });
99
+
100
+ test("fetchLmStudioModelInfo returns null on network error", async () => {
101
+ const mockFetch = async (): Promise<Response> => {
102
+ throw new Error("ECONNREFUSED");
103
+ };
104
+
105
+ const result = await fetchLmStudioModelInfo({
106
+ apiRoot: "http://localhost:1234/api/v0",
107
+ modelId: "some-model",
108
+ fetchImpl: mockFetch as typeof fetch,
109
+ });
110
+
111
+ assert.equal(result, null);
112
+ });
113
+
114
+ test("fetchLmStudioModelInfo returns null on invalid JSON", async () => {
115
+ const mockFetch = async () => new Response("not json{{{", { status: 200 });
116
+
117
+ const result = await fetchLmStudioModelInfo({
118
+ apiRoot: "http://localhost:1234/api/v0",
119
+ modelId: "some-model",
120
+ fetchImpl: mockFetch as typeof fetch,
121
+ });
122
+
123
+ assert.equal(result, null);
124
+ });
125
+
126
+ test("fetchLmStudioModelInfo returns null when response is missing id field", async () => {
127
+ const mockFetch = async () =>
128
+ new Response(JSON.stringify({ object: "model", type: "vlm" }), { status: 200 });
129
+
130
+ const result = await fetchLmStudioModelInfo({
131
+ apiRoot: "http://localhost:1234/api/v0",
132
+ modelId: "some-model",
133
+ fetchImpl: mockFetch as typeof fetch,
134
+ });
135
+
136
+ assert.equal(result, null);
137
+ });
138
+
139
+ test("parseLmStudioModelsResponse parses LM Studio data array list response", () => {
140
+ const parsed = parseLmStudioModelsResponse(LIST_FIXTURE);
141
+
142
+ assert.equal(parsed?.object, "list");
143
+ assert.equal(parsed?.data[0]?.id, "qwen/qwen3.6-27b");
144
+ assert.equal(parsed?.data[0]?.state, "loaded");
145
+ assert.equal(parsed?.data[0]?.loaded_context_length, 32000);
146
+ assert.equal(parsed?.data[0]?.max_context_length, 262144);
147
+ assert.deepEqual(parsed?.data[0]?.capabilities, ["tool_use"]);
148
+ });
149
+
150
+ test("parseLmStudioModelsResponse does not expect the response itself to be an array", () => {
151
+ assert.equal(parseLmStudioModelsResponse([LIST_FIXTURE.data[0]]), null);
152
+ });
153
+
154
+ test("fetchLmStudioModels reads native /api/v0/models list endpoint", async () => {
155
+ let capturedUrl = "";
156
+ const mockFetch = async (url: string) => {
157
+ capturedUrl = url;
158
+ return new Response(JSON.stringify(LIST_FIXTURE), { status: 200 });
159
+ };
160
+
161
+ const result = await fetchLmStudioModels({
162
+ apiRoot: "http://localhost:1234/api/v0",
163
+ fetchImpl: mockFetch as typeof fetch,
164
+ });
165
+
166
+ assert.equal(capturedUrl, "http://localhost:1234/api/v0/models");
167
+ assert.equal(result?.data[0]?.id, "qwen/qwen3.6-27b");
168
+ });
@@ -0,0 +1,118 @@
1
+ export interface LmStudioModelInfo {
2
+ id: string;
3
+ object?: string;
4
+ type?: string;
5
+ publisher?: string;
6
+ arch?: string;
7
+ compatibility_type?: string;
8
+ quantization?: string;
9
+ state?: string;
10
+ max_context_length?: number;
11
+ loaded_context_length?: number;
12
+ capabilities?: string[];
13
+ }
14
+
15
+ export interface LmStudioModelList {
16
+ data: LmStudioModelInfo[];
17
+ object?: string;
18
+ }
19
+
20
+ export function deriveLmStudioApiRoot(baseUrl: string): string | null {
21
+ try {
22
+ return `${new URL(baseUrl).origin}/api/v0`;
23
+ } catch {
24
+ return null;
25
+ }
26
+ }
27
+
28
+ export async function fetchLmStudioModelInfo(options: {
29
+ apiRoot: string;
30
+ modelId: string;
31
+ fetchImpl?: typeof fetch;
32
+ signal?: AbortSignal;
33
+ }): Promise<LmStudioModelInfo | null> {
34
+ const { apiRoot, modelId, signal } = options;
35
+ const fetchImpl = options.fetchImpl ?? globalThis.fetch;
36
+ try {
37
+ const url = `${apiRoot}/models/${encodeURIComponent(modelId)}`;
38
+ const response = await fetchImpl(url, { method: "GET", signal });
39
+ if (!response.ok) return null;
40
+ let parsed: unknown;
41
+ try {
42
+ parsed = await response.json();
43
+ } catch {
44
+ return null;
45
+ }
46
+ if (
47
+ typeof parsed !== "object"
48
+ || parsed === null
49
+ || typeof (parsed as { id?: unknown }).id !== "string"
50
+ ) {
51
+ return null;
52
+ }
53
+ return parsed as LmStudioModelInfo;
54
+ } catch {
55
+ return null;
56
+ }
57
+ }
58
+
59
+ function isRecord(value: unknown): value is Record<string, unknown> {
60
+ return typeof value === "object" && value !== null && !Array.isArray(value);
61
+ }
62
+
63
+ function parseModelInfo(value: unknown): LmStudioModelInfo | null {
64
+ if (!isRecord(value) || typeof value.id !== "string" || !value.id.trim()) {
65
+ return null;
66
+ }
67
+ return {
68
+ id: value.id,
69
+ ...(typeof value.object === "string" ? { object: value.object } : {}),
70
+ ...(typeof value.type === "string" ? { type: value.type } : {}),
71
+ ...(typeof value.publisher === "string" ? { publisher: value.publisher } : {}),
72
+ ...(typeof value.arch === "string" ? { arch: value.arch } : {}),
73
+ ...(typeof value.compatibility_type === "string" ? { compatibility_type: value.compatibility_type } : {}),
74
+ ...(typeof value.quantization === "string" ? { quantization: value.quantization } : {}),
75
+ ...(typeof value.state === "string" ? { state: value.state } : {}),
76
+ ...(typeof value.max_context_length === "number" ? { max_context_length: value.max_context_length } : {}),
77
+ ...(typeof value.loaded_context_length === "number" ? { loaded_context_length: value.loaded_context_length } : {}),
78
+ ...(Array.isArray(value.capabilities)
79
+ ? { capabilities: value.capabilities.filter((item): item is string => typeof item === "string") }
80
+ : {}),
81
+ };
82
+ }
83
+
84
+ export function parseLmStudioModelsResponse(body: unknown): LmStudioModelList | null {
85
+ if (!isRecord(body) || !Array.isArray(body.data)) {
86
+ return null;
87
+ }
88
+ return {
89
+ object: typeof body.object === "string" ? body.object : undefined,
90
+ data: body.data
91
+ .map(parseModelInfo)
92
+ .filter((model): model is LmStudioModelInfo => model !== null),
93
+ };
94
+ }
95
+
96
+ export async function fetchLmStudioModels(options: {
97
+ apiRoot: string;
98
+ fetchImpl?: typeof fetch;
99
+ signal?: AbortSignal;
100
+ }): Promise<LmStudioModelList | null> {
101
+ const fetchImpl = options.fetchImpl ?? globalThis.fetch;
102
+ try {
103
+ const response = await fetchImpl(`${options.apiRoot}/models`, {
104
+ method: "GET",
105
+ signal: options.signal,
106
+ });
107
+ if (!response.ok) return null;
108
+ let parsed: unknown;
109
+ try {
110
+ parsed = await response.json();
111
+ } catch {
112
+ return null;
113
+ }
114
+ return parseLmStudioModelsResponse(parsed);
115
+ } catch {
116
+ return null;
117
+ }
118
+ }