Skip to content

Commit ae181fe

Browse files
steebchenclaude
andcommitted
feat(gateway): add verbosity param for gpt-5.6 models
Support OpenAI's verbosity parameter (low/medium/high) end to end: - accept top-level verbosity in /v1/chat/completions and text.verbosity in /v1/responses - validate against a new verbosity capability flag on provider mappings (400 for unsupported models) - forward as text.verbosity on the Responses API and top-level verbosity on Chat Completions; strip it when auto routing or retry fallback lands on a mapping without support - enable the flag on the gpt-5.6-sol/terra/luna OpenAI mappings Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent 2c63fea commit ae181fe

11 files changed

Lines changed: 207 additions & 1 deletion

File tree

apps/gateway/src/chat/chat.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1453,6 +1453,7 @@ chat.openapi(completions, async (c) => {
14531453
sensitive_word_check,
14541454
image_config,
14551455
effort,
1456+
verbosity,
14561457
service_tier,
14571458
web_search,
14581459
plugins,
@@ -2498,6 +2499,7 @@ chat.openapi(completions, async (c) => {
24982499
response_format,
24992500
reasoning_effort,
25002501
reasoning_max_tokens,
2502+
verbosity,
25012503
tools,
25022504
tool_choice,
25032505
webSearchTool,
@@ -6109,6 +6111,7 @@ chat.openapi(completions, async (c) => {
61096111
service_tier,
61106112
configIndex,
61116113
),
6114+
verbosity,
61126115
);
61136116
} catch (e) {
61146117
// Surface typed pre-upstream input errors in the activity feed as a
@@ -6316,6 +6319,7 @@ chat.openapi(completions, async (c) => {
63166319
n,
63176320
providerCacheControlEnabled,
63186321
service_tier,
6322+
verbosity,
63196323
},
63206324
);
63216325
}

apps/gateway/src/chat/schemas/completions.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -304,6 +304,16 @@ export const completionsRequestSchema = z.object({
304304
"Controls the computational effort for supported models (currently only claude-opus-4-5-20251101)",
305305
example: "medium",
306306
}),
307+
verbosity: z
308+
.enum(["low", "medium", "high"])
309+
.nullable()
310+
.optional()
311+
.transform((val) => (val === null ? undefined : val))
312+
.openapi({
313+
description:
314+
"Controls how detailed the model's responses are. Only supported by OpenAI GPT-5.6 and later models; requests to models without verbosity support return a 400 error.",
315+
example: "low",
316+
}),
307317
service_tier: z
308318
.enum(["auto", "default", "flex", "priority"])
309319
.optional()

apps/gateway/src/chat/tools/resolve-provider-context.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,7 @@ export interface ProviderContextOptions {
140140
n?: number;
141141
providerCacheControlEnabled: boolean;
142142
service_tier?: "auto" | "default" | "flex" | "priority";
143+
verbosity?: "low" | "medium" | "high";
143144
}
144145

145146
interface ProjectInfo {
@@ -643,6 +644,7 @@ export async function resolveProviderContext(
643644
options.providerCacheControlEnabled,
644645
options.n,
645646
options.service_tier,
647+
options.verbosity,
646648
);
647649

648650
// Post-validation of max_tokens in request body

apps/gateway/src/chat/tools/validate-model-capabilities.spec.ts

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,50 @@ describe("validateModelCapabilities - vision", () => {
132132
});
133133
});
134134

135+
describe("validateModelCapabilities - verbosity", () => {
136+
const verbosityModel = getModel("gpt-5.6-terra");
137+
138+
it("allows verbosity for models that support it", () => {
139+
expect(() =>
140+
validateModelCapabilities(verbosityModel, "gpt-5.6-terra", undefined, {
141+
verbosity: "low",
142+
}),
143+
).not.toThrow();
144+
});
145+
146+
it("rejects verbosity for models without support", () => {
147+
expect(() =>
148+
validateModelCapabilities(noVisionModel, "deepseek-v4-flash", undefined, {
149+
verbosity: "low",
150+
}),
151+
).toThrow(HTTPException);
152+
});
153+
154+
it("skips the verbosity check for auto and custom models", () => {
155+
expect(() =>
156+
validateModelCapabilities(noVisionModel, "auto", undefined, {
157+
verbosity: "low",
158+
}),
159+
).not.toThrow();
160+
expect(() =>
161+
validateModelCapabilities(noVisionModel, "custom", undefined, {
162+
verbosity: "low",
163+
}),
164+
).not.toThrow();
165+
});
166+
167+
it("does not check verbosity when it is not specified", () => {
168+
expect(() =>
169+
validateModelCapabilities(
170+
noVisionModel,
171+
"deepseek-v4-flash",
172+
undefined,
173+
{},
174+
),
175+
).not.toThrow();
176+
});
177+
});
178+
135179
describe("validateModelCapabilities - custom providers", () => {
136180
it("skips all capability checks when the provider is custom", () => {
137181
expect(() =>

apps/gateway/src/chat/tools/validate-model-capabilities.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ export interface ValidateModelCapabilitiesOptions {
1717
};
1818
reasoning_effort?: string;
1919
reasoning_max_tokens?: number;
20+
verbosity?: string;
2021
tools?: unknown[];
2122
tool_choice?: unknown;
2223
webSearchTool?: WebSearchTool;
@@ -42,6 +43,7 @@ export function validateModelCapabilities(
4243
response_format,
4344
reasoning_effort,
4445
reasoning_max_tokens,
46+
verbosity,
4547
tools,
4648
tool_choice,
4749
webSearchTool,
@@ -189,6 +191,30 @@ export function validateModelCapabilities(
189191
}
190192
}
191193

194+
// Check if verbosity is specified but model doesn't support it
195+
// Skip this check for "auto" and "custom" models as they will be resolved dynamically
196+
if (
197+
verbosity !== undefined &&
198+
requestedModel !== "auto" &&
199+
requestedModel !== "custom"
200+
) {
201+
const providersToCheck = requestedProvider
202+
? modelInfo.providers.filter(
203+
(p) => (p as ProviderModelMapping).providerId === requestedProvider,
204+
)
205+
: modelInfo.providers;
206+
207+
const supportsVerbosity = providersToCheck.some(
208+
(provider) => (provider as ProviderModelMapping).verbosity === true,
209+
);
210+
211+
if (!supportsVerbosity) {
212+
throw new HTTPException(400, {
213+
message: `Model ${requestedModel} does not support the verbosity parameter. Remove the verbosity parameter or use a model that supports it (OpenAI GPT-5.6 and later).`,
214+
});
215+
}
216+
}
217+
192218
// Check if reasoning.max_tokens is specified but model doesn't support it
193219
// Skip this check for "auto" and "custom" models as they will be resolved dynamically
194220
if (

apps/gateway/src/responses/responses.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -324,6 +324,9 @@ responses.post("/", async (c) => {
324324
if (req.reasoning?.effort) {
325325
chatRequest.reasoning_effort = req.reasoning.effort;
326326
}
327+
if (req.text?.verbosity !== undefined) {
328+
chatRequest.verbosity = req.text.verbosity;
329+
}
327330
if (req.prompt_cache_key !== undefined) {
328331
chatRequest.prompt_cache_key = req.prompt_cache_key;
329332
}

packages/actions/src/prepare-request-body.spec.ts

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ async function prepareOpenAITextRequest(options: {
5151
promptCacheKey?: string;
5252
promptCacheRetention?: "in_memory" | "24h";
5353
serviceTier?: "flex" | "priority";
54+
verbosity?: "low" | "medium" | "high";
5455
}) {
5556
const model = options.model ?? "gpt-5.5";
5657
return await prepareRequestBody(
@@ -85,6 +86,7 @@ async function prepareOpenAITextRequest(options: {
8586
true,
8687
undefined,
8788
options.serviceTier,
89+
options.verbosity,
8890
);
8991
}
9092

@@ -622,6 +624,86 @@ describe("prepareRequestBody - OpenAI service tiers", () => {
622624
});
623625
});
624626

627+
describe("prepareRequestBody - verbosity", () => {
628+
test("forwards verbosity to gpt-5.6 chat completions", async () => {
629+
const requestBody = (await prepareOpenAITextRequest({
630+
model: "gpt-5.6-terra",
631+
verbosity: "low",
632+
})) as { verbosity?: string };
633+
634+
expect(requestBody.verbosity).toBe("low");
635+
});
636+
637+
test("forwards verbosity as text.verbosity to gpt-5.6 Responses API", async () => {
638+
const requestBody = (await prepareOpenAITextRequest({
639+
model: "gpt-5.6-sol",
640+
useResponsesApi: true,
641+
verbosity: "high",
642+
})) as { text?: { verbosity?: string } };
643+
644+
expect(requestBody.text?.verbosity).toBe("high");
645+
});
646+
647+
test("keeps text.format when verbosity is combined with response_format", async () => {
648+
const requestBody = (await prepareRequestBody(
649+
"openai",
650+
"gpt-5.6-luna",
651+
null,
652+
"gpt-5.6-luna",
653+
[{ role: "user", content: "Hello!" }],
654+
false,
655+
undefined,
656+
undefined,
657+
undefined,
658+
undefined,
659+
undefined,
660+
{ type: "json_object" },
661+
undefined,
662+
undefined,
663+
undefined,
664+
false,
665+
false,
666+
20,
667+
null,
668+
undefined,
669+
undefined,
670+
undefined,
671+
undefined,
672+
undefined,
673+
undefined,
674+
true, // useResponsesApi
675+
undefined,
676+
undefined,
677+
true,
678+
undefined,
679+
undefined,
680+
"medium", // verbosity
681+
)) as { text?: { format?: { type: string }; verbosity?: string } };
682+
683+
expect(requestBody.text?.format?.type).toBe("json_object");
684+
expect(requestBody.text?.verbosity).toBe("medium");
685+
});
686+
687+
test("strips verbosity for models without verbosity support", async () => {
688+
const requestBody = (await prepareOpenAITextRequest({
689+
model: "gpt-4o",
690+
verbosity: "low",
691+
})) as { verbosity?: string };
692+
693+
expect(requestBody.verbosity).toBeUndefined();
694+
});
695+
696+
test("strips verbosity from the Responses API body for unsupported models", async () => {
697+
const requestBody = (await prepareOpenAITextRequest({
698+
model: "gpt-5.5",
699+
useResponsesApi: true,
700+
verbosity: "low",
701+
})) as { text?: { verbosity?: string } };
702+
703+
expect(requestBody.text?.verbosity).toBeUndefined();
704+
});
705+
});
706+
625707
describe("prepareRequestBody - reasoning_effort none", () => {
626708
async function prepare(options: {
627709
provider: Parameters<typeof prepareRequestBody>[0];

packages/actions/src/prepare-request-body.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -891,6 +891,7 @@ export async function prepareRequestBody(
891891
providerCacheControlEnabled = true,
892892
n?: number,
893893
service_tier?: "auto" | "default" | "flex" | "priority",
894+
verbosity?: "low" | "medium" | "high",
894895
): Promise<ProviderRequestBody | FormData> {
895896
tools = normalizeToolParameters(tools);
896897
const modelDef = models.find((m) => m.id === usedInternalModel);
@@ -928,6 +929,17 @@ export async function prepareRequestBody(
928929
reasoning_effort = undefined;
929930
}
930931

932+
// `verbosity` is only understood by OpenAI GPT-5.6+ models. Capability
933+
// validation rejects unsupported pinned models upfront, but auto routing and
934+
// retry fallbacks can still land on a mapping without verbosity support, so
935+
// strip it here instead of forwarding an unknown parameter upstream.
936+
if (
937+
verbosity !== undefined &&
938+
providerMappingForOptions?.verbosity !== true
939+
) {
940+
verbosity = undefined;
941+
}
942+
931943
// `max` is Anthropic's top effort tier (above `xhigh`). Providers without a
932944
// native `max` level treat it as an alias for `high` (e.g. OpenAI, Google,
933945
// DeepSeek). Anthropic-family branches use `reasoning_effort` directly and
@@ -1632,6 +1644,13 @@ export async function prepareRequestBody(
16321644
}
16331645
}
16341646

1647+
if (verbosity !== undefined) {
1648+
responsesBody.text = {
1649+
...responsesBody.text,
1650+
verbosity,
1651+
};
1652+
}
1653+
16351654
return responsesBody;
16361655
} else {
16371656
// Use regular chat completions format
@@ -1731,6 +1750,9 @@ export async function prepareRequestBody(
17311750
requestBody.reasoning_effort = genericReasoningEffort;
17321751
}
17331752
}
1753+
if (verbosity !== undefined) {
1754+
requestBody.verbosity = verbosity;
1755+
}
17341756
if (n !== undefined && n > 1) {
17351757
requestBody.n = n;
17361758
}

packages/models/src/models.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -329,6 +329,11 @@ export interface ProviderModelMapping {
329329
* Whether this model supports reasoning mode
330330
*/
331331
reasoning?: boolean;
332+
/**
333+
* Whether this model supports the OpenAI `verbosity` parameter
334+
* (low/medium/high response detail control, GPT-5.6 and later)
335+
*/
336+
verbosity?: boolean;
332337
/**
333338
* Whether the provider returns reasoning inside tagged content (e.g. &lt;think&gt;...&lt;/think&gt;)
334339
* that needs to be split into separate reasoning and content fields

packages/models/src/models/openai.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1751,6 +1751,7 @@ export const openaiModels = [
17511751
webSearchPrice: "0.01",
17521752
reasoning: true,
17531753
reasoningOutput: "omit",
1754+
verbosity: true,
17541755
supportsResponsesApi: true,
17551756
jsonOutputSchema: true,
17561757
supportedParameters: [
@@ -1759,6 +1760,7 @@ export const openaiModels = [
17591760
"frequency_penalty",
17601761
"presence_penalty",
17611762
"response_format",
1763+
"verbosity",
17621764
],
17631765
jsonOutput: true,
17641766
},
@@ -1790,6 +1792,7 @@ export const openaiModels = [
17901792
webSearchPrice: "0.01",
17911793
reasoning: true,
17921794
reasoningOutput: "omit",
1795+
verbosity: true,
17931796
supportsResponsesApi: true,
17941797
jsonOutputSchema: true,
17951798
supportedParameters: [
@@ -1798,6 +1801,7 @@ export const openaiModels = [
17981801
"frequency_penalty",
17991802
"presence_penalty",
18001803
"response_format",
1804+
"verbosity",
18011805
],
18021806
jsonOutput: true,
18031807
},
@@ -1829,6 +1833,7 @@ export const openaiModels = [
18291833
webSearchPrice: "0.01",
18301834
reasoning: true,
18311835
reasoningOutput: "omit",
1836+
verbosity: true,
18321837
supportsResponsesApi: true,
18331838
jsonOutputSchema: true,
18341839
supportedParameters: [
@@ -1837,6 +1842,7 @@ export const openaiModels = [
18371842
"frequency_penalty",
18381843
"presence_penalty",
18391844
"response_format",
1845+
"verbosity",
18401846
],
18411847
jsonOutput: true,
18421848
},

0 commit comments

Comments
 (0)