@jsonstudio/llms 0.6.567 → 0.6.568
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.
- package/dist/conversion/codecs/gemini-openai-codec.js +33 -4
- package/dist/conversion/codecs/openai-openai-codec.js +2 -1
- package/dist/conversion/codecs/responses-openai-codec.js +3 -2
- package/dist/conversion/compat/actions/glm-history-image-trim.d.ts +2 -0
- package/dist/conversion/compat/actions/glm-history-image-trim.js +88 -0
- package/dist/conversion/hub/pipeline/hub-pipeline.d.ts +6 -1
- package/dist/conversion/hub/pipeline/hub-pipeline.js +25 -13
- package/dist/conversion/hub/process/chat-process.js +65 -11
- package/dist/conversion/hub/semantic-mappers/gemini-mapper.js +16 -3
- package/dist/conversion/hub/semantic-mappers/responses-mapper.js +51 -2
- package/dist/conversion/hub/types/chat-envelope.d.ts +1 -0
- package/dist/conversion/shared/anthropic-message-utils.js +54 -0
- package/dist/conversion/shared/args-mapping.js +11 -3
- package/dist/conversion/shared/responses-output-builder.js +42 -21
- package/dist/conversion/shared/streaming-text-extractor.d.ts +25 -0
- package/dist/conversion/shared/streaming-text-extractor.js +31 -38
- package/dist/conversion/shared/text-markup-normalizer.js +42 -27
- package/dist/conversion/shared/tool-filter-pipeline.js +2 -1
- package/dist/conversion/shared/tool-harvester.js +43 -12
- package/dist/conversion/shared/tool-mapping.d.ts +1 -0
- package/dist/conversion/shared/tool-mapping.js +33 -19
- package/dist/filters/index.d.ts +1 -0
- package/dist/filters/index.js +1 -0
- package/dist/filters/special/request-tools-normalize.js +14 -4
- package/dist/filters/special/response-apply-patch-toon-decode.d.ts +23 -0
- package/dist/filters/special/response-apply-patch-toon-decode.js +109 -0
- package/dist/filters/special/response-tool-arguments-toon-decode.d.ts +10 -0
- package/dist/filters/special/response-tool-arguments-toon-decode.js +55 -13
- package/dist/guidance/index.js +69 -42
- package/dist/router/virtual-router/bootstrap.js +10 -5
- package/dist/router/virtual-router/classifier.js +9 -4
- package/dist/router/virtual-router/engine-health.d.ts +11 -0
- package/dist/router/virtual-router/engine-health.js +217 -4
- package/dist/router/virtual-router/engine-logging.d.ts +2 -1
- package/dist/router/virtual-router/engine-logging.js +35 -3
- package/dist/router/virtual-router/engine.d.ts +17 -1
- package/dist/router/virtual-router/engine.js +154 -6
- package/dist/router/virtual-router/routing-instructions.d.ts +2 -0
- package/dist/router/virtual-router/routing-instructions.js +19 -1
- package/dist/router/virtual-router/tool-signals.js +57 -11
- package/dist/router/virtual-router/types.d.ts +30 -0
- package/dist/router/virtual-router/types.js +1 -1
- package/dist/servertool/engine.js +3 -0
- package/dist/servertool/handlers/iflow-model-error-retry.d.ts +1 -0
- package/dist/servertool/handlers/iflow-model-error-retry.js +93 -0
- package/dist/servertool/handlers/stop-message-auto.js +61 -4
- package/dist/servertool/server-side-tools.d.ts +1 -0
- package/dist/servertool/server-side-tools.js +27 -0
- package/dist/sse/sse-to-json/builders/anthropic-response-builder.js +16 -0
- package/dist/tools/apply-patch-structured.d.ts +20 -0
- package/dist/tools/apply-patch-structured.js +239 -0
- package/dist/tools/tool-description-utils.d.ts +5 -0
- package/dist/tools/tool-description-utils.js +50 -0
- package/dist/tools/tool-registry.js +11 -193
- package/package.json +2 -2
|
@@ -360,7 +360,9 @@ export function buildOpenAIChatFromGeminiResponse(payload) {
|
|
|
360
360
|
if (Number.isFinite(totalTokens))
|
|
361
361
|
usage.total_tokens = totalTokens;
|
|
362
362
|
const combinedText = textParts.join('\n');
|
|
363
|
-
const normalized = combinedText.length
|
|
363
|
+
const normalized = combinedText.length
|
|
364
|
+
? normalizeChatMessageContent(combinedText)
|
|
365
|
+
: { contentText: undefined, reasoningText: undefined };
|
|
364
366
|
const baseContent = normalized.contentText ?? combinedText ?? '';
|
|
365
367
|
const toolResultBlock = toolResultTexts.length ? toolResultTexts.join('\n') : '';
|
|
366
368
|
const finalContent = toolResultBlock && baseContent
|
|
@@ -525,16 +527,43 @@ export function buildGeminiFromOpenAIChat(chatResp) {
|
|
|
525
527
|
let argsStruct;
|
|
526
528
|
const rawArgs = fn.arguments;
|
|
527
529
|
if (typeof rawArgs === 'string') {
|
|
530
|
+
const trimmed = rawArgs.trim();
|
|
531
|
+
if (trimmed.startsWith('{')) {
|
|
532
|
+
try {
|
|
533
|
+
const parsed = JSON.parse(rawArgs);
|
|
534
|
+
if (isObject(parsed)) {
|
|
535
|
+
argsStruct = parsed;
|
|
536
|
+
}
|
|
537
|
+
else {
|
|
538
|
+
argsStruct = { _raw: rawArgs };
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
catch {
|
|
542
|
+
argsStruct = { _raw: rawArgs };
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
else {
|
|
546
|
+
argsStruct = { _raw: rawArgs };
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
else if (isObject(rawArgs)) {
|
|
550
|
+
argsStruct = rawArgs;
|
|
551
|
+
}
|
|
552
|
+
else if (Array.isArray(rawArgs)) {
|
|
528
553
|
try {
|
|
529
|
-
argsStruct = JSON.
|
|
554
|
+
argsStruct = { _raw: JSON.stringify(rawArgs) };
|
|
530
555
|
}
|
|
531
556
|
catch {
|
|
532
|
-
argsStruct = { _raw: rawArgs };
|
|
557
|
+
argsStruct = { _raw: String(rawArgs) };
|
|
533
558
|
}
|
|
534
559
|
}
|
|
560
|
+
else if (rawArgs != null) {
|
|
561
|
+
argsStruct = { _raw: String(rawArgs) };
|
|
562
|
+
}
|
|
535
563
|
else {
|
|
536
|
-
argsStruct =
|
|
564
|
+
argsStruct = {};
|
|
537
565
|
}
|
|
566
|
+
// Gemini request/response wire uses `args` for functionCall payload.
|
|
538
567
|
const functionCall = { name, args: argsStruct };
|
|
539
568
|
const id = typeof tc.id === 'string' ? String(tc.id) : undefined;
|
|
540
569
|
if (id)
|
|
@@ -89,8 +89,9 @@ export class OpenAIOpenAIConversionCodec {
|
|
|
89
89
|
// Response-side filters (idempotent w.r.t existing logic)
|
|
90
90
|
engine.registerFilter(new ResponseToolTextCanonicalizeFilter()); // response_pre
|
|
91
91
|
try {
|
|
92
|
-
const { ResponseToolArgumentsToonDecodeFilter } = await import('../../filters/index.js');
|
|
92
|
+
const { ResponseToolArgumentsToonDecodeFilter, ResponseApplyPatchToonDecodeFilter } = await import('../../filters/index.js');
|
|
93
93
|
engine.registerFilter(new ResponseToolArgumentsToonDecodeFilter()); // response_pre, runs before stringify
|
|
94
|
+
engine.registerFilter(new ResponseApplyPatchToonDecodeFilter()); // response_pre, runs before stringify
|
|
94
95
|
}
|
|
95
96
|
catch { /* optional */ }
|
|
96
97
|
engine.registerFilter(new ResponseToolArgumentsStringifyFilter()); // response_post
|
|
@@ -147,11 +147,12 @@ export class ResponsesOpenAIConversionCodec {
|
|
|
147
147
|
debug: { emit: () => { } }
|
|
148
148
|
};
|
|
149
149
|
const engine = new FilterEngine();
|
|
150
|
-
// Response-side filters:文本标准化 → TOON decode(可选)→ shell/basics → finish_reason 不变式
|
|
150
|
+
// Response-side filters:文本标准化 → TOON decode(可选)→ apply_patch 结构化补丁规范化 → shell/basics → finish_reason 不变式
|
|
151
151
|
engine.registerFilter(new ResponseToolTextCanonicalizeFilter()); // response_pre
|
|
152
152
|
try {
|
|
153
|
-
const { ResponseToolArgumentsToonDecodeFilter } = await import('../../filters/index.js');
|
|
153
|
+
const { ResponseToolArgumentsToonDecodeFilter, ResponseApplyPatchToonDecodeFilter } = await import('../../filters/index.js');
|
|
154
154
|
engine.registerFilter(new ResponseToolArgumentsToonDecodeFilter()); // response_pre, runs before stringify
|
|
155
|
+
engine.registerFilter(new ResponseApplyPatchToonDecodeFilter()); // response_pre, runs before stringify
|
|
155
156
|
}
|
|
156
157
|
catch { /* optional */ }
|
|
157
158
|
engine.registerFilter(new ResponseToolArgumentsStringifyFilter()); // response_post
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
const isRecord = (value) => typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
2
|
+
function shouldDropInlineImagePart(part) {
|
|
3
|
+
const rawType = typeof part.type === 'string' ? part.type.toLowerCase() : '';
|
|
4
|
+
if (rawType !== 'image' && rawType !== 'image_url' && rawType !== 'input_image') {
|
|
5
|
+
return false;
|
|
6
|
+
}
|
|
7
|
+
const imageUrlBlock = isRecord(part.image_url)
|
|
8
|
+
? part.image_url
|
|
9
|
+
: part;
|
|
10
|
+
const urlRaw = typeof imageUrlBlock.url === 'string'
|
|
11
|
+
? imageUrlBlock.url
|
|
12
|
+
: typeof imageUrlBlock.data === 'string'
|
|
13
|
+
? imageUrlBlock.data
|
|
14
|
+
: '';
|
|
15
|
+
const url = urlRaw.trim();
|
|
16
|
+
if (!url) {
|
|
17
|
+
return false;
|
|
18
|
+
}
|
|
19
|
+
// GLM 4.7 在历史消息中携带 data:image/base64 时会返回 1210,
|
|
20
|
+
// 因此仅在历史中丢弃这类 inline image 片段。
|
|
21
|
+
return url.startsWith('data:image');
|
|
22
|
+
}
|
|
23
|
+
export function applyGlmHistoryImageTrim(payload) {
|
|
24
|
+
const root = structuredClone(payload);
|
|
25
|
+
const modelRaw = root.model;
|
|
26
|
+
const modelId = typeof modelRaw === 'string' ? modelRaw.trim().toLowerCase() : '';
|
|
27
|
+
if (!modelId || !modelId.startsWith('glm-4.7')) {
|
|
28
|
+
return root;
|
|
29
|
+
}
|
|
30
|
+
const messagesValue = root.messages;
|
|
31
|
+
if (!Array.isArray(messagesValue)) {
|
|
32
|
+
return root;
|
|
33
|
+
}
|
|
34
|
+
const messages = messagesValue.filter(msg => isRecord(msg));
|
|
35
|
+
if (!messages.length) {
|
|
36
|
+
return root;
|
|
37
|
+
}
|
|
38
|
+
// 仅在历史消息中进行裁剪:保留最后一条 user 完整内容。
|
|
39
|
+
let lastUserIdx = -1;
|
|
40
|
+
for (let i = messages.length - 1; i >= 0; i -= 1) {
|
|
41
|
+
const msg = messages[i];
|
|
42
|
+
const role = typeof msg.role === 'string' ? msg.role.toLowerCase() : '';
|
|
43
|
+
if (role === 'user') {
|
|
44
|
+
lastUserIdx = i;
|
|
45
|
+
break;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
if (lastUserIdx === -1) {
|
|
49
|
+
return root;
|
|
50
|
+
}
|
|
51
|
+
const nextMessages = [];
|
|
52
|
+
for (let i = 0; i < messages.length; i += 1) {
|
|
53
|
+
const msg = messages[i];
|
|
54
|
+
const role = typeof msg.role === 'string' ? msg.role.toLowerCase() : '';
|
|
55
|
+
if (i < lastUserIdx && role === 'user') {
|
|
56
|
+
const contentValue = msg.content;
|
|
57
|
+
if (!Array.isArray(contentValue)) {
|
|
58
|
+
nextMessages.push(msg);
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
const newContent = [];
|
|
62
|
+
for (const part of contentValue) {
|
|
63
|
+
if (!isRecord(part)) {
|
|
64
|
+
newContent.push(part);
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
if (shouldDropInlineImagePart(part)) {
|
|
68
|
+
// 丢弃历史中的 data:image/* 片段
|
|
69
|
+
// eslint-disable-next-line no-continue
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
newContent.push(part);
|
|
73
|
+
}
|
|
74
|
+
if (!newContent.length) {
|
|
75
|
+
// 历史消息只剩下 inline image 时,直接移除整条消息。
|
|
76
|
+
// 避免向 GLM 发送纯图片历史导致 1210。
|
|
77
|
+
// eslint-disable-next-line no-continue
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
const cloned = { ...msg, content: newContent };
|
|
81
|
+
nextMessages.push(cloned);
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
nextMessages.push(msg);
|
|
85
|
+
}
|
|
86
|
+
root.messages = nextMessages;
|
|
87
|
+
return root;
|
|
88
|
+
}
|
|
@@ -1,10 +1,15 @@
|
|
|
1
1
|
import { Readable } from 'node:stream';
|
|
2
2
|
import type { StandardizedRequest, ProcessedRequest } from '../types/standardized.js';
|
|
3
3
|
import type { JsonObject } from '../types/json.js';
|
|
4
|
-
import type { VirtualRouterConfig, RoutingDecision, RoutingDiagnostics, TargetMetadata } from '../../../router/virtual-router/types.js';
|
|
4
|
+
import type { VirtualRouterConfig, RoutingDecision, RoutingDiagnostics, TargetMetadata, VirtualRouterHealthStore } from '../../../router/virtual-router/types.js';
|
|
5
5
|
import { type HubProcessNodeResult } from '../process/chat-process.js';
|
|
6
6
|
export interface HubPipelineConfig {
|
|
7
7
|
virtualRouter: VirtualRouterConfig;
|
|
8
|
+
/**
|
|
9
|
+
* 可选:供 VirtualRouterEngine 使用的健康状态持久化存储。
|
|
10
|
+
* 当提供时,VirtualRouterEngine 将在初始化时恢复上一次快照,并在 cooldown/熔断变化时调用 persistSnapshot。
|
|
11
|
+
*/
|
|
12
|
+
healthStore?: VirtualRouterHealthStore;
|
|
8
13
|
}
|
|
9
14
|
export interface HubPipelineRequestMetadata extends Record<string, unknown> {
|
|
10
15
|
entryEndpoint?: string;
|
|
@@ -28,7 +28,9 @@ export class HubPipeline {
|
|
|
28
28
|
unsubscribeProviderErrors;
|
|
29
29
|
constructor(config) {
|
|
30
30
|
this.config = config;
|
|
31
|
-
this.routerEngine = new VirtualRouterEngine(
|
|
31
|
+
this.routerEngine = new VirtualRouterEngine({
|
|
32
|
+
healthStore: config.healthStore
|
|
33
|
+
});
|
|
32
34
|
this.routerEngine.initialize(config.virtualRouter);
|
|
33
35
|
try {
|
|
34
36
|
this.unsubscribeProviderErrors = providerErrorCenter.subscribe((event) => {
|
|
@@ -242,19 +244,22 @@ export class HubPipeline {
|
|
|
242
244
|
}
|
|
243
245
|
// 为响应侧 servertool/web_search 提供一次性 Chat 请求快照,便于在 Hub 内部实现
|
|
244
246
|
// 第三跳(将工具结果注入消息历史后重新调用主模型)。
|
|
247
|
+
//
|
|
248
|
+
// 注意:这里不再根据 processMode(passthrough/chat) 做分支判断——即使某些
|
|
249
|
+
// route 将 processMode 标记为 passthrough,我们仍然需要保留一次规范化后的
|
|
250
|
+
// Chat 请求快照,供 stopMessage / gemini_empty_reply_continue 等被动触发型
|
|
251
|
+
// servertool 在响应阶段使用。
|
|
245
252
|
let capturedChatRequest;
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
capturedChatRequest = undefined;
|
|
257
|
-
}
|
|
253
|
+
try {
|
|
254
|
+
capturedChatRequest = JSON.parse(JSON.stringify({
|
|
255
|
+
model: workingRequest.model,
|
|
256
|
+
messages: workingRequest.messages,
|
|
257
|
+
tools: workingRequest.tools,
|
|
258
|
+
parameters: workingRequest.parameters
|
|
259
|
+
}));
|
|
260
|
+
}
|
|
261
|
+
catch {
|
|
262
|
+
capturedChatRequest = undefined;
|
|
258
263
|
}
|
|
259
264
|
const metadata = {
|
|
260
265
|
...normalized.metadata,
|
|
@@ -397,6 +402,13 @@ export class HubPipeline {
|
|
|
397
402
|
if (conversationId) {
|
|
398
403
|
adapterContext.conversationId = conversationId;
|
|
399
404
|
}
|
|
405
|
+
const responsesResume = metadata.responsesResume &&
|
|
406
|
+
typeof metadata.responsesResume === 'object'
|
|
407
|
+
? metadata.responsesResume
|
|
408
|
+
: undefined;
|
|
409
|
+
if (responsesResume) {
|
|
410
|
+
adapterContext.responsesResume = responsesResume;
|
|
411
|
+
}
|
|
400
412
|
if (target?.compatibilityProfile && typeof target.compatibilityProfile === 'string') {
|
|
401
413
|
adapterContext.compatibilityProfile = target.compatibilityProfile;
|
|
402
414
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { runChatRequestToolFilters } from '../../shared/tool-filter-pipeline.js';
|
|
2
2
|
import { ToolGovernanceEngine } from '../tool-governance/index.js';
|
|
3
3
|
import { detectLastAssistantToolCategory } from '../../../router/virtual-router/tool-signals.js';
|
|
4
|
+
import { ensureApplyPatchSchema } from '../../shared/tool-mapping.js';
|
|
4
5
|
const toolGovernanceEngine = new ToolGovernanceEngine();
|
|
5
6
|
export async function runHubChatProcess(options) {
|
|
6
7
|
const startTime = Date.now();
|
|
@@ -72,6 +73,12 @@ async function applyRequestToolGovernance(request, context) {
|
|
|
72
73
|
governanceTimestamp: Date.now()
|
|
73
74
|
}
|
|
74
75
|
};
|
|
76
|
+
// 清理历史图片:仅保留「最新一条 user 消息」中的图片分段,
|
|
77
|
+
// 避免历史对话中的图片在后续多轮工具 / 普通对话中继续作为多模态负载发给不支持图片的模型。
|
|
78
|
+
merged = {
|
|
79
|
+
...merged,
|
|
80
|
+
messages: stripHistoricalImageAttachments(merged.messages)
|
|
81
|
+
};
|
|
75
82
|
if (containsImageAttachment(merged.messages)) {
|
|
76
83
|
if (!merged.metadata) {
|
|
77
84
|
merged.metadata = {
|
|
@@ -205,6 +212,63 @@ function castSingleTool(tool) {
|
|
|
205
212
|
}
|
|
206
213
|
};
|
|
207
214
|
}
|
|
215
|
+
function stripHistoricalImageAttachments(messages) {
|
|
216
|
+
if (!Array.isArray(messages) || !messages.length) {
|
|
217
|
+
return messages;
|
|
218
|
+
}
|
|
219
|
+
// 找到最新一条 user 消息,仅允许该消息保留图片分段;
|
|
220
|
+
// 更早的 user 消息中若存在图片,则移除其 image* 分段,保留纯文本与非图片内容。
|
|
221
|
+
let latestUserIndex = -1;
|
|
222
|
+
for (let idx = messages.length - 1; idx >= 0; idx -= 1) {
|
|
223
|
+
const candidate = messages[idx];
|
|
224
|
+
if (candidate && typeof candidate === 'object' && candidate.role === 'user') {
|
|
225
|
+
latestUserIndex = idx;
|
|
226
|
+
break;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
if (latestUserIndex < 0) {
|
|
230
|
+
return messages;
|
|
231
|
+
}
|
|
232
|
+
let changed = false;
|
|
233
|
+
const next = messages.slice();
|
|
234
|
+
for (let idx = 0; idx < messages.length; idx += 1) {
|
|
235
|
+
if (idx === latestUserIndex) {
|
|
236
|
+
continue;
|
|
237
|
+
}
|
|
238
|
+
const message = messages[idx];
|
|
239
|
+
if (!message || typeof message !== 'object') {
|
|
240
|
+
continue;
|
|
241
|
+
}
|
|
242
|
+
if (message.role !== 'user') {
|
|
243
|
+
continue;
|
|
244
|
+
}
|
|
245
|
+
const content = message.content;
|
|
246
|
+
if (!Array.isArray(content) || !content.length) {
|
|
247
|
+
continue;
|
|
248
|
+
}
|
|
249
|
+
const filtered = [];
|
|
250
|
+
let removed = false;
|
|
251
|
+
for (const part of content) {
|
|
252
|
+
if (part && typeof part === 'object' && !Array.isArray(part)) {
|
|
253
|
+
const typeValue = part.type;
|
|
254
|
+
if (typeof typeValue === 'string' && typeValue.toLowerCase().includes('image')) {
|
|
255
|
+
removed = true;
|
|
256
|
+
continue;
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
filtered.push(part);
|
|
260
|
+
}
|
|
261
|
+
if (removed) {
|
|
262
|
+
const cloned = {
|
|
263
|
+
...message,
|
|
264
|
+
content: filtered
|
|
265
|
+
};
|
|
266
|
+
next[idx] = cloned;
|
|
267
|
+
changed = true;
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
return changed ? next : messages;
|
|
271
|
+
}
|
|
208
272
|
function containsImageAttachment(messages) {
|
|
209
273
|
if (!Array.isArray(messages) || !messages.length) {
|
|
210
274
|
return false;
|
|
@@ -278,17 +342,7 @@ function castCustomTool(tool) {
|
|
|
278
342
|
function: {
|
|
279
343
|
name: 'apply_patch',
|
|
280
344
|
description,
|
|
281
|
-
parameters:
|
|
282
|
-
type: 'object',
|
|
283
|
-
properties: {
|
|
284
|
-
patch: {
|
|
285
|
-
type: 'string',
|
|
286
|
-
description: 'Unified diff patch content (FREEFORM, not JSON)'
|
|
287
|
-
}
|
|
288
|
-
},
|
|
289
|
-
required: ['patch'],
|
|
290
|
-
additionalProperties: false
|
|
291
|
-
},
|
|
345
|
+
parameters: ensureApplyPatchSchema(),
|
|
292
346
|
strict: true
|
|
293
347
|
}
|
|
294
348
|
};
|
|
@@ -286,6 +286,14 @@ function appendChatContentToGeminiParts(message, targetParts) {
|
|
|
286
286
|
function buildGeminiRequestFromChat(chat, metadata) {
|
|
287
287
|
const contents = [];
|
|
288
288
|
const emittedToolOutputs = new Set();
|
|
289
|
+
const adapterContext = metadata?.context;
|
|
290
|
+
const rawProviderId = adapterContext?.providerId;
|
|
291
|
+
const normalizedProviderId = typeof rawProviderId === 'string' ? rawProviderId.toLowerCase() : '';
|
|
292
|
+
const providerIdPrefix = normalizedProviderId.split('.')[0];
|
|
293
|
+
// 保持对通用 gemini-cli 的保护(避免上游直接执行 functionCall),
|
|
294
|
+
// 但对于 antigravity.* 明确允许通过 Gemini functionCall 协议执行工具,
|
|
295
|
+
// 以便完整打通 tools → functionCall → functionResponse 链路。
|
|
296
|
+
const omitFunctionCallPartsForCli = providerIdPrefix === 'gemini-cli';
|
|
289
297
|
for (const message of chat.messages) {
|
|
290
298
|
if (!message || typeof message !== 'object')
|
|
291
299
|
continue;
|
|
@@ -304,10 +312,15 @@ function buildGeminiRequestFromChat(chat, metadata) {
|
|
|
304
312
|
parts: []
|
|
305
313
|
};
|
|
306
314
|
appendChatContentToGeminiParts(message, entry.parts);
|
|
307
|
-
const toolCalls = Array.isArray(message.tool_calls)
|
|
315
|
+
const toolCalls = Array.isArray(message.tool_calls)
|
|
316
|
+
? message.tool_calls
|
|
317
|
+
: [];
|
|
308
318
|
for (const tc of toolCalls) {
|
|
309
319
|
if (!tc || typeof tc !== 'object')
|
|
310
320
|
continue;
|
|
321
|
+
if (omitFunctionCallPartsForCli) {
|
|
322
|
+
continue;
|
|
323
|
+
}
|
|
311
324
|
const fn = tc.function || {};
|
|
312
325
|
const name = typeof fn.name === 'string' ? fn.name : undefined;
|
|
313
326
|
if (!name)
|
|
@@ -324,7 +337,8 @@ function buildGeminiRequestFromChat(chat, metadata) {
|
|
|
324
337
|
else {
|
|
325
338
|
argsStruct = fn.arguments ?? {};
|
|
326
339
|
}
|
|
327
|
-
const
|
|
340
|
+
const functionCall = { name, args: cloneAsJsonValue(argsStruct) };
|
|
341
|
+
const part = { functionCall };
|
|
328
342
|
if (typeof tc.id === 'string') {
|
|
329
343
|
part.functionCall.id = tc.id;
|
|
330
344
|
}
|
|
@@ -409,7 +423,6 @@ function buildGeminiRequestFromChat(chat, metadata) {
|
|
|
409
423
|
}
|
|
410
424
|
// Apply claude-thinking compat directly at Gemini mapping time to ensure it is always active
|
|
411
425
|
// for antigravity.*.claude-sonnet-4-5-thinking, regardless of compatibilityProfile wiring.
|
|
412
|
-
const adapterContext = metadata?.context;
|
|
413
426
|
const compatRequest = applyClaudeThinkingToolSchemaCompat(request, adapterContext);
|
|
414
427
|
return compatRequest;
|
|
415
428
|
}
|
|
@@ -53,6 +53,41 @@ function mapToolOutputs(entries, missing) {
|
|
|
53
53
|
});
|
|
54
54
|
return outputs.length ? outputs : undefined;
|
|
55
55
|
}
|
|
56
|
+
function deriveResumeToolOutputs(ctx) {
|
|
57
|
+
if (!ctx || typeof ctx !== 'object') {
|
|
58
|
+
return undefined;
|
|
59
|
+
}
|
|
60
|
+
const resume = ctx.responsesResume;
|
|
61
|
+
if (!resume || typeof resume !== 'object') {
|
|
62
|
+
return undefined;
|
|
63
|
+
}
|
|
64
|
+
const detailed = Array.isArray(resume.toolOutputsDetailed)
|
|
65
|
+
? resume.toolOutputsDetailed
|
|
66
|
+
: undefined;
|
|
67
|
+
if (!detailed || detailed.length === 0) {
|
|
68
|
+
return undefined;
|
|
69
|
+
}
|
|
70
|
+
const outputs = [];
|
|
71
|
+
detailed.forEach((entry, index) => {
|
|
72
|
+
if (!entry || typeof entry !== 'object') {
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
const callIdRaw = typeof entry.callId === 'string' && entry.callId.trim().length
|
|
76
|
+
? entry.callId.trim()
|
|
77
|
+
: typeof entry.originalId === 'string' && entry.originalId.trim().length
|
|
78
|
+
? entry.originalId.trim()
|
|
79
|
+
: `resume_tool_${index}`;
|
|
80
|
+
if (!callIdRaw) {
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
const outputText = typeof entry.outputText === 'string' ? entry.outputText : '';
|
|
84
|
+
outputs.push({
|
|
85
|
+
tool_call_id: callIdRaw,
|
|
86
|
+
content: outputText
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
return outputs.length ? outputs : undefined;
|
|
90
|
+
}
|
|
56
91
|
function collectParameters(payload, streamHint) {
|
|
57
92
|
const params = {};
|
|
58
93
|
for (const key of RESPONSES_PARAMETER_KEYS) {
|
|
@@ -169,16 +204,30 @@ export class ResponsesSemanticMapper {
|
|
|
169
204
|
const { request, toolsNormalized } = buildChatRequestFromResponses(payload, responsesContext);
|
|
170
205
|
const missingFields = [];
|
|
171
206
|
const messages = normalizeMessages(request.messages, missingFields);
|
|
172
|
-
|
|
207
|
+
let toolOutputs = mapToolOutputs(payload.tool_outputs, missingFields);
|
|
208
|
+
if (!toolOutputs || toolOutputs.length === 0) {
|
|
209
|
+
const resumeToolOutputs = deriveResumeToolOutputs(ctx);
|
|
210
|
+
if (resumeToolOutputs && resumeToolOutputs.length) {
|
|
211
|
+
toolOutputs = resumeToolOutputs;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
173
214
|
const parameters = collectParameters(payload, responsesContext.stream);
|
|
174
215
|
const metadata = { context: ctx };
|
|
175
216
|
try {
|
|
176
217
|
const bridgePolicy = resolveBridgePolicy({ protocol: 'openai-responses', moduleType: 'openai-responses' });
|
|
177
218
|
const actions = resolvePolicyActions(bridgePolicy, 'request_inbound');
|
|
178
219
|
if (actions?.length) {
|
|
220
|
+
const capturedToolResults = Array.isArray(toolOutputs)
|
|
221
|
+
? toolOutputs.map((entry) => ({
|
|
222
|
+
tool_call_id: entry.tool_call_id,
|
|
223
|
+
output: entry.content,
|
|
224
|
+
name: entry.name
|
|
225
|
+
}))
|
|
226
|
+
: undefined;
|
|
179
227
|
const actionState = createBridgeActionState({
|
|
180
228
|
rawRequest: payload,
|
|
181
|
-
metadata: metadata
|
|
229
|
+
metadata: metadata,
|
|
230
|
+
capturedToolResults
|
|
182
231
|
});
|
|
183
232
|
runBridgeActionPipeline({
|
|
184
233
|
stage: 'request_inbound',
|
|
@@ -647,6 +647,56 @@ function createAnthropicToolNameResolver(source) {
|
|
|
647
647
|
return lookup.get(trimmed) ?? lookup.get(trimmed.toLowerCase()) ?? trimmed;
|
|
648
648
|
};
|
|
649
649
|
}
|
|
650
|
+
function normalizeAnthropicToolChoice(value) {
|
|
651
|
+
if (value === undefined || value === null) {
|
|
652
|
+
return undefined;
|
|
653
|
+
}
|
|
654
|
+
if (isPlainRecord(value)) {
|
|
655
|
+
// Already an object – best-effort clone while trimming type, and also support
|
|
656
|
+
// Chat-style { type: 'function', function: { name } } by mapping to Anthropic's
|
|
657
|
+
// { type: 'tool', name } shape.
|
|
658
|
+
const cloned = cloneAnthropicSchema(value);
|
|
659
|
+
const rawType = typeof cloned.type === 'string' ? String(cloned.type).trim() : '';
|
|
660
|
+
if (rawType) {
|
|
661
|
+
cloned.type = rawType;
|
|
662
|
+
return cloned;
|
|
663
|
+
}
|
|
664
|
+
const selectorType = typeof cloned.type === 'string' ? String(cloned.type).trim() : '';
|
|
665
|
+
const fn = cloned.function;
|
|
666
|
+
if (selectorType === 'function' &&
|
|
667
|
+
fn &&
|
|
668
|
+
typeof fn === 'object' &&
|
|
669
|
+
typeof fn.name === 'string' &&
|
|
670
|
+
String(fn.name).trim().length) {
|
|
671
|
+
return { type: 'tool', name: String(fn.name).trim() };
|
|
672
|
+
}
|
|
673
|
+
return cloned;
|
|
674
|
+
}
|
|
675
|
+
if (typeof value === 'string') {
|
|
676
|
+
const trimmed = value.trim();
|
|
677
|
+
if (!trimmed.length) {
|
|
678
|
+
return undefined;
|
|
679
|
+
}
|
|
680
|
+
const lower = trimmed.toLowerCase();
|
|
681
|
+
if (lower === 'auto') {
|
|
682
|
+
return { type: 'auto' };
|
|
683
|
+
}
|
|
684
|
+
if (lower === 'none') {
|
|
685
|
+
return { type: 'none' };
|
|
686
|
+
}
|
|
687
|
+
if (lower === 'any') {
|
|
688
|
+
return { type: 'any' };
|
|
689
|
+
}
|
|
690
|
+
if (lower === 'required') {
|
|
691
|
+
// "required" in canonical Chat roughly maps to Anthropic's "any" semantics:
|
|
692
|
+
// the model must choose some tool if available.
|
|
693
|
+
return { type: 'any' };
|
|
694
|
+
}
|
|
695
|
+
// Fallback: preserve custom mode as-is in type field.
|
|
696
|
+
return { type: trimmed };
|
|
697
|
+
}
|
|
698
|
+
return undefined;
|
|
699
|
+
}
|
|
650
700
|
export function buildAnthropicRequestFromOpenAIChat(chatReq) {
|
|
651
701
|
const requestBody = isObject(chatReq) ? chatReq : {};
|
|
652
702
|
const model = String(requestBody?.model || 'unknown');
|
|
@@ -882,6 +932,10 @@ export function buildAnthropicRequestFromOpenAIChat(chatReq) {
|
|
|
882
932
|
if (anthropicTools !== undefined) {
|
|
883
933
|
out.tools = anthropicTools;
|
|
884
934
|
}
|
|
935
|
+
const normalizedToolChoice = normalizeAnthropicToolChoice(requestBody.tool_choice);
|
|
936
|
+
if (normalizedToolChoice !== undefined) {
|
|
937
|
+
out.tool_choice = normalizedToolChoice;
|
|
938
|
+
}
|
|
885
939
|
try {
|
|
886
940
|
if (requestBody.metadata && typeof requestBody.metadata === 'object') {
|
|
887
941
|
out.metadata = JSON.parse(JSON.stringify(requestBody.metadata));
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
|
|
1
|
+
import { buildShellDescription, hasApplyPatchToolDeclared, isShellToolName } from '../../tools/tool-description-utils.js';
|
|
2
2
|
function isObject(v) {
|
|
3
3
|
return !!v && typeof v === 'object' && !Array.isArray(v);
|
|
4
4
|
}
|
|
@@ -155,6 +155,7 @@ export function normalizeTools(tools) {
|
|
|
155
155
|
if (!Array.isArray(tools))
|
|
156
156
|
return [];
|
|
157
157
|
const out = [];
|
|
158
|
+
const applyPatchAvailable = hasApplyPatchToolDeclared(tools);
|
|
158
159
|
for (const t of tools) {
|
|
159
160
|
if (!t || typeof t !== 'object')
|
|
160
161
|
continue;
|
|
@@ -175,7 +176,7 @@ export function normalizeTools(tools) {
|
|
|
175
176
|
}
|
|
176
177
|
// Enforce schema for known tools with minimal, compatible constraints
|
|
177
178
|
let finalParams;
|
|
178
|
-
if (
|
|
179
|
+
if (isShellToolName(name)) {
|
|
179
180
|
// Do NOT downgrade an existing schema; prefer string command, allow argv array as fallback
|
|
180
181
|
const base = isObject(params) ? params : {};
|
|
181
182
|
const props = isObject(base.properties) ? base.properties : {};
|
|
@@ -205,7 +206,14 @@ export function normalizeTools(tools) {
|
|
|
205
206
|
else {
|
|
206
207
|
finalParams = { type: 'object', properties: {}, additionalProperties: true };
|
|
207
208
|
}
|
|
208
|
-
const
|
|
209
|
+
const functionNode = { name, ...(desc ? { description: desc } : {}), parameters: finalParams };
|
|
210
|
+
if (isShellToolName(name)) {
|
|
211
|
+
const display = (typeof name === 'string' && name.trim().length > 0)
|
|
212
|
+
? name.trim()
|
|
213
|
+
: ((typeof topName === 'string' && topName.trim().length > 0) ? topName.trim() : 'shell');
|
|
214
|
+
functionNode.description = buildShellDescription(display, applyPatchAvailable);
|
|
215
|
+
}
|
|
216
|
+
const norm = { type: 'function', function: functionNode };
|
|
209
217
|
if (norm.function?.name)
|
|
210
218
|
out.push(norm);
|
|
211
219
|
}
|