@office-ai/aioncli-core 0.18.6 → 0.18.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -13,6 +13,7 @@ export declare class OpenAIContentGenerator implements ContentGenerator {
|
|
|
13
13
|
private model;
|
|
14
14
|
private config;
|
|
15
15
|
private streamingToolCalls;
|
|
16
|
+
private streamingReasoningDetails;
|
|
16
17
|
constructor(apiKey: string, model: string, config: Config);
|
|
17
18
|
/**
|
|
18
19
|
* Hook for subclasses to customize error handling behavior
|
|
@@ -52,11 +53,35 @@ export declare class OpenAIContentGenerator implements ContentGenerator {
|
|
|
52
53
|
* Check if the current model is a DeepSeek Reasoner model
|
|
53
54
|
*/
|
|
54
55
|
private isDeepSeekReasonerModel;
|
|
56
|
+
/**
|
|
57
|
+
* Check if the current model is a Gemini reasoning model (Gemini 3, 2.5 series)
|
|
58
|
+
* These models require reasoning_details/thought_signature for tool calls
|
|
59
|
+
*/
|
|
60
|
+
private isGeminiReasoningModel;
|
|
61
|
+
/**
|
|
62
|
+
* Validate thoughtSignature to prevent "Corrupted thought signature" errors.
|
|
63
|
+
* Valid thoughtSignature should have properly encrypted data (long base64 strings).
|
|
64
|
+
* Invalid ones may contain short UUIDs or corrupted data.
|
|
65
|
+
*
|
|
66
|
+
* 验证 thoughtSignature 以防止 "Corrupted thought signature" 错误。
|
|
67
|
+
* 有效的 thoughtSignature 应该包含正确加密的数据(长 base64 字符串)。
|
|
68
|
+
* 无效的可能包含短 UUID 或损坏的数据。
|
|
69
|
+
*/
|
|
70
|
+
private isValidThoughtSignature;
|
|
55
71
|
/**
|
|
56
72
|
* Add reasoning_content field to assistant messages for DeepSeek Reasoner compatibility.
|
|
57
73
|
* DeepSeek Reasoner models require reasoning_content field in assistant messages during tool calls.
|
|
58
74
|
*/
|
|
59
75
|
private addReasoningContentForDeepSeek;
|
|
76
|
+
/**
|
|
77
|
+
* Filter out REASONING_DETAILS_MARKER from text to prevent it from leaking
|
|
78
|
+
* into messages sent to the model. This can happen when AionUI includes
|
|
79
|
+
* conversation history that contains the marker.
|
|
80
|
+
*
|
|
81
|
+
* 过滤文本中的 REASONING_DETAILS_MARKER,防止其泄露到发送给模型的消息中。
|
|
82
|
+
* 这种情况可能发生在 AionUI 包含含有 marker 的对话历史时。
|
|
83
|
+
*/
|
|
84
|
+
private filterReasoningDetailsMarker;
|
|
60
85
|
/**
|
|
61
86
|
* 清理消息历史中的孤立工具调用,防止 OpenAI API 报错。
|
|
62
87
|
* 同时对相同 tool_call_id 的工具响应进行去重,防止不接受重复响应的 API 返回 400 错误。
|
|
@@ -9,11 +9,15 @@ import { logApiResponse } from '../telemetry/loggers.js';
|
|
|
9
9
|
import { toContents } from '../code_assist/converter.js';
|
|
10
10
|
import { ApiResponseEvent } from '../telemetry/types.js';
|
|
11
11
|
import { safeJsonParse } from '../utils/safeJsonParse.js';
|
|
12
|
+
// Special marker for reasoning_details in Part text
|
|
13
|
+
const REASONING_DETAILS_MARKER = '__OPENROUTER_REASONING_DETAILS__:';
|
|
12
14
|
export class OpenAIContentGenerator {
|
|
13
15
|
client;
|
|
14
16
|
model;
|
|
15
17
|
config;
|
|
16
18
|
streamingToolCalls = new Map();
|
|
19
|
+
// Store reasoning_details for OpenRouter reasoning models (Gemini 3 Pro, etc.)
|
|
20
|
+
streamingReasoningDetails = null;
|
|
17
21
|
constructor(apiKey, model, config) {
|
|
18
22
|
this.model = model;
|
|
19
23
|
this.config = config;
|
|
@@ -163,7 +167,17 @@ export class OpenAIContentGenerator {
|
|
|
163
167
|
else if (request.config?.tools) {
|
|
164
168
|
createParams.tools = await this.convertGeminiToolsToOpenAI(request.config.tools);
|
|
165
169
|
}
|
|
166
|
-
//
|
|
170
|
+
// Enable reasoning for Gemini 3 models on OpenRouter
|
|
171
|
+
// This ensures reasoning_details are returned and can be preserved
|
|
172
|
+
const baseURL = this.client?.baseURL || '';
|
|
173
|
+
const isOpenRouter = baseURL.includes('openrouter.ai');
|
|
174
|
+
if (isOpenRouter && this.isGeminiReasoningModel()) {
|
|
175
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
176
|
+
createParams.reasoning = {
|
|
177
|
+
// Use 'high' effort for reliable reasoning token generation
|
|
178
|
+
effort: 'high',
|
|
179
|
+
};
|
|
180
|
+
}
|
|
167
181
|
const completion = (await this.client.chat.completions.create(createParams));
|
|
168
182
|
// Check if this was a JSON schema request
|
|
169
183
|
const isJsonSchemaRequest = !!(request.config?.responseJsonSchema &&
|
|
@@ -276,7 +290,17 @@ export class OpenAIContentGenerator {
|
|
|
276
290
|
else if (request.config?.tools) {
|
|
277
291
|
createParams.tools = await this.convertGeminiToolsToOpenAI(request.config.tools);
|
|
278
292
|
}
|
|
279
|
-
//
|
|
293
|
+
// Enable reasoning for Gemini 3 models on OpenRouter
|
|
294
|
+
// This ensures reasoning_details are returned and can be preserved
|
|
295
|
+
const baseURL = this.client?.baseURL || '';
|
|
296
|
+
const isOpenRouter = baseURL.includes('openrouter.ai');
|
|
297
|
+
if (isOpenRouter && this.isGeminiReasoningModel()) {
|
|
298
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
299
|
+
createParams.reasoning = {
|
|
300
|
+
// Use 'high' effort for reliable reasoning token generation
|
|
301
|
+
effort: 'high',
|
|
302
|
+
};
|
|
303
|
+
}
|
|
280
304
|
const stream = (await this.client.chat.completions.create(createParams));
|
|
281
305
|
// Check if this was a JSON schema request
|
|
282
306
|
const isJsonSchemaRequest = !!(request.config?.responseJsonSchema &&
|
|
@@ -412,8 +436,9 @@ export class OpenAIContentGenerator {
|
|
|
412
436
|
}
|
|
413
437
|
}
|
|
414
438
|
async *streamGenerator(stream, isJsonSchemaRequest = false) {
|
|
415
|
-
// Reset the
|
|
439
|
+
// Reset the accumulators for each new stream
|
|
416
440
|
this.streamingToolCalls.clear();
|
|
441
|
+
this.streamingReasoningDetails = null;
|
|
417
442
|
for await (const chunk of stream) {
|
|
418
443
|
yield this.convertStreamChunkToGeminiFormat(chunk, isJsonSchemaRequest);
|
|
419
444
|
}
|
|
@@ -732,15 +757,47 @@ export class OpenAIContentGenerator {
|
|
|
732
757
|
const functionCalls = [];
|
|
733
758
|
const functionResponses = [];
|
|
734
759
|
const textParts = [];
|
|
760
|
+
let reasoningDetails = null;
|
|
735
761
|
for (const part of content.parts || []) {
|
|
736
762
|
if (typeof part === 'string') {
|
|
737
|
-
|
|
763
|
+
// Filter out any leaked reasoning_details marker from string parts
|
|
764
|
+
const cleanedString = this.filterReasoningDetailsMarker(part);
|
|
765
|
+
if (cleanedString) {
|
|
766
|
+
textParts.push(cleanedString);
|
|
767
|
+
}
|
|
738
768
|
}
|
|
739
769
|
else if ('text' in part && part.text) {
|
|
740
|
-
|
|
770
|
+
// Check for special reasoning_details marker (OpenRouter reasoning models)
|
|
771
|
+
if (part.thought &&
|
|
772
|
+
part.text.startsWith(REASONING_DETAILS_MARKER)) {
|
|
773
|
+
try {
|
|
774
|
+
reasoningDetails = JSON.parse(part.text.slice(REASONING_DETAILS_MARKER.length));
|
|
775
|
+
}
|
|
776
|
+
catch {
|
|
777
|
+
// Invalid JSON, ignore
|
|
778
|
+
}
|
|
779
|
+
}
|
|
780
|
+
else if (!part.thought) {
|
|
781
|
+
// Only include non-thought text parts, filter out any leaked markers
|
|
782
|
+
const cleanedText = this.filterReasoningDetailsMarker(part.text);
|
|
783
|
+
if (cleanedText) {
|
|
784
|
+
textParts.push(cleanedText);
|
|
785
|
+
}
|
|
786
|
+
}
|
|
741
787
|
}
|
|
742
788
|
else if ('functionCall' in part && part.functionCall) {
|
|
743
789
|
functionCalls.push(part.functionCall);
|
|
790
|
+
// Check for thoughtSignature on the Part (Gemini format)
|
|
791
|
+
// This is required for OpenRouter/Gemini reasoning models
|
|
792
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
793
|
+
const partAny = part;
|
|
794
|
+
if (partAny.thoughtSignature && !reasoningDetails) {
|
|
795
|
+
// Only use the first thoughtSignature (parallel calls only have one)
|
|
796
|
+
// Validate thoughtSignature before using it to prevent "Corrupted thought signature" errors
|
|
797
|
+
if (this.isValidThoughtSignature(partAny.thoughtSignature)) {
|
|
798
|
+
reasoningDetails = partAny.thoughtSignature;
|
|
799
|
+
}
|
|
800
|
+
}
|
|
744
801
|
}
|
|
745
802
|
else if ('functionResponse' in part && part.functionResponse) {
|
|
746
803
|
functionResponses.push(part.functionResponse);
|
|
@@ -768,11 +825,16 @@ export class OpenAIContentGenerator {
|
|
|
768
825
|
arguments: JSON.stringify(fc.args || {}),
|
|
769
826
|
},
|
|
770
827
|
}));
|
|
771
|
-
|
|
828
|
+
const assistantMessage = {
|
|
772
829
|
role: 'assistant',
|
|
773
830
|
content: textParts.join('\n') || null,
|
|
774
831
|
tool_calls: toolCalls,
|
|
775
|
-
}
|
|
832
|
+
};
|
|
833
|
+
// Add reasoning_details if present (required for OpenRouter reasoning models)
|
|
834
|
+
if (reasoningDetails) {
|
|
835
|
+
assistantMessage.reasoning_details = reasoningDetails;
|
|
836
|
+
}
|
|
837
|
+
messages.push(assistantMessage);
|
|
776
838
|
}
|
|
777
839
|
// Handle regular text messages
|
|
778
840
|
else {
|
|
@@ -781,7 +843,12 @@ export class OpenAIContentGenerator {
|
|
|
781
843
|
: 'user';
|
|
782
844
|
const text = textParts.join('\n');
|
|
783
845
|
if (text) {
|
|
784
|
-
|
|
846
|
+
const message = { role, content: text };
|
|
847
|
+
// Add reasoning_details if present and this is an assistant message
|
|
848
|
+
if (role === 'assistant' && reasoningDetails) {
|
|
849
|
+
message.reasoning_details = reasoningDetails;
|
|
850
|
+
}
|
|
851
|
+
messages.push(message);
|
|
785
852
|
}
|
|
786
853
|
}
|
|
787
854
|
}
|
|
@@ -789,7 +856,10 @@ export class OpenAIContentGenerator {
|
|
|
789
856
|
}
|
|
790
857
|
else if (request.contents) {
|
|
791
858
|
if (typeof request.contents === 'string') {
|
|
792
|
-
|
|
859
|
+
const cleanedContent = this.filterReasoningDetailsMarker(request.contents);
|
|
860
|
+
if (cleanedContent) {
|
|
861
|
+
messages.push({ role: 'user', content: cleanedContent });
|
|
862
|
+
}
|
|
793
863
|
}
|
|
794
864
|
else if ('role' in request.contents && 'parts' in request.contents) {
|
|
795
865
|
const content = request.contents;
|
|
@@ -797,7 +867,10 @@ export class OpenAIContentGenerator {
|
|
|
797
867
|
const text = content.parts
|
|
798
868
|
?.map((p) => typeof p === 'string' ? p : 'text' in p ? p.text : '')
|
|
799
869
|
.join('\n') || '';
|
|
800
|
-
|
|
870
|
+
const cleanedText = this.filterReasoningDetailsMarker(text);
|
|
871
|
+
if (cleanedText) {
|
|
872
|
+
messages.push({ role, content: cleanedText });
|
|
873
|
+
}
|
|
801
874
|
}
|
|
802
875
|
}
|
|
803
876
|
// Clean up orphaned tool calls and merge consecutive assistant messages
|
|
@@ -817,6 +890,67 @@ export class OpenAIContentGenerator {
|
|
|
817
890
|
return (modelName.includes('deepseek-reasoner') ||
|
|
818
891
|
modelName.includes('deepseek-r1'));
|
|
819
892
|
}
|
|
893
|
+
/**
|
|
894
|
+
* Check if the current model is a Gemini reasoning model (Gemini 3, 2.5 series)
|
|
895
|
+
* These models require reasoning_details/thought_signature for tool calls
|
|
896
|
+
*/
|
|
897
|
+
isGeminiReasoningModel() {
|
|
898
|
+
const modelName = this.model.toLowerCase();
|
|
899
|
+
return (modelName.includes('gemini-3') ||
|
|
900
|
+
modelName.includes('gemini-2.5') ||
|
|
901
|
+
modelName.includes('gemini-exp') ||
|
|
902
|
+
modelName.includes('gemini-2.0-flash-thinking'));
|
|
903
|
+
}
|
|
904
|
+
/**
|
|
905
|
+
* Validate thoughtSignature to prevent "Corrupted thought signature" errors.
|
|
906
|
+
* Valid thoughtSignature should have properly encrypted data (long base64 strings).
|
|
907
|
+
* Invalid ones may contain short UUIDs or corrupted data.
|
|
908
|
+
*
|
|
909
|
+
* 验证 thoughtSignature 以防止 "Corrupted thought signature" 错误。
|
|
910
|
+
* 有效的 thoughtSignature 应该包含正确加密的数据(长 base64 字符串)。
|
|
911
|
+
* 无效的可能包含短 UUID 或损坏的数据。
|
|
912
|
+
*/
|
|
913
|
+
isValidThoughtSignature(thoughtSignature) {
|
|
914
|
+
if (!thoughtSignature) {
|
|
915
|
+
return false;
|
|
916
|
+
}
|
|
917
|
+
// thoughtSignature can be an array of signature objects or a single object
|
|
918
|
+
const signatures = Array.isArray(thoughtSignature)
|
|
919
|
+
? thoughtSignature
|
|
920
|
+
: [thoughtSignature];
|
|
921
|
+
for (const sig of signatures) {
|
|
922
|
+
if (typeof sig !== 'object' || sig === null) {
|
|
923
|
+
continue;
|
|
924
|
+
}
|
|
925
|
+
const sigObj = sig;
|
|
926
|
+
const data = sigObj['data'];
|
|
927
|
+
// Check if data field exists and is a string
|
|
928
|
+
if (typeof data !== 'string') {
|
|
929
|
+
return false;
|
|
930
|
+
}
|
|
931
|
+
// Valid encrypted data should be significantly longer than a UUID (36 chars)
|
|
932
|
+
// A UUID in base64 is about 48 chars. Valid reasoning data is typically 100+ chars.
|
|
933
|
+
// We use 64 as a threshold to filter out UUID-like corrupted data.
|
|
934
|
+
const MIN_VALID_DATA_LENGTH = 64;
|
|
935
|
+
if (data.length < MIN_VALID_DATA_LENGTH) {
|
|
936
|
+
return false;
|
|
937
|
+
}
|
|
938
|
+
// Additional check: valid data should start with certain prefixes (Ci, Ev, etc.)
|
|
939
|
+
// which are common in Google's encrypted reasoning data
|
|
940
|
+
const validPrefixes = ['Ci', 'Ev', 'Ch', 'Co'];
|
|
941
|
+
const hasValidPrefix = validPrefixes.some((prefix) => data.startsWith(prefix));
|
|
942
|
+
if (!hasValidPrefix) {
|
|
943
|
+
// Check if it looks like a base64-encoded UUID (starts with ZT, ND, etc.)
|
|
944
|
+
// These are typically invalid
|
|
945
|
+
const uuidBase64Prefixes = ['ZT', 'ND', 'MW', 'Yz', 'OD'];
|
|
946
|
+
const looksLikeUuid = uuidBase64Prefixes.some((prefix) => data.startsWith(prefix));
|
|
947
|
+
if (looksLikeUuid && data.length < 100) {
|
|
948
|
+
return false;
|
|
949
|
+
}
|
|
950
|
+
}
|
|
951
|
+
}
|
|
952
|
+
return true;
|
|
953
|
+
}
|
|
820
954
|
/**
|
|
821
955
|
* Add reasoning_content field to assistant messages for DeepSeek Reasoner compatibility.
|
|
822
956
|
* DeepSeek Reasoner models require reasoning_content field in assistant messages during tool calls.
|
|
@@ -834,6 +968,23 @@ export class OpenAIContentGenerator {
|
|
|
834
968
|
return message;
|
|
835
969
|
});
|
|
836
970
|
}
|
|
971
|
+
/**
|
|
972
|
+
* Filter out REASONING_DETAILS_MARKER from text to prevent it from leaking
|
|
973
|
+
* into messages sent to the model. This can happen when AionUI includes
|
|
974
|
+
* conversation history that contains the marker.
|
|
975
|
+
*
|
|
976
|
+
* 过滤文本中的 REASONING_DETAILS_MARKER,防止其泄露到发送给模型的消息中。
|
|
977
|
+
* 这种情况可能发生在 AionUI 包含含有 marker 的对话历史时。
|
|
978
|
+
*/
|
|
979
|
+
filterReasoningDetailsMarker(text) {
|
|
980
|
+
if (!text.includes(REASONING_DETAILS_MARKER)) {
|
|
981
|
+
return text;
|
|
982
|
+
}
|
|
983
|
+
// Remove lines containing the marker (typically: "Assistant: __OPENROUTER_REASONING_DETAILS__:[...]")
|
|
984
|
+
const lines = text.split('\n');
|
|
985
|
+
const filteredLines = lines.filter((line) => !line.includes(REASONING_DETAILS_MARKER));
|
|
986
|
+
return filteredLines.join('\n').trim();
|
|
987
|
+
}
|
|
837
988
|
/**
|
|
838
989
|
* 清理消息历史中的孤立工具调用,防止 OpenAI API 报错。
|
|
839
990
|
* 同时对相同 tool_call_id 的工具响应进行去重,防止不接受重复响应的 API 返回 400 错误。
|
|
@@ -1018,17 +1169,48 @@ export class OpenAIContentGenerator {
|
|
|
1018
1169
|
}
|
|
1019
1170
|
else {
|
|
1020
1171
|
// Regular tool call handling
|
|
1021
|
-
|
|
1172
|
+
// Include thoughtSignature if present (required for Gemini reasoning models)
|
|
1173
|
+
// Note: thoughtSignature is a SIBLING of functionCall in the Part, not inside it
|
|
1174
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1175
|
+
const functionCallPart = {
|
|
1022
1176
|
functionCall: {
|
|
1023
1177
|
id: toolCall.id,
|
|
1024
1178
|
name: toolCall.function.name,
|
|
1025
1179
|
args,
|
|
1026
1180
|
},
|
|
1027
|
-
}
|
|
1181
|
+
};
|
|
1182
|
+
// Check for thought_signature in the tool call (OpenRouter/Gemini)
|
|
1183
|
+
// Validate before attaching to prevent "Corrupted thought signature" errors
|
|
1184
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1185
|
+
const toolCallAny = toolCall;
|
|
1186
|
+
const thoughtSig = toolCallAny.thought_signature ||
|
|
1187
|
+
toolCallAny.thoughtSignature ||
|
|
1188
|
+
toolCallAny.function?.thought_signature;
|
|
1189
|
+
if (thoughtSig && this.isValidThoughtSignature(thoughtSig)) {
|
|
1190
|
+
functionCallPart.thoughtSignature = thoughtSig;
|
|
1191
|
+
}
|
|
1192
|
+
parts.push(functionCallPart);
|
|
1028
1193
|
}
|
|
1029
1194
|
}
|
|
1030
1195
|
}
|
|
1031
1196
|
}
|
|
1197
|
+
// Handle reasoning_details from OpenRouter reasoning models (Gemini 3 Pro, etc.)
|
|
1198
|
+
// Only preserve reasoning_details when there are functionCall parts (for multi-turn tool calls)
|
|
1199
|
+
// For text-only responses, reasoning_details is not needed
|
|
1200
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1201
|
+
const messageReasoningDetails = choice.message?.reasoning_details;
|
|
1202
|
+
// Validate reasoning_details before attaching to prevent "Corrupted thought signature" errors
|
|
1203
|
+
if (messageReasoningDetails &&
|
|
1204
|
+
this.isValidThoughtSignature(messageReasoningDetails)) {
|
|
1205
|
+
// Attach reasoning_details to the FIRST functionCall Part as thoughtSignature
|
|
1206
|
+
// This is required for Gemini API to process tool calls correctly
|
|
1207
|
+
const firstFunctionCallPart = parts.find((p) => 'functionCall' in p);
|
|
1208
|
+
if (firstFunctionCallPart && !firstFunctionCallPart.thoughtSignature) {
|
|
1209
|
+
firstFunctionCallPart.thoughtSignature = messageReasoningDetails;
|
|
1210
|
+
}
|
|
1211
|
+
// For text-only responses, we don't need to preserve reasoning_details
|
|
1212
|
+
// as they are only required for multi-turn tool calls
|
|
1213
|
+
}
|
|
1032
1214
|
response.responseId = openaiResponse.id;
|
|
1033
1215
|
response.createTime = openaiResponse.created
|
|
1034
1216
|
? openaiResponse.created.toString()
|
|
@@ -1072,6 +1254,8 @@ export class OpenAIContentGenerator {
|
|
|
1072
1254
|
return response;
|
|
1073
1255
|
}
|
|
1074
1256
|
convertStreamChunkToGeminiFormat(chunk, isJsonSchemaRequest = false) {
|
|
1257
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1258
|
+
const chunkAny = chunk;
|
|
1075
1259
|
const choice = chunk.choices?.[0];
|
|
1076
1260
|
const response = new GenerateContentResponse();
|
|
1077
1261
|
if (choice) {
|
|
@@ -1080,6 +1264,18 @@ export class OpenAIContentGenerator {
|
|
|
1080
1264
|
if (choice.delta?.content) {
|
|
1081
1265
|
parts.push({ text: choice.delta.content });
|
|
1082
1266
|
}
|
|
1267
|
+
// Capture reasoning_details from OpenRouter reasoning models (Gemini 3 Pro, etc.)
|
|
1268
|
+
// Check multiple possible locations where it might be returned
|
|
1269
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1270
|
+
const choiceAny = choice;
|
|
1271
|
+
const deltaReasoningDetails = choiceAny.delta?.reasoning_details ||
|
|
1272
|
+
choiceAny.reasoning_details ||
|
|
1273
|
+
chunkAny.reasoning_details;
|
|
1274
|
+
// Validate reasoning_details before storing to prevent corrupted data propagation
|
|
1275
|
+
if (deltaReasoningDetails &&
|
|
1276
|
+
this.isValidThoughtSignature(deltaReasoningDetails)) {
|
|
1277
|
+
this.streamingReasoningDetails = deltaReasoningDetails;
|
|
1278
|
+
}
|
|
1083
1279
|
// Handle tool calls - only accumulate during streaming, emit when complete
|
|
1084
1280
|
if (choice.delta?.tool_calls) {
|
|
1085
1281
|
for (const toolCall of choice.delta.tool_calls) {
|
|
@@ -1100,6 +1296,16 @@ export class OpenAIContentGenerator {
|
|
|
1100
1296
|
if (toolCall.function?.arguments) {
|
|
1101
1297
|
accumulatedCall.arguments += toolCall.function.arguments;
|
|
1102
1298
|
}
|
|
1299
|
+
// Capture thought_signature for OpenRouter/Gemini reasoning models
|
|
1300
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1301
|
+
const toolCallAny = toolCall;
|
|
1302
|
+
if (toolCallAny.thought_signature) {
|
|
1303
|
+
accumulatedCall.thought_signature = toolCallAny.thought_signature;
|
|
1304
|
+
}
|
|
1305
|
+
else if (toolCallAny.function?.thought_signature) {
|
|
1306
|
+
accumulatedCall.thought_signature =
|
|
1307
|
+
toolCallAny.function.thought_signature;
|
|
1308
|
+
}
|
|
1103
1309
|
}
|
|
1104
1310
|
}
|
|
1105
1311
|
// Only emit function calls when streaming is complete (finish_reason is present)
|
|
@@ -1120,18 +1326,41 @@ export class OpenAIContentGenerator {
|
|
|
1120
1326
|
}
|
|
1121
1327
|
else {
|
|
1122
1328
|
// Regular tool call handling
|
|
1123
|
-
|
|
1329
|
+
// Include thoughtSignature if present (required for Gemini reasoning models)
|
|
1330
|
+
// Note: thoughtSignature is a SIBLING of functionCall in the Part, not inside it
|
|
1331
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1332
|
+
const functionCallPart = {
|
|
1124
1333
|
functionCall: {
|
|
1125
1334
|
id: accumulatedCall.id,
|
|
1126
1335
|
name: accumulatedCall.name,
|
|
1127
1336
|
args,
|
|
1128
1337
|
},
|
|
1129
|
-
}
|
|
1338
|
+
};
|
|
1339
|
+
// Add thoughtSignature as sibling of functionCall (Gemini format)
|
|
1340
|
+
// For the FIRST functionCall, attach the reasoning_details as thoughtSignature
|
|
1341
|
+
// Validate before attaching to prevent "Corrupted thought signature" errors
|
|
1342
|
+
if (accumulatedCall.thought_signature &&
|
|
1343
|
+
this.isValidThoughtSignature(accumulatedCall.thought_signature)) {
|
|
1344
|
+
functionCallPart.thoughtSignature =
|
|
1345
|
+
accumulatedCall.thought_signature;
|
|
1346
|
+
}
|
|
1347
|
+
else if (this.streamingReasoningDetails &&
|
|
1348
|
+
parts.filter((p) => 'functionCall' in p).length === 0) {
|
|
1349
|
+
// Only attach to the first functionCall (for parallel calls)
|
|
1350
|
+
// Note: streamingReasoningDetails is already validated when stored
|
|
1351
|
+
functionCallPart.thoughtSignature =
|
|
1352
|
+
this.streamingReasoningDetails;
|
|
1353
|
+
}
|
|
1354
|
+
parts.push(functionCallPart);
|
|
1130
1355
|
}
|
|
1131
1356
|
}
|
|
1132
1357
|
}
|
|
1133
1358
|
// Clear all accumulated tool calls
|
|
1134
1359
|
this.streamingToolCalls.clear();
|
|
1360
|
+
// Clear reasoning_details after processing
|
|
1361
|
+
// For text-only responses, we don't need to preserve reasoning_details
|
|
1362
|
+
// as they are only required for multi-turn tool calls
|
|
1363
|
+
this.streamingReasoningDetails = null;
|
|
1135
1364
|
}
|
|
1136
1365
|
response.candidates = [
|
|
1137
1366
|
{
|