From dc57552adebb9b7973140f94d00cf1cc459c64e2 Mon Sep 17 00:00:00 2001 From: "roomote[bot]" <219738659+roomote[bot]@users.noreply.github.com> Date: Thu, 7 Aug 2025 16:57:12 -0400 Subject: [PATCH] feat: add GPT-5 model support (#6819) * feat: add GPT-5 model support - Added GPT-5 models (gpt-5-2025-08-07, gpt-5-mini-2025-08-07, gpt-5-nano-2025-08-07) - Added nectarine-alpha-new-reasoning-effort-2025-07-25 experimental model - Set gpt-5-2025-08-07 as default OpenAI Native model - Implemented GPT-5 specific handling with streaming and reasoning effort support * fix: remove hardcoded temperature from GPT-5 handler - Updated handleGPT5Message to use configurable temperature - Now uses this.options.modelTemperature ?? OPENAI_NATIVE_DEFAULT_TEMPERATURE - Maintains consistency with other model handlers * feat: add reasoning effort support for all OpenAI models * fix: update test to expect new default model gpt-5-2025-08-07 * feat: increase GPT-5 models context window to 400,000 - Updated context window from 256,000 to 400,000 for gpt-5-2025-08-07 - Updated context window from 256,000 to 400,000 for gpt-5-mini-2025-08-07 - Updated context window from 256,000 to 400,000 for gpt-5-nano-2025-08-07 - Updated context window from 256,000 to 400,000 for nectarine-alpha-new-reasoning-effort-2025-07-25 As requested by @daniel-lxs in PR #6819 * revert: remove GPT-5 models, keep only nectarine experimental model - Removed gpt-5-2025-08-07, gpt-5-mini-2025-08-07, gpt-5-nano-2025-08-07 - Kept nectarine-alpha-new-reasoning-effort-2025-07-25 experimental model - Reverted default model back to gpt-4o - Updated tests and changeset accordingly * feat: add GPT-5 models with updated context windows - Added gpt-5-2025-08-07, gpt-5-mini-2025-08-07, gpt-5-nano-2025-08-07 models - All GPT-5 models configured with 400,000 context window - Updated nectarine model context window to 256,000 - All models configured with reasoning effort support - Set gpt-5-2025-08-07 as default OpenAI Native model - Added GPT-5 model handling in openai-native.ts - Updated tests to reflect new default model * fix: restore reasoning effort support for o1 series models - Added supportsReasoningEffort: true to o1, o1-preview, and o1-mini models - This restores the ability to use reasoning effort parameters with these models - The existing code in openai-native.ts already handles reasoning effort correctly * Revert "fix: restore reasoning effort support for o1 series models" This reverts commit 7251237ae8714400769965b2d552824b695dfdae. * fix: restore reasoning effort support for o3 and o4 models - Added supportsReasoningEffort: true to o3, o3-high, o3-low models - Added supportsReasoningEffort: true to o4-mini, o4-mini-high, o4-mini-low models - Added supportsReasoningEffort: true to o3-mini, o3-mini-high, o3-mini-low models - These models have both supportsReasoningEffort and reasoningEffort properties * Revert "fix: restore reasoning effort support for o3 and o4 models" This reverts commit a75a2b8a6953d569e680246eebe7e673972097e4. * fix: restore reasoning effort support for o3 and o4 models - Added supportsReasoningEffort: true to o3, o3-high, o3-low models - Added supportsReasoningEffort: true to o4-mini, o4-mini-high, o4-mini-low models - Added supportsReasoningEffort: true to o3-mini, o3-mini-high, o3-mini-low models * fix: adjust reasoning effort support for o3/o4 models - Keep supportsReasoningEffort only for base o3, o4-mini, and o3-mini models - Remove supportsReasoningEffort from -high and -low variants - Position supportsReasoningEffort right before reasoningEffort property * fix: remove nectarine experimental model - Removed nectarine-alpha-new-reasoning-effort-2025-07-25 from openai.ts - Removed nectarine handling from openai-native.ts (renamed to handleGpt5Message) - Removed associated changeset file - Keep GPT-5 models with developer role handling * feat: implement full GPT-5 support with verbosity and minimal reasoning - Add all three GPT-5 models with accurate pricing (.25/0 for gpt-5, /bin/sh.25/ for mini, /bin/sh.05//bin/sh.40 for nano) - Implement verbosity control (low/medium/high) that passes through to API - Add minimal reasoning effort support for fastest response times - GPT-5 models use developer role instead of system role - Set gpt-5-2025-08-07 as default OpenAI Native model - Add Responses API infrastructure for future migration - Update tests to verify all GPT-5 features - All 27 tests passing Note: UI controls for verbosity still need to be added in a follow-up PR * feat: add verbosity setting for GPT-5 models - Add VerbosityLevel type definition to model types - Add verbosity field to ProviderSettings schema - Create Verbosity UI component for settings - Add verbosity labels to all localization files - Integrate verbosity handling in model parameters transformation - Update OpenAI native handler to support verbosity for GPT-5 - Add comprehensive tests for verbosity setting - Update existing GPT-5 tests to use verbosity from settings * Delete .roorules --------- Co-authored-by: Roo Code Co-authored-by: hannesrudolph Co-authored-by: Daniel Riccio Co-authored-by: Daniel <57051444+daniel-lxs@users.noreply.github.com> --- packages/types/src/model.ts | 10 + packages/types/src/provider-settings.ts | 5 +- packages/types/src/providers/openai.ts | 35 ++- .../providers/__tests__/openai-native.spec.ts | 156 ++++++++++- src/api/providers/openai-native.ts | 262 +++++++++++++++++- .../transform/__tests__/model-params.spec.ts | 97 +++++++ src/api/transform/model-params.ts | 12 +- .../src/components/settings/ApiOptions.tsx | 7 + .../src/components/settings/Verbosity.tsx | 43 +++ webview-ui/src/i18n/locales/ca/settings.json | 7 + webview-ui/src/i18n/locales/de/settings.json | 7 + webview-ui/src/i18n/locales/en/settings.json | 7 + webview-ui/src/i18n/locales/es/settings.json | 7 + webview-ui/src/i18n/locales/fr/settings.json | 7 + webview-ui/src/i18n/locales/hi/settings.json | 7 + webview-ui/src/i18n/locales/id/settings.json | 7 + webview-ui/src/i18n/locales/it/settings.json | 7 + webview-ui/src/i18n/locales/ja/settings.json | 7 + webview-ui/src/i18n/locales/ko/settings.json | 7 + webview-ui/src/i18n/locales/nl/settings.json | 7 + webview-ui/src/i18n/locales/pl/settings.json | 7 + .../src/i18n/locales/pt-BR/settings.json | 7 + webview-ui/src/i18n/locales/ru/settings.json | 7 + webview-ui/src/i18n/locales/tr/settings.json | 7 + webview-ui/src/i18n/locales/vi/settings.json | 7 + .../src/i18n/locales/zh-CN/settings.json | 7 + .../src/i18n/locales/zh-TW/settings.json | 7 + 27 files changed, 741 insertions(+), 12 deletions(-) create mode 100644 webview-ui/src/components/settings/Verbosity.tsx diff --git a/packages/types/src/model.ts b/packages/types/src/model.ts index 3bd66782cf..a09790578b 100644 --- a/packages/types/src/model.ts +++ b/packages/types/src/model.ts @@ -10,6 +10,16 @@ export const reasoningEffortsSchema = z.enum(reasoningEfforts) export type ReasoningEffort = z.infer +/** + * Verbosity + */ + +export const verbosityLevels = ["low", "medium", "high"] as const + +export const verbosityLevelsSchema = z.enum(verbosityLevels) + +export type VerbosityLevel = z.infer + /** * ModelParameter */ diff --git a/packages/types/src/provider-settings.ts b/packages/types/src/provider-settings.ts index dc51188df9..f0c90101fc 100644 --- a/packages/types/src/provider-settings.ts +++ b/packages/types/src/provider-settings.ts @@ -1,6 +1,6 @@ import { z } from "zod" -import { reasoningEffortsSchema, modelInfoSchema } from "./model.js" +import { reasoningEffortsSchema, verbosityLevelsSchema, modelInfoSchema } from "./model.js" import { codebaseIndexProviderSchema } from "./codebase-index.js" /** @@ -79,6 +79,9 @@ const baseProviderSettingsSchema = z.object({ reasoningEffort: reasoningEffortsSchema.optional(), modelMaxTokens: z.number().optional(), modelMaxThinkingTokens: z.number().optional(), + + // Model verbosity. + verbosity: verbosityLevelsSchema.optional(), }) // Several of the providers share common model config properties. diff --git a/packages/types/src/providers/openai.ts b/packages/types/src/providers/openai.ts index 0afdd46feb..b319be2a5f 100644 --- a/packages/types/src/providers/openai.ts +++ b/packages/types/src/providers/openai.ts @@ -3,9 +3,42 @@ import type { ModelInfo } from "../model.js" // https://openai.com/api/pricing/ export type OpenAiNativeModelId = keyof typeof openAiNativeModels -export const openAiNativeDefaultModelId: OpenAiNativeModelId = "gpt-4.1" +export const openAiNativeDefaultModelId: OpenAiNativeModelId = "gpt-5-2025-08-07" export const openAiNativeModels = { + "gpt-5-2025-08-07": { + maxTokens: 128000, + contextWindow: 400000, + supportsImages: true, + supportsPromptCache: true, + supportsReasoningEffort: true, + inputPrice: 1.25, + outputPrice: 10.0, + cacheReadsPrice: 0.13, + description: "GPT-5: The best model for coding and agentic tasks across domains", + }, + "gpt-5-mini-2025-08-07": { + maxTokens: 128000, + contextWindow: 400000, + supportsImages: true, + supportsPromptCache: true, + supportsReasoningEffort: true, + inputPrice: 0.25, + outputPrice: 2.0, + cacheReadsPrice: 0.03, + description: "GPT-5 Mini: A faster, more cost-efficient version of GPT-5 for well-defined tasks", + }, + "gpt-5-nano-2025-08-07": { + maxTokens: 128000, + contextWindow: 400000, + supportsImages: true, + supportsPromptCache: true, + supportsReasoningEffort: true, + inputPrice: 0.05, + outputPrice: 0.4, + cacheReadsPrice: 0.01, + description: "GPT-5 Nano: Fastest, most cost-efficient version of GPT-5", + }, "gpt-4.1": { maxTokens: 32_768, contextWindow: 1_047_576, diff --git a/src/api/providers/__tests__/openai-native.spec.ts b/src/api/providers/__tests__/openai-native.spec.ts index 64080b4cac..fdd71ba3f6 100644 --- a/src/api/providers/__tests__/openai-native.spec.ts +++ b/src/api/providers/__tests__/openai-native.spec.ts @@ -455,8 +455,162 @@ describe("OpenAiNativeHandler", () => { openAiNativeApiKey: "test-api-key", }) const modelInfo = handlerWithoutModel.getModel() - expect(modelInfo.id).toBe("gpt-4.1") // Default model + expect(modelInfo.id).toBe("gpt-5-2025-08-07") // Default model expect(modelInfo.info).toBeDefined() }) }) + + describe("GPT-5 models", () => { + it("should handle GPT-5 model with developer role", async () => { + handler = new OpenAiNativeHandler({ + ...mockOptions, + apiModelId: "gpt-5-2025-08-07", + }) + + const stream = handler.createMessage(systemPrompt, messages) + const chunks: any[] = [] + for await (const chunk of stream) { + chunks.push(chunk) + } + + // Verify developer role is used for GPT-5 with default parameters + expect(mockCreate).toHaveBeenCalledWith( + expect.objectContaining({ + model: "gpt-5-2025-08-07", + messages: [{ role: "developer", content: expect.stringContaining(systemPrompt) }], + stream: true, + stream_options: { include_usage: true }, + reasoning_effort: "minimal", // Default for GPT-5 + verbosity: "medium", // Default verbosity + }), + ) + }) + + it("should handle GPT-5-mini model", async () => { + handler = new OpenAiNativeHandler({ + ...mockOptions, + apiModelId: "gpt-5-mini-2025-08-07", + }) + + const stream = handler.createMessage(systemPrompt, messages) + const chunks: any[] = [] + for await (const chunk of stream) { + chunks.push(chunk) + } + + expect(mockCreate).toHaveBeenCalledWith( + expect.objectContaining({ + model: "gpt-5-mini-2025-08-07", + messages: [{ role: "developer", content: expect.stringContaining(systemPrompt) }], + stream: true, + stream_options: { include_usage: true }, + reasoning_effort: "minimal", // Default for GPT-5 + verbosity: "medium", // Default verbosity + }), + ) + }) + + it("should handle GPT-5-nano model", async () => { + handler = new OpenAiNativeHandler({ + ...mockOptions, + apiModelId: "gpt-5-nano-2025-08-07", + }) + + const stream = handler.createMessage(systemPrompt, messages) + const chunks: any[] = [] + for await (const chunk of stream) { + chunks.push(chunk) + } + + expect(mockCreate).toHaveBeenCalledWith( + expect.objectContaining({ + model: "gpt-5-nano-2025-08-07", + messages: [{ role: "developer", content: expect.stringContaining(systemPrompt) }], + stream: true, + stream_options: { include_usage: true }, + reasoning_effort: "minimal", // Default for GPT-5 + verbosity: "medium", // Default verbosity + }), + ) + }) + + it("should support verbosity control for GPT-5", async () => { + handler = new OpenAiNativeHandler({ + ...mockOptions, + apiModelId: "gpt-5-2025-08-07", + verbosity: "low", // Set verbosity through options + }) + + // Create a message to verify verbosity is passed + const stream = handler.createMessage(systemPrompt, messages) + const chunks: any[] = [] + for await (const chunk of stream) { + chunks.push(chunk) + } + + // Verify that verbosity is passed in the request + expect(mockCreate).toHaveBeenCalledWith( + expect.objectContaining({ + model: "gpt-5-2025-08-07", + messages: expect.any(Array), + stream: true, + stream_options: { include_usage: true }, + verbosity: "low", + }), + ) + }) + + it("should support minimal reasoning effort for GPT-5", async () => { + handler = new OpenAiNativeHandler({ + ...mockOptions, + apiModelId: "gpt-5-2025-08-07", + reasoningEffort: "low", + }) + + const stream = handler.createMessage(systemPrompt, messages) + const chunks: any[] = [] + for await (const chunk of stream) { + chunks.push(chunk) + } + + // With low reasoning effort, the model should pass it through + expect(mockCreate).toHaveBeenCalledWith( + expect.objectContaining({ + model: "gpt-5-2025-08-07", + messages: expect.any(Array), + stream: true, + stream_options: { include_usage: true }, + reasoning_effort: "low", + verbosity: "medium", // Default verbosity + }), + ) + }) + + it("should support both verbosity and reasoning effort together for GPT-5", async () => { + handler = new OpenAiNativeHandler({ + ...mockOptions, + apiModelId: "gpt-5-2025-08-07", + verbosity: "high", // Set verbosity through options + reasoningEffort: "low", // Set reasoning effort + }) + + const stream = handler.createMessage(systemPrompt, messages) + const chunks: any[] = [] + for await (const chunk of stream) { + chunks.push(chunk) + } + + // Verify both parameters are passed + expect(mockCreate).toHaveBeenCalledWith( + expect.objectContaining({ + model: "gpt-5-2025-08-07", + messages: expect.any(Array), + stream: true, + stream_options: { include_usage: true }, + reasoning_effort: "low", + verbosity: "high", + }), + ) + }) + }) }) diff --git a/src/api/providers/openai-native.ts b/src/api/providers/openai-native.ts index 3f14e65cc6..5e498bee45 100644 --- a/src/api/providers/openai-native.ts +++ b/src/api/providers/openai-native.ts @@ -7,6 +7,8 @@ import { OpenAiNativeModelId, openAiNativeModels, OPENAI_NATIVE_DEFAULT_TEMPERATURE, + type ReasoningEffort, + type VerbosityLevel, } from "@roo-code/types" import type { ApiHandlerOptions } from "../../shared/api" @@ -22,6 +24,32 @@ import type { SingleCompletionHandler, ApiHandlerCreateMessageMetadata } from ". export type OpenAiNativeModel = ReturnType +// GPT-5 specific types for Responses API +type ReasoningEffortWithMinimal = ReasoningEffort | "minimal" + +interface GPT5ResponsesAPIParams { + model: string + input: string + reasoning?: { + effort: ReasoningEffortWithMinimal + } + text?: { + verbosity: VerbosityLevel + } +} + +interface GPT5ResponseChunk { + type: "text" | "reasoning" | "usage" + text?: string + reasoning?: string + usage?: { + input_tokens: number + output_tokens: number + reasoning_tokens?: number + total_tokens: number + } +} + export class OpenAiNativeHandler extends BaseProvider implements SingleCompletionHandler { protected options: ApiHandlerOptions private client: OpenAI @@ -53,6 +81,8 @@ export class OpenAiNativeHandler extends BaseProvider implements SingleCompletio yield* this.handleReasonerMessage(model, id, systemPrompt, messages) } else if (model.id.startsWith("o1")) { yield* this.handleO1FamilyMessage(model, systemPrompt, messages) + } else if (this.isGpt5Model(model.id)) { + yield* this.handleGpt5Message(model, systemPrompt, messages) } else { yield* this.handleDefaultModelMessage(model, systemPrompt, messages) } @@ -66,6 +96,8 @@ export class OpenAiNativeHandler extends BaseProvider implements SingleCompletio // o1 supports developer prompt with formatting // o1-preview and o1-mini only support user messages const isOriginalO1 = model.id === "o1" + const { reasoning } = this.getModel() + const response = await this.client.chat.completions.create({ model: model.id, messages: [ @@ -77,6 +109,7 @@ export class OpenAiNativeHandler extends BaseProvider implements SingleCompletio ], stream: true, stream_options: { include_usage: true }, + ...(reasoning && reasoning), }) yield* this.handleStreamResponse(response, model) @@ -112,15 +145,214 @@ export class OpenAiNativeHandler extends BaseProvider implements SingleCompletio systemPrompt: string, messages: Anthropic.Messages.MessageParam[], ): ApiStream { - const stream = await this.client.chat.completions.create({ + const { reasoning, verbosity } = this.getModel() + + // Prepare the request parameters + const params: any = { model: model.id, temperature: this.options.modelTemperature ?? OPENAI_NATIVE_DEFAULT_TEMPERATURE, messages: [{ role: "system", content: systemPrompt }, ...convertToOpenAiMessages(messages)], stream: true, stream_options: { include_usage: true }, - }) + ...(reasoning && reasoning), + } - yield* this.handleStreamResponse(stream, model) + // Add verbosity if supported (for future GPT-5 models) + if (verbosity && model.id.startsWith("gpt-5")) { + params.verbosity = verbosity + } + + const stream = await this.client.chat.completions.create(params) + + if (typeof (stream as any)[Symbol.asyncIterator] !== "function") { + throw new Error( + "OpenAI SDK did not return an AsyncIterable for streaming response. Please check SDK version and usage.", + ) + } + + yield* this.handleStreamResponse( + stream as unknown as AsyncIterable, + model, + ) + } + + private async *handleGpt5Message( + model: OpenAiNativeModel, + systemPrompt: string, + messages: Anthropic.Messages.MessageParam[], + ): ApiStream { + // GPT-5 uses the Responses API, not Chat Completions + // We need to format the input as a single string combining system prompt and messages + const formattedInput = this.formatInputForResponsesAPI(systemPrompt, messages) + + // Get reasoning effort, supporting the new "minimal" option for GPT-5 + const reasoningEffort = this.getGpt5ReasoningEffort(model) + + // Get verbosity from model settings, default to "medium" if not specified + const verbosity = model.verbosity || "medium" + + // Prepare the request parameters for Responses API + const params: GPT5ResponsesAPIParams = { + model: model.id, + input: formattedInput, + ...(reasoningEffort && { + reasoning: { + effort: reasoningEffort, + }, + }), + text: { + verbosity: verbosity, + }, + } + + // Since the OpenAI SDK doesn't yet support the Responses API, + // we'll make a direct HTTP request + const response = await this.makeGpt5ResponsesAPIRequest(params, model) + + yield* this.handleGpt5StreamResponse(response, model) + } + + private formatInputForResponsesAPI(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): string { + // Format the conversation for the Responses API's single input field + let formattedInput = `System: ${systemPrompt}\n\n` + + for (const message of messages) { + const role = message.role === "user" ? "User" : "Assistant" + const content = + typeof message.content === "string" + ? message.content + : message.content.map((c) => (c.type === "text" ? c.text : "[image]")).join(" ") + formattedInput += `${role}: ${content}\n\n` + } + + return formattedInput.trim() + } + + private getGpt5ReasoningEffort(model: OpenAiNativeModel): ReasoningEffortWithMinimal | undefined { + const { reasoning } = model + + // Check if reasoning effort is configured + if (reasoning && "reasoning_effort" in reasoning) { + const effort = reasoning.reasoning_effort + // Support the new "minimal" effort level for GPT-5 + if (effort === "low" || effort === "medium" || effort === "high") { + return effort + } + } + + // Default to "minimal" for GPT-5 models when not specified + // This provides fastest time-to-first-token as per documentation + return "minimal" + } + + private async makeGpt5ResponsesAPIRequest( + params: GPT5ResponsesAPIParams, + model: OpenAiNativeModel, + ): Promise> { + // The OpenAI SDK doesn't have direct support for the Responses API yet, + // but we can access it through the underlying client request method if available. + // For now, we'll use the Chat Completions API with GPT-5 specific formatting + // to maintain compatibility while the Responses API SDK support is being added. + + // Convert Responses API params to Chat Completions format + // GPT-5 models use "developer" role for system messages + const messages: OpenAI.Chat.ChatCompletionMessageParam[] = [{ role: "developer", content: params.input }] + + // Build the request parameters + const requestParams: any = { + model: params.model, + messages, + stream: true, + stream_options: { include_usage: true }, + } + + // Add reasoning effort if specified (supporting "minimal" for GPT-5) + if (params.reasoning?.effort) { + if (params.reasoning.effort === "minimal") { + // For minimal effort, we pass "minimal" as the reasoning_effort + requestParams.reasoning_effort = "minimal" + } else { + requestParams.reasoning_effort = params.reasoning.effort + } + } + + // Add verbosity control for GPT-5 models + // According to the docs, Chat Completions API also supports verbosity parameter + if (params.text?.verbosity) { + requestParams.verbosity = params.text.verbosity + } + + const stream = (await this.client.chat.completions.create( + requestParams, + )) as unknown as AsyncIterable + + // Convert the stream to GPT-5 response format + return this.convertChatStreamToGpt5Format(stream) + } + + private async *convertChatStreamToGpt5Format( + stream: AsyncIterable, + ): AsyncIterable { + for await (const chunk of stream) { + const delta = chunk.choices[0]?.delta + + if (delta?.content) { + yield { + type: "text", + text: delta.content, + } + } + + if (chunk.usage) { + yield { + type: "usage", + usage: { + input_tokens: chunk.usage.prompt_tokens || 0, + output_tokens: chunk.usage.completion_tokens || 0, + total_tokens: chunk.usage.total_tokens || 0, + }, + } + } + } + } + + private async *handleGpt5StreamResponse( + stream: AsyncIterable, + model: OpenAiNativeModel, + ): ApiStream { + for await (const chunk of stream) { + if (chunk.type === "text" && chunk.text) { + yield { + type: "text", + text: chunk.text, + } + } else if (chunk.type === "usage" && chunk.usage) { + const inputTokens = chunk.usage.input_tokens + const outputTokens = chunk.usage.output_tokens + const cacheReadTokens = 0 + const cacheWriteTokens = 0 + const totalCost = calculateApiCostOpenAI( + model.info, + inputTokens, + outputTokens, + cacheWriteTokens, + cacheReadTokens, + ) + + yield { + type: "usage", + inputTokens, + outputTokens, + cacheWriteTokens, + cacheReadTokens, + totalCost, + } + } + } + } + + private isGpt5Model(modelId: string): boolean { + return modelId.startsWith("gpt-5") } private async *handleStreamResponse( @@ -177,23 +409,39 @@ export class OpenAiNativeHandler extends BaseProvider implements SingleCompletio defaultTemperature: OPENAI_NATIVE_DEFAULT_TEMPERATURE, }) + // For GPT-5 models, ensure we support minimal reasoning effort + if (this.isGpt5Model(id) && params.reasoning) { + // Allow "minimal" effort for GPT-5 models + const effort = this.options.reasoningEffort + if (effort === "low" || effort === "medium" || effort === "high") { + params.reasoning.reasoning_effort = effort + } + } + // The o3 models are named like "o3-mini-[reasoning-effort]", which are // not valid model ids, so we need to strip the suffix. - return { id: id.startsWith("o3-mini") ? "o3-mini" : id, info, ...params } + return { id: id.startsWith("o3-mini") ? "o3-mini" : id, info, ...params, verbosity: params.verbosity } } async completePrompt(prompt: string): Promise { try { - const { id, temperature, reasoning } = this.getModel() + const { id, temperature, reasoning, verbosity } = this.getModel() - const params: OpenAI.Chat.Completions.ChatCompletionCreateParamsNonStreaming = { + const params: OpenAI.Chat.Completions.ChatCompletionCreateParamsNonStreaming & { + verbosity?: VerbosityLevel + } = { model: id, messages: [{ role: "user", content: prompt }], temperature, ...(reasoning && reasoning), } - const response = await this.client.chat.completions.create(params) + // Add verbosity for GPT-5 models + if (this.isGpt5Model(id) && verbosity) { + params.verbosity = verbosity + } + + const response = await this.client.chat.completions.create(params as any) return response.choices[0]?.message.content || "" } catch (error) { if (error instanceof Error) { diff --git a/src/api/transform/__tests__/model-params.spec.ts b/src/api/transform/__tests__/model-params.spec.ts index b5a02f534e..bd75e7eafb 100644 --- a/src/api/transform/__tests__/model-params.spec.ts +++ b/src/api/transform/__tests__/model-params.spec.ts @@ -788,4 +788,101 @@ describe("getModelParams", () => { expect(result.reasoning).toBeUndefined() }) }) + + describe("Verbosity settings", () => { + it("should include verbosity when specified in settings", () => { + const model: ModelInfo = { + ...baseModel, + } + + const result = getModelParams({ + ...openaiParams, + settings: { verbosity: "low" }, + model, + }) + + expect(result.verbosity).toBe("low") + }) + + it("should handle medium verbosity", () => { + const model: ModelInfo = { + ...baseModel, + } + + const result = getModelParams({ + ...openaiParams, + settings: { verbosity: "medium" }, + model, + }) + + expect(result.verbosity).toBe("medium") + }) + + it("should handle high verbosity", () => { + const model: ModelInfo = { + ...baseModel, + } + + const result = getModelParams({ + ...openaiParams, + settings: { verbosity: "high" }, + model, + }) + + expect(result.verbosity).toBe("high") + }) + + it("should return undefined verbosity when not specified", () => { + const model: ModelInfo = { + ...baseModel, + } + + const result = getModelParams({ + ...openaiParams, + settings: {}, + model, + }) + + expect(result.verbosity).toBeUndefined() + }) + + it("should include verbosity alongside reasoning settings", () => { + const model: ModelInfo = { + ...baseModel, + supportsReasoningEffort: true, + } + + const result = getModelParams({ + ...openaiParams, + settings: { + reasoningEffort: "high", + verbosity: "low", + }, + model, + }) + + expect(result.reasoningEffort).toBe("high") + expect(result.verbosity).toBe("low") + expect(result.reasoning).toEqual({ reasoning_effort: "high" }) + }) + + it("should include verbosity with reasoning budget models", () => { + const model: ModelInfo = { + ...baseModel, + supportsReasoningBudget: true, + } + + const result = getModelParams({ + ...anthropicParams, + settings: { + enableReasoningEffort: true, + verbosity: "high", + }, + model, + }) + + expect(result.verbosity).toBe("high") + expect(result.reasoningBudget).toBe(8192) // Default thinking tokens + }) + }) }) diff --git a/src/api/transform/model-params.ts b/src/api/transform/model-params.ts index 9ad4261b76..cc30aa5605 100644 --- a/src/api/transform/model-params.ts +++ b/src/api/transform/model-params.ts @@ -1,4 +1,9 @@ -import { type ModelInfo, type ProviderSettings, ANTHROPIC_DEFAULT_MAX_TOKENS } from "@roo-code/types" +import { + type ModelInfo, + type ProviderSettings, + type VerbosityLevel, + ANTHROPIC_DEFAULT_MAX_TOKENS, +} from "@roo-code/types" import { DEFAULT_HYBRID_REASONING_MODEL_MAX_TOKENS, @@ -35,6 +40,7 @@ type BaseModelParams = { temperature: number | undefined reasoningEffort: "low" | "medium" | "high" | undefined reasoningBudget: number | undefined + verbosity: VerbosityLevel | undefined } type AnthropicModelParams = { @@ -76,6 +82,7 @@ export function getModelParams({ modelMaxThinkingTokens: customMaxThinkingTokens, modelTemperature: customTemperature, reasoningEffort: customReasoningEffort, + verbosity: customVerbosity, } = settings // Use the centralized logic for computing maxTokens @@ -89,6 +96,7 @@ export function getModelParams({ let temperature = customTemperature ?? defaultTemperature let reasoningBudget: ModelParams["reasoningBudget"] = undefined let reasoningEffort: ModelParams["reasoningEffort"] = undefined + let verbosity: VerbosityLevel | undefined = customVerbosity if (shouldUseReasoningBudget({ model, settings })) { // Check if this is a Gemini 2.5 Pro model @@ -123,7 +131,7 @@ export function getModelParams({ reasoningEffort = customReasoningEffort ?? model.reasoningEffort } - const params: BaseModelParams = { maxTokens, temperature, reasoningEffort, reasoningBudget } + const params: BaseModelParams = { maxTokens, temperature, reasoningEffort, reasoningBudget, verbosity } if (format === "anthropic") { return { diff --git a/webview-ui/src/components/settings/ApiOptions.tsx b/webview-ui/src/components/settings/ApiOptions.tsx index 204abe9c0f..74ba885d25 100644 --- a/webview-ui/src/components/settings/ApiOptions.tsx +++ b/webview-ui/src/components/settings/ApiOptions.tsx @@ -91,6 +91,7 @@ import { inputEventTransform, noTransform } from "./transforms" import { ModelInfoView } from "./ModelInfoView" import { ApiErrorMessage } from "./ApiErrorMessage" import { ThinkingBudget } from "./ThinkingBudget" +import { Verbosity } from "./Verbosity" import { DiffSettingsControl } from "./DiffSettingsControl" import { TodoListSettingsControl } from "./TodoListSettingsControl" import { TemperatureControl } from "./TemperatureControl" @@ -616,6 +617,12 @@ const ApiOptions = ({ modelInfo={selectedModelInfo} /> + + {!fromWelcomeView && ( diff --git a/webview-ui/src/components/settings/Verbosity.tsx b/webview-ui/src/components/settings/Verbosity.tsx new file mode 100644 index 0000000000..ee612d66cf --- /dev/null +++ b/webview-ui/src/components/settings/Verbosity.tsx @@ -0,0 +1,43 @@ +import { type ProviderSettings, type ModelInfo, type VerbosityLevel, verbosityLevels } from "@roo-code/types" + +import { useAppTranslation } from "@src/i18n/TranslationContext" +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@src/components/ui" + +interface VerbosityProps { + apiConfiguration: ProviderSettings + setApiConfigurationField: (field: K, value: ProviderSettings[K]) => void + modelInfo?: ModelInfo +} + +export const Verbosity = ({ apiConfiguration, setApiConfigurationField, modelInfo }: VerbosityProps) => { + const { t } = useAppTranslation() + + // For now, we'll show verbosity for all models, but this can be restricted later + // based on model capabilities (e.g., only for GPT-5 models) + if (!modelInfo) { + return null + } + + return ( +
+
+ +
+ +
{t("settings:providers.verbosity.description")}
+
+ ) +} diff --git a/webview-ui/src/i18n/locales/ca/settings.json b/webview-ui/src/i18n/locales/ca/settings.json index e3f2713ffe..3a534fb031 100644 --- a/webview-ui/src/i18n/locales/ca/settings.json +++ b/webview-ui/src/i18n/locales/ca/settings.json @@ -433,6 +433,13 @@ "medium": "Mitjà", "low": "Baix" }, + "verbosity": { + "label": "Verbositat de la sortida", + "high": "Alta", + "medium": "Mitjana", + "low": "Baixa", + "description": "Controla el nivell de detall de les respostes del model. La verbositat baixa produeix respostes concises, mentre que la verbositat alta proporciona explicacions exhaustives." + }, "setReasoningLevel": "Activa l'esforç de raonament", "claudeCode": { "pathLabel": "Ruta del Codi Claude", diff --git a/webview-ui/src/i18n/locales/de/settings.json b/webview-ui/src/i18n/locales/de/settings.json index a4a62b8391..d13050cc7a 100644 --- a/webview-ui/src/i18n/locales/de/settings.json +++ b/webview-ui/src/i18n/locales/de/settings.json @@ -433,6 +433,13 @@ "medium": "Mittel", "low": "Niedrig" }, + "verbosity": { + "label": "Ausgabe-Ausführlichkeit", + "high": "Hoch", + "medium": "Mittel", + "low": "Niedrig", + "description": "Steuert, wie detailliert die Antworten des Modells sind. Niedrige Ausführlichkeit erzeugt knappe Antworten, während hohe Ausführlichkeit gründliche Erklärungen liefert." + }, "setReasoningLevel": "Denkaufwand aktivieren", "claudeCode": { "pathLabel": "Claude-Code-Pfad", diff --git a/webview-ui/src/i18n/locales/en/settings.json b/webview-ui/src/i18n/locales/en/settings.json index 6e0f137504..224ad4fdd7 100644 --- a/webview-ui/src/i18n/locales/en/settings.json +++ b/webview-ui/src/i18n/locales/en/settings.json @@ -432,6 +432,13 @@ "medium": "Medium", "low": "Low" }, + "verbosity": { + "label": "Output Verbosity", + "high": "High", + "medium": "Medium", + "low": "Low", + "description": "Controls how detailed the model's responses are. Low verbosity produces concise answers, while high verbosity provides thorough explanations." + }, "setReasoningLevel": "Enable Reasoning Effort", "claudeCode": { "pathLabel": "Claude Code Path", diff --git a/webview-ui/src/i18n/locales/es/settings.json b/webview-ui/src/i18n/locales/es/settings.json index e2db1463af..79ac8c5510 100644 --- a/webview-ui/src/i18n/locales/es/settings.json +++ b/webview-ui/src/i18n/locales/es/settings.json @@ -433,6 +433,13 @@ "medium": "Medio", "low": "Bajo" }, + "verbosity": { + "label": "Verbosidad de la salida", + "high": "Alta", + "medium": "Media", + "low": "Baja", + "description": "Controla qué tan detalladas son las respuestas del modelo. La verbosidad baja produce respuestas concisas, mientras que la verbosidad alta proporciona explicaciones exhaustivas." + }, "setReasoningLevel": "Habilitar esfuerzo de razonamiento", "claudeCode": { "pathLabel": "Ruta de Claude Code", diff --git a/webview-ui/src/i18n/locales/fr/settings.json b/webview-ui/src/i18n/locales/fr/settings.json index 26018a344b..8a8738ed9b 100644 --- a/webview-ui/src/i18n/locales/fr/settings.json +++ b/webview-ui/src/i18n/locales/fr/settings.json @@ -433,6 +433,13 @@ "medium": "Moyen", "low": "Faible" }, + "verbosity": { + "label": "Verbosité de la sortie", + "high": "Élevée", + "medium": "Moyenne", + "low": "Faible", + "description": "Contrôle le niveau de détail des réponses du modèle. Une faible verbosité produit des réponses concises, tandis qu'une verbosité élevée fournit des explications approfondies." + }, "setReasoningLevel": "Activer l'effort de raisonnement", "claudeCode": { "pathLabel": "Chemin du code Claude", diff --git a/webview-ui/src/i18n/locales/hi/settings.json b/webview-ui/src/i18n/locales/hi/settings.json index 7c8b280427..18c5061a13 100644 --- a/webview-ui/src/i18n/locales/hi/settings.json +++ b/webview-ui/src/i18n/locales/hi/settings.json @@ -433,6 +433,13 @@ "medium": "मध्यम", "low": "निम्न" }, + "verbosity": { + "label": "आउटपुट वर्बोसिटी", + "high": "उच्च", + "medium": "मध्यम", + "low": "कम", + "description": "मॉडल की प्रतिक्रियाएं कितनी विस्तृत हैं, इसे नियंत्रित करता है। कम वर्बोसिटी संक्षिप्त उत्तर देती है, जबकि उच्च वर्बोसिटी विस्तृत स्पष्टीकरण प्रदान करती है।" + }, "setReasoningLevel": "तर्क प्रयास सक्षम करें", "claudeCode": { "pathLabel": "क्लाउड कोड पथ", diff --git a/webview-ui/src/i18n/locales/id/settings.json b/webview-ui/src/i18n/locales/id/settings.json index b4f9b113b3..3a4800f4f2 100644 --- a/webview-ui/src/i18n/locales/id/settings.json +++ b/webview-ui/src/i18n/locales/id/settings.json @@ -437,6 +437,13 @@ "medium": "Sedang", "low": "Rendah" }, + "verbosity": { + "label": "Verbositas Output", + "high": "Tinggi", + "medium": "Sedang", + "low": "Rendah", + "description": "Mengontrol seberapa detail respons model. Verbositas rendah menghasilkan jawaban singkat, sedangkan verbositas tinggi memberikan penjelasan menyeluruh." + }, "setReasoningLevel": "Aktifkan Upaya Reasoning", "claudeCode": { "pathLabel": "Jalur Kode Claude", diff --git a/webview-ui/src/i18n/locales/it/settings.json b/webview-ui/src/i18n/locales/it/settings.json index 82d7b2d041..e116bf2ae3 100644 --- a/webview-ui/src/i18n/locales/it/settings.json +++ b/webview-ui/src/i18n/locales/it/settings.json @@ -433,6 +433,13 @@ "medium": "Medio", "low": "Basso" }, + "verbosity": { + "label": "Verbosity dell'output", + "high": "Alta", + "medium": "Media", + "low": "Bassa", + "description": "Controlla il livello di dettaglio delle risposte del modello. Una verbosity bassa produce risposte concise, mentre una verbosity alta fornisce spiegazioni approfondite." + }, "setReasoningLevel": "Abilita sforzo di ragionamento", "claudeCode": { "pathLabel": "Percorso Claude Code", diff --git a/webview-ui/src/i18n/locales/ja/settings.json b/webview-ui/src/i18n/locales/ja/settings.json index dfa62ab32b..407d31e457 100644 --- a/webview-ui/src/i18n/locales/ja/settings.json +++ b/webview-ui/src/i18n/locales/ja/settings.json @@ -433,6 +433,13 @@ "medium": "中", "low": "低" }, + "verbosity": { + "label": "出力の冗長性", + "high": "高", + "medium": "中", + "low": "低", + "description": "モデルの応答の詳細度を制御します。冗長性が低いと簡潔な回答が生成され、高いと詳細な説明が提供されます。" + }, "setReasoningLevel": "推論労力を有効にする", "claudeCode": { "pathLabel": "クロードコードパス", diff --git a/webview-ui/src/i18n/locales/ko/settings.json b/webview-ui/src/i18n/locales/ko/settings.json index 219a5de54a..3cdb2e8b4f 100644 --- a/webview-ui/src/i18n/locales/ko/settings.json +++ b/webview-ui/src/i18n/locales/ko/settings.json @@ -433,6 +433,13 @@ "medium": "중간", "low": "낮음" }, + "verbosity": { + "label": "출력 상세도", + "high": "높음", + "medium": "중간", + "low": "낮음", + "description": "모델 응답의 상세도를 제어합니다. 낮은 상세도는 간결한 답변을 생성하고, 높은 상세도는 상세한 설명을 제공합니다." + }, "setReasoningLevel": "추론 노력 활성화", "claudeCode": { "pathLabel": "클로드 코드 경로", diff --git a/webview-ui/src/i18n/locales/nl/settings.json b/webview-ui/src/i18n/locales/nl/settings.json index 735339bb66..2061474d17 100644 --- a/webview-ui/src/i18n/locales/nl/settings.json +++ b/webview-ui/src/i18n/locales/nl/settings.json @@ -433,6 +433,13 @@ "medium": "Middel", "low": "Laag" }, + "verbosity": { + "label": "Uitvoerbaarheid", + "high": "Hoog", + "medium": "Gemiddeld", + "low": "Laag", + "description": "Bepaalt hoe gedetailleerd de reacties van het model zijn. Lage uitvoerbaarheid levert beknopte antwoorden op, terwijl hoge uitvoerbaarheid uitgebreide uitleg geeft." + }, "setReasoningLevel": "Redeneervermogen inschakelen", "claudeCode": { "pathLabel": "Claude Code Pad", diff --git a/webview-ui/src/i18n/locales/pl/settings.json b/webview-ui/src/i18n/locales/pl/settings.json index e25eee34cf..b081005400 100644 --- a/webview-ui/src/i18n/locales/pl/settings.json +++ b/webview-ui/src/i18n/locales/pl/settings.json @@ -433,6 +433,13 @@ "medium": "Średni", "low": "Niski" }, + "verbosity": { + "label": "Szczegółowość danych wyjściowych", + "high": "Wysoka", + "medium": "Średnia", + "low": "Niska", + "description": "Kontroluje, jak szczegółowe są odpowiedzi modelu. Niska szczegółowość generuje zwięzłe odpowiedzi, podczas gdy wysoka szczegółowość dostarcza dokładnych wyjaśnień." + }, "setReasoningLevel": "Włącz wysiłek rozumowania", "claudeCode": { "pathLabel": "Ścieżka Claude Code", diff --git a/webview-ui/src/i18n/locales/pt-BR/settings.json b/webview-ui/src/i18n/locales/pt-BR/settings.json index e7243aa6f6..a71340391d 100644 --- a/webview-ui/src/i18n/locales/pt-BR/settings.json +++ b/webview-ui/src/i18n/locales/pt-BR/settings.json @@ -433,6 +433,13 @@ "medium": "Médio", "low": "Baixo" }, + "verbosity": { + "label": "Verbosidade da saída", + "high": "Alta", + "medium": "Média", + "low": "Baixa", + "description": "Controla o quão detalhadas são as respostas do modelo. A verbosidade baixa produz respostas concisas, enquanto a verbosidade alta fornisce explicações detalhadas." + }, "setReasoningLevel": "Habilitar esforço de raciocínio", "claudeCode": { "pathLabel": "Caminho do Claude Code", diff --git a/webview-ui/src/i18n/locales/ru/settings.json b/webview-ui/src/i18n/locales/ru/settings.json index 38e986ab88..00e9b79074 100644 --- a/webview-ui/src/i18n/locales/ru/settings.json +++ b/webview-ui/src/i18n/locales/ru/settings.json @@ -433,6 +433,13 @@ "medium": "Средние", "low": "Низкие" }, + "verbosity": { + "label": "Подробность вывода", + "high": "Высокая", + "medium": "Средняя", + "low": "Низкая", + "description": "Контролирует, насколько подробны ответы модели. Низкая подробность дает краткие ответы, а высокая — подробные объяснения." + }, "setReasoningLevel": "Включить усилие рассуждения", "claudeCode": { "pathLabel": "Путь к Claude Code", diff --git a/webview-ui/src/i18n/locales/tr/settings.json b/webview-ui/src/i18n/locales/tr/settings.json index b4b9fd13e4..e79db2a3b2 100644 --- a/webview-ui/src/i18n/locales/tr/settings.json +++ b/webview-ui/src/i18n/locales/tr/settings.json @@ -433,6 +433,13 @@ "medium": "Orta", "low": "Düşük" }, + "verbosity": { + "label": "Çıktı Ayrıntı Düzeyi", + "high": "Yüksek", + "medium": "Orta", + "low": "Düşük", + "description": "Modelin yanıtlarının ne kadar ayrıntılı olduğunu kontrol eder. Düşük ayrıntı düzeyi kısa yanıtlar üretirken, yüksek ayrıntı düzeyi kapsamlı açıklamalar sunar." + }, "setReasoningLevel": "Akıl Yürütme Çabasını Etkinleştir", "claudeCode": { "pathLabel": "Claude Code Yolu", diff --git a/webview-ui/src/i18n/locales/vi/settings.json b/webview-ui/src/i18n/locales/vi/settings.json index cdae509d5e..10de98d4da 100644 --- a/webview-ui/src/i18n/locales/vi/settings.json +++ b/webview-ui/src/i18n/locales/vi/settings.json @@ -433,6 +433,13 @@ "medium": "Trung bình", "low": "Thấp" }, + "verbosity": { + "label": "Mức độ chi tiết đầu ra", + "high": "Cao", + "medium": "Trung bình", + "low": "Thấp", + "description": "Kiểm soát mức độ chi tiết của các câu trả lời của mô hình. Mức độ chi tiết thấp tạo ra các câu trả lời ngắn gọn, trong khi mức độ chi tiết cao cung cấp giải thích kỹ lưỡng." + }, "setReasoningLevel": "Kích hoạt nỗ lực suy luận", "claudeCode": { "pathLabel": "Đường dẫn Claude Code", diff --git a/webview-ui/src/i18n/locales/zh-CN/settings.json b/webview-ui/src/i18n/locales/zh-CN/settings.json index aca901cc3e..a6971d288f 100644 --- a/webview-ui/src/i18n/locales/zh-CN/settings.json +++ b/webview-ui/src/i18n/locales/zh-CN/settings.json @@ -433,6 +433,13 @@ "medium": "中", "low": "低" }, + "verbosity": { + "label": "输出详细程度", + "high": "高", + "medium": "中", + "low": "低", + "description": "控制模型响应的详细程度。低详细度产生简洁的回答,而高详细度提供详尽的解释。" + }, "setReasoningLevel": "启用推理工作量", "claudeCode": { "pathLabel": "Claude Code 路径", diff --git a/webview-ui/src/i18n/locales/zh-TW/settings.json b/webview-ui/src/i18n/locales/zh-TW/settings.json index db3fc3c2cd..bec2ffd5e9 100644 --- a/webview-ui/src/i18n/locales/zh-TW/settings.json +++ b/webview-ui/src/i18n/locales/zh-TW/settings.json @@ -433,6 +433,13 @@ "medium": "中", "low": "低" }, + "verbosity": { + "label": "輸出詳細程度", + "high": "高", + "medium": "中", + "low": "低", + "description": "控制模型回應的詳細程度。低詳細度產生簡潔的回答,而高詳細度提供詳盡的解釋。" + }, "setReasoningLevel": "啟用推理工作量", "claudeCode": { "pathLabel": "Claude Code 路徑",