Skip to content

Commit 9dc0d5d

Browse files
committed
feat(core,storage): add user-overridable model facts
Generated-by: Codex
1 parent 82b36dc commit 9dc0d5d

29 files changed

Lines changed: 2074 additions & 82 deletions
19.6 KB
Loading
9.01 KB
Loading

‎packages/core/package.json‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,7 @@
8383
"./llm-connections": "./dist/llm-connections.js",
8484
"./provider-registry": "./dist/provider-registry.js",
8585
"./model-catalog": "./dist/model-catalog.js",
86+
"./model-facts": "./dist/model-facts.js",
8687
"./model-metadata": "./dist/model-metadata.js",
8788
"./model-web-search": "./dist/model-web-search.js",
8889
"./model-thinking": "./dist/model-thinking.js",

‎packages/core/src/__tests__/llm-connections.test.ts‎

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,33 @@ test('model reconciliation never invents a default the user cleared', () => {
168168
);
169169
});
170170

171+
test('model reconciliation preserves enabled model-fact overrides outside live inventory', () => {
172+
assert.deepEqual(
173+
reconcileConnectionAfterModelFetch(
174+
{
175+
defaultModel: 'custom-model',
176+
enabledModelIds: ['custom-model'],
177+
hasModelInventory: true,
178+
},
179+
[{ id: 'live-model' }],
180+
{ factBackedModelIds: new Set(['custom-model']) },
181+
),
182+
{ defaultModel: 'custom-model', enabledModelIds: ['custom-model'] },
183+
);
184+
assert.deepEqual(
185+
reconcileConnectionAfterModelFetch(
186+
{
187+
defaultModel: 'live-model',
188+
enabledModelIds: ['live-model', 'custom-model'],
189+
hasModelInventory: true,
190+
},
191+
[{ id: 'live-model' }, { id: 'other-live-model' }],
192+
{ factBackedModelIds: new Set(['custom-model']) },
193+
),
194+
{ defaultModel: 'live-model', enabledModelIds: ['live-model', 'custom-model'] },
195+
);
196+
});
197+
171198
test('a renamed id follows its model, and only for a caller that supplies the table', () => {
172199
const curated = [{ id: 'claude-opus-5' }, { id: 'claude-haiku-4-5' }];
173200
const stored = {

‎packages/core/src/__tests__/model-catalog.test.ts‎

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -285,6 +285,72 @@ test('every picker sees a model the user enabled but no catalog describes', () =
285285
assert.deepEqual(declared?.provenance.sources?.userChoice, ['saved_model']);
286286
});
287287

288+
test('catalog provenance follows the projected model facts marker used in production', () => {
289+
const [entry] = buildConnectionModelCatalogEntries({
290+
connection: {
291+
slug: 'facts',
292+
providerType: 'openai',
293+
defaultModel: 'custom-model',
294+
models: [
295+
{
296+
id: 'custom-model',
297+
contextWindow: 200_000,
298+
capabilities: { chat: true },
299+
factOverriddenFields: ['contextWindow', 'capabilities'],
300+
},
301+
],
302+
modelSource: 'fetched',
303+
},
304+
});
305+
assert.equal(entry?.capabilitySource, 'user_override');
306+
assert.equal(entry?.contextWindow, 200_000);
307+
});
308+
309+
test('fallback provider catalogs include projected facts-backed models', () => {
310+
const entries = buildConnectionModelCatalogEntries({
311+
connection: {
312+
slug: 'opencode-free-facts',
313+
providerType: 'opencode-free',
314+
defaultModel: 'custom-free-model',
315+
models: [
316+
{
317+
id: 'custom-free-model',
318+
contextWindow: 128_000,
319+
factOverriddenFields: ['contextWindow', 'capabilities'],
320+
},
321+
],
322+
modelSource: 'fallback',
323+
},
324+
});
325+
const entry = entries.find((candidate) => candidate.id === 'custom-free-model');
326+
assert.equal(entry?.contextWindow, 128_000);
327+
assert.equal(entry?.capabilitySource, 'user_override');
328+
});
329+
330+
test('fallback provider catalogs apply facts to known fallback models', () => {
331+
const entries = buildConnectionModelCatalogEntries({
332+
connection: {
333+
slug: 'opencode-free-known-facts',
334+
providerType: 'opencode-free',
335+
defaultModel: 'nemotron-3-ultra-free',
336+
models: [
337+
{
338+
id: 'nemotron-3-ultra-free',
339+
contextWindow: 200_000,
340+
inputLimit: 200_000,
341+
capabilities: { chat: true },
342+
factOverriddenFields: ['contextWindow', 'inputLimit', 'capabilities'],
343+
},
344+
],
345+
modelSource: 'fallback',
346+
},
347+
});
348+
const entry = entries.find((candidate) => candidate.id === 'nemotron-3-ultra-free');
349+
assert.equal(entry?.contextWindow, 200_000);
350+
assert.equal(entry?.inputLimit, 200_000);
351+
assert.equal(entry?.capabilitySource, 'user_override');
352+
});
353+
288354
test('unknown persisted provider ids return an empty catalog', () => {
289355
assert.deepEqual(
290356
buildConnectionModelCatalogEntries({
@@ -378,7 +444,53 @@ test('DeepSeek catalogs the V4 vision model display metadata from a bare provide
378444
reasoning: true,
379445
functionCalling: true,
380446
vision: true,
447+
webSearch: true,
381448
});
382449
assert.deepEqual(model?.modalities, { input: ['text', 'image'], output: ['text'] });
383450
assert.equal(model?.canUseAsChatDefault, true);
384451
});
452+
453+
test('saved model choices can materialize a user fact override without exposing unrelated entries', () => {
454+
const entries = buildConnectionModelCatalogEntries({
455+
connection: {
456+
slug: 'zai-live',
457+
providerType: 'zai-coding-plan',
458+
defaultModel: 'glm-4.7',
459+
models: [{ id: 'glm-4.7' }],
460+
modelSource: 'fetched',
461+
},
462+
savedModelIds: [{ id: 'saved-custom', source: 'session_model' }],
463+
modelFactOverrides: {
464+
'zai-coding-plan:saved-custom': {
465+
displayName: 'Saved Custom',
466+
contextWindow: 88_000,
467+
knowledgeCutoff: '2026-01-01',
468+
structuredOutput: true,
469+
lastUpdated: '2026-02-01',
470+
modalities: { input: ['text'], output: ['text'] },
471+
},
472+
'zai-coding-plan:hidden': { contextWindow: 1_000 },
473+
},
474+
});
475+
const saved = entries.find((entry) => entry.id === 'saved-custom');
476+
assert.equal(saved?.displayName, 'Saved Custom');
477+
assert.equal(saved?.contextWindow, 88_000);
478+
assert.equal(saved?.knowledgeCutoff, '2026-01-01');
479+
assert.equal(saved?.structuredOutput, true);
480+
assert.equal(saved?.lastUpdated, '2026-02-01');
481+
assert.deepEqual(saved?.modalities, { input: ['text'], output: ['text'] });
482+
assert.equal(
483+
entries.some((entry) => entry.id === 'hidden'),
484+
false,
485+
);
486+
});
487+
488+
test('catalog capability merges retain provider web search facts alongside overrides', () => {
489+
const entries = buildModelCatalogEntries({
490+
providerType: 'deepseek',
491+
models: [{ id: 'deepseek-v4-flash', capabilities: { webSearch: true } }],
492+
modelSource: 'fetched',
493+
modelFactOverrides: { 'deepseek:deepseek-v4-flash': { capabilities: { chat: true } } },
494+
});
495+
assert.equal(entries[0]?.capabilities.webSearch, true);
496+
});
Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
20+
import assert from 'node:assert/strict';
21+
import test from 'node:test';
22+
import {
23+
applyModelFactOverride,
24+
applyModelFactOverridesToConnection,
25+
decodeModelFactsDocument,
26+
modelFactKey,
27+
} from '../model-facts.js';
28+
29+
test('model facts use provider:model keys and merge fields without replacing provider facts', () => {
30+
const key = modelFactKey('openai', 'o4-mini');
31+
const document = decodeModelFactsDocument({
32+
schemaVersion: 1,
33+
overrides: { [key]: { contextWindow: 200_000, capabilities: { vision: false } } },
34+
});
35+
const model = applyModelFactOverride(
36+
{
37+
id: 'o4-mini',
38+
displayName: 'Provider name',
39+
maxOutputTokens: 4_000,
40+
capabilities: { chat: true, vision: true },
41+
},
42+
document.overrides[key],
43+
);
44+
assert.equal(model.displayName, 'Provider name');
45+
assert.equal(model.contextWindow, 200_000);
46+
assert.deepEqual(model.capabilities, { chat: true, vision: false });
47+
});
48+
49+
test('malformed and unknown model fact fields are rejected', () => {
50+
assert.throws(() =>
51+
decodeModelFactsDocument({ schemaVersion: 1, overrides: { 'openai:o4-mini': {} } }),
52+
);
53+
assert.throws(() =>
54+
decodeModelFactsDocument({ schemaVersion: 1, overrides: { 'openai:o4-mini': { nope: true } } }),
55+
);
56+
assert.throws(() =>
57+
decodeModelFactsDocument({ schemaVersion: 1, overrides: { 'o4-mini': { contextWindow: 1 } } }),
58+
);
59+
assert.throws(() =>
60+
decodeModelFactsDocument({
61+
schemaVersion: 1,
62+
overrides: { 'openai:o4-mini': { contextWindow: 0 } },
63+
}),
64+
);
65+
assert.throws(() =>
66+
decodeModelFactsDocument({
67+
schemaVersion: 1,
68+
overrides: { 'openai:o4-mini': { capabilities: { toString: true } } },
69+
}),
70+
);
71+
assert.throws(() =>
72+
decodeModelFactsDocument({
73+
schemaVersion: 1,
74+
overrides: { 'toString:model': { contextWindow: 1 } },
75+
}),
76+
);
77+
});
78+
79+
test('model fact keys preserve colons in provider model ids', () => {
80+
const key = modelFactKey('ollama-cloud', 'gpt-oss:120b');
81+
assert.equal(key, 'ollama-cloud:gpt-oss:120b');
82+
const document = decodeModelFactsDocument({
83+
schemaVersion: 1,
84+
overrides: { [key]: { contextWindow: 131_072 } },
85+
});
86+
assert.equal(document.overrides[key]?.contextWindow, 131_072);
87+
});
88+
89+
test('override-only models are projected only when enabled', () => {
90+
const connection = {
91+
slug: 'openai',
92+
providerType: 'openai' as const,
93+
defaultModel: 'custom',
94+
enabledModelIds: ['custom'],
95+
models: [{ id: 'provider-model' }],
96+
};
97+
const result = applyModelFactOverridesToConnection(connection, {
98+
'openai:custom': { contextWindow: 64_000 },
99+
'openai:hidden': { contextWindow: 1_000 },
100+
});
101+
assert.deepEqual(result.models, [
102+
{ id: 'provider-model' },
103+
{
104+
id: 'custom',
105+
contextWindow: 64_000,
106+
inputLimit: 64_000,
107+
factOverriddenFields: ['contextWindow', 'inputLimit'],
108+
},
109+
]);
110+
});
111+
112+
test('context window facts cannot be truncated by an older input limit', () => {
113+
const result = applyModelFactOverride(
114+
{ id: 'model', contextWindow: 8_192, inputLimit: 8_192 },
115+
{ contextWindow: 200_000 },
116+
);
117+
assert.equal(result.contextWindow, 200_000);
118+
assert.equal(result.inputLimit, 200_000);
119+
});
120+
121+
test('overrides replace fields on discovered models while preserving untouched provider facts', () => {
122+
const result = applyModelFactOverridesToConnection(
123+
{
124+
providerType: 'openai',
125+
defaultModel: 'provider-model',
126+
enabledModelIds: ['provider-model'],
127+
models: [
128+
{ id: 'provider-model', contextWindow: 8_000, capabilities: { chat: true, vision: true } },
129+
],
130+
},
131+
{ 'openai:provider-model': { contextWindow: 64_000, capabilities: { vision: false } } },
132+
);
133+
assert.equal(result.models?.[0]?.contextWindow, 64_000);
134+
assert.deepEqual(result.models?.[0]?.capabilities, { chat: true, vision: false });
135+
});
136+
137+
test('modality overrides merge directions independently', () => {
138+
const result = applyModelFactOverride(
139+
{
140+
id: 'multimodal',
141+
modalities: { input: ['text', 'image', 'pdf'], output: ['text', 'audio'] },
142+
},
143+
{ modalities: { input: ['text'] } },
144+
);
145+
assert.deepEqual(result.modalities, {
146+
input: ['text'],
147+
output: ['text', 'audio'],
148+
});
149+
});
150+
151+
test('model capabilities preserve web search facts from metadata and overrides', () => {
152+
const result = applyModelFactOverride(
153+
{ id: 'web-model', capabilities: { webSearch: true } },
154+
{ capabilities: { chat: true } },
155+
);
156+
assert.deepEqual(result.capabilities, { webSearch: true, chat: true });
157+
});
158+
159+
test('model fact overrides accept parallel tool-call capability metadata', () => {
160+
const document = decodeModelFactsDocument({
161+
schemaVersion: 1,
162+
overrides: { 'openai:tool-model': { capabilities: { parallelToolCalls: false } } },
163+
});
164+
assert.deepEqual(document.overrides['openai:tool-model']?.capabilities, {
165+
parallelToolCalls: false,
166+
});
167+
});

‎packages/core/src/__tests__/runtime-policy-codec.test.ts‎

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -484,6 +484,50 @@ test('normalizes exact bounded model discovery results', () => {
484484
}
485485
});
486486

487+
test('normalizes extended model facts used by the runtime host catalog', () => {
488+
const result = normalizeConnectionModelDiscoveryResult({
489+
models: [
490+
{
491+
id: 'custom-model',
492+
description: 'A custom model',
493+
inputLimit: 120_000,
494+
knowledgeCutoff: '2025-01',
495+
structuredOutput: true,
496+
lastUpdated: '2026-01-01',
497+
modalities: { input: ['text', 'image'], output: ['text'] },
498+
},
499+
],
500+
source: 'fetched',
501+
fetchedAt: 42,
502+
});
503+
assert.deepEqual(result.models[0], {
504+
id: 'custom-model',
505+
description: 'A custom model',
506+
inputLimit: 120_000,
507+
knowledgeCutoff: '2025-01',
508+
structuredOutput: true,
509+
lastUpdated: '2026-01-01',
510+
modalities: { input: ['text', 'image'], output: ['text'] },
511+
});
512+
});
513+
514+
test('rejects sparse model modality arrays', () => {
515+
assert.throws(
516+
() =>
517+
normalizeConnectionModelDiscoveryResult({
518+
models: [
519+
{
520+
id: 'custom-model',
521+
modalities: { input: Array(1), output: ['text'] },
522+
},
523+
],
524+
source: 'fetched',
525+
fetchedAt: 42,
526+
}),
527+
RuntimePolicyDomainDecodeError,
528+
);
529+
});
530+
487531
test('credential domain validation requires material but leaves capacity to callers', () => {
488532
const input = normalizeSetCredentialInput({
489533
locator: {

0 commit comments

Comments
 (0)