forked from boos/BoosAPI
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:
@@ -7,6 +7,7 @@ import type {
|
||||
OpenAiChatToolCall,
|
||||
OpenAiChatUsageDetails,
|
||||
} from './model.js';
|
||||
import { fromTransformerMetadataRecord } from '../../shared/normalized.js';
|
||||
|
||||
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||
return !!value && typeof value === 'object' && !Array.isArray(value);
|
||||
@@ -345,7 +346,9 @@ function extractCitationsFromPayload(
|
||||
citations.add(citation);
|
||||
}
|
||||
|
||||
const transformerMetadata = asRecord(payload.transformer_metadata) ?? asRecord(payload.transformerMetadata);
|
||||
const transformerMetadata = fromTransformerMetadataRecord(
|
||||
payload.transformer_metadata ?? payload.transformerMetadata,
|
||||
);
|
||||
if (transformerMetadata) {
|
||||
for (const citation of dedupeCitations(transformerMetadata.citations)) {
|
||||
citations.add(citation);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import { createClaudeDownstreamContext } from '../../shared/normalized.js';
|
||||
import { toTransformerMetadataRecord } from '../../shared/normalized.js';
|
||||
import { openAiChatTransformer } from './index.js';
|
||||
|
||||
function parseSsePayloads(lines: string[]): Array<Record<string, unknown>> {
|
||||
@@ -154,6 +155,32 @@ describe('openAiChatTransformer.outbound', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('consumes shared transformer metadata citations on openai-compatible final responses', () => {
|
||||
const normalized = openAiChatTransformer.transformFinalResponse({
|
||||
id: 'chatcmpl-shared-metadata',
|
||||
model: 'gpt-5',
|
||||
created: 123,
|
||||
transformer_metadata: toTransformerMetadataRecord({
|
||||
citations: [{ uri: 'https://shared.example/citation' }],
|
||||
thoughtSignature: 'sig-final',
|
||||
thoughtSignatures: ['sig-tool', 'sig-final'],
|
||||
passthrough: {
|
||||
cachedContent: 'cached/item-1',
|
||||
},
|
||||
}),
|
||||
choices: [{
|
||||
index: 0,
|
||||
finish_reason: 'stop',
|
||||
message: {
|
||||
role: 'assistant',
|
||||
content: 'hello',
|
||||
},
|
||||
}],
|
||||
}, 'gpt-5');
|
||||
|
||||
expect((normalized as any).citations).toEqual(['https://shared.example/citation']);
|
||||
});
|
||||
|
||||
it('serializes multi-choice chat responses with per-choice annotations, reasoning, tool calls, and shared citations', () => {
|
||||
const normalized = openAiChatTransformer.transformFinalResponse({
|
||||
id: 'chatcmpl-multi',
|
||||
|
||||
@@ -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