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:
cita
2026-03-08 23:03:33 +08:00
parent 7bd3114ea0
commit 3dcf45851f
12 changed files with 830 additions and 56 deletions
@@ -5,6 +5,7 @@ import {
createGeminiGenerateContentAggregateState,
} from './aggregator.js';
import {
extractTransformerMetadata,
extractResponseMetadata,
serializeGeminiAggregateResponse,
} from './outbound.js';
@@ -240,6 +241,60 @@ describe('Gemini aggregate/outbound/usage', () => {
});
});
it('projects Gemini richer metadata into the shared transformer metadata contract', () => {
const state = createGeminiGenerateContentAggregateState();
applyGeminiGenerateContentAggregate(state, {
responseId: 'resp-shared-meta',
modelVersion: 'gemini-3.1-pro-preview',
candidates: [
{
index: 0,
finishReason: 'STOP',
groundingMetadata: { webSearchQueries: ['cats'] },
citationMetadata: { citations: [{ uri: 'https://example.com/cats' }] },
content: {
parts: [
{ text: 'reasoning', thought: true, thoughtSignature: 'sig-final' },
{ text: 'answer' },
],
},
},
],
usageMetadata: {
promptTokenCount: 15,
candidatesTokenCount: 5,
totalTokenCount: 24,
},
});
const metadata = extractTransformerMetadata(state, {
cachedContent: 'cached/item-1',
safetySettings: [{ category: 'SAFE', threshold: 'BLOCK_NONE' }],
toolConfig: { functionCallingConfig: { mode: 'ANY' } },
generationConfig: {
imageConfig: { aspectRatio: '16:9' },
},
});
expect(metadata).toEqual({
citations: [{ citations: [{ uri: 'https://example.com/cats' }] }],
groundingMetadata: [{ webSearchQueries: ['cats'] }],
thoughtSignature: 'sig-final',
thoughtSignatures: ['sig-final'],
usageMetadata: {
promptTokenCount: 15,
candidatesTokenCount: 5,
totalTokenCount: 24,
},
geminiSafetySettings: [{ category: 'SAFE', threshold: 'BLOCK_NONE' }],
geminiImageConfig: { aspectRatio: '16:9' },
passthrough: {
cachedContent: 'cached/item-1',
toolConfig: { functionCallingConfig: { mode: 'ANY' } },
},
});
});
it('merges split candidate metadata and emits top-level metadata in candidate order', () => {
const state = createGeminiGenerateContentAggregateState();
applyGeminiGenerateContentAggregate(state, [
@@ -3,6 +3,10 @@ import {
createGeminiGenerateContentAggregateState,
type GeminiGenerateContentAggregateState,
} from './aggregator.js';
import {
toTransformerMetadataRecord,
type TransformerMetadata,
} from '../../shared/normalized.js';
function normalizeBaseUrl(baseUrl: string): string {
return (baseUrl || '').replace(/\/+$/, '');
@@ -176,14 +180,28 @@ function extractOrderedThoughtSignatures(
};
}
function extractRequestSemantics(requestPayload: unknown): GeminiRecord {
function ensurePassthrough(metadata: TransformerMetadata): Record<string, unknown> {
if (!metadata.passthrough) metadata.passthrough = {};
return metadata.passthrough;
}
function extractRequestSemantics(requestPayload: unknown): TransformerMetadata {
if (!isRecord(requestPayload)) return {};
const metadata: GeminiRecord = {};
if (requestPayload.systemInstruction !== undefined) metadata.systemInstruction = cloneJsonValue(requestPayload.systemInstruction);
if (requestPayload.cachedContent !== undefined) metadata.cachedContent = cloneJsonValue(requestPayload.cachedContent);
if (requestPayload.safetySettings !== undefined) metadata.safetySettings = cloneJsonValue(requestPayload.safetySettings);
if (requestPayload.toolConfig !== undefined) metadata.toolConfig = cloneJsonValue(requestPayload.toolConfig);
const metadata: TransformerMetadata = {};
const passthrough = ensurePassthrough(metadata);
if (requestPayload.systemInstruction !== undefined) {
passthrough.systemInstruction = cloneJsonValue(requestPayload.systemInstruction);
}
if (requestPayload.cachedContent !== undefined) {
passthrough.cachedContent = cloneJsonValue(requestPayload.cachedContent);
}
if (requestPayload.safetySettings !== undefined) {
metadata.geminiSafetySettings = cloneJsonValue(requestPayload.safetySettings);
}
if (requestPayload.toolConfig !== undefined) {
passthrough.toolConfig = cloneJsonValue(requestPayload.toolConfig);
}
const generationConfig = isRecord(requestPayload.generationConfig) ? requestPayload.generationConfig : null;
if (generationConfig) {
@@ -207,7 +225,11 @@ function extractRequestSemantics(requestPayload: unknown): GeminiRecord {
];
for (const key of preservedKeys) {
if (generationConfig[key] !== undefined) {
metadata[key] = cloneJsonValue(generationConfig[key]);
if (key === 'imageConfig') {
metadata.geminiImageConfig = cloneJsonValue(generationConfig[key]);
} else {
passthrough[key] = cloneJsonValue(generationConfig[key]);
}
}
}
}
@@ -226,10 +248,14 @@ function extractRequestSemantics(requestPayload: unknown): GeminiRecord {
.filter((item) => Object.keys(item).length > 0);
if (requestTools.length > 0) {
metadata.tools = requestTools;
passthrough.tools = requestTools;
}
}
if (metadata.passthrough && Object.keys(metadata.passthrough).length <= 0) {
delete metadata.passthrough;
}
return metadata;
}
@@ -251,9 +277,9 @@ export function serializeGeminiAggregateResponse(
return response;
}
export function extractResponseMetadata(payload: unknown, requestPayload?: unknown): GeminiRecord {
export function extractTransformerMetadata(payload: unknown, requestPayload?: unknown): TransformerMetadata {
const state = ensureAggregateState(payload);
const metadata: GeminiRecord = extractRequestSemantics(requestPayload);
const metadata = extractRequestSemantics(requestPayload);
const citations = extractOrderedCandidateMetadata(state, 'citationMetadata');
const groundingMetadata = extractOrderedCandidateMetadata(state, 'groundingMetadata');
@@ -273,10 +299,15 @@ export function extractResponseMetadata(payload: unknown, requestPayload?: unkno
return metadata;
}
export function extractResponseMetadata(payload: unknown, requestPayload?: unknown): GeminiRecord {
return toTransformerMetadataRecord(extractTransformerMetadata(payload, requestPayload)) ?? {};
}
export const geminiGenerateContentOutbound = {
resolveBaseUrl,
resolveModelsUrl,
resolveActionUrl,
extractTransformerMetadata,
extractResponseMetadata,
serializeAggregateResponse: serializeGeminiAggregateResponse,
};