Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@
"./llm-connections": "./dist/llm-connections.js",
"./provider-registry": "./dist/provider-registry.js",
"./model-catalog": "./dist/model-catalog.js",
"./model-facts": "./dist/model-facts.js",
"./model-metadata": "./dist/model-metadata.js",
"./model-web-search": "./dist/model-web-search.js",
"./model-thinking": "./dist/model-thinking.js",
Expand Down
67 changes: 67 additions & 0 deletions packages/core/src/__tests__/model-catalog.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -285,6 +285,72 @@ test('every picker sees a model the user enabled but no catalog describes', () =
assert.deepEqual(declared?.provenance.sources?.userChoice, ['saved_model']);
});

test('catalog provenance follows the projected model facts marker used in production', () => {
const [entry] = buildConnectionModelCatalogEntries({
connection: {
slug: 'facts',
providerType: 'openai',
defaultModel: 'custom-model',
models: [
{
id: 'custom-model',
contextWindow: 200_000,
capabilities: { chat: true },
factOverriddenFields: ['contextWindow', 'capabilities'],
},
],
modelSource: 'fetched',
},
});
assert.equal(entry?.capabilitySource, 'user_override');
assert.equal(entry?.contextWindow, 200_000);
});

test('fallback provider catalogs include projected facts-backed models', () => {
const entries = buildConnectionModelCatalogEntries({
connection: {
slug: 'opencode-free-facts',
providerType: 'opencode-free',
defaultModel: 'custom-free-model',
models: [
{
id: 'custom-free-model',
contextWindow: 128_000,
factOverriddenFields: ['contextWindow', 'capabilities'],
},
],
modelSource: 'fallback',
},
});
const entry = entries.find((candidate) => candidate.id === 'custom-free-model');
assert.equal(entry?.contextWindow, 128_000);
assert.equal(entry?.capabilitySource, 'user_override');
});

test('fallback provider catalogs apply facts to known fallback models', () => {
const entries = buildConnectionModelCatalogEntries({
connection: {
slug: 'opencode-free-known-facts',
providerType: 'opencode-free',
defaultModel: 'nemotron-3-ultra-free',
models: [
{
id: 'nemotron-3-ultra-free',
contextWindow: 200_000,
inputLimit: 200_000,
capabilities: { chat: true },
factOverriddenFields: ['contextWindow', 'inputLimit', 'capabilities'],
},
],
modelSource: 'fallback',
},
});
const entry = entries.find((candidate) => candidate.id === 'nemotron-3-ultra-free');
assert.equal(entry?.contextWindow, 200_000);
assert.equal(entry?.inputLimit, 200_000);
assert.equal(entry?.capabilitySource, 'user_override');
});

test('unknown persisted provider ids return an empty catalog', () => {
assert.deepEqual(
buildConnectionModelCatalogEntries({
Expand Down Expand Up @@ -378,6 +444,7 @@ test('DeepSeek catalogs the V4 vision model display metadata from a bare provide
reasoning: true,
functionCalling: true,
vision: true,
webSearch: true,
});
assert.deepEqual(model?.modalities, { input: ['text', 'image'], output: ['text'] });
assert.equal(model?.canUseAsChatDefault, true);
Expand Down
187 changes: 187 additions & 0 deletions packages/core/src/__tests__/model-facts.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/

import assert from 'node:assert/strict';
import test from 'node:test';
import {
applyModelFactOverride,
applyModelFactOverridesToConnection,
decodeModelFactsDocument,
modelFactKey,
} from '../model-facts.js';
import { CONNECTION_CATALOG_MAX_MODELS_PER_CONNECTION } from '../runtime-policy.js';

test('model facts use provider:model keys and merge fields without replacing provider facts', () => {
const key = modelFactKey('openai', 'o4-mini');
const document = decodeModelFactsDocument({
schemaVersion: 1,
overrides: { [key]: { contextWindow: 200_000, capabilities: { vision: false } } },
});
const model = applyModelFactOverride(
{
id: 'o4-mini',
displayName: 'Provider name',
maxOutputTokens: 4_000,
capabilities: { chat: true, vision: true },
},
document.overrides[key],
);
assert.equal(model.displayName, 'Provider name');
assert.equal(model.contextWindow, 200_000);
assert.deepEqual(model.capabilities, { chat: true, vision: false });
});

test('malformed and unknown model fact fields are rejected', () => {
assert.throws(() =>
decodeModelFactsDocument({ schemaVersion: 1, overrides: { 'openai:o4-mini': {} } }),
);
assert.throws(() =>
decodeModelFactsDocument({ schemaVersion: 1, overrides: { 'openai:o4-mini': { nope: true } } }),
);
assert.throws(() =>
decodeModelFactsDocument({ schemaVersion: 1, overrides: { 'o4-mini': { contextWindow: 1 } } }),
);
assert.throws(() =>
decodeModelFactsDocument({
schemaVersion: 1,
overrides: { 'openai:o4-mini': { contextWindow: 0 } },
}),
);
assert.throws(() =>
decodeModelFactsDocument({
schemaVersion: 1,
overrides: { 'openai:o4-mini': { capabilities: { toString: true } } },
}),
);
assert.throws(() =>
decodeModelFactsDocument({
schemaVersion: 1,
overrides: { 'toString:model': { contextWindow: 1 } },
}),
);
});

test('model fact keys preserve colons in provider model ids', () => {
const key = modelFactKey('ollama-cloud', 'gpt-oss:120b');
assert.equal(key, 'ollama-cloud:gpt-oss:120b');
const document = decodeModelFactsDocument({
schemaVersion: 1,
overrides: { [key]: { contextWindow: 131_072 } },
});
assert.equal(document.overrides[key]?.contextWindow, 131_072);
});

test('override-only models are projected only when enabled', () => {
const connection = {
slug: 'openai',
providerType: 'openai' as const,
defaultModel: 'custom',
enabledModelIds: ['custom'],
models: [{ id: 'provider-model' }],
};
const result = applyModelFactOverridesToConnection(connection, {
'openai:custom': { contextWindow: 64_000 },
'openai:hidden': { contextWindow: 1_000 },
});
assert.deepEqual(result.models, [
{ id: 'provider-model' },
{
id: 'custom',
contextWindow: 64_000,
inputLimit: 64_000,
factOverriddenFields: ['contextWindow', 'inputLimit'],
},
]);
});

test('context window facts cannot be truncated by an older input limit', () => {
const result = applyModelFactOverride(
{ id: 'model', contextWindow: 8_192, inputLimit: 8_192 },
{ contextWindow: 200_000 },
);
assert.equal(result.contextWindow, 200_000);
assert.equal(result.inputLimit, 200_000);
});

test('overrides replace fields on discovered models while preserving untouched provider facts', () => {
const result = applyModelFactOverridesToConnection(
{
providerType: 'openai',
defaultModel: 'provider-model',
enabledModelIds: ['provider-model'],
models: [
{ id: 'provider-model', contextWindow: 8_000, capabilities: { chat: true, vision: true } },
],
},
{ 'openai:provider-model': { contextWindow: 64_000, capabilities: { vision: false } } },
);
assert.equal(result.models?.[0]?.contextWindow, 64_000);
assert.deepEqual(result.models?.[0]?.capabilities, { chat: true, vision: false });
});

test('model-fact projection preserves the catalog model bound', () => {
const projected = applyModelFactOverridesToConnection(
{
providerType: 'openai',
enabledModelIds: ['custom-model'],
models: Array.from({ length: CONNECTION_CATALOG_MAX_MODELS_PER_CONNECTION }, (_, index) => ({
id: `provider-model-${index}`,
})),
},
{ 'openai:custom-model': { contextWindow: 64_000 } },
);

assert.equal(projected.models?.length, CONNECTION_CATALOG_MAX_MODELS_PER_CONNECTION);
assert.equal(
projected.models?.some((model) => model.id === 'custom-model'),
false,
);
});

test('modality overrides merge directions independently', () => {
const result = applyModelFactOverride(
{
id: 'multimodal',
modalities: { input: ['text', 'image', 'pdf'], output: ['text', 'audio'] },
},
{ modalities: { input: ['text'] } },
);
assert.deepEqual(result.modalities, {
input: ['text'],
output: ['text', 'audio'],
});
});

test('model capabilities preserve web search facts from metadata and overrides', () => {
const result = applyModelFactOverride(
{ id: 'web-model', capabilities: { webSearch: true } },
{ capabilities: { chat: true } },
);
assert.deepEqual(result.capabilities, { webSearch: true, chat: true });
});

test('model fact overrides accept parallel tool-call capability metadata', () => {
const document = decodeModelFactsDocument({
schemaVersion: 1,
overrides: { 'openai:tool-model': { capabilities: { parallelToolCalls: false } } },
});
assert.deepEqual(document.overrides['openai:tool-model']?.capabilities, {
parallelToolCalls: false,
});
});
44 changes: 44 additions & 0 deletions packages/core/src/__tests__/runtime-policy-codec.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -484,6 +484,50 @@ test('normalizes exact bounded model discovery results', () => {
}
});

test('normalizes extended model facts used by the runtime host catalog', () => {
const result = normalizeConnectionModelDiscoveryResult({
models: [
{
id: 'custom-model',
description: 'A custom model',
inputLimit: 120_000,
knowledgeCutoff: '2025-01',
structuredOutput: true,
lastUpdated: '2026-01-01',
modalities: { input: ['text', 'image'], output: ['text'] },
},
],
source: 'fetched',
fetchedAt: 42,
});
assert.deepEqual(result.models[0], {
id: 'custom-model',
description: 'A custom model',
inputLimit: 120_000,
knowledgeCutoff: '2025-01',
structuredOutput: true,
lastUpdated: '2026-01-01',
modalities: { input: ['text', 'image'], output: ['text'] },
});
});

test('rejects sparse model modality arrays', () => {
assert.throws(
() =>
normalizeConnectionModelDiscoveryResult({
models: [
{
id: 'custom-model',
modalities: { input: Array(1), output: ['text'] },
},
],
source: 'fetched',
fetchedAt: 42,
}),
RuntimePolicyDomainDecodeError,
);
});

test('credential domain validation requires material but leaves capacity to callers', () => {
const input = normalizeSetCredentialInput({
locator: {
Expand Down
20 changes: 19 additions & 1 deletion packages/core/src/llm-connections.ts
Original file line number Diff line number Diff line change
Expand Up @@ -115,8 +115,27 @@ export interface ModelInfo {
input: Array<'text' | 'image' | 'audio' | 'pdf'>;
output: Array<'text' | 'image' | 'audio'>;
};
/**
* Read-time provenance for values overlaid from model-facts.json. This is
* never persisted in a provider inventory; it lets catalog consumers show
* where a projected value came from.
*/
factOverriddenFields?: readonly ModelFactField[];
}

export type ModelFactField =
| 'displayName'
| 'description'
| 'apiProtocol'
| 'contextWindow'
| 'inputLimit'
| 'maxOutputTokens'
| 'knowledgeCutoff'
| 'structuredOutput'
| 'lastUpdated'
| 'capabilities'
| 'modalities';

export type ModelDiscoverySource = 'fetched' | 'fallback';

export interface ModelDiscoveryResult {
Expand Down Expand Up @@ -411,7 +430,6 @@ export function reconcileConnectionAfterModelFetch(
),
),
];

// Seed a first choice only for a connection that has never had a list to
// pick from: four providers ship no `fallbackModels`, so for them discovery
// is the only place a first default can come from.
Expand Down
Loading
Loading