Merge origin/master (v0.5.69) into gitea/new_feature
This commit is contained in:
@@ -6,6 +6,7 @@ import { toOpenAIUsage } from "../concerns/usage.js";
|
||||
import { reasoningDelta } from "../concerns/reasoning.js";
|
||||
import { encodeDataUri } from "../concerns/image.js";
|
||||
import { toOpenAIFinish } from "../concerns/finishReason.js";
|
||||
import { storeGeminiThoughtSignature } from "../../services/thoughtSignatureStore.js";
|
||||
|
||||
// Build chunk meta for current gemini state
|
||||
function chunkMeta(state) {
|
||||
@@ -13,14 +14,18 @@ function chunkMeta(state) {
|
||||
}
|
||||
|
||||
// Build a tool_call chunk from a gemini functionCall part (shared by sig/non-sig branches)
|
||||
function emitFunctionCall(functionCall, state) {
|
||||
function emitFunctionCall(functionCall, state, signature = null) {
|
||||
const rawName = functionCall.name;
|
||||
// Restore original tool name from mapping (AG cloaking)
|
||||
const fcName = state.toolNameMap?.get(rawName) || rawName;
|
||||
const fcArgs = functionCall.args || {};
|
||||
const toolCallIndex = state.functionIndex++;
|
||||
const callId = functionCall.id || `${fcName}-${Date.now()}-${toolCallIndex}`;
|
||||
if (signature) {
|
||||
storeGeminiThoughtSignature(callId, signature, state.sessionId);
|
||||
}
|
||||
const toolCall = {
|
||||
id: `${fcName}-${Date.now()}-${toolCallIndex}`,
|
||||
id: callId,
|
||||
index: toolCallIndex,
|
||||
type: OPENAI_BLOCK.FUNCTION,
|
||||
function: { name: fcName, arguments: JSON.stringify(fcArgs) },
|
||||
@@ -57,13 +62,21 @@ export function geminiToOpenAIResponse(chunk, state) {
|
||||
if (content?.parts) {
|
||||
for (const part of content.parts) {
|
||||
const hasThoughtSig = part.thoughtSignature || part.thought_signature;
|
||||
if (hasThoughtSig && typeof hasThoughtSig === "string") {
|
||||
state.pendingThoughtSignature = hasThoughtSig;
|
||||
}
|
||||
const isThought = part.thought === true;
|
||||
|
||||
|
||||
// Handle thought signature (thinking mode)
|
||||
if (hasThoughtSig) {
|
||||
const hasTextContent = part.text !== undefined && part.text !== "";
|
||||
const hasFunctionCall = !!part.functionCall;
|
||||
|
||||
|
||||
// Standalone thoughtSignature part (no text, no functionCall): keep pending for next functionCall
|
||||
if (!hasTextContent && !hasFunctionCall) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (hasTextContent) {
|
||||
results.push(buildChunk(
|
||||
chunkMeta(state),
|
||||
@@ -71,9 +84,10 @@ export function geminiToOpenAIResponse(chunk, state) {
|
||||
null
|
||||
));
|
||||
}
|
||||
|
||||
|
||||
if (hasFunctionCall) {
|
||||
results.push(emitFunctionCall(part.functionCall, state));
|
||||
results.push(emitFunctionCall(part.functionCall, state, hasThoughtSig));
|
||||
state.pendingThoughtSignature = null;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
@@ -92,7 +106,9 @@ export function geminiToOpenAIResponse(chunk, state) {
|
||||
|
||||
// Function call
|
||||
if (part.functionCall) {
|
||||
results.push(emitFunctionCall(part.functionCall, state));
|
||||
const sig = state.pendingThoughtSignature || null;
|
||||
results.push(emitFunctionCall(part.functionCall, state, sig));
|
||||
state.pendingThoughtSignature = null;
|
||||
}
|
||||
|
||||
// Inline data (images)
|
||||
|
||||
@@ -446,6 +446,13 @@ export function openaiResponsesToOpenAIResponse(chunk, state) {
|
||||
state.created = Math.floor(Date.now() / 1000);
|
||||
state.toolCallIndex = 0;
|
||||
state.currentToolCallId = null;
|
||||
// item_id → chat tool_calls index. Deltas carry item_id; keying on it (not
|
||||
// stream position) keeps parallel calls separate when upstream emits all
|
||||
// output_item.added events before any done/delta. Lazily created so callers
|
||||
// that build their own state object (stream.js) need no changes.
|
||||
state.respToolChatIndex ??= new Map();
|
||||
// Indices that already received argument deltas (guards done-with-args).
|
||||
state.respToolArgsEmitted ??= new Set();
|
||||
}
|
||||
|
||||
// Text content delta
|
||||
@@ -464,16 +471,29 @@ export function openaiResponsesToOpenAIResponse(chunk, state) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Function call started (standard function_call or custom_tool_call)
|
||||
// Function call started (standard function_call or custom_tool_call).
|
||||
// Index is assigned here (not on done): attributing deltas by stream position
|
||||
// merges parallel calls into index 0 whenever upstream emits all addeds
|
||||
// before dones — the client then concatenates N JSON payloads into one
|
||||
// tool input and fails validation. The server item id is the correlator.
|
||||
if (eventType === "response.output_item.added" && (data.item?.type === RESPONSES_ITEM.FUNCTION_CALL || data.item?.type === "custom_tool_call")) {
|
||||
const item = data.item;
|
||||
state.currentToolCallId = item.call_id || fallbackToolCallId();
|
||||
state.respToolChatIndex ??= new Map();
|
||||
const key = item.id || data.item_id || state.currentToolCallId;
|
||||
let idx;
|
||||
if (key && state.respToolChatIndex.has(key)) {
|
||||
idx = state.respToolChatIndex.get(key); // duplicate added (retry) — reuse
|
||||
} else {
|
||||
idx = state.toolCallIndex++;
|
||||
if (key) state.respToolChatIndex.set(key, idx);
|
||||
}
|
||||
|
||||
return buildChunk(
|
||||
{ id: state.chatId, created: state.created, model: state.model || MODEL_FALLBACK },
|
||||
{
|
||||
tool_calls: [{
|
||||
index: state.toolCallIndex,
|
||||
index: idx,
|
||||
id: state.currentToolCallId,
|
||||
type: OPENAI_BLOCK.FUNCTION,
|
||||
function: { name: item.name || "", arguments: "" }
|
||||
@@ -482,20 +502,39 @@ export function openaiResponsesToOpenAIResponse(chunk, state) {
|
||||
);
|
||||
}
|
||||
|
||||
// Function call arguments delta (standard or custom_tool_call variant)
|
||||
// Function call arguments delta (standard or custom_tool_call variant).
|
||||
// Routed by item_id so interleaved parallel fragments stay on their own call.
|
||||
if (eventType === "response.function_call_arguments.delta" || eventType === "response.custom_tool_call_input.delta") {
|
||||
const argsDelta = data.delta || "";
|
||||
if (!argsDelta) return null;
|
||||
|
||||
const known = data.item_id ? state.respToolChatIndex?.get(data.item_id) : undefined;
|
||||
const idx = known ?? Math.max(0, (state.toolCallIndex || 1) - 1);
|
||||
state.respToolArgsEmitted ??= new Set();
|
||||
state.respToolArgsEmitted.add(idx);
|
||||
return buildChunk(
|
||||
{ id: state.chatId, created: state.created, model: state.model || MODEL_FALLBACK },
|
||||
{ tool_calls: [{ index: state.toolCallIndex, function: { arguments: argsDelta } }] }
|
||||
{ tool_calls: [{ index: idx, function: { arguments: argsDelta } }] }
|
||||
);
|
||||
}
|
||||
|
||||
// Function call done (standard or custom_tool_call variant)
|
||||
// Function call done (standard or custom_tool_call variant).
|
||||
// Index was assigned at added-time; nothing to advance. Some upstreams send
|
||||
// complete arguments only here (no deltas) — emit them once in that case.
|
||||
if (eventType === "response.output_item.done" && (data.item?.type === RESPONSES_ITEM.FUNCTION_CALL || data.item?.type === "custom_tool_call")) {
|
||||
state.toolCallIndex++;
|
||||
const key = data.item?.id || data.item_id;
|
||||
const idx = (key && state.respToolChatIndex?.get(key)) ?? Math.max(0, (state.toolCallIndex || 1) - 1);
|
||||
const fullArgs = data.item?.arguments;
|
||||
if (typeof fullArgs === "string" && fullArgs) {
|
||||
state.respToolArgsEmitted ??= new Set();
|
||||
if (!state.respToolArgsEmitted.has(idx)) {
|
||||
state.respToolArgsEmitted.add(idx);
|
||||
return buildChunk(
|
||||
{ id: state.chatId, created: state.created, model: state.model || MODEL_FALLBACK },
|
||||
{ tool_calls: [{ index: idx, function: { arguments: fullArgs } }] }
|
||||
);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user