ubume 0.1.0

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 (182) hide show
  1. package/README.md +88 -0
  2. package/bin/codexa-local-harness-bridge.js +3 -0
  3. package/bin/codexa.js +28 -0
  4. package/bin/ubume-local-harness-bridge.js +325 -0
  5. package/bin/ubume.js +398 -0
  6. package/package.json +66 -0
  7. package/src/app.tsx +5759 -0
  8. package/src/commands/handler.ts +889 -0
  9. package/src/config/appVersion.ts +69 -0
  10. package/src/config/buildInfo.ts +3 -0
  11. package/src/config/launchArgs.ts +196 -0
  12. package/src/config/layeredConfig.ts +853 -0
  13. package/src/config/legacyEnv.ts +16 -0
  14. package/src/config/persistence.ts +377 -0
  15. package/src/config/runtimeConfig.ts +558 -0
  16. package/src/config/settings.ts +405 -0
  17. package/src/config/toml-serialize.ts +109 -0
  18. package/src/config/trustStore.ts +84 -0
  19. package/src/config/updateCheckCache.ts +85 -0
  20. package/src/core/README.md +55 -0
  21. package/src/core/agent/loop.ts +464 -0
  22. package/src/core/agent/protocol.ts +345 -0
  23. package/src/core/agent/tools.ts +423 -0
  24. package/src/core/auth/codexAuth.ts +359 -0
  25. package/src/core/codex/codexExecArgs.ts +166 -0
  26. package/src/core/codex/codexLaunch.ts +163 -0
  27. package/src/core/codex/codexPrompt.ts +429 -0
  28. package/src/core/debug/inputDebug.ts +51 -0
  29. package/src/core/debug/localStreamDebug.ts +50 -0
  30. package/src/core/debug/modelStateDebug.ts +35 -0
  31. package/src/core/executables/antigravityExecutable.ts +48 -0
  32. package/src/core/executables/claudeExecutable.ts +63 -0
  33. package/src/core/executables/codexExecutable.ts +160 -0
  34. package/src/core/executables/executableResolver.ts +164 -0
  35. package/src/core/executables/geminiExecutable.ts +78 -0
  36. package/src/core/models/codexCapabilities.ts +97 -0
  37. package/src/core/models/codexModelCapabilities.ts +624 -0
  38. package/src/core/models/codexModelsCacheSeed.ts +153 -0
  39. package/src/core/models/modelSpecs.ts +303 -0
  40. package/src/core/models/providerModelCache.ts +94 -0
  41. package/src/core/perf/profiler.ts +125 -0
  42. package/src/core/perf/renderDebug.ts +398 -0
  43. package/src/core/process/CommandRunner.ts +280 -0
  44. package/src/core/process/processValidation.ts +111 -0
  45. package/src/core/providerLauncher/launcher.ts +220 -0
  46. package/src/core/providerLauncher/registry.ts +354 -0
  47. package/src/core/providerLauncher/types.ts +95 -0
  48. package/src/core/providerLauncher/workspaceConfig.ts +487 -0
  49. package/src/core/providerRuntime/anthropic.ts +580 -0
  50. package/src/core/providerRuntime/antigravity.ts +500 -0
  51. package/src/core/providerRuntime/capabilityProfile.ts +383 -0
  52. package/src/core/providerRuntime/claudeCodeDiscovery.ts +724 -0
  53. package/src/core/providerRuntime/claudeCodeDiscoveryDebug.ts +55 -0
  54. package/src/core/providerRuntime/codexaCupy.ts +97 -0
  55. package/src/core/providerRuntime/codexaNative.ts +425 -0
  56. package/src/core/providerRuntime/contextMetadata.ts +397 -0
  57. package/src/core/providerRuntime/gemini.ts +789 -0
  58. package/src/core/providerRuntime/lmstudio.ts +118 -0
  59. package/src/core/providerRuntime/local.ts +770 -0
  60. package/src/core/providerRuntime/localHarness/runtime.ts +1090 -0
  61. package/src/core/providerRuntime/localOutputBudget.ts +17 -0
  62. package/src/core/providerRuntime/mistralVibe.ts +667 -0
  63. package/src/core/providerRuntime/models.ts +175 -0
  64. package/src/core/providerRuntime/reasoning.ts +20 -0
  65. package/src/core/providerRuntime/registry.ts +286 -0
  66. package/src/core/providerRuntime/types.ts +142 -0
  67. package/src/core/providerRuntime/unsloth.ts +216 -0
  68. package/src/core/providers/codexJsonStream.ts +305 -0
  69. package/src/core/providers/codexSubprocess.ts +378 -0
  70. package/src/core/providers/codexTranscript.ts +695 -0
  71. package/src/core/providers/openaiNative.ts +13 -0
  72. package/src/core/providers/registry.ts +21 -0
  73. package/src/core/providers/types.ts +91 -0
  74. package/src/core/shared/attachments.ts +101 -0
  75. package/src/core/shared/cleanupFastFail.ts +67 -0
  76. package/src/core/shared/clipboard.ts +24 -0
  77. package/src/core/shared/clipboardImage.ts +111 -0
  78. package/src/core/shared/githubDiagnostics.ts +222 -0
  79. package/src/core/shared/hollowResponseFormat.ts +39 -0
  80. package/src/core/terminal/clearFrameBoundary.ts +852 -0
  81. package/src/core/terminal/frameLock.ts +110 -0
  82. package/src/core/terminal/inkRenderReset.ts +123 -0
  83. package/src/core/terminal/startupClear.ts +20 -0
  84. package/src/core/terminal/terminalCapabilities.ts +100 -0
  85. package/src/core/terminal/terminalControl.ts +169 -0
  86. package/src/core/terminal/terminalSanitize.ts +147 -0
  87. package/src/core/terminal/terminalTitle.ts +400 -0
  88. package/src/core/version/channel.ts +27 -0
  89. package/src/core/version/packageManager.ts +119 -0
  90. package/src/core/version/updateCheck.ts +203 -0
  91. package/src/core/workspace/appData.ts +107 -0
  92. package/src/core/workspace/conversationStore.ts +335 -0
  93. package/src/core/workspace/launchContext.ts +259 -0
  94. package/src/core/workspace/planStorage.ts +135 -0
  95. package/src/core/workspace/projectInstructions.ts +54 -0
  96. package/src/core/workspace/scratchDir.ts +64 -0
  97. package/src/core/workspace/workspaceActivity.ts +384 -0
  98. package/src/core/workspace/workspaceGuard.ts +377 -0
  99. package/src/core/workspace/workspaceRoot.ts +47 -0
  100. package/src/exec.ts +73 -0
  101. package/src/headless/execArgs.ts +296 -0
  102. package/src/headless/execRunner.ts +304 -0
  103. package/src/index.tsx +270 -0
  104. package/src/legacyEnvBootstrap.ts +5 -0
  105. package/src/session/appSession.ts +771 -0
  106. package/src/session/chatLifecycle.ts +994 -0
  107. package/src/session/conversation.ts +107 -0
  108. package/src/session/liveRenderScheduler.ts +214 -0
  109. package/src/session/persistedResponse.ts +93 -0
  110. package/src/session/planFlow.ts +159 -0
  111. package/src/session/planTranscript.ts +19 -0
  112. package/src/session/promptRunSchedule.ts +26 -0
  113. package/src/session/types.ts +234 -0
  114. package/src/test/runtimeTestUtils.ts +14 -0
  115. package/src/types/react-dom.d.ts +3 -0
  116. package/src/ui/chrome/ActivityBars.tsx +68 -0
  117. package/src/ui/chrome/ActivityIndicator.tsx +58 -0
  118. package/src/ui/chrome/AnimatedStatusText.tsx +69 -0
  119. package/src/ui/chrome/AppShell.tsx +474 -0
  120. package/src/ui/chrome/BottomComposer.tsx +1135 -0
  121. package/src/ui/chrome/DashCard.tsx +82 -0
  122. package/src/ui/chrome/RunFooter.tsx +65 -0
  123. package/src/ui/chrome/RuntimeStatusBar.tsx +108 -0
  124. package/src/ui/chrome/Spinner.tsx +25 -0
  125. package/src/ui/chrome/TopHeader.tsx +439 -0
  126. package/src/ui/chrome/UpdateAvailableCard.tsx +42 -0
  127. package/src/ui/chrome/busyStatusAnimation.ts +11 -0
  128. package/src/ui/input/commandNormalize.ts +66 -0
  129. package/src/ui/input/focus.ts +73 -0
  130. package/src/ui/input/imageAttachments.ts +18 -0
  131. package/src/ui/input/inputBuffer.ts +203 -0
  132. package/src/ui/input/pastedContent.ts +75 -0
  133. package/src/ui/input/rawArrowKeys.ts +28 -0
  134. package/src/ui/input/slashCommands.ts +43 -0
  135. package/src/ui/input/useStdinRawModeLease.ts +23 -0
  136. package/src/ui/layout.ts +560 -0
  137. package/src/ui/panels/AttachmentImportPanel.tsx +131 -0
  138. package/src/ui/panels/AuthPanel.tsx +149 -0
  139. package/src/ui/panels/BackendPicker.tsx +28 -0
  140. package/src/ui/panels/ModePicker.tsx +31 -0
  141. package/src/ui/panels/ModelPicker.tsx +31 -0
  142. package/src/ui/panels/ModelPickerScreen.tsx +761 -0
  143. package/src/ui/panels/ModelReasoningPicker.tsx +458 -0
  144. package/src/ui/panels/Panel.tsx +51 -0
  145. package/src/ui/panels/PermissionsPanel.tsx +78 -0
  146. package/src/ui/panels/PlanActionPicker.tsx +187 -0
  147. package/src/ui/panels/ProviderPicker.tsx +753 -0
  148. package/src/ui/panels/ProviderSetupPrompt.tsx +52 -0
  149. package/src/ui/panels/ReasoningPicker.tsx +46 -0
  150. package/src/ui/panels/ResumePicker.tsx +90 -0
  151. package/src/ui/panels/SelectionPanel.tsx +138 -0
  152. package/src/ui/panels/SettingsPanel.tsx +156 -0
  153. package/src/ui/panels/TextEntryPanel.tsx +139 -0
  154. package/src/ui/panels/ThemePicker.tsx +32 -0
  155. package/src/ui/panels/ToolApprovalPanel.tsx +46 -0
  156. package/src/ui/panels/UpdatePromptPanel.tsx +236 -0
  157. package/src/ui/panels/responsivePickerViewport.ts +64 -0
  158. package/src/ui/render/Markdown.tsx +331 -0
  159. package/src/ui/render/diffRenderer.ts +116 -0
  160. package/src/ui/render/logoVariants.ts +113 -0
  161. package/src/ui/render/modeDisplay.ts +52 -0
  162. package/src/ui/render/outputPipeline.ts +64 -0
  163. package/src/ui/render/runtimeDisplay.ts +128 -0
  164. package/src/ui/render/terminalAnswerFormat.ts +128 -0
  165. package/src/ui/render/textLayout.ts +392 -0
  166. package/src/ui/theme.tsx +274 -0
  167. package/src/ui/themeFlow.ts +41 -0
  168. package/src/ui/timeline/ActionRequiredBlock.tsx +38 -0
  169. package/src/ui/timeline/AgentBlock.tsx +130 -0
  170. package/src/ui/timeline/StaticIntroItem.tsx +54 -0
  171. package/src/ui/timeline/ThinkingBlock.tsx +100 -0
  172. package/src/ui/timeline/Timeline.tsx +1410 -0
  173. package/src/ui/timeline/TranscriptShell.tsx +302 -0
  174. package/src/ui/timeline/TurnGroup.tsx +673 -0
  175. package/src/ui/timeline/layoutListWindow.ts +145 -0
  176. package/src/ui/timeline/liveViewportWindow.ts +68 -0
  177. package/src/ui/timeline/progressEntries.ts +156 -0
  178. package/src/ui/timeline/runActivityView.ts +37 -0
  179. package/src/ui/timeline/staticTranscriptCache.ts +174 -0
  180. package/src/ui/timeline/streamCoalesce.ts +53 -0
  181. package/src/ui/timeline/timelineMeasure.ts +3273 -0
  182. package/src/ui/useThrottledValue.ts +31 -0
@@ -0,0 +1,383 @@
1
+ import type { ProviderId, ProviderWorkspaceOverride } from "../providerLauncher/types.js";
2
+
3
+ export type CapabilitySource = "api" | "cli" | "config" | "detected-family" | "known-registry" | "unknown";
4
+ export type CapabilityConfidence = "verified" | "configured" | "detected" | "known" | "unknown";
5
+ export type LocalModelFamily = "deepseek";
6
+
7
+ export interface ModelCapabilityProfile {
8
+ providerId: ProviderId;
9
+ modelId: string;
10
+ family: LocalModelFamily | null;
11
+ maxOutputTokens: number | null;
12
+ supportsStreaming: boolean | null;
13
+ supportsToolCalls: boolean | null;
14
+ supportsSystemPrompt: boolean | null;
15
+ supportsVision: boolean | null;
16
+ source: CapabilitySource;
17
+ confidence: CapabilityConfidence;
18
+ raw?: unknown;
19
+ error?: string;
20
+ }
21
+
22
+ export interface ResolveModelCapabilityProfileOptions {
23
+ providerId: ProviderId;
24
+ modelId: string;
25
+ providerConfig?: ProviderWorkspaceOverride | null;
26
+ rawMetadata?: unknown;
27
+ }
28
+
29
+ interface KnownCapabilityRegistryEntry {
30
+ maxOutputTokens?: number;
31
+ supportsStreaming?: boolean;
32
+ supportsToolCalls?: boolean;
33
+ supportsSystemPrompt?: boolean;
34
+ supportsVision?: boolean;
35
+ }
36
+
37
+ // Empty by default — only exact "providerId:modelId" matches are allowed.
38
+ // Do NOT add wildcard or family-based entries.
39
+ const KNOWN_CAPABILITY_REGISTRY: Record<string, KnownCapabilityRegistryEntry> = {};
40
+
41
+ const SYSTEM_PROMPT_CANDIDATES = [
42
+ "supports_system_prompt",
43
+ "supportsSystemPrompt",
44
+ "system_prompt",
45
+ "has_system_prompt",
46
+ ] as const;
47
+
48
+ const STREAMING_CANDIDATES = [
49
+ "supports_streaming",
50
+ "supportsStreaming",
51
+ "streaming",
52
+ "stream_supported",
53
+ ] as const;
54
+
55
+ const TOOL_CALLS_CANDIDATES = [
56
+ "supports_tool_calls",
57
+ "supportsToolCalls",
58
+ "tool_calls",
59
+ "supports_tools",
60
+ "function_calling",
61
+ "has_tools",
62
+ ] as const;
63
+
64
+ const MAX_OUTPUT_TOKENS_CANDIDATES = [
65
+ "max_output_tokens",
66
+ "maxOutputTokens",
67
+ "max_new_tokens",
68
+ "max_tokens_output",
69
+ ] as const;
70
+
71
+ const NESTED_METADATA_KEYS = [
72
+ "model_info",
73
+ "modelInfo",
74
+ "metadata",
75
+ "details",
76
+ "config",
77
+ ] as const;
78
+
79
+ const MODEL_IDENTITY_FIELDS = [
80
+ "id",
81
+ "name",
82
+ "model",
83
+ "model_id",
84
+ "modelId",
85
+ "display_name",
86
+ "displayName",
87
+ "repository",
88
+ "repo",
89
+ "path",
90
+ "publisher",
91
+ ] as const;
92
+
93
+ const capabilityCache = new Map<string, ModelCapabilityProfile>();
94
+
95
+ function isRecord(value: unknown): value is Record<string, unknown> {
96
+ return typeof value === "object" && value !== null && !Array.isArray(value);
97
+ }
98
+
99
+ function validBoolean(value: unknown): boolean | null {
100
+ if (value === true || value === false) return value;
101
+ return null;
102
+ }
103
+
104
+ function validPositiveInteger(value: unknown): number | null {
105
+ if (typeof value !== "number" || !Number.isInteger(value) || value <= 0) return null;
106
+ return value;
107
+ }
108
+
109
+ function cacheKey(options: ResolveModelCapabilityProfileOptions): string {
110
+ return JSON.stringify({
111
+ providerId: options.providerId,
112
+ modelId: options.modelId,
113
+ models: options.providerConfig?.models ?? null,
114
+ });
115
+ }
116
+
117
+ function deepSeekIdentity(value: string): boolean {
118
+ return /(^|[^a-z0-9])deep[\s_\-./:]*seek(?=$|[^a-z0-9])/i.test(value);
119
+ }
120
+
121
+ function metadataIdentityValues(raw: unknown): string[] {
122
+ if (!isRecord(raw)) return [];
123
+ const values: string[] = [];
124
+ for (const field of MODEL_IDENTITY_FIELDS) {
125
+ if (typeof raw[field] === "string") values.push(raw[field]);
126
+ }
127
+ for (const nestedKey of NESTED_METADATA_KEYS) {
128
+ const nested = raw[nestedKey];
129
+ if (!isRecord(nested)) continue;
130
+ for (const field of MODEL_IDENTITY_FIELDS) {
131
+ if (typeof nested[field] === "string") values.push(nested[field]);
132
+ }
133
+ }
134
+ return values;
135
+ }
136
+
137
+ export function detectLocalModelFamily(modelId: string, rawMetadata?: unknown): LocalModelFamily | null {
138
+ return [modelId, ...metadataIdentityValues(rawMetadata)].some(deepSeekIdentity)
139
+ ? "deepseek"
140
+ : null;
141
+ }
142
+
143
+ export function clearModelCapabilityProfileCache(): void {
144
+ capabilityCache.clear();
145
+ }
146
+
147
+ function unknownProfile(
148
+ providerId: ProviderId,
149
+ modelId: string,
150
+ error?: string,
151
+ ): ModelCapabilityProfile {
152
+ return {
153
+ providerId,
154
+ modelId,
155
+ family: null,
156
+ maxOutputTokens: null,
157
+ supportsStreaming: null,
158
+ supportsToolCalls: null,
159
+ supportsSystemPrompt: null,
160
+ supportsVision: null,
161
+ source: "unknown",
162
+ confidence: "unknown",
163
+ ...(error ? { error } : {}),
164
+ };
165
+ }
166
+
167
+ function findBooleanField(
168
+ raw: Record<string, unknown>,
169
+ candidates: readonly string[],
170
+ ): boolean | null {
171
+ for (const field of candidates) {
172
+ const value = validBoolean(raw[field]);
173
+ if (value !== null) return value;
174
+ }
175
+ for (const nestedKey of NESTED_METADATA_KEYS) {
176
+ const nested = raw[nestedKey];
177
+ if (!isRecord(nested)) continue;
178
+ for (const field of candidates) {
179
+ const value = validBoolean(nested[field]);
180
+ if (value !== null) return value;
181
+ }
182
+ }
183
+ return null;
184
+ }
185
+
186
+ function findIntegerField(
187
+ raw: Record<string, unknown>,
188
+ candidates: readonly string[],
189
+ ): number | null {
190
+ for (const field of candidates) {
191
+ const value = validPositiveInteger(raw[field]);
192
+ if (value !== null) return value;
193
+ }
194
+ for (const nestedKey of NESTED_METADATA_KEYS) {
195
+ const nested = raw[nestedKey];
196
+ if (!isRecord(nested)) continue;
197
+ for (const field of candidates) {
198
+ const value = validPositiveInteger(nested[field]);
199
+ if (value !== null) return value;
200
+ }
201
+ }
202
+ return null;
203
+ }
204
+
205
+ function resolveFromRawMetadata(
206
+ providerId: ProviderId,
207
+ modelId: string,
208
+ raw: unknown,
209
+ ): ModelCapabilityProfile | null {
210
+ if (providerId !== "local") return null;
211
+ if (!isRecord(raw)) return null;
212
+
213
+ const supportsSystemPrompt = findBooleanField(raw, SYSTEM_PROMPT_CANDIDATES);
214
+ const supportsStreaming = findBooleanField(raw, STREAMING_CANDIDATES);
215
+ let supportsToolCalls = findBooleanField(raw, TOOL_CALLS_CANDIDATES);
216
+ const maxOutputTokens = findIntegerField(raw, MAX_OUTPUT_TOKENS_CANDIDATES);
217
+
218
+ if (supportsToolCalls === null && Array.isArray(raw.capabilities)) {
219
+ if ((raw.capabilities as unknown[]).includes("tool_use")) {
220
+ supportsToolCalls = true;
221
+ }
222
+ // absence of "tool_use" in capabilities ≠ not supported — leave null
223
+ }
224
+
225
+ if (
226
+ supportsSystemPrompt === null
227
+ && supportsStreaming === null
228
+ && supportsToolCalls === null
229
+ && maxOutputTokens === null
230
+ ) {
231
+ return null;
232
+ }
233
+
234
+ return {
235
+ providerId,
236
+ modelId,
237
+ family: null,
238
+ maxOutputTokens,
239
+ supportsStreaming,
240
+ supportsToolCalls,
241
+ supportsSystemPrompt,
242
+ supportsVision: null,
243
+ source: "api",
244
+ confidence: "verified",
245
+ raw,
246
+ };
247
+ }
248
+
249
+ function resolveFromConfig(
250
+ providerId: ProviderId,
251
+ modelId: string,
252
+ providerConfig?: ProviderWorkspaceOverride | null,
253
+ ): ModelCapabilityProfile | null {
254
+ const modelOverride = providerConfig?.models?.[modelId];
255
+ if (!modelOverride) return null;
256
+
257
+ const supportsStreaming = validBoolean(modelOverride.supportsStreaming);
258
+ const supportsToolCalls = validBoolean(modelOverride.supportsToolCalls);
259
+ const supportsSystemPrompt = validBoolean(modelOverride.supportsSystemPrompt);
260
+ const maxOutputTokens = validPositiveInteger(modelOverride.maxOutputTokens);
261
+
262
+ const hasAny =
263
+ supportsSystemPrompt !== null
264
+ || supportsStreaming !== null
265
+ || supportsToolCalls !== null
266
+ || maxOutputTokens !== null;
267
+
268
+ if (!hasAny) return null;
269
+
270
+ return {
271
+ providerId,
272
+ modelId,
273
+ family: null,
274
+ maxOutputTokens,
275
+ supportsStreaming,
276
+ supportsToolCalls,
277
+ supportsSystemPrompt,
278
+ supportsVision: validBoolean(modelOverride.supportsVision),
279
+ source: "config",
280
+ confidence: "configured",
281
+ };
282
+ }
283
+
284
+ function resolveFromKnownRegistry(
285
+ providerId: ProviderId,
286
+ modelId: string,
287
+ ): ModelCapabilityProfile | null {
288
+ const entry = KNOWN_CAPABILITY_REGISTRY[`${providerId}:${modelId}`];
289
+ if (!entry) return null;
290
+ return {
291
+ providerId,
292
+ modelId,
293
+ family: null,
294
+ maxOutputTokens: entry.maxOutputTokens ?? null,
295
+ supportsStreaming: entry.supportsStreaming ?? null,
296
+ supportsToolCalls: entry.supportsToolCalls ?? null,
297
+ supportsSystemPrompt: entry.supportsSystemPrompt ?? null,
298
+ supportsVision: entry.supportsVision ?? null,
299
+ source: "known-registry",
300
+ confidence: "known",
301
+ };
302
+ }
303
+
304
+ function resolveFromDetectedFamily(
305
+ providerId: ProviderId,
306
+ modelId: string,
307
+ rawMetadata?: unknown,
308
+ ): ModelCapabilityProfile | null {
309
+ if (providerId !== "local") return null;
310
+ const family = detectLocalModelFamily(modelId, rawMetadata);
311
+ if (family !== "deepseek") return null;
312
+ return {
313
+ providerId,
314
+ modelId,
315
+ family,
316
+ maxOutputTokens: null,
317
+ supportsStreaming: true,
318
+ supportsToolCalls: true,
319
+ supportsSystemPrompt: null,
320
+ supportsVision: null,
321
+ source: "detected-family",
322
+ confidence: "detected",
323
+ };
324
+ }
325
+
326
+ function mergeProfiles(options: {
327
+ providerId: ProviderId;
328
+ modelId: string;
329
+ rawMetadata?: unknown;
330
+ profiles: readonly (ModelCapabilityProfile | null)[];
331
+ }): ModelCapabilityProfile {
332
+ const profiles = options.profiles.filter((profile): profile is ModelCapabilityProfile => profile !== null);
333
+ const first = profiles[0];
334
+ const family = detectLocalModelFamily(options.modelId, options.rawMetadata)
335
+ ?? profiles.find((profile) => profile.family)?.family
336
+ ?? null;
337
+ const value = <K extends keyof Pick<ModelCapabilityProfile,
338
+ "maxOutputTokens" | "supportsStreaming" | "supportsToolCalls" | "supportsSystemPrompt" | "supportsVision"
339
+ >>(field: K): ModelCapabilityProfile[K] => {
340
+ for (const profile of profiles) {
341
+ if (profile[field] !== null) return profile[field];
342
+ }
343
+ return null as ModelCapabilityProfile[K];
344
+ };
345
+
346
+ if (!first && !family) return unknownProfile(options.providerId, options.modelId);
347
+ return {
348
+ providerId: options.providerId,
349
+ modelId: options.modelId,
350
+ family,
351
+ maxOutputTokens: value("maxOutputTokens"),
352
+ supportsStreaming: value("supportsStreaming"),
353
+ supportsToolCalls: value("supportsToolCalls"),
354
+ supportsSystemPrompt: value("supportsSystemPrompt"),
355
+ supportsVision: value("supportsVision"),
356
+ source: first?.source ?? "unknown",
357
+ confidence: first?.confidence ?? "unknown",
358
+ ...(options.rawMetadata !== undefined ? { raw: options.rawMetadata } : {}),
359
+ };
360
+ }
361
+
362
+ export function resolveModelCapabilityProfileCached(
363
+ options: ResolveModelCapabilityProfileOptions,
364
+ ): ModelCapabilityProfile {
365
+ const key = cacheKey(options);
366
+ const cached = capabilityCache.get(key);
367
+ if (cached && options.rawMetadata === undefined) {
368
+ return cached;
369
+ }
370
+
371
+ const raw = resolveFromRawMetadata(options.providerId, options.modelId, options.rawMetadata);
372
+ const config = resolveFromConfig(options.providerId, options.modelId, options.providerConfig);
373
+ const detected = resolveFromDetectedFamily(options.providerId, options.modelId, options.rawMetadata);
374
+ const registry = resolveFromKnownRegistry(options.providerId, options.modelId);
375
+ const result = mergeProfiles({
376
+ providerId: options.providerId,
377
+ modelId: options.modelId,
378
+ rawMetadata: options.rawMetadata,
379
+ profiles: [raw, config, detected, registry],
380
+ });
381
+ capabilityCache.set(key, result);
382
+ return result;
383
+ }