🎯 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:
Danny Avila 2026-07-28 16:31:51 -04:00
parent 0928ac7d7f
commit fefc0ce686
2 changed files with 48 additions and 5 deletions

View file

@ -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

View file

@ -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) {