diff --git a/packages/ai/src/protocols/open-responses.ts b/packages/ai/src/protocols/open-responses.ts index 45c2220568..3fc5028c77 100644 --- a/packages/ai/src/protocols/open-responses.ts +++ b/packages/ai/src/protocols/open-responses.ts @@ -159,6 +159,14 @@ export const coreFields = { tools: optionalArray(Tool), tool_choice: Schema.optional(ToolChoice), store: Schema.optional(Schema.Boolean), + metadata: Schema.optional(Schema.Record(Schema.String, Schema.String)), + safety_identifier: Schema.optional(Schema.String), + stream_options: Schema.optional( + Schema.Struct({ + include_obfuscation: Schema.optional(Schema.Boolean), + }), + ), + top_logprobs: Schema.optional(Schema.Int.check(Schema.isBetween({ minimum: 0, maximum: 20 }))), truncation: Schema.optional(OpenResponsesOptions.TruncationSchema), service_tier: Schema.optional(OpenResponsesOptions.ServiceTierSchema), prompt_cache_key: Schema.optional(Schema.String), @@ -179,6 +187,8 @@ export const coreFields = { parallel_tool_calls: Schema.optional(Schema.Boolean), temperature: Schema.optional(Schema.Number), top_p: Schema.optional(Schema.Number), + presence_penalty: Schema.optional(Schema.Number), + frequency_penalty: Schema.optional(Schema.Number), } const OpenResponsesBody = Schema.Struct({ @@ -578,6 +588,12 @@ const lowerOptions = (request: LLMRequest) => { return { ...(options.instructions ? { instructions: options.instructions } : {}), ...(options.store !== undefined ? { store: options.store } : {}), + ...(options.metadata ? { metadata: options.metadata } : {}), + ...(options.safetyIdentifier ? { safety_identifier: options.safetyIdentifier } : {}), + ...(options.streamOptions?.includeObfuscation !== undefined + ? { stream_options: { include_obfuscation: options.streamOptions.includeObfuscation } } + : {}), + ...(options.topLogprobs !== undefined ? { top_logprobs: options.topLogprobs } : {}), ...(request.promptCacheKey ? { prompt_cache_key: request.promptCacheKey } : {}), ...(options.include ? { include: options.include } : {}), ...(options.reasoningEffort || options.reasoningSummary @@ -627,6 +643,8 @@ export const fromRequestWithExtension = Effect.fn("OpenResponses.fromRequestWith max_output_tokens: generation?.maxTokens, temperature: generation?.temperature, top_p: generation?.topP, + presence_penalty: generation?.presencePenalty, + frequency_penalty: generation?.frequencyPenalty, ...lowerOptions(request), } }) diff --git a/packages/ai/src/protocols/utils/open-responses-options.ts b/packages/ai/src/protocols/utils/open-responses-options.ts index 4877d17e69..89b72b1fdb 100644 --- a/packages/ai/src/protocols/utils/open-responses-options.ts +++ b/packages/ai/src/protocols/utils/open-responses-options.ts @@ -47,9 +47,17 @@ export const AllowedTools = Schema.Struct({ }) export type AllowedTools = typeof AllowedTools.Type +export const StreamOptions = Schema.Struct({ + includeObfuscation: Schema.optional(Schema.Boolean), +}) + export const Options = Schema.Struct({ instructions: Schema.optional(Schema.String), store: Schema.optional(Schema.Boolean), + metadata: Schema.optional(Schema.Record(Schema.String, Schema.String)), + safetyIdentifier: Schema.optional(Schema.String), + streamOptions: Schema.optional(StreamOptions), + topLogprobs: Schema.optional(Schema.Int.check(Schema.isBetween({ minimum: 0, maximum: 20 }))), reasoningEffort: Schema.optional(ReasoningEffort), reasoningSummary: Schema.optional(Schema.Literals(["auto", "concise", "detailed"])), include: Schema.optional(Schema.Array(ResponseIncludableSchema)), diff --git a/packages/ai/test/provider/openai-compatible-responses.test.ts b/packages/ai/test/provider/openai-compatible-responses.test.ts index 7d38e4267c..5462812cd9 100644 --- a/packages/ai/test/provider/openai-compatible-responses.test.ts +++ b/packages/ai/test/provider/openai-compatible-responses.test.ts @@ -126,6 +126,10 @@ describe("Open Responses-compatible route", () => { providerOptions: { reasoningEffort: "low", store: true, + metadata: { environment: "test" }, + safetyIdentifier: "user_123", + streamOptions: { includeObfuscation: false }, + topLogprobs: 3, truncation: "auto", allowedTools: { toolNames: ["lookup"] }, maxToolCalls: 2, @@ -136,6 +140,7 @@ describe("Open Responses-compatible route", () => { LLM.request({ model, prompt: "Think.", + generation: { presencePenalty: 0.2, frequencyPenalty: -0.1 }, tools: [ToolDefinition.make({ name: "lookup", description: "Lookup data", inputSchema: { type: "object" } })], }), ) @@ -143,6 +148,12 @@ describe("Open Responses-compatible route", () => { expect(prepared.body).toMatchObject({ reasoning: { effort: "low" }, store: true, + metadata: { environment: "test" }, + safety_identifier: "user_123", + stream_options: { include_obfuscation: false }, + top_logprobs: 3, + presence_penalty: 0.2, + frequency_penalty: -0.1, truncation: "auto", tool_choice: { type: "allowed_tools", diff --git a/packages/ai/test/provider/openai-responses.test.ts b/packages/ai/test/provider/openai-responses.test.ts index dab5c5fe40..8c5c47cde1 100644 --- a/packages/ai/test/provider/openai-responses.test.ts +++ b/packages/ai/test/provider/openai-responses.test.ts @@ -1284,6 +1284,7 @@ describe("OpenAI Responses route", () => { model: OpenAI.configure({ baseURL: "https://api.openai.test/v1/", apiKey: "test" }).model("gpt-5.2"), prompt: "think", promptCacheKey: "session_123", + generation: { presencePenalty: 0.25, frequencyPenalty: -0.25 }, tools: [ ToolDefinition.make({ name: "read", description: "Read a file", inputSchema: { type: "object" } }), ToolDefinition.make({ name: "grep", description: "Search files", inputSchema: { type: "object" } }), @@ -1293,6 +1294,10 @@ describe("OpenAI Responses route", () => { reasoningEffort: "high", reasoningSummary: "auto", include: ["reasoning.encrypted_content"], + metadata: { environment: "test", tenant: "acme" }, + safetyIdentifier: "user_123", + streamOptions: { includeObfuscation: false }, + topLogprobs: 5, truncation: "disabled", allowedTools: { toolNames: ["read", "grep"], mode: "required" }, maxToolCalls: 4, @@ -1306,6 +1311,12 @@ describe("OpenAI Responses route", () => { expect(prepared.body.include).toEqual(["reasoning.encrypted_content"]) expect(prepared.body.reasoning).toEqual({ effort: "high", summary: "auto" }) expect(prepared.body.text).toEqual({ verbosity: "low" }) + expect(prepared.body.metadata).toEqual({ environment: "test", tenant: "acme" }) + expect(prepared.body.safety_identifier).toBe("user_123") + expect(prepared.body.stream_options).toEqual({ include_obfuscation: false }) + expect(prepared.body.top_logprobs).toBe(5) + expect(prepared.body.presence_penalty).toBe(0.25) + expect(prepared.body.frequency_penalty).toBe(-0.25) expect(prepared.body.truncation).toBe("disabled") expect(prepared.body.tool_choice).toEqual({ type: "allowed_tools",