Skip to content

Commit 9e39240

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 9bf8f3c commit 9e39240

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
@@ -24,7 +24,11 @@ import {
2424
validateSlug,
2525
type ProviderType,
2626
} from '../llm-connections.js';
27-
import { MAX_PREPENDED_FALLBACK_MODELS } from '../model-catalog.js';
27+
import {
28+
CONNECTION_MODEL_DESCRIPTION_MAX_LENGTH,
29+
CONNECTION_MODEL_DISPLAY_NAME_MAX_LENGTH,
30+
MAX_PREPENDED_FALLBACK_MODELS,
31+
} from '../model-catalog.js';
2832
import {
2933
DECLARABLE_RELAY_THINKING_LEVELS,
3034
isThinkingLevel,
@@ -546,10 +550,22 @@ export function decodeConnectionModel(value: unknown): ConnectionModel {
546550
id: decodeConnectionModelId(item.id),
547551
...(item.displayName === undefined
548552
? {}
549-
: { displayName: stringValue(item.displayName, 'model display name', 512) }),
553+
: {
554+
displayName: stringValue(
555+
item.displayName,
556+
'model display name',
557+
CONNECTION_MODEL_DISPLAY_NAME_MAX_LENGTH,
558+
),
559+
}),
550560
...(item.description === undefined
551561
? {}
552-
: { description: stringValue(item.description, 'model description', 2048) }),
562+
: {
563+
description: stringValue(
564+
item.description,
565+
'model description',
566+
CONNECTION_MODEL_DESCRIPTION_MAX_LENGTH,
567+
),
568+
}),
553569
...(item.apiProtocol === undefined ? {} : { apiProtocol: item.apiProtocol }),
554570
...(item.contextWindow === undefined
555571
? {}

0 commit comments

Comments
 (0)