Skip to content

Commit cba0090

Browse files
committed
fix(core): keep catalog strings inside the wire limits that decode them
The codec caps a model's display name at 512 characters and its description at 2048. Nothing upstream of it did. A refreshed models.dev row longer than either built a catalog entry the Host could encode but no client could decode, and the whole page was rejected — every model gone, not the one long string. Name the two limits where the catalog is built, truncate there, and have the codec read them. A producer that cannot exceed the limit is why the decoder never has to reject a page. Generated-by: Claude Code
1 parent 7f5e5fc commit cba0090

2 files changed

Lines changed: 45 additions & 6 deletions

File tree

packages/core/src/model-catalog.ts

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,12 @@ export function buildModelCatalogEntries(input: BuildModelCatalogInput): ModelCa
167167
* unencodable. Providers that do discover models substitute their fallback
168168
* list instead of prepending it, so they add nothing here.
169169
*/
170+
// What a model row may carry on the wire. The producer reads them here and the
171+
// decoder enforces them: an over-long string caught only on arrival is one the
172+
// catalog already built.
173+
export const CONNECTION_MODEL_DISPLAY_NAME_MAX_LENGTH = 512;
174+
export const CONNECTION_MODEL_DESCRIPTION_MAX_LENGTH = 2_048;
175+
170176
export const MAX_PREPENDED_FALLBACK_MODELS: number = Object.keys(PROVIDER_REGISTRY).reduce(
171177
(largest, providerType) => {
172178
if (providerSupportsModelDiscovery(providerType as ProviderType)) return largest;
@@ -461,7 +467,9 @@ function makeEntry(
461467
return {
462468
id: normalizedModel.id,
463469
...displayNameForModel(input.providerType, normalizedModel),
464-
...(description !== undefined ? { description } : {}),
470+
...(description !== undefined
471+
? { description: withinWireLimit(description, CONNECTION_MODEL_DESCRIPTION_MAX_LENGTH) }
472+
: {}),
465473
canUseAsChatDefault,
466474
isDefault: overrides.isDefault ?? normalizedModel.id === normalizedDefaultModel,
467475
supportsVision: capabilities.vision === true,
@@ -494,7 +502,9 @@ function displayNameForModel(
494502
model: ModelInfo,
495503
): { displayName?: string } {
496504
const displayName = model.displayName?.trim();
497-
if (displayName && displayName !== model.id) return { displayName };
505+
if (displayName && displayName !== model.id) {
506+
return { displayName: withinWireLimit(displayName, CONNECTION_MODEL_DISPLAY_NAME_MAX_LENGTH) };
507+
}
498508
return displayNameForKnownModel(providerType, model.id);
499509
}
500510

@@ -503,7 +513,20 @@ function displayNameForKnownModel(
503513
id: string,
504514
): { displayName?: string } {
505515
const displayName = lookupModelMetadata(providerType, id).displayName;
506-
return displayName ? { displayName } : {};
516+
return displayName
517+
? { displayName: withinWireLimit(displayName, CONNECTION_MODEL_DISPLAY_NAME_MAX_LENGTH) }
518+
: {};
519+
}
520+
521+
/**
522+
* Entries are what the connection catalog puts on the wire, and its decoder
523+
* refuses an over-long string by failing the whole catalog read. Every text a
524+
* model row or metadata table can carry passes through here, so this is where
525+
* a source that grew past the bound gets cut rather than where it takes the
526+
* catalog down.
527+
*/
528+
function withinWireLimit(value: string, maxLength: number): string {
529+
return value.length <= maxLength ? value : value.slice(0, maxLength);
507530
}
508531

509532
/**

packages/core/src/runtime-policy/connection-catalog-codec.ts

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,11 @@ import {
2626
type ModelModality,
2727
type ProviderType,
2828
} from '../llm-connections.js';
29-
import { MAX_PREPENDED_FALLBACK_MODELS } from '../model-catalog.js';
29+
import {
30+
CONNECTION_MODEL_DESCRIPTION_MAX_LENGTH,
31+
CONNECTION_MODEL_DISPLAY_NAME_MAX_LENGTH,
32+
MAX_PREPENDED_FALLBACK_MODELS,
33+
} from '../model-catalog.js';
3034
import {
3135
DECLARABLE_RELAY_THINKING_LEVELS,
3236
isThinkingLevel,
@@ -548,10 +552,22 @@ export function decodeConnectionModel(value: unknown): ConnectionModel {
548552
id: decodeConnectionModelId(item.id),
549553
...(item.displayName === undefined
550554
? {}
551-
: { displayName: stringValue(item.displayName, 'model display name', 512) }),
555+
: {
556+
displayName: stringValue(
557+
item.displayName,
558+
'model display name',
559+
CONNECTION_MODEL_DISPLAY_NAME_MAX_LENGTH,
560+
),
561+
}),
552562
...(item.description === undefined
553563
? {}
554-
: { description: stringValue(item.description, 'model description', 2048) }),
564+
: {
565+
description: stringValue(
566+
item.description,
567+
'model description',
568+
CONNECTION_MODEL_DESCRIPTION_MAX_LENGTH,
569+
),
570+
}),
555571
...(item.apiProtocol === undefined ? {} : { apiProtocol: item.apiProtocol }),
556572
...(item.contextWindow === undefined
557573
? {}

0 commit comments

Comments
 (0)