coding-tool-x 3.7.7 → 3.7.9

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.
@@ -0,0 +1,570 @@
1
+ 'use strict';
2
+
3
+ const MODEL_SCHEMA_VERSION = 2;
4
+ const MODEL_METADATA_MODES = ['auto', 'hybrid', 'manual'];
5
+ const THINKING_EFFORT_LEVELS = ['minimal', 'low', 'medium', 'high', 'xhigh'];
6
+ const THINKING_SELECTOR_LEVELS = [...THINKING_EFFORT_LEVELS, 'off'];
7
+ const THINKING_MODES = [
8
+ 'effort',
9
+ 'budget',
10
+ 'google-level',
11
+ 'anthropic-adaptive',
12
+ 'anthropic-budget-effort'
13
+ ];
14
+ const MODEL_APIS = [
15
+ 'openai-completions',
16
+ 'openai-responses',
17
+ 'openai-codex-responses',
18
+ 'azure-openai-responses',
19
+ 'anthropic-messages',
20
+ 'google-generative-ai',
21
+ 'google-gemini-cli',
22
+ 'google-vertex'
23
+ ];
24
+
25
+ const MODEL_FIELDS = {
26
+ id: { type: 'string', required: true, group: 'identity' },
27
+ name: { type: 'string', group: 'identity' },
28
+ api: { type: 'enum', values: MODEL_APIS, group: 'identity' },
29
+ baseUrl: { type: 'string', group: 'identity' },
30
+ reasoning: { type: 'boolean', group: 'capabilities' },
31
+ thinking: { type: 'thinking', group: 'capabilities' },
32
+ input: { type: 'enum-array', values: ['text', 'image'], group: 'capabilities' },
33
+ supportsTools: { type: 'boolean', group: 'capabilities' },
34
+ cost: { type: 'cost', group: 'limits' },
35
+ premiumMultiplier: { type: 'non-negative-number', group: 'limits' },
36
+ contextWindow: { type: 'positive-number', group: 'limits' },
37
+ maxTokens: { type: 'positive-number', group: 'limits' },
38
+ omitMaxOutputTokens: { type: 'boolean', group: 'limits' },
39
+ headers: { type: 'string-map', sensitive: true, group: 'request' },
40
+ compat: { type: 'compat', group: 'request' },
41
+ contextPromotionTarget: { type: 'string', group: 'compaction' },
42
+ compactionModel: { type: 'string', group: 'compaction' },
43
+ remoteCompaction: { type: 'remote-compaction', group: 'compaction' }
44
+ };
45
+
46
+ const COMPAT_FIELDS = {
47
+ supportsStore: 'boolean',
48
+ supportsDeveloperRole: 'boolean',
49
+ supportsMultipleSystemMessages: 'boolean',
50
+ supportsReasoningEffort: 'boolean',
51
+ reasoningEffortMap: 'object',
52
+ maxTokensField: ['max_completion_tokens', 'max_tokens'],
53
+ supportsUsageInStreaming: 'boolean',
54
+ requiresToolResultName: 'boolean',
55
+ requiresMistralToolIds: 'boolean',
56
+ requiresAssistantAfterToolResult: 'boolean',
57
+ requiresThinkingAsText: 'boolean',
58
+ reasoningContentField: ['reasoning_content', 'reasoning', 'reasoning_text'],
59
+ requiresReasoningContentForToolCalls: 'boolean',
60
+ allowsSyntheticReasoningContentForToolCalls: 'boolean',
61
+ requiresAssistantContentForToolCalls: 'boolean',
62
+ supportsToolChoice: 'boolean',
63
+ supportsForcedToolChoice: 'boolean',
64
+ disableReasoningOnForcedToolChoice: 'boolean',
65
+ disableReasoningOnToolChoice: 'boolean',
66
+ thinkingFormat: ['openai', 'openrouter', 'zai', 'qwen', 'qwen-chat-template'],
67
+ openRouterRouting: 'object',
68
+ vercelGatewayRouting: 'object',
69
+ extraBody: 'object',
70
+ cacheControlFormat: ['anthropic'],
71
+ supportsStrictMode: 'boolean',
72
+ toolStrictMode: ['all_strict', 'none'],
73
+ streamIdleTimeoutMs: 'positive-number',
74
+ supportsLongPromptCacheRetention: 'boolean',
75
+ supportsReasoningParams: 'boolean',
76
+ alwaysSendMaxTokens: 'boolean',
77
+ strictResponsesPairing: 'boolean',
78
+ supportsImageDetailOriginal: 'boolean',
79
+ requiresToolResultId: 'boolean',
80
+ replayUnsignedThinking: 'boolean',
81
+ nested: 'object'
82
+ };
83
+
84
+ const REMOTE_COMPACTION_FIELDS = {
85
+ enabled: 'boolean',
86
+ api: 'string',
87
+ endpoint: 'string',
88
+ model: 'string',
89
+ v2StreamingEnabled: 'boolean',
90
+ v2Endpoint: 'string',
91
+ streamingEndpoint: 'string'
92
+ };
93
+
94
+ const PROVIDER_FIELDS = {
95
+ baseUrl: { type: 'string' },
96
+ apiKey: { type: 'string', sensitive: true },
97
+ api: { type: 'enum', values: MODEL_APIS },
98
+ headers: { type: 'string-map', sensitive: true },
99
+ compat: { type: 'compat' },
100
+ remoteCompaction: { type: 'remote-compaction' },
101
+ authHeader: { type: 'boolean' },
102
+ auth: { type: 'enum', values: ['apiKey', 'none', 'oauth'] },
103
+ discovery: {
104
+ type: 'enum',
105
+ values: ['ollama', 'llama.cpp', 'lm-studio', 'openai-models-list', 'proxy', 'litellm']
106
+ },
107
+ modelOverrides: { type: 'object' },
108
+ disableStrictTools: { type: 'boolean' },
109
+ transport: { type: 'enum', values: ['pi-native'] }
110
+ };
111
+
112
+ const MANAGEMENT_FIELDS = new Set([
113
+ 'schemaVersion',
114
+ 'metadataMode',
115
+ 'source',
116
+ 'overrides',
117
+ 'defaultThinkingLevel',
118
+ 'passthrough'
119
+ ]);
120
+
121
+ function isPlainObject(value) {
122
+ return value !== null && typeof value === 'object' && !Array.isArray(value);
123
+ }
124
+
125
+ function cloneValue(value) {
126
+ if (Array.isArray(value)) return value.map(cloneValue);
127
+ if (isPlainObject(value)) {
128
+ return Object.fromEntries(Object.entries(value).map(([key, item]) => [key, cloneValue(item)]));
129
+ }
130
+ return value;
131
+ }
132
+
133
+ function isSensitiveKey(key) {
134
+ const normalized = String(key || '').replace(/[^a-z0-9]/gi, '').toLowerCase();
135
+ if (!normalized) return false;
136
+ if ([
137
+ 'apikey',
138
+ 'authorization',
139
+ 'token',
140
+ 'accesstoken',
141
+ 'refreshtoken',
142
+ 'authtoken',
143
+ 'bearertoken',
144
+ 'idtoken',
145
+ 'secret',
146
+ 'clientsecret',
147
+ 'password',
148
+ 'cookie',
149
+ 'setcookie'
150
+ ].includes(normalized)) return true;
151
+ return normalized.endsWith('apikey')
152
+ || normalized.endsWith('token')
153
+ || normalized.endsWith('secret')
154
+ || normalized.endsWith('password');
155
+ }
156
+
157
+ function redactSensitiveFields(value, key = '') {
158
+ if (Array.isArray(value)) return value.map(item => redactSensitiveFields(item));
159
+ if (!isPlainObject(value)) {
160
+ return isSensitiveKey(key) ? '[redacted]' : value;
161
+ }
162
+ if (key === 'headers') {
163
+ return Object.fromEntries(Object.keys(value).map(header => [header, '[redacted]']));
164
+ }
165
+ return Object.fromEntries(
166
+ Object.entries(value).map(([childKey, item]) => [childKey, redactSensitiveFields(item, childKey)])
167
+ );
168
+ }
169
+
170
+ function compactObject(value = {}) {
171
+ return Object.fromEntries(Object.entries(value).filter(([, item]) => item !== undefined));
172
+ }
173
+
174
+ function splitModelSelector(value = '') {
175
+ const raw = String(value || '').trim();
176
+ const match = raw.match(/:(minimal|low|medium|high|xhigh|off)$/i);
177
+ if (!match) return { modelId: raw, thinkingLevel: null };
178
+ return {
179
+ modelId: raw.slice(0, -match[0].length),
180
+ thinkingLevel: match[1].toLowerCase()
181
+ };
182
+ }
183
+
184
+ function serializeModelSelector(modelId, thinkingLevel) {
185
+ const base = splitModelSelector(modelId).modelId;
186
+ const level = String(thinkingLevel || '').trim().toLowerCase();
187
+ return base && THINKING_SELECTOR_LEVELS.includes(level) ? `${base}:${level}` : base;
188
+ }
189
+
190
+ function normalizeNumber(value, { positive = false, nonNegative = false } = {}) {
191
+ if (value === undefined) return undefined;
192
+ const number = Number(value);
193
+ if (!Number.isFinite(number)) return undefined;
194
+ if (positive && number <= 0) return undefined;
195
+ if (nonNegative && number < 0) return undefined;
196
+ return number;
197
+ }
198
+
199
+ function normalizeStringMap(value) {
200
+ if (!isPlainObject(value)) return undefined;
201
+ const result = {};
202
+ for (const [key, item] of Object.entries(value)) {
203
+ if (typeof item === 'string') result[key] = item;
204
+ }
205
+ return Object.keys(result).length > 0 ? result : undefined;
206
+ }
207
+
208
+ function normalizeCost(value) {
209
+ if (!isPlainObject(value)) return undefined;
210
+ const result = {};
211
+ const aliases = {
212
+ input: ['input'],
213
+ output: ['output'],
214
+ cacheRead: ['cacheRead', 'cache_read'],
215
+ cacheWrite: ['cacheWrite', 'cache_write', 'cacheCreation']
216
+ };
217
+ for (const [target, candidates] of Object.entries(aliases)) {
218
+ const key = candidates.find(candidate => value[candidate] !== undefined);
219
+ if (!key) continue;
220
+ const number = normalizeNumber(value[key], { nonNegative: true });
221
+ if (number !== undefined) result[target] = number;
222
+ }
223
+ if (Object.keys(result).length === 0) return undefined;
224
+ // OMP's cost object is all-or-nothing: when present, all four numeric keys
225
+ // are required. Zero here means “no known charge for this dimension”; an
226
+ // entirely unknown cost remains omitted above.
227
+ return {
228
+ input: 0,
229
+ output: 0,
230
+ cacheRead: 0,
231
+ cacheWrite: 0,
232
+ ...result
233
+ };
234
+ }
235
+
236
+ function normalizeThinking(value) {
237
+ if (Array.isArray(value)) {
238
+ const efforts = [...new Set(value.map(item => String(item || '').trim()).filter(item => THINKING_EFFORT_LEVELS.includes(item)))];
239
+ return efforts.length > 0 ? { mode: 'effort', efforts } : undefined;
240
+ }
241
+ if (typeof value === 'string') {
242
+ const level = value.trim();
243
+ return THINKING_EFFORT_LEVELS.includes(level) ? { mode: 'effort', efforts: [level] } : undefined;
244
+ }
245
+ if (!isPlainObject(value)) return undefined;
246
+ const mode = THINKING_MODES.includes(value.mode) ? value.mode : undefined;
247
+ if (!mode) return undefined;
248
+ const efforts = Array.isArray(value.efforts || value.levels)
249
+ ? [...new Set((value.efforts || value.levels)
250
+ .map(item => String(item || '').trim())
251
+ .filter(item => THINKING_EFFORT_LEVELS.includes(item)))]
252
+ : undefined;
253
+ const defaultLevel = THINKING_EFFORT_LEVELS.includes(value.defaultLevel) ? value.defaultLevel : undefined;
254
+ const minLevel = THINKING_EFFORT_LEVELS.includes(value.minLevel) ? value.minLevel : undefined;
255
+ const maxLevel = THINKING_EFFORT_LEVELS.includes(value.maxLevel) ? value.maxLevel : undefined;
256
+ return compactObject({
257
+ mode,
258
+ efforts: efforts?.length ? efforts : undefined,
259
+ defaultLevel,
260
+ minLevel: efforts?.length ? undefined : minLevel,
261
+ maxLevel: efforts?.length ? undefined : maxLevel,
262
+ levels: Array.isArray(value.levels) ? cloneValue(value.levels) : undefined,
263
+ effortMap: isPlainObject(value.effortMap) ? cloneValue(value.effortMap) : undefined,
264
+ supportsDisplay: typeof value.supportsDisplay === 'boolean' ? value.supportsDisplay : undefined
265
+ });
266
+ }
267
+
268
+ function normalizeTypedObject(value, fields, warnings, prefix) {
269
+ if (!isPlainObject(value)) return undefined;
270
+ const result = {};
271
+ for (const [key, item] of Object.entries(value)) {
272
+ const descriptor = fields[key];
273
+ if (!descriptor) {
274
+ warnings.push(`${prefix}.${key} is not supported by the current coding-tool-x schema`);
275
+ continue;
276
+ }
277
+ let normalized;
278
+ if (descriptor === 'boolean') normalized = typeof item === 'boolean' ? item : undefined;
279
+ else if (descriptor === 'string') normalized = typeof item === 'string' ? item : undefined;
280
+ else if (descriptor === 'object') normalized = isPlainObject(item) ? cloneValue(item) : undefined;
281
+ else if (descriptor === 'positive-number') normalized = normalizeNumber(item, { positive: true });
282
+ else if (Array.isArray(descriptor)) normalized = descriptor.includes(item) ? item : undefined;
283
+ if (normalized === undefined) warnings.push(`${prefix}.${key} has an invalid value`);
284
+ else result[key] = normalized;
285
+ }
286
+ return Object.keys(result).length > 0 ? result : undefined;
287
+ }
288
+
289
+ function extractPassthrough(value) {
290
+ if (!isPlainObject(value)) return {};
291
+ const result = isPlainObject(value.passthrough) ? cloneValue(value.passthrough) : {};
292
+ for (const [key, item] of Object.entries(value)) {
293
+ if (!MODEL_FIELDS[key] && !MANAGEMENT_FIELDS.has(key) && key !== 'limit' && key !== 'pricing') {
294
+ result[key] = cloneValue(item);
295
+ }
296
+ }
297
+ if (isPlainObject(value.compat)) {
298
+ const unknownCompat = Object.fromEntries(
299
+ Object.entries(value.compat).filter(([key]) => !COMPAT_FIELDS[key]).map(([key, item]) => [key, cloneValue(item)])
300
+ );
301
+ if (Object.keys(unknownCompat).length > 0) {
302
+ result.compat = { ...(isPlainObject(result.compat) ? result.compat : {}), ...unknownCompat };
303
+ }
304
+ }
305
+ if (isPlainObject(value.remoteCompaction)) {
306
+ const unknownRemote = Object.fromEntries(
307
+ Object.entries(value.remoteCompaction)
308
+ .filter(([key]) => !REMOTE_COMPACTION_FIELDS[key])
309
+ .map(([key, item]) => [key, cloneValue(item)])
310
+ );
311
+ if (Object.keys(unknownRemote).length > 0) {
312
+ result.remoteCompaction = {
313
+ ...(isPlainObject(result.remoteCompaction) ? result.remoteCompaction : {}),
314
+ ...unknownRemote
315
+ };
316
+ }
317
+ }
318
+ return result;
319
+ }
320
+
321
+ function normalizeModelSpec(value = {}, fallbackId = '') {
322
+ const warnings = [];
323
+ const source = isPlainObject(value) ? value : {};
324
+ for (const key of Object.keys(source)) {
325
+ if (!MODEL_FIELDS[key] && !MANAGEMENT_FIELDS.has(key) && key !== 'limit' && key !== 'pricing') {
326
+ warnings.push(`${key} is not supported by the current coding-tool-x schema`);
327
+ }
328
+ }
329
+ const rawId = source.id || source.name || fallbackId;
330
+ const { modelId } = splitModelSelector(rawId);
331
+ if (!modelId) return { spec: null, passthrough: extractPassthrough(source), warnings: ['model id is required'] };
332
+
333
+ const legacyLimit = isPlainObject(source.limit) ? source.limit : {};
334
+ const legacyPricing = isPlainObject(source.pricing) ? source.pricing : {};
335
+ const input = Array.isArray(source.input)
336
+ ? [...new Set(source.input.filter(item => item === 'text' || item === 'image'))]
337
+ : undefined;
338
+ const api = source.api === undefined || MODEL_APIS.includes(source.api) ? source.api : undefined;
339
+ if (source.api !== undefined && api === undefined) warnings.push('api has an invalid value');
340
+
341
+ const spec = compactObject({
342
+ id: modelId,
343
+ name: typeof source.name === 'string' && source.name.trim() ? source.name.trim() : undefined,
344
+ api,
345
+ baseUrl: typeof source.baseUrl === 'string' && source.baseUrl.trim() ? source.baseUrl.trim() : undefined,
346
+ reasoning: typeof source.reasoning === 'boolean' ? source.reasoning : undefined,
347
+ thinking: normalizeThinking(source.thinking),
348
+ input: input?.length ? input : undefined,
349
+ supportsTools: typeof source.supportsTools === 'boolean' ? source.supportsTools : undefined,
350
+ cost: normalizeCost(source.cost || legacyPricing),
351
+ premiumMultiplier: normalizeNumber(source.premiumMultiplier, { nonNegative: true }),
352
+ contextWindow: normalizeNumber(source.contextWindow ?? source.context_window ?? legacyLimit.context, { positive: true }),
353
+ maxTokens: normalizeNumber(source.maxTokens ?? source.max_tokens ?? legacyLimit.output, { positive: true }),
354
+ omitMaxOutputTokens: typeof source.omitMaxOutputTokens === 'boolean' ? source.omitMaxOutputTokens : undefined,
355
+ headers: normalizeStringMap(source.headers),
356
+ compat: normalizeTypedObject(source.compat, COMPAT_FIELDS, warnings, 'compat'),
357
+ contextPromotionTarget: typeof source.contextPromotionTarget === 'string' ? source.contextPromotionTarget : undefined,
358
+ compactionModel: typeof source.compactionModel === 'string' ? source.compactionModel : undefined,
359
+ remoteCompaction: normalizeTypedObject(source.remoteCompaction, REMOTE_COMPACTION_FIELDS, warnings, 'remoteCompaction')
360
+ });
361
+ const invalidWhenPresent = (key, valid) => {
362
+ if (source[key] !== undefined && !valid) warnings.push(`${key} has an invalid value`);
363
+ };
364
+ invalidWhenPresent('name', typeof source.name === 'string');
365
+ invalidWhenPresent('baseUrl', typeof source.baseUrl === 'string');
366
+ invalidWhenPresent('reasoning', typeof source.reasoning === 'boolean');
367
+ invalidWhenPresent('thinking', spec.thinking !== undefined);
368
+ invalidWhenPresent('input', Array.isArray(source.input) && source.input.length > 0 && spec.input !== undefined);
369
+ invalidWhenPresent('supportsTools', typeof source.supportsTools === 'boolean');
370
+ invalidWhenPresent('cost', isPlainObject(source.cost) && spec.cost !== undefined);
371
+ invalidWhenPresent('premiumMultiplier', spec.premiumMultiplier !== undefined);
372
+ if (source.contextWindow !== undefined || source.context_window !== undefined) {
373
+ invalidWhenPresent(source.contextWindow !== undefined ? 'contextWindow' : 'context_window', spec.contextWindow !== undefined);
374
+ }
375
+ if (source.maxTokens !== undefined || source.max_tokens !== undefined) {
376
+ invalidWhenPresent(source.maxTokens !== undefined ? 'maxTokens' : 'max_tokens', spec.maxTokens !== undefined);
377
+ }
378
+ invalidWhenPresent('omitMaxOutputTokens', typeof source.omitMaxOutputTokens === 'boolean');
379
+ invalidWhenPresent('headers', isPlainObject(source.headers)
380
+ && Object.values(source.headers).every(item => typeof item === 'string'));
381
+ invalidWhenPresent('compat', isPlainObject(source.compat));
382
+ invalidWhenPresent('contextPromotionTarget', typeof source.contextPromotionTarget === 'string');
383
+ invalidWhenPresent('compactionModel', typeof source.compactionModel === 'string');
384
+ invalidWhenPresent('remoteCompaction', isPlainObject(source.remoteCompaction));
385
+ return { spec, passthrough: extractPassthrough(source), warnings };
386
+ }
387
+
388
+ function normalizeCatalogModelList(value) {
389
+ const selected = new Map();
390
+ const warnings = [];
391
+ for (const rawModel of Array.isArray(value) ? value : []) {
392
+ const rawId = String(rawModel?.id || rawModel?.name || '').trim();
393
+ const normalized = normalizeModelSpec(rawModel, rawId);
394
+ if (!normalized.spec?.id) continue;
395
+ warnings.push(...normalized.warnings.map(message => `${normalized.spec.id}: ${message}`));
396
+ const key = normalized.spec.id.toLowerCase();
397
+ const canonical = splitModelSelector(rawId).thinkingLevel === null;
398
+ const score = Object.keys(normalized.spec).length;
399
+ const previous = selected.get(key);
400
+ if (!previous || (canonical && !previous.canonical) || (canonical === previous.canonical && score > previous.score)) {
401
+ selected.set(key, { spec: normalized.spec, canonical, score });
402
+ }
403
+ }
404
+ return {
405
+ models: [...selected.values()].map(item => item.spec),
406
+ warnings: [...new Set(warnings)]
407
+ };
408
+ }
409
+
410
+ function normalizeProviderConfig(value = {}) {
411
+ const source = isPlainObject(value) ? value : {};
412
+ const warnings = [];
413
+ const passthrough = {};
414
+ const config = {};
415
+ for (const [key, item] of Object.entries(source)) {
416
+ const descriptor = PROVIDER_FIELDS[key];
417
+ if (!descriptor) {
418
+ passthrough[key] = cloneValue(item);
419
+ warnings.push(`${key} is not supported by the current coding-tool-x provider schema`);
420
+ continue;
421
+ }
422
+ let normalized;
423
+ if (descriptor.type === 'string') normalized = typeof item === 'string' ? item : undefined;
424
+ else if (descriptor.type === 'boolean') normalized = typeof item === 'boolean' ? item : undefined;
425
+ else if (descriptor.type === 'object') normalized = isPlainObject(item) ? cloneValue(item) : undefined;
426
+ else if (descriptor.type === 'string-map') normalized = normalizeStringMap(item);
427
+ else if (descriptor.type === 'compat') {
428
+ normalized = isPlainObject(item) ? (normalizeTypedObject(item, COMPAT_FIELDS, warnings, 'compat') || {}) : undefined;
429
+ }
430
+ else if (descriptor.type === 'remote-compaction') {
431
+ normalized = isPlainObject(item)
432
+ ? (normalizeTypedObject(item, REMOTE_COMPACTION_FIELDS, warnings, 'remoteCompaction') || {})
433
+ : undefined;
434
+ } else if (descriptor.type === 'enum') {
435
+ normalized = descriptor.values.includes(item) ? item : undefined;
436
+ }
437
+ if (normalized === undefined) warnings.push(`${key} has an invalid value`);
438
+ else config[key] = normalized;
439
+ }
440
+ return { config, passthrough, warnings };
441
+ }
442
+
443
+ function mergeModelSpecs(...values) {
444
+ const result = {};
445
+ for (const value of values) {
446
+ if (!isPlainObject(value)) continue;
447
+ for (const [key, item] of Object.entries(value)) {
448
+ if (item === undefined) continue;
449
+ if (isPlainObject(item) && isPlainObject(result[key])) result[key] = { ...result[key], ...cloneValue(item) };
450
+ else result[key] = cloneValue(item);
451
+ }
452
+ }
453
+ return result;
454
+ }
455
+
456
+ function markProvenance(target, value, sourceName, prefix = '') {
457
+ if (!isPlainObject(value)) return;
458
+ for (const [key, item] of Object.entries(value)) {
459
+ const path = prefix ? `${prefix}.${key}` : key;
460
+ target[path] = sourceName;
461
+ if (isPlainObject(item)) markProvenance(target, item, sourceName, path);
462
+ }
463
+ }
464
+
465
+ function resolveModelDefinition(definition, sources = {}) {
466
+ const raw = typeof definition === 'string' ? { id: definition } : (definition || {});
467
+ const direct = Object.fromEntries(Object.entries(raw).filter(([key]) => !MANAGEMENT_FIELDS.has(key)));
468
+ const explicitRaw = isPlainObject(raw.overrides) ? { id: raw.id, ...direct, ...raw.overrides } : direct;
469
+ const explicit = normalizeModelSpec(explicitRaw, raw.id);
470
+ const builtin = normalizeModelSpec(sources.builtin || {}, raw.id);
471
+ const catalog = normalizeModelSpec(sources.catalog || {}, raw.id);
472
+ const mode = MODEL_METADATA_MODES.includes(raw.metadataMode)
473
+ ? raw.metadataMode
474
+ : (typeof definition === 'string' ? 'auto' : 'hybrid');
475
+ const automaticSpec = mergeModelSpecs(builtin.spec, catalog.spec);
476
+ const spec = mode === 'manual'
477
+ ? explicit.spec
478
+ : mergeModelSpecs(automaticSpec, explicit.spec);
479
+ const provenance = {};
480
+ if (mode !== 'manual') {
481
+ markProvenance(provenance, builtin.spec, 'builtin');
482
+ markProvenance(provenance, catalog.spec, 'omp-catalog');
483
+ }
484
+ markProvenance(provenance, explicit.spec, 'explicit');
485
+ return {
486
+ schemaVersion: MODEL_SCHEMA_VERSION,
487
+ metadataMode: mode,
488
+ spec,
489
+ provenance,
490
+ passthrough: {
491
+ ...builtin.passthrough,
492
+ ...catalog.passthrough,
493
+ ...explicit.passthrough
494
+ },
495
+ warnings: [...builtin.warnings, ...catalog.warnings, ...explicit.warnings]
496
+ };
497
+ }
498
+
499
+ function validateModelDefinitions(value) {
500
+ if (!Array.isArray(value)) return { valid: false, error: 'models must be an array', warnings: [] };
501
+ const warnings = [];
502
+ const seen = new Set();
503
+ for (const definition of value) {
504
+ if (typeof definition !== 'string' && !isPlainObject(definition)) {
505
+ return { valid: false, error: 'each model must be a string or object', warnings };
506
+ }
507
+ const resolved = resolveModelDefinition(definition);
508
+ if (!resolved.spec?.id) return { valid: false, error: 'each model requires a non-empty id', warnings };
509
+ const invalid = resolved.warnings.find(message => message.endsWith('has an invalid value'));
510
+ if (invalid) return { valid: false, error: `${resolved.spec.id}: ${invalid}`, warnings };
511
+ const key = resolved.spec.id.toLowerCase();
512
+ if (seen.has(key)) return { valid: false, error: `duplicate model id: ${resolved.spec.id}`, warnings };
513
+ seen.add(key);
514
+ warnings.push(...resolved.warnings.map(message => `${resolved.spec.id}: ${message}`));
515
+ }
516
+ return { valid: true, error: null, warnings };
517
+ }
518
+
519
+ function validateProviderConfig(value) {
520
+ if (!isPlainObject(value)) return { valid: false, error: 'providerConfig must be an object', warnings: [] };
521
+ const normalized = normalizeProviderConfig(value);
522
+ const invalid = normalized.warnings.find(message => message.endsWith('has an invalid value'));
523
+ return {
524
+ valid: !invalid,
525
+ error: invalid || null,
526
+ warnings: normalized.warnings,
527
+ config: normalized.config,
528
+ passthrough: normalized.passthrough
529
+ };
530
+ }
531
+
532
+ function getPublicModelFieldSchema() {
533
+ return cloneValue({
534
+ schemaVersion: MODEL_SCHEMA_VERSION,
535
+ metadataModes: MODEL_METADATA_MODES,
536
+ thinkingEffortLevels: THINKING_EFFORT_LEVELS,
537
+ thinkingSelectorLevels: THINKING_SELECTOR_LEVELS,
538
+ thinkingModes: THINKING_MODES,
539
+ modelApis: MODEL_APIS,
540
+ modelFields: MODEL_FIELDS,
541
+ compatFields: COMPAT_FIELDS,
542
+ remoteCompactionFields: REMOTE_COMPACTION_FIELDS,
543
+ providerFields: PROVIDER_FIELDS
544
+ });
545
+ }
546
+
547
+ module.exports = {
548
+ MODEL_SCHEMA_VERSION,
549
+ MODEL_METADATA_MODES,
550
+ THINKING_EFFORT_LEVELS,
551
+ THINKING_SELECTOR_LEVELS,
552
+ THINKING_MODES,
553
+ MODEL_APIS,
554
+ MODEL_FIELDS,
555
+ COMPAT_FIELDS,
556
+ REMOTE_COMPACTION_FIELDS,
557
+ PROVIDER_FIELDS,
558
+ getPublicModelFieldSchema,
559
+ isPlainObject,
560
+ mergeModelSpecs,
561
+ normalizeModelSpec,
562
+ normalizeCatalogModelList,
563
+ normalizeProviderConfig,
564
+ redactSensitiveFields,
565
+ resolveModelDefinition,
566
+ serializeModelSelector,
567
+ splitModelSelector,
568
+ validateModelDefinitions,
569
+ validateProviderConfig
570
+ };