Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions apps/gateway/src/chat/chat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1453,6 +1453,7 @@ chat.openapi(completions, async (c) => {
sensitive_word_check,
image_config,
effort,
verbosity,
service_tier,
web_search,
plugins,
Expand Down Expand Up @@ -2498,6 +2499,7 @@ chat.openapi(completions, async (c) => {
response_format,
reasoning_effort,
reasoning_max_tokens,
verbosity,
tools,
tool_choice,
webSearchTool,
Expand Down Expand Up @@ -6109,6 +6111,7 @@ chat.openapi(completions, async (c) => {
service_tier,
configIndex,
),
verbosity,
);
} catch (e) {
// Surface typed pre-upstream input errors in the activity feed as a
Expand Down Expand Up @@ -6316,6 +6319,7 @@ chat.openapi(completions, async (c) => {
n,
providerCacheControlEnabled,
service_tier,
verbosity,
},
);
}
Expand Down
10 changes: 10 additions & 0 deletions apps/gateway/src/chat/schemas/completions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -304,6 +304,16 @@ export const completionsRequestSchema = z.object({
"Controls the computational effort for supported models (currently only claude-opus-4-5-20251101)",
example: "medium",
}),
verbosity: z
.enum(["low", "medium", "high"])
.nullable()
.optional()
.transform((val) => (val === null ? undefined : val))
.openapi({
description:
"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.",
example: "low",
}),
service_tier: z
.enum(["auto", "default", "flex", "priority"])
.optional()
Expand Down
2 changes: 2 additions & 0 deletions apps/gateway/src/chat/tools/resolve-provider-context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,7 @@ export interface ProviderContextOptions {
n?: number;
providerCacheControlEnabled: boolean;
service_tier?: "auto" | "default" | "flex" | "priority";
verbosity?: "low" | "medium" | "high";
}

interface ProjectInfo {
Expand Down Expand Up @@ -643,6 +644,7 @@ export async function resolveProviderContext(
options.providerCacheControlEnabled,
options.n,
options.service_tier,
options.verbosity,
);

// Post-validation of max_tokens in request body
Expand Down
44 changes: 44 additions & 0 deletions apps/gateway/src/chat/tools/validate-model-capabilities.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,50 @@ describe("validateModelCapabilities - vision", () => {
});
});

describe("validateModelCapabilities - verbosity", () => {
const verbosityModel = getModel("gpt-5.6-terra");

it("allows verbosity for models that support it", () => {
expect(() =>
validateModelCapabilities(verbosityModel, "gpt-5.6-terra", undefined, {
verbosity: "low",
}),
).not.toThrow();
});

it("rejects verbosity for models without support", () => {
expect(() =>
validateModelCapabilities(noVisionModel, "deepseek-v4-flash", undefined, {
verbosity: "low",
}),
).toThrow(HTTPException);
});

it("skips the verbosity check for auto and custom models", () => {
expect(() =>
validateModelCapabilities(noVisionModel, "auto", undefined, {
verbosity: "low",
}),
).not.toThrow();
expect(() =>
validateModelCapabilities(noVisionModel, "custom", undefined, {
verbosity: "low",
}),
).not.toThrow();
});

it("does not check verbosity when it is not specified", () => {
expect(() =>
validateModelCapabilities(
noVisionModel,
"deepseek-v4-flash",
undefined,
{},
),
).not.toThrow();
});
});

describe("validateModelCapabilities - custom providers", () => {
it("skips all capability checks when the provider is custom", () => {
expect(() =>
Expand Down
26 changes: 26 additions & 0 deletions apps/gateway/src/chat/tools/validate-model-capabilities.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ export interface ValidateModelCapabilitiesOptions {
};
reasoning_effort?: string;
reasoning_max_tokens?: number;
verbosity?: string;
tools?: unknown[];
tool_choice?: unknown;
webSearchTool?: WebSearchTool;
Expand All @@ -42,6 +43,7 @@ export function validateModelCapabilities(
response_format,
reasoning_effort,
reasoning_max_tokens,
verbosity,
tools,
tool_choice,
webSearchTool,
Expand Down Expand Up @@ -189,6 +191,30 @@ export function validateModelCapabilities(
}
}

// Check if verbosity is specified but model doesn't support it
// Skip this check for "auto" and "custom" models as they will be resolved dynamically
if (
verbosity !== undefined &&
requestedModel !== "auto" &&
requestedModel !== "custom"
) {
const providersToCheck = requestedProvider
? modelInfo.providers.filter(
(p) => (p as ProviderModelMapping).providerId === requestedProvider,
)
: modelInfo.providers;

const supportsVerbosity = providersToCheck.some(
(provider) => (provider as ProviderModelMapping).verbosity === true,
);

if (!supportsVerbosity) {
throw new HTTPException(400, {
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).`,
});
}
}

// Check if reasoning.max_tokens is specified but model doesn't support it
// Skip this check for "auto" and "custom" models as they will be resolved dynamically
if (
Expand Down
3 changes: 3 additions & 0 deletions apps/gateway/src/responses/responses.ts
Original file line number Diff line number Diff line change
Expand Up @@ -324,6 +324,9 @@ responses.post("/", async (c) => {
if (req.reasoning?.effort) {
chatRequest.reasoning_effort = req.reasoning.effort;
}
if (req.text?.verbosity !== undefined) {
chatRequest.verbosity = req.text.verbosity;
}
if (req.prompt_cache_key !== undefined) {
chatRequest.prompt_cache_key = req.prompt_cache_key;
}
Expand Down
82 changes: 82 additions & 0 deletions packages/actions/src/prepare-request-body.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ async function prepareOpenAITextRequest(options: {
promptCacheKey?: string;
promptCacheRetention?: "in_memory" | "24h";
serviceTier?: "flex" | "priority";
verbosity?: "low" | "medium" | "high";
}) {
const model = options.model ?? "gpt-5.5";
return await prepareRequestBody(
Expand Down Expand Up @@ -85,6 +86,7 @@ async function prepareOpenAITextRequest(options: {
true,
undefined,
options.serviceTier,
options.verbosity,
);
}

Expand Down Expand Up @@ -622,6 +624,86 @@ describe("prepareRequestBody - OpenAI service tiers", () => {
});
});

describe("prepareRequestBody - verbosity", () => {
test("forwards verbosity to gpt-5.6 chat completions", async () => {
const requestBody = (await prepareOpenAITextRequest({
model: "gpt-5.6-terra",
verbosity: "low",
})) as { verbosity?: string };

expect(requestBody.verbosity).toBe("low");
});

test("forwards verbosity as text.verbosity to gpt-5.6 Responses API", async () => {
const requestBody = (await prepareOpenAITextRequest({
model: "gpt-5.6-sol",
useResponsesApi: true,
verbosity: "high",
})) as { text?: { verbosity?: string } };

expect(requestBody.text?.verbosity).toBe("high");
});

test("keeps text.format when verbosity is combined with response_format", async () => {
const requestBody = (await prepareRequestBody(
"openai",
"gpt-5.6-luna",
null,
"gpt-5.6-luna",
[{ role: "user", content: "Hello!" }],
false,
undefined,
undefined,
undefined,
undefined,
undefined,
{ type: "json_object" },
undefined,
undefined,
undefined,
false,
false,
20,
null,
undefined,
undefined,
undefined,
undefined,
undefined,
undefined,
true, // useResponsesApi
undefined,
undefined,
true,
undefined,
undefined,
"medium", // verbosity
)) as { text?: { format?: { type: string }; verbosity?: string } };

expect(requestBody.text?.format?.type).toBe("json_object");
expect(requestBody.text?.verbosity).toBe("medium");
});

test("strips verbosity for models without verbosity support", async () => {
const requestBody = (await prepareOpenAITextRequest({
model: "gpt-4o",
verbosity: "low",
})) as { verbosity?: string };

expect(requestBody.verbosity).toBeUndefined();
});

test("strips verbosity from the Responses API body for unsupported models", async () => {
const requestBody = (await prepareOpenAITextRequest({
model: "gpt-5.5",
useResponsesApi: true,
verbosity: "low",
})) as { text?: { verbosity?: string } };

expect(requestBody.text?.verbosity).toBeUndefined();
});
});

describe("prepareRequestBody - reasoning_effort none", () => {
async function prepare(options: {
provider: Parameters<typeof prepareRequestBody>[0];
Expand Down
22 changes: 22 additions & 0 deletions packages/actions/src/prepare-request-body.ts
Original file line number Diff line number Diff line change
Expand Up @@ -891,6 +891,7 @@ export async function prepareRequestBody(
providerCacheControlEnabled = true,
n?: number,
service_tier?: "auto" | "default" | "flex" | "priority",
verbosity?: "low" | "medium" | "high",
): Promise<ProviderRequestBody | FormData> {
tools = normalizeToolParameters(tools);
const modelDef = models.find((m) => m.id === usedInternalModel);
Expand Down Expand Up @@ -928,6 +929,17 @@ export async function prepareRequestBody(
reasoning_effort = undefined;
}

// `verbosity` is only understood by OpenAI GPT-5.6+ models. Capability
// validation rejects unsupported pinned models upfront, but auto routing and
// retry fallbacks can still land on a mapping without verbosity support, so
// strip it here instead of forwarding an unknown parameter upstream.
if (
verbosity !== undefined &&
providerMappingForOptions?.verbosity !== true
) {
verbosity = undefined;
}

// `max` is Anthropic's top effort tier (above `xhigh`). Providers without a
// native `max` level treat it as an alias for `high` (e.g. OpenAI, Google,
// DeepSeek). Anthropic-family branches use `reasoning_effort` directly and
Expand Down Expand Up @@ -1632,6 +1644,13 @@ export async function prepareRequestBody(
}
}

if (verbosity !== undefined) {
responsesBody.text = {
...responsesBody.text,
verbosity,
};
}

return responsesBody;
} else {
// Use regular chat completions format
Expand Down Expand Up @@ -1731,6 +1750,9 @@ export async function prepareRequestBody(
requestBody.reasoning_effort = genericReasoningEffort;
}
}
if (verbosity !== undefined) {
requestBody.verbosity = verbosity;
}
if (n !== undefined && n > 1) {
requestBody.n = n;
}
Expand Down
5 changes: 5 additions & 0 deletions packages/models/src/models.ts
Original file line number Diff line number Diff line change
Expand Up @@ -329,6 +329,11 @@ export interface ProviderModelMapping {
* Whether this model supports reasoning mode
*/
reasoning?: boolean;
/**
* Whether this model supports the OpenAI `verbosity` parameter
* (low/medium/high response detail control, GPT-5.6 and later)
*/
verbosity?: boolean;
/**
* Whether the provider returns reasoning inside tagged content (e.g. &lt;think&gt;...&lt;/think&gt;)
* that needs to be split into separate reasoning and content fields
Expand Down
6 changes: 6 additions & 0 deletions packages/models/src/models/openai.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1751,6 +1751,7 @@ export const openaiModels = [
webSearchPrice: "0.01",
reasoning: true,
reasoningOutput: "omit",
verbosity: true,
supportsResponsesApi: true,
jsonOutputSchema: true,
supportedParameters: [
Expand All @@ -1759,6 +1760,7 @@ export const openaiModels = [
"frequency_penalty",
"presence_penalty",
"response_format",
"verbosity",
],
jsonOutput: true,
},
Expand Down Expand Up @@ -1790,6 +1792,7 @@ export const openaiModels = [
webSearchPrice: "0.01",
reasoning: true,
reasoningOutput: "omit",
verbosity: true,
supportsResponsesApi: true,
jsonOutputSchema: true,
supportedParameters: [
Expand All @@ -1798,6 +1801,7 @@ export const openaiModels = [
"frequency_penalty",
"presence_penalty",
"response_format",
"verbosity",
],
jsonOutput: true,
},
Expand Down Expand Up @@ -1829,6 +1833,7 @@ export const openaiModels = [
webSearchPrice: "0.01",
reasoning: true,
reasoningOutput: "omit",
verbosity: true,
supportsResponsesApi: true,
jsonOutputSchema: true,
supportedParameters: [
Expand All @@ -1837,6 +1842,7 @@ export const openaiModels = [
"frequency_penalty",
"presence_penalty",
"response_format",
"verbosity",
],
jsonOutput: true,
},
Expand Down
Loading
Loading