From ed623cc6480bc74b04786ff012371027f994755e Mon Sep 17 00:00:00 2001 From: Aiden Cline <63023139+rekram1-node@users.noreply.github.com> Date: Sat, 24 Oct 2026 23:22:31 -0500 Subject: [PATCH] feat(ai): add OpenAI Responses reasoning mode and context options (#58872) --- packages/ai/src/protocols/openai-responses.ts | 34 +++++++++++++++++-- packages/ai/src/providers/openai-options.ts | 4 ++- packages/ai/test/effort-updates.test.ts | 15 ++++++++ .../ai/test/provider/openai-responses.test.ts | 14 ++++++++ 4 files changed, 64 insertions(+), 3 deletions(-) diff --git a/packages/ai/src/protocols/openai-responses.ts b/packages/ai/src/protocols/openai-responses.ts index 468a7e11ae9..42c598a3db8 100644 --- a/packages/ai/src/protocols/openai-responses.ts +++ b/packages/ai/src/protocols/openai-responses.ts @@ -10,7 +10,7 @@ import { LLMRequest, type ToolDefinition, type ToolEntry } from "../schema/index import { resolveEffortUpdates } from "../effort-updates.js" import { OpenResponses } from "./open-responses.js" import { OpenResponsesOptions } from "./utils/open-responses-options.js" -import { JsonObject, optionalArray, optionalNull, ProviderShared } from "./shared.js" +import { JsonObject, lenient, optionalArray, optionalNull, ProviderShared } from "./shared.js" import { ResponsesHostedTools } from "./utils/responses-hosted-tools.js" import { OpenResponsesChannel } from "./open-responses-channel.js" import { ResponsesCompaction } from "./utils/responses-compaction.js" @@ -31,6 +31,16 @@ export const ContextManagement = Schema.Array( ) export type ContextManagement = typeof ContextManagement.Type +export const ReasoningMode = ProviderShared.knownString<"standard" | "pro">() +export type ReasoningMode = typeof ReasoningMode.Type +export const ReasoningContext = ProviderShared.knownString<"auto" | "current_turn" | "all_turns">() +export type ReasoningContext = typeof ReasoningContext.Type + +const ReasoningOptions = Schema.Struct({ + reasoningMode: lenient(ReasoningMode), + reasoningContext: lenient(ReasoningContext), +}) + const OpenAIResponsesImageGenerationTool = Schema.Struct({ type: Schema.tag("image_generation"), action: Schema.optional(Schema.Literals(["auto", "generate", "edit"])), @@ -108,6 +118,14 @@ const OpenAIResponsesCoreFields = { ...OpenResponses.coreFields, tools: optionalArray(OpenAIResponsesTools), tool_choice: Schema.optional(OpenAIResponsesToolChoice), + reasoning: Schema.optional( + Schema.Struct({ + effort: Schema.optional(OpenResponsesOptions.ReasoningEffort), + summary: Schema.optional(Schema.Literals(["auto", "concise", "detailed"])), + mode: Schema.optional(ReasoningMode), + context: Schema.optional(ReasoningContext), + }), + ), context_management: Schema.optional( Schema.Array( Schema.Struct({ @@ -141,6 +159,7 @@ const adapter = { // or alongside automatic `context_management` compaction. const supportsEffortUpdates = (request: LLMRequest) => { if (request.providerOptions?.contextManagement !== undefined) return false + if (request.providerOptions?.reasoningMode === "pro") return false if (Schema.is(Schema.Struct({ mode: Schema.Literal("pro") }))(request.http?.body?.reasoning)) return false const override = request.model.compatibility?.supportsEffortUpdates if (override !== undefined) return override @@ -201,10 +220,21 @@ const fromRequest = Effect.fn("OpenAIResponses.fromRequest")(function* (request: Schema.decodeUnknownEffect(Schema.UndefinedOr(ContextManagement)), )(request.providerOptions?.contextManagement) const options = OpenResponsesOptions.resolve(request) + const reasoning = Schema.decodeUnknownSync(ReasoningOptions)(request.providerOptions ?? {}) const updates = resolveEffortUpdates(request, options.reasoningEffort) + const generation = OpenResponses.lowerGeneration(request, { ...options, reasoningEffort: updates.effort }) return { ...(yield* OpenResponses.lowerConversation(updates.request, adapter)), - ...OpenResponses.lowerGeneration(request, { ...options, reasoningEffort: updates.effort }), + ...generation, + ...(generation.reasoning || reasoning.reasoningMode || reasoning.reasoningContext + ? { + reasoning: { + ...generation.reasoning, + mode: reasoning.reasoningMode, + context: reasoning.reasoningContext, + }, + } + : {}), context_management: management?.map((edit) => ({ type: edit.type, compact_threshold: edit.compactThreshold })), tools: request.tools.length === 0 ? undefined : yield* lowerTools(request), tool_choice: diff --git a/packages/ai/src/providers/openai-options.ts b/packages/ai/src/providers/openai-options.ts index 9bcf36791f3..d829928cb36 100644 --- a/packages/ai/src/providers/openai-options.ts +++ b/packages/ai/src/providers/openai-options.ts @@ -1,6 +1,6 @@ import { mergeProviderOptions, type ProviderOptions } from "../schema/index.js" import type { Options, ResponseIncludable, ServiceTier } from "../protocols/utils/open-responses-options.js" -import type { ContextManagement } from "../protocols/openai-responses.js" +import type { ContextManagement, ReasoningContext, ReasoningMode } from "../protocols/openai-responses.js" // Mirrors OpenAI's `ResponseIncludable` union from the official SDK. Keep this // in lockstep with `openai-node/src/resources/responses/responses.ts`. @@ -11,6 +11,8 @@ export type OpenAIServiceTier = ServiceTier | "scale" | "fast" | "ultrafast" export type OpenAIOptionsInput = Omit & { /** Advanced in-band compaction. The caller owns checkpoint persistence and recovery. */ readonly contextManagement?: ContextManagement + readonly reasoningMode?: ReasoningMode + readonly reasoningContext?: ReasoningContext readonly serviceTier?: OpenAIServiceTier readonly [key: string]: unknown } diff --git a/packages/ai/test/effort-updates.test.ts b/packages/ai/test/effort-updates.test.ts index f5ae76ae45f..73fa3e4d284 100644 --- a/packages/ai/test/effort-updates.test.ts +++ b/packages/ai/test/effort-updates.test.ts @@ -449,6 +449,21 @@ describe("OpenAI Responses effort updates", () => { }), ) + it.effect("strips markers when provider options select pro reasoning mode", () => + Effect.gen(function* () { + const prepared = yield* compileRequest( + LLM.request({ + model: OpenAI.configure({ apiKey: "fixture" }).responses("gpt-6-sol"), + messages: conversation, + providerOptions: { reasoningEffort: "low", reasoningMode: "pro" }, + }), + ) + + expect(updates(prepared.body)).toEqual([]) + expect(prepared.body.reasoning).toEqual({ effort: "low", mode: "pro" }) + }), + ) + for (const [id, supported] of [ ["gpt-6-astra", true], ["openai/gpt-6-astra", true], diff --git a/packages/ai/test/provider/openai-responses.test.ts b/packages/ai/test/provider/openai-responses.test.ts index 28d21a4afb2..58cd5484e92 100644 --- a/packages/ai/test/provider/openai-responses.test.ts +++ b/packages/ai/test/provider/openai-responses.test.ts @@ -317,6 +317,20 @@ describe("OpenAI Responses route", () => { }), ) + it.effect("lowers reasoning mode and context into the reasoning object", () => + Effect.gen(function* () { + const prepared = yield* compileRequest( + LLMRequest.update(request, { + providerOptions: { reasoningEffort: "high", reasoningMode: "pro", reasoningContext: "current_turn" }, + }), + ) + + expect(prepared.body.reasoning).toEqual({ effort: "high", mode: "pro", context: "current_turn" }) + expect(prepared.body).not.toHaveProperty("reasoningMode") + expect(prepared.body).not.toHaveProperty("reasoningContext") + }), + ) + it.effect("passes through custom OpenAI text verbosity strings", () => Effect.gen(function* () { const prepared = yield* compileRequest(