mirror of
https://github.com/danny-avila/LibreChat.git
synced 2026-08-04 14:57:42 +00:00
🎯 fix: Route the Label Cap Per Model Family
Round-twenty review: the 256-token label cap set maxTokens unconditionally, but GPT-5+ rejects max_tokens (the OpenAI builder routes its cap into modelKwargs.max_completion_tokens / max_output_tokens) and o-series models reject it with no stable kwargs alternative — every label on those models would have failed. The cap now mirrors the builder: modelKwargs for GPT-5+ (responses-API aware), no cap for o-series (title parity; the 200-char persistence bound still applies), maxOutputTokens for Google, maxTokens otherwise. Pinned in host.spec for both reasoning families. The round's other finding is the tenth instance of the documented edited+reconnect index limitation, answered on-thread.
This commit is contained in:
parent
0928ac7d7f
commit
fefc0ce686
2 changed files with 48 additions and 5 deletions
|
|
@ -197,6 +197,31 @@ describe('resolveActivityLabelModel model precedence', () => {
|
|||
expect((resolved.clientOptions as Record<string, unknown>).maxTokens).toBe(256);
|
||||
});
|
||||
|
||||
/** GPT-5+ rejects `max_tokens`: the label cap must ride in modelKwargs
|
||||
* exactly as the OpenAI builder routes primary caps. */
|
||||
it('routes the label cap into modelKwargs for GPT-5-family models', async () => {
|
||||
mockGetOptions.mockResolvedValueOnce({
|
||||
llmConfig: { model: 'gpt-5.2' },
|
||||
} as never);
|
||||
const resolved = await resolve({ activityLabel: true, activityModel: 'gpt-5.2' });
|
||||
const clientOptions = resolved.clientOptions as Record<string, unknown>;
|
||||
expect(clientOptions.maxTokens).toBeUndefined();
|
||||
expect(clientOptions.modelKwargs).toEqual({ max_completion_tokens: 256 });
|
||||
});
|
||||
|
||||
/** o-series models reject `max_tokens` and get NO cap — title parity;
|
||||
* the 200-char persistence bound still applies. */
|
||||
it('sets no cap at all for o-series reasoning models', async () => {
|
||||
mockGetOptions.mockResolvedValueOnce({
|
||||
llmConfig: { model: 'o3-mini' },
|
||||
} as never);
|
||||
const resolved = await resolve({ activityLabel: true, activityModel: 'o3-mini' });
|
||||
const clientOptions = resolved.clientOptions as Record<string, unknown>;
|
||||
expect(clientOptions.maxTokens).toBeUndefined();
|
||||
expect(clientOptions.modelKwargs).toBeUndefined();
|
||||
expect(clientOptions.maxOutputTokens).toBeUndefined();
|
||||
});
|
||||
|
||||
/** The Anthropic carrier holds client CONSTRUCTION options — for
|
||||
* user-provided base URLs that includes the SSRF-safe fetch dispatcher —
|
||||
* so it must survive the strip even with no custom headers, and by the
|
||||
|
|
|
|||
|
|
@ -334,17 +334,35 @@ export async function resolveActivityLabelModel({
|
|||
const anthropicCarrier = rawOptions.clientOptions;
|
||||
const clientOptions = Object.fromEntries(
|
||||
Object.entries(rawOptions).filter(([key]) => !omitTitleOptions.has(key)),
|
||||
) as MaybeAzureConfig & { clientOptions?: { defaultHeaders?: unknown } };
|
||||
) as MaybeAzureConfig & {
|
||||
clientOptions?: { defaultHeaders?: unknown };
|
||||
modelKwargs?: Record<string, unknown>;
|
||||
};
|
||||
if (anthropicCarrier != null && clientOptions.clientOptions == null) {
|
||||
clientOptions.clientOptions = anthropicCarrier;
|
||||
}
|
||||
/** Replace the stripped primary caps with a SMALL one — see
|
||||
* {@link LABEL_MAX_OUTPUT_TOKENS}. Installed after the filter so the
|
||||
* omit set cannot remove it; keyed per provider family (Google-style
|
||||
* wrappers read `maxOutputTokens`, the rest `maxTokens`). */
|
||||
* {@link LABEL_MAX_OUTPUT_TOKENS} — routed with the SAME model/API
|
||||
* conversion as the OpenAI builder (endpoints/openai/llm.ts): GPT-5+
|
||||
* rejects `max_tokens`, so its cap goes to `modelKwargs`
|
||||
* (responses-API aware); o-series reasoning models get NO cap at all,
|
||||
* matching the title path, since they reject `max_tokens` and have no
|
||||
* stable kwargs cap across API surfaces (the 200-char persistence bound
|
||||
* still applies); Google-family wrappers read `maxOutputTokens`. */
|
||||
const isGpt5Plus = model != null && /\bgpt-[5-9](?:\.\d+)?\b/i.test(model);
|
||||
const isOSeries = model != null && /\bo[1-9](?:[-.]|\b)/i.test(model);
|
||||
if (provider === Providers.GOOGLE || provider === Providers.VERTEXAI) {
|
||||
(clientOptions as { maxOutputTokens?: number }).maxOutputTokens = LABEL_MAX_OUTPUT_TOKENS;
|
||||
} else {
|
||||
} else if (isGpt5Plus) {
|
||||
const paramName =
|
||||
(rawOptions as { useResponsesApi?: boolean }).useResponsesApi === true
|
||||
? 'max_output_tokens'
|
||||
: 'max_completion_tokens';
|
||||
clientOptions.modelKwargs = {
|
||||
...(clientOptions.modelKwargs ?? {}),
|
||||
[paramName]: LABEL_MAX_OUTPUT_TOKENS,
|
||||
};
|
||||
} else if (!isOSeries) {
|
||||
(clientOptions as { maxTokens?: number }).maxTokens = LABEL_MAX_OUTPUT_TOKENS;
|
||||
}
|
||||
if (options.configOptions) {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue