@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,409 @@
1
+ import type { ModelSpec } from "../models/modelSpecs.js";
2
+ import type { ProviderId, ProviderWorkspaceOverride } from "../providerLauncher/types.js";
3
+
4
+ export type ContextLengthSource = "api" | "cli" | "config" | "known-registry" | "lmstudio-api" | "unknown";
5
+ export type ContextConfidence = "verified" | "configured" | "known" | "estimated" | "unknown";
6
+
7
+ export interface ModelContextMetadata {
8
+ providerId: ProviderId;
9
+ modelId: string;
10
+ contextLength: number | null;
11
+ source: ContextLengthSource;
12
+ confidence: ContextConfidence;
13
+ raw?: unknown;
14
+ rawField?: string;
15
+ error?: string;
16
+ }
17
+
18
+ export interface ResolveModelContextLengthOptions {
19
+ providerId: ProviderId;
20
+ modelId: string;
21
+ providerConfig?: ProviderWorkspaceOverride | null;
22
+ rawMetadata?: unknown;
23
+ }
24
+
25
+ interface KnownContextRegistryEntry {
26
+ contextLength: number;
27
+ sourceUrl: string;
28
+ note: string;
29
+ estimated?: boolean;
30
+ }
31
+
32
+ const KNOWN_CONTEXT_REGISTRY: Record<string, KnownContextRegistryEntry> = {
33
+ // Exact documented Gemini API model IDs only.
34
+ // Source: https://ai.google.dev/gemini-api/docs/models
35
+ "google:gemini-2.5-pro": {
36
+ contextLength: 1_048_576,
37
+ sourceUrl: "https://ai.google.dev/gemini-api/docs/models",
38
+ note: "Gemini API documented input token limit for this exact model ID.",
39
+ },
40
+ "google:gemini-2.5-flash": {
41
+ contextLength: 1_048_576,
42
+ sourceUrl: "https://ai.google.dev/gemini-api/docs/models",
43
+ note: "Gemini API documented input token limit for this exact model ID.",
44
+ },
45
+ "google:gemini-2.5-flash-lite": {
46
+ contextLength: 1_048_576,
47
+ sourceUrl: "https://ai.google.dev/gemini-api/docs/models",
48
+ note: "Gemini API documented input token limit for this exact model ID.",
49
+ },
50
+ // Gemini 3 preview model IDs — verified route IDs from GEMINI_VERIFIED_MODEL_IDS.
51
+ // Source: https://ai.google.dev/gemini-api/docs/models
52
+ "google:gemini-3-flash-preview": {
53
+ contextLength: 1_048_576,
54
+ sourceUrl: "https://ai.google.dev/gemini-api/docs/models",
55
+ note: "Gemini 3 Flash Preview — 1M token input limit.",
56
+ },
57
+ "google:gemini-3.1-pro-preview": {
58
+ contextLength: 1_048_576,
59
+ sourceUrl: "https://ai.google.dev/gemini-api/docs/models",
60
+ note: "Gemini 3.1 Pro Preview — 1M token input limit.",
61
+ },
62
+ "google:gemini-3.1-flash-lite-preview": {
63
+ contextLength: 1_048_576,
64
+ sourceUrl: "https://ai.google.dev/gemini-api/docs/models",
65
+ note: "Gemini 3.1 Flash Lite Preview — 1M token input limit.",
66
+ },
67
+ // Exact documented Anthropic model IDs only. Provider aliases such as
68
+ // "haiku" remain unknown unless configured or discovered by CLI metadata.
69
+ // Source: https://docs.anthropic.com/en/docs/about-claude/models
70
+ "anthropic:claude-opus-4-7": {
71
+ contextLength: 200_000,
72
+ sourceUrl: "https://docs.anthropic.com/en/docs/about-claude/models",
73
+ note: "Anthropic documented context window for this exact model ID.",
74
+ },
75
+ "anthropic:claude-sonnet-4-6": {
76
+ contextLength: 200_000,
77
+ sourceUrl: "https://docs.anthropic.com/en/docs/about-claude/models",
78
+ note: "Anthropic documented context window for this exact model ID.",
79
+ },
80
+ "anthropic:claude-haiku-4-5": {
81
+ contextLength: 200_000,
82
+ sourceUrl: "https://docs.anthropic.com/en/docs/about-claude/models",
83
+ note: "Anthropic documented context window for this exact model ID.",
84
+ },
85
+ // OpenAI/Codex model IDs as used by the Codex CLI ("openai" provider).
86
+ // All values are estimated — Codex CLI effective context may differ from the
87
+ // OpenAI API. Keys are lowercase-normalised (resolveFromKnownRegistry normalises
88
+ // before lookup so "GPT-5 Codex" → "gpt-5-codex" matches here).
89
+ // Source: https://platform.openai.com/docs/models
90
+ "openai:gpt-5.5": {
91
+ contextLength: 1_048_576,
92
+ estimated: true,
93
+ sourceUrl: "https://platform.openai.com/docs/models",
94
+ note: "Estimated — Codex CLI effective context unverified.",
95
+ },
96
+ "openai:gpt-5.5-mini": {
97
+ contextLength: 200_000,
98
+ estimated: true,
99
+ sourceUrl: "https://platform.openai.com/docs/models",
100
+ note: "Estimated.",
101
+ },
102
+ "openai:gpt-5.4": {
103
+ contextLength: 400_000,
104
+ estimated: true,
105
+ sourceUrl: "https://platform.openai.com/docs/models",
106
+ note: "Estimated.",
107
+ },
108
+ "openai:gpt-5.4-mini": {
109
+ contextLength: 200_000,
110
+ estimated: true,
111
+ sourceUrl: "https://platform.openai.com/docs/models",
112
+ note: "Estimated.",
113
+ },
114
+ "openai:gpt-5.3-codex": {
115
+ contextLength: 400_000,
116
+ estimated: true,
117
+ sourceUrl: "https://platform.openai.com/docs/models",
118
+ note: "Estimated — Codex variant.",
119
+ },
120
+ "openai:gpt-5.2": {
121
+ contextLength: 200_000,
122
+ estimated: true,
123
+ sourceUrl: "https://platform.openai.com/docs/models",
124
+ note: "Estimated.",
125
+ },
126
+ "openai:gpt-5.1-codex": {
127
+ contextLength: 400_000,
128
+ estimated: true,
129
+ sourceUrl: "https://platform.openai.com/docs/models",
130
+ note: "Estimated — Codex variant.",
131
+ },
132
+ "openai:gpt-5.1-codex-mini": {
133
+ contextLength: 200_000,
134
+ estimated: true,
135
+ sourceUrl: "https://platform.openai.com/docs/models",
136
+ note: "Estimated — Codex mini variant.",
137
+ },
138
+ "openai:gpt-5-codex": {
139
+ contextLength: 400_000,
140
+ estimated: true,
141
+ sourceUrl: "https://platform.openai.com/docs/models",
142
+ note: "Estimated — Codex variant.",
143
+ },
144
+ "openai:gpt-5-codex-mini": {
145
+ contextLength: 200_000,
146
+ estimated: true,
147
+ sourceUrl: "https://platform.openai.com/docs/models",
148
+ note: "Estimated — Codex mini variant.",
149
+ },
150
+ };
151
+
152
+ // Short alias IDs used by ANTHROPIC_FALLBACK_MODELS → canonical versioned IDs in the registry.
153
+ const ANTHROPIC_MODEL_ALIAS_MAP: Record<string, string> = {
154
+ opus: "claude-opus-4-7",
155
+ sonnet: "claude-sonnet-4-6",
156
+ haiku: "claude-haiku-4-5",
157
+ };
158
+
159
+ // Normalise OpenAI/Codex model IDs to lowercase-dashed form before registry lookup.
160
+ // Handles display-label variants like "GPT-5 Codex" → "gpt-5-codex".
161
+ function normalizeOpenAIModelId(modelId: string): string {
162
+ return modelId.toLowerCase().replace(/\s+/g, "-");
163
+ }
164
+
165
+ const CONTEXT_FIELD_CANDIDATES = [
166
+ "loaded_context_length",
167
+ "context_length",
168
+ "contextLength",
169
+ "max_context_length",
170
+ "maxContextLength",
171
+ "n_ctx",
172
+ "ctx_len",
173
+ "max_tokens",
174
+ "max_input_tokens",
175
+ ] as const;
176
+
177
+ const NESTED_METADATA_KEYS = [
178
+ "model_info",
179
+ "modelInfo",
180
+ "metadata",
181
+ "details",
182
+ "config",
183
+ "raw",
184
+ ] as const;
185
+
186
+ const contextCache = new Map<string, ModelContextMetadata>();
187
+
188
+ function isRecord(value: unknown): value is Record<string, unknown> {
189
+ return typeof value === "object" && value !== null && !Array.isArray(value);
190
+ }
191
+
192
+ function validContextLength(value: unknown): number | null {
193
+ if (typeof value !== "number" || !Number.isInteger(value) || value <= 0) {
194
+ return null;
195
+ }
196
+ return value;
197
+ }
198
+
199
+ function cacheKey(options: ResolveModelContextLengthOptions): string {
200
+ return JSON.stringify({
201
+ providerId: options.providerId,
202
+ modelId: options.modelId,
203
+ models: options.providerConfig?.models ?? null,
204
+ });
205
+ }
206
+
207
+ export function clearModelContextMetadataCache(): void {
208
+ contextCache.clear();
209
+ }
210
+
211
+ export function formatContextLength(value: number | null): string {
212
+ return value === null ? "Unknown" : value.toLocaleString("en-US");
213
+ }
214
+
215
+ export function formatContextCompact(n: number): string {
216
+ if (n >= 1_000_000) return `${(n / 1_000_000).toFixed(1)}M`;
217
+ if (n >= 100_000) return `${Math.round(n / 1_000)}K`;
218
+ if (n >= 1_000) return `${(n / 1_000).toFixed(1)}k`;
219
+ return `${n}`;
220
+ }
221
+
222
+ export function formatContextMeter(
223
+ usedTokens: number | null | undefined,
224
+ contextLimit: number | null,
225
+ ): string {
226
+ if (contextLimit === null || contextLimit === undefined) return "Unknown";
227
+ const used = usedTokens ?? 0;
228
+ const pct = contextLimit > 0
229
+ ? Math.min(100, Math.floor((used / contextLimit) * 100))
230
+ : 0;
231
+ return `${used.toLocaleString("en-US")} / ${contextLimit.toLocaleString("en-US")} · ${pct}%`;
232
+ }
233
+
234
+ export function contextMetadataToModelSpec(metadata: ModelContextMetadata): ModelSpec {
235
+ if (metadata.contextLength === null) {
236
+ return {
237
+ status: "unknown",
238
+ contextWindow: null,
239
+ maxOutputTokens: null,
240
+ sourceUrl: "",
241
+ verifiedAt: null,
242
+ error: metadata.error ?? null,
243
+ };
244
+ }
245
+
246
+ return {
247
+ status: "verified",
248
+ contextWindow: metadata.contextLength,
249
+ maxOutputTokens: metadata.contextLength,
250
+ sourceUrl: metadata.source,
251
+ verifiedAt: Date.now(),
252
+ isEstimated: metadata.confidence === "estimated",
253
+ };
254
+ }
255
+
256
+ function unknownMetadata(
257
+ providerId: ProviderId,
258
+ modelId: string,
259
+ error = "Context length metadata is unavailable for this provider/model.",
260
+ ): ModelContextMetadata {
261
+ return {
262
+ providerId,
263
+ modelId,
264
+ contextLength: null,
265
+ source: "unknown",
266
+ confidence: "unknown",
267
+ error,
268
+ };
269
+ }
270
+
271
+ function findContextField(raw: unknown): { value: number; field: string } | null {
272
+ if (!isRecord(raw)) return null;
273
+
274
+ for (const field of CONTEXT_FIELD_CANDIDATES) {
275
+ const value = validContextLength(raw[field]);
276
+ if (value !== null) return { value, field };
277
+ }
278
+
279
+ for (const nestedKey of NESTED_METADATA_KEYS) {
280
+ const nested = raw[nestedKey];
281
+ if (!isRecord(nested)) continue;
282
+ for (const field of CONTEXT_FIELD_CANDIDATES) {
283
+ const value = validContextLength(nested[field]);
284
+ if (value !== null) return { value, field: `${nestedKey}.${field}` };
285
+ }
286
+ }
287
+
288
+ return null;
289
+ }
290
+
291
+ function resolveFromRawMetadata(providerId: ProviderId, modelId: string, raw: unknown): ModelContextMetadata | null {
292
+ const found = findContextField(raw);
293
+ if (!found) return null;
294
+ const source: ContextLengthSource =
295
+ found.field === "loaded_context_length" || found.field.endsWith(".loaded_context_length")
296
+ ? "lmstudio-api"
297
+ : providerId === "local" ? "api" : "cli";
298
+ return {
299
+ providerId,
300
+ modelId,
301
+ contextLength: found.value,
302
+ source,
303
+ confidence: "verified",
304
+ raw,
305
+ rawField: found.field,
306
+ };
307
+ }
308
+
309
+ function resolveFromConfig(
310
+ providerId: ProviderId,
311
+ modelId: string,
312
+ providerConfig?: ProviderWorkspaceOverride | null,
313
+ ): ModelContextMetadata | null {
314
+ const value = providerConfig?.models?.[modelId]?.contextLength;
315
+ if (value === undefined) return null;
316
+ const contextLength = validContextLength(value);
317
+ if (contextLength === null) {
318
+ return {
319
+ providerId,
320
+ modelId,
321
+ contextLength: null,
322
+ source: "unknown",
323
+ confidence: "unknown",
324
+ error: `Invalid configured contextLength for ${modelId}. Expected a positive integer.`,
325
+ };
326
+ }
327
+ return {
328
+ providerId,
329
+ modelId,
330
+ contextLength,
331
+ source: "config",
332
+ confidence: "configured",
333
+ };
334
+ }
335
+
336
+ function resolveFromKnownRegistry(providerId: ProviderId, modelId: string): ModelContextMetadata | null {
337
+ const lookupId = providerId === "anthropic"
338
+ ? (ANTHROPIC_MODEL_ALIAS_MAP[modelId] ?? modelId)
339
+ : providerId === "openai"
340
+ ? normalizeOpenAIModelId(modelId)
341
+ : modelId;
342
+ const entry = KNOWN_CONTEXT_REGISTRY[`${providerId}:${lookupId}`];
343
+ if (!entry) return null;
344
+ return {
345
+ providerId,
346
+ modelId,
347
+ contextLength: entry.contextLength,
348
+ source: "known-registry",
349
+ confidence: entry.estimated ? "estimated" : "known",
350
+ raw: {
351
+ sourceUrl: entry.sourceUrl,
352
+ note: entry.note,
353
+ },
354
+ };
355
+ }
356
+
357
+ export async function resolveModelContextLength(
358
+ options: ResolveModelContextLengthOptions,
359
+ ): Promise<ModelContextMetadata> {
360
+ const key = cacheKey(options);
361
+ const cached = contextCache.get(key);
362
+ if (cached && !(options.rawMetadata !== undefined && cached.source === "unknown")) {
363
+ return cached;
364
+ }
365
+
366
+ const rawMetadata = options.rawMetadata ?? null;
367
+ const resolved =
368
+ resolveFromRawMetadata(options.providerId, options.modelId, rawMetadata)
369
+ ?? resolveFromConfig(options.providerId, options.modelId, options.providerConfig)
370
+ ?? resolveFromKnownRegistry(options.providerId, options.modelId)
371
+ ?? unknownMetadata(
372
+ options.providerId,
373
+ options.modelId,
374
+ options.providerId === "local"
375
+ ? "/v1/models did not include context length metadata."
376
+ : "Provider metadata did not include context length.",
377
+ );
378
+
379
+ contextCache.set(key, resolved);
380
+ return resolved;
381
+ }
382
+
383
+ export function resolveModelContextLengthCached(
384
+ options: ResolveModelContextLengthOptions,
385
+ ): ModelContextMetadata {
386
+ const key = cacheKey(options);
387
+ const cached = contextCache.get(key);
388
+ if (cached && !(options.rawMetadata !== undefined && cached.source === "unknown")) return cached;
389
+
390
+ const raw = resolveFromRawMetadata(options.providerId, options.modelId, options.rawMetadata);
391
+ if (raw) {
392
+ contextCache.set(key, raw);
393
+ return raw;
394
+ }
395
+
396
+ const config = resolveFromConfig(options.providerId, options.modelId, options.providerConfig);
397
+ if (config) {
398
+ contextCache.set(key, config);
399
+ return config;
400
+ }
401
+
402
+ const registry = resolveFromKnownRegistry(options.providerId, options.modelId);
403
+ if (registry) {
404
+ contextCache.set(key, registry);
405
+ return registry;
406
+ }
407
+
408
+ return unknownMetadata(options.providerId, options.modelId);
409
+ }