Refactor reasoning handling and metadata management in responses
- Updated chat stream tests to clarify reasoning behavior and ensure correct handling of include lists. - Enhanced responses proxy route to normalize include lists and apply default reasoning includes based on request context. - Improved transformer metadata handling, ensuring proper serialization and deserialization of metadata records. - Added tests for preserving reasoning signatures and managing citations in OpenAI-compatible responses. - Refined the logic for injecting default includes in responses and ensured compatibility with codex surface defaults.
This commit is contained in:
@@ -81,7 +81,31 @@ describe('sanitizeResponsesBodyForProxy', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('defaults encrypted reasoning include when reasoning is requested without an explicit include list', () => {
|
||||
it('defaults encrypted reasoning include when reasoning is requested without an explicit include list on the codex responses surface', () => {
|
||||
const result = sanitizeResponsesBodyForProxy(
|
||||
{
|
||||
model: 'gpt-5',
|
||||
input: 'hello',
|
||||
reasoning: {
|
||||
effort: 'high',
|
||||
summary: 'auto',
|
||||
},
|
||||
},
|
||||
'gpt-5',
|
||||
false,
|
||||
{ defaultEncryptedReasoningInclude: true },
|
||||
);
|
||||
|
||||
expect(result).toMatchObject({
|
||||
include: ['reasoning.encrypted_content'],
|
||||
reasoning: {
|
||||
effort: 'high',
|
||||
summary: 'auto',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('does not inject default include on generic responses requests when the codex surface default is disabled', () => {
|
||||
const result = sanitizeResponsesBodyForProxy(
|
||||
{
|
||||
model: 'gpt-5',
|
||||
@@ -95,8 +119,68 @@ describe('sanitizeResponsesBodyForProxy', () => {
|
||||
false,
|
||||
);
|
||||
|
||||
expect(result.include).toBeUndefined();
|
||||
});
|
||||
|
||||
it('defaults encrypted reasoning include on codex responses requests even without explicit reasoning config', () => {
|
||||
const result = sanitizeResponsesBodyForProxy(
|
||||
{
|
||||
model: 'gpt-5',
|
||||
input: 'hello',
|
||||
},
|
||||
'gpt-5',
|
||||
false,
|
||||
{ defaultEncryptedReasoningInclude: true },
|
||||
);
|
||||
|
||||
expect(result).toMatchObject({
|
||||
include: ['reasoning.encrypted_content'],
|
||||
});
|
||||
});
|
||||
|
||||
it('respects an explicit empty include list when the codex responses default is enabled', () => {
|
||||
const result = sanitizeResponsesBodyForProxy(
|
||||
{
|
||||
model: 'gpt-5',
|
||||
input: 'hello',
|
||||
reasoning: {
|
||||
effort: 'high',
|
||||
summary: 'auto',
|
||||
},
|
||||
include: [],
|
||||
},
|
||||
'gpt-5',
|
||||
false,
|
||||
{ defaultEncryptedReasoningInclude: true },
|
||||
);
|
||||
|
||||
expect(result).toMatchObject({
|
||||
include: [],
|
||||
reasoning: {
|
||||
effort: 'high',
|
||||
summary: 'auto',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('respects an explicit custom include list when the codex responses default is enabled', () => {
|
||||
const result = sanitizeResponsesBodyForProxy(
|
||||
{
|
||||
model: 'gpt-5',
|
||||
input: 'hello',
|
||||
reasoning: {
|
||||
effort: 'high',
|
||||
summary: 'auto',
|
||||
},
|
||||
include: ['message.input_image.image_url'],
|
||||
},
|
||||
'gpt-5',
|
||||
false,
|
||||
{ defaultEncryptedReasoningInclude: true },
|
||||
);
|
||||
|
||||
expect(result).toMatchObject({
|
||||
include: ['message.input_image.image_url'],
|
||||
reasoning: {
|
||||
effort: 'high',
|
||||
summary: 'auto',
|
||||
@@ -296,6 +380,19 @@ describe('convertOpenAiBodyToResponsesBody', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('does not inject default include when converting non-responses OpenAI input into Responses bodies', () => {
|
||||
const result = convertOpenAiBodyToResponsesBody(
|
||||
{
|
||||
model: 'gpt-5',
|
||||
messages: [{ role: 'user', content: 'hello' }],
|
||||
},
|
||||
'gpt-5',
|
||||
true,
|
||||
);
|
||||
|
||||
expect(result.include).toBeUndefined();
|
||||
});
|
||||
|
||||
it('maps OpenAI file-style content blocks into Responses input_file blocks', () => {
|
||||
const result = convertOpenAiBodyToResponsesBody(
|
||||
{
|
||||
@@ -557,7 +654,7 @@ describe('convertResponsesBodyToOpenAiBody', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('adds encrypted reasoning include for OpenAI-compatible fallback when reasoning options are present', () => {
|
||||
it('does not inject default include for generic OpenAI-compatible fallback when reasoning options are present', () => {
|
||||
const result = convertResponsesBodyToOpenAiBody(
|
||||
{
|
||||
model: 'gpt-5',
|
||||
@@ -570,6 +667,23 @@ describe('convertResponsesBodyToOpenAiBody', () => {
|
||||
true,
|
||||
);
|
||||
|
||||
expect(result.include).toBeUndefined();
|
||||
});
|
||||
|
||||
it('adds encrypted reasoning include for OpenAI-compatible fallback when the codex surface default is enabled', () => {
|
||||
const result = convertResponsesBodyToOpenAiBody(
|
||||
{
|
||||
model: 'gpt-5',
|
||||
input: 'hello',
|
||||
reasoning: {
|
||||
effort: 'high',
|
||||
},
|
||||
},
|
||||
'gpt-5',
|
||||
true,
|
||||
{ defaultEncryptedReasoningInclude: true },
|
||||
);
|
||||
|
||||
expect(result).toMatchObject({
|
||||
include: ['reasoning.encrypted_content'],
|
||||
reasoning: {
|
||||
@@ -578,6 +692,81 @@ describe('convertResponsesBodyToOpenAiBody', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('does not inject default include for generic OpenAI-compatible fallback when responses input omits include and reasoning', () => {
|
||||
const result = convertResponsesBodyToOpenAiBody(
|
||||
{
|
||||
model: 'gpt-5',
|
||||
input: 'hello',
|
||||
},
|
||||
'gpt-5',
|
||||
true,
|
||||
);
|
||||
|
||||
expect(result.include).toBeUndefined();
|
||||
});
|
||||
|
||||
it('adds encrypted reasoning include for OpenAI-compatible fallback when the codex surface default is enabled even without explicit reasoning config', () => {
|
||||
const result = convertResponsesBodyToOpenAiBody(
|
||||
{
|
||||
model: 'gpt-5',
|
||||
input: 'hello',
|
||||
},
|
||||
'gpt-5',
|
||||
true,
|
||||
{ defaultEncryptedReasoningInclude: true },
|
||||
);
|
||||
|
||||
expect(result).toMatchObject({
|
||||
include: ['reasoning.encrypted_content'],
|
||||
});
|
||||
});
|
||||
|
||||
it('respects an explicit empty include list for OpenAI-compatible fallback when the codex surface default is enabled', () => {
|
||||
const result = convertResponsesBodyToOpenAiBody(
|
||||
{
|
||||
model: 'gpt-5',
|
||||
input: 'hello',
|
||||
reasoning: {
|
||||
effort: 'high',
|
||||
},
|
||||
include: [],
|
||||
},
|
||||
'gpt-5',
|
||||
true,
|
||||
{ defaultEncryptedReasoningInclude: true },
|
||||
);
|
||||
|
||||
expect(result).toMatchObject({
|
||||
include: [],
|
||||
reasoning: {
|
||||
effort: 'high',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('respects an explicit custom include list for OpenAI-compatible fallback when the codex surface default is enabled', () => {
|
||||
const result = convertResponsesBodyToOpenAiBody(
|
||||
{
|
||||
model: 'gpt-5',
|
||||
input: 'hello',
|
||||
reasoning: {
|
||||
effort: 'high',
|
||||
},
|
||||
include: ['message.input_image.image_url'],
|
||||
},
|
||||
'gpt-5',
|
||||
true,
|
||||
{ defaultEncryptedReasoningInclude: true },
|
||||
);
|
||||
|
||||
expect(result).toMatchObject({
|
||||
include: ['message.input_image.image_url'],
|
||||
reasoning: {
|
||||
effort: 'high',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('keeps Responses input_file items when converting back to OpenAI-compatible bodies', () => {
|
||||
const result = convertResponsesBodyToOpenAiBody(
|
||||
{
|
||||
|
||||
@@ -82,30 +82,17 @@ function normalizeIncludeList(value: unknown): unknown {
|
||||
.filter((item) => item.length > 0);
|
||||
}
|
||||
|
||||
function hasReasoningRequest(value: unknown): boolean {
|
||||
if (!isRecord(value)) return false;
|
||||
const relevantKeys = ['effort', 'budget_tokens', 'budgetTokens', 'max_tokens', 'maxTokens', 'summary'];
|
||||
return relevantKeys.some((key) => {
|
||||
const entry = value[key];
|
||||
if (typeof entry === 'string') return entry.trim().length > 0;
|
||||
return entry !== undefined && entry !== null;
|
||||
});
|
||||
function hasExplicitInclude(body: Record<string, unknown>): boolean {
|
||||
return Object.prototype.hasOwnProperty.call(body, 'include');
|
||||
}
|
||||
|
||||
function ensureEncryptedReasoningInclude(body: Record<string, unknown>): void {
|
||||
if (!hasReasoningRequest(body.reasoning)) return;
|
||||
|
||||
const normalizedInclude = normalizeIncludeList(body.include);
|
||||
const includeList = Array.isArray(normalizedInclude) ? normalizedInclude : [];
|
||||
const alreadyRequested = includeList.some((item) => (
|
||||
typeof item === 'string' && item.trim().toLowerCase() === 'reasoning.encrypted_content'
|
||||
));
|
||||
if (alreadyRequested) {
|
||||
body.include = includeList;
|
||||
function applyDefaultResponsesInclude(body: Record<string, unknown>): void {
|
||||
if (hasExplicitInclude(body)) {
|
||||
body.include = normalizeIncludeList(body.include);
|
||||
return;
|
||||
}
|
||||
|
||||
body.include = [...includeList, 'reasoning.encrypted_content'];
|
||||
body.include = ['reasoning.encrypted_content'];
|
||||
}
|
||||
|
||||
function normalizeTextConfig(
|
||||
@@ -137,7 +124,10 @@ function normalizeStreamOptions(rawStreamOptions: unknown): unknown {
|
||||
|
||||
function normalizeResponsesRequestFieldParity(
|
||||
body: Record<string, unknown>,
|
||||
options?: { verbositySource?: unknown },
|
||||
options?: {
|
||||
verbositySource?: unknown;
|
||||
defaultEncryptedReasoningInclude?: boolean;
|
||||
},
|
||||
): Record<string, unknown> {
|
||||
const normalized: Record<string, unknown> = { ...body };
|
||||
|
||||
@@ -174,7 +164,9 @@ function normalizeResponsesRequestFieldParity(
|
||||
if (normalized.include !== undefined) {
|
||||
normalized.include = normalizeIncludeList(normalized.include);
|
||||
}
|
||||
ensureEncryptedReasoningInclude(normalized);
|
||||
if (options?.defaultEncryptedReasoningInclude) {
|
||||
applyDefaultResponsesInclude(normalized);
|
||||
}
|
||||
|
||||
if (normalized.stream_options !== undefined) {
|
||||
normalized.stream_options = normalizeStreamOptions(normalized.stream_options);
|
||||
@@ -376,6 +368,7 @@ export function sanitizeResponsesBodyForProxy(
|
||||
body: Record<string, unknown>,
|
||||
modelName: string,
|
||||
stream: boolean,
|
||||
options?: { defaultEncryptedReasoningInclude?: boolean },
|
||||
): Record<string, unknown> {
|
||||
let normalized = normalizeResponsesBodyForCompatibility({
|
||||
...body,
|
||||
@@ -401,6 +394,7 @@ export function sanitizeResponsesBodyForProxy(
|
||||
|
||||
normalized = normalizeResponsesRequestFieldParity(normalized, {
|
||||
verbositySource: body.verbosity,
|
||||
defaultEncryptedReasoningInclude: options?.defaultEncryptedReasoningInclude,
|
||||
});
|
||||
|
||||
const sanitized: Record<string, unknown> = {};
|
||||
@@ -717,8 +711,11 @@ export function convertResponsesBodyToOpenAiBody(
|
||||
body: Record<string, unknown>,
|
||||
modelName: string,
|
||||
stream: boolean,
|
||||
options?: { defaultEncryptedReasoningInclude?: boolean },
|
||||
): Record<string, unknown> {
|
||||
const normalizedBody = normalizeResponsesRequestFieldParity(body);
|
||||
const normalizedBody = normalizeResponsesRequestFieldParity(body, {
|
||||
defaultEncryptedReasoningInclude: options?.defaultEncryptedReasoningInclude,
|
||||
});
|
||||
const messages: Array<Record<string, unknown>> = [];
|
||||
const input = normalizedBody.input;
|
||||
let functionCallIndex = 0;
|
||||
|
||||
Reference in New Issue
Block a user