@jsonstudio/llms 0.6.567 → 0.6.586
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 -2
- package/dist/conversion/hub/pipeline/hub-pipeline.js +72 -81
- package/dist/conversion/hub/pipeline/stages/resp_outbound/resp_outbound_stage1_client_remap/index.js +0 -34
- package/dist/conversion/hub/process/chat-process.js +68 -24
- package/dist/conversion/hub/response/provider-response.js +0 -8
- package/dist/conversion/hub/semantic-mappers/gemini-mapper.js +22 -3
- package/dist/conversion/hub/semantic-mappers/responses-mapper.js +267 -14
- package/dist/conversion/hub/types/chat-envelope.d.ts +1 -0
- package/dist/conversion/responses/responses-openai-bridge.d.ts +3 -2
- package/dist/conversion/responses/responses-openai-bridge.js +1 -13
- 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.d.ts +20 -0
- package/dist/conversion/shared/text-markup-normalizer.js +118 -31
- package/dist/conversion/shared/tool-filter-pipeline.js +56 -30
- 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 +117 -0
- package/dist/filters/special/response-tool-arguments-toon-decode.d.ts +10 -0
- package/dist/filters/special/response-tool-arguments-toon-decode.js +154 -26
- package/dist/guidance/index.js +71 -42
- package/dist/router/virtual-router/bootstrap.js +10 -5
- package/dist/router/virtual-router/classifier.js +16 -7
- 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 +184 -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.d.ts +2 -1
- package/dist/router/virtual-router/tool-signals.js +324 -119
- package/dist/router/virtual-router/types.d.ts +31 -1
- package/dist/router/virtual-router/types.js +2 -2
- 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/json-to-sse/event-generators/responses.js +9 -2
- package/dist/sse/sse-to-json/builders/anthropic-response-builder.js +23 -3
- package/dist/tools/apply-patch-structured.d.ts +20 -0
- package/dist/tools/apply-patch-structured.js +240 -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 +1 -1
|
@@ -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;
|
|
@@ -56,7 +61,6 @@ export declare class HubPipeline {
|
|
|
56
61
|
private buildAdapterContext;
|
|
57
62
|
private maybeCreateStageRecorder;
|
|
58
63
|
private asJsonObject;
|
|
59
|
-
private pickRawRequestBody;
|
|
60
64
|
private normalizeRequest;
|
|
61
65
|
private convertProcessNodeResult;
|
|
62
66
|
private materializePayload;
|
|
@@ -22,13 +22,16 @@ import { runReqOutboundStage1SemanticMap } from './stages/req_outbound/req_outbo
|
|
|
22
22
|
import { runReqOutboundStage2FormatBuild } from './stages/req_outbound/req_outbound_stage2_format_build/index.js';
|
|
23
23
|
import { runReqOutboundStage3Compat } from './stages/req_outbound/req_outbound_stage3_compat/index.js';
|
|
24
24
|
import { extractSessionIdentifiersFromMetadata } from './session-identifiers.js';
|
|
25
|
+
import { computeRequestTokens } from '../../../router/virtual-router/token-estimator.js';
|
|
25
26
|
export class HubPipeline {
|
|
26
27
|
routerEngine;
|
|
27
28
|
config;
|
|
28
29
|
unsubscribeProviderErrors;
|
|
29
30
|
constructor(config) {
|
|
30
31
|
this.config = config;
|
|
31
|
-
this.routerEngine = new VirtualRouterEngine(
|
|
32
|
+
this.routerEngine = new VirtualRouterEngine({
|
|
33
|
+
healthStore: config.healthStore
|
|
34
|
+
});
|
|
32
35
|
this.routerEngine.initialize(config.virtualRouter);
|
|
33
36
|
try {
|
|
34
37
|
this.unsubscribeProviderErrors = providerErrorCenter.subscribe((event) => {
|
|
@@ -117,6 +120,18 @@ export class HubPipeline {
|
|
|
117
120
|
}
|
|
118
121
|
}
|
|
119
122
|
const workingRequest = processedRequest ?? standardizedRequest;
|
|
123
|
+
// 使用与 VirtualRouter 一致的 tiktoken 计数逻辑,对标准化请求进行一次
|
|
124
|
+
// 上下文 token 估算,供后续 usage 归一化与统计使用。
|
|
125
|
+
try {
|
|
126
|
+
const estimatedTokens = computeRequestTokens(workingRequest, '');
|
|
127
|
+
if (typeof estimatedTokens === 'number' && Number.isFinite(estimatedTokens) && estimatedTokens > 0) {
|
|
128
|
+
normalized.metadata = normalized.metadata || {};
|
|
129
|
+
normalized.metadata.estimatedInputTokens = estimatedTokens;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
catch {
|
|
133
|
+
// 估算失败不应影响主流程
|
|
134
|
+
}
|
|
120
135
|
const normalizedMeta = normalized.metadata;
|
|
121
136
|
const responsesResume = normalizedMeta && typeof normalizedMeta.responsesResume === 'object'
|
|
122
137
|
? normalizedMeta.responsesResume
|
|
@@ -180,81 +195,60 @@ export class HubPipeline {
|
|
|
180
195
|
const outboundEndpoint = resolveEndpointForProviderProtocol(outboundAdapterContext.providerProtocol);
|
|
181
196
|
const outboundRecorder = this.maybeCreateStageRecorder(outboundAdapterContext, outboundEndpoint);
|
|
182
197
|
const outboundStart = Date.now();
|
|
183
|
-
const isResponsesSubmit = normalized.entryEndpoint === '/v1/responses.submit_tool_outputs' &&
|
|
184
|
-
outboundProtocol === 'openai-responses';
|
|
185
198
|
let providerPayload;
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
formatAdapter: outboundFormatAdapter,
|
|
219
|
-
stageRecorder: outboundRecorder
|
|
220
|
-
});
|
|
221
|
-
formattedPayload = await runReqOutboundStage3Compat({
|
|
222
|
-
payload: formattedPayload,
|
|
223
|
-
adapterContext: outboundAdapterContext,
|
|
224
|
-
stageRecorder: outboundRecorder
|
|
225
|
-
});
|
|
226
|
-
providerPayload = formattedPayload;
|
|
227
|
-
const outboundEnd = Date.now();
|
|
228
|
-
nodeResults.push({
|
|
229
|
-
id: 'req_outbound',
|
|
230
|
-
success: true,
|
|
231
|
-
metadata: {
|
|
232
|
-
node: 'req_outbound',
|
|
233
|
-
executionTime: outboundEnd - outboundStart,
|
|
234
|
-
startTime: outboundStart,
|
|
235
|
-
endTime: outboundEnd,
|
|
236
|
-
dataProcessed: {
|
|
237
|
-
messages: workingRequest.messages.length,
|
|
238
|
-
tools: workingRequest.tools?.length ?? 0
|
|
239
|
-
}
|
|
199
|
+
const outboundStage1 = await runReqOutboundStage1SemanticMap({
|
|
200
|
+
request: workingRequest,
|
|
201
|
+
adapterContext: outboundAdapterContext,
|
|
202
|
+
semanticMapper: outboundSemanticMapper,
|
|
203
|
+
contextSnapshot: outboundContextSnapshot,
|
|
204
|
+
contextMetadataKey: outboundContextMetadataKey,
|
|
205
|
+
stageRecorder: outboundRecorder
|
|
206
|
+
});
|
|
207
|
+
let formattedPayload = await runReqOutboundStage2FormatBuild({
|
|
208
|
+
formatEnvelope: outboundStage1.formatEnvelope,
|
|
209
|
+
adapterContext: outboundAdapterContext,
|
|
210
|
+
formatAdapter: outboundFormatAdapter,
|
|
211
|
+
stageRecorder: outboundRecorder
|
|
212
|
+
});
|
|
213
|
+
formattedPayload = await runReqOutboundStage3Compat({
|
|
214
|
+
payload: formattedPayload,
|
|
215
|
+
adapterContext: outboundAdapterContext,
|
|
216
|
+
stageRecorder: outboundRecorder
|
|
217
|
+
});
|
|
218
|
+
providerPayload = formattedPayload;
|
|
219
|
+
const outboundEnd = Date.now();
|
|
220
|
+
nodeResults.push({
|
|
221
|
+
id: 'req_outbound',
|
|
222
|
+
success: true,
|
|
223
|
+
metadata: {
|
|
224
|
+
node: 'req_outbound',
|
|
225
|
+
executionTime: outboundEnd - outboundStart,
|
|
226
|
+
startTime: outboundStart,
|
|
227
|
+
endTime: outboundEnd,
|
|
228
|
+
dataProcessed: {
|
|
229
|
+
messages: workingRequest.messages.length,
|
|
230
|
+
tools: workingRequest.tools?.length ?? 0
|
|
240
231
|
}
|
|
241
|
-
}
|
|
242
|
-
}
|
|
232
|
+
}
|
|
233
|
+
});
|
|
243
234
|
// 为响应侧 servertool/web_search 提供一次性 Chat 请求快照,便于在 Hub 内部实现
|
|
244
235
|
// 第三跳(将工具结果注入消息历史后重新调用主模型)。
|
|
236
|
+
//
|
|
237
|
+
// 注意:这里不再根据 processMode(passthrough/chat) 做分支判断——即使某些
|
|
238
|
+
// route 将 processMode 标记为 passthrough,我们仍然需要保留一次规范化后的
|
|
239
|
+
// Chat 请求快照,供 stopMessage / gemini_empty_reply_continue 等被动触发型
|
|
240
|
+
// servertool 在响应阶段使用。
|
|
245
241
|
let capturedChatRequest;
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
capturedChatRequest = undefined;
|
|
257
|
-
}
|
|
242
|
+
try {
|
|
243
|
+
capturedChatRequest = JSON.parse(JSON.stringify({
|
|
244
|
+
model: workingRequest.model,
|
|
245
|
+
messages: workingRequest.messages,
|
|
246
|
+
tools: workingRequest.tools,
|
|
247
|
+
parameters: workingRequest.parameters
|
|
248
|
+
}));
|
|
249
|
+
}
|
|
250
|
+
catch {
|
|
251
|
+
capturedChatRequest = undefined;
|
|
258
252
|
}
|
|
259
253
|
const metadata = {
|
|
260
254
|
...normalized.metadata,
|
|
@@ -397,6 +391,13 @@ export class HubPipeline {
|
|
|
397
391
|
if (conversationId) {
|
|
398
392
|
adapterContext.conversationId = conversationId;
|
|
399
393
|
}
|
|
394
|
+
const responsesResume = metadata.responsesResume &&
|
|
395
|
+
typeof metadata.responsesResume === 'object'
|
|
396
|
+
? metadata.responsesResume
|
|
397
|
+
: undefined;
|
|
398
|
+
if (responsesResume) {
|
|
399
|
+
adapterContext.responsesResume = responsesResume;
|
|
400
|
+
}
|
|
400
401
|
if (target?.compatibilityProfile && typeof target.compatibilityProfile === 'string') {
|
|
401
402
|
adapterContext.compatibilityProfile = target.compatibilityProfile;
|
|
402
403
|
}
|
|
@@ -420,16 +421,6 @@ export class HubPipeline {
|
|
|
420
421
|
}
|
|
421
422
|
return value;
|
|
422
423
|
}
|
|
423
|
-
pickRawRequestBody(metadata) {
|
|
424
|
-
if (!metadata || typeof metadata !== 'object') {
|
|
425
|
-
return undefined;
|
|
426
|
-
}
|
|
427
|
-
const raw = metadata.__raw_request_body;
|
|
428
|
-
if (!raw || typeof raw !== 'object') {
|
|
429
|
-
return undefined;
|
|
430
|
-
}
|
|
431
|
-
return raw;
|
|
432
|
-
}
|
|
433
424
|
async normalizeRequest(request) {
|
|
434
425
|
if (!request || typeof request !== 'object') {
|
|
435
426
|
throw new Error('HubPipeline requires request payload');
|
package/dist/conversion/hub/pipeline/stages/resp_outbound/resp_outbound_stage1_client_remap/index.js
CHANGED
|
@@ -15,7 +15,6 @@ export function runRespOutboundStage1ClientRemap(options) {
|
|
|
15
15
|
clientPayload = buildResponsesPayloadFromChat(options.payload, {
|
|
16
16
|
requestId: options.requestId
|
|
17
17
|
});
|
|
18
|
-
mergeOriginalResponsesPayload(clientPayload, options.adapterContext);
|
|
19
18
|
}
|
|
20
19
|
recordStage(options.stageRecorder, 'resp_outbound_stage1_client_remap', clientPayload);
|
|
21
20
|
return clientPayload;
|
|
@@ -42,36 +41,3 @@ function resolveAliasMapFromContext(adapterContext) {
|
|
|
42
41
|
}
|
|
43
42
|
return Object.keys(map).length ? map : undefined;
|
|
44
43
|
}
|
|
45
|
-
function mergeOriginalResponsesPayload(payload, adapterContext) {
|
|
46
|
-
if (!adapterContext) {
|
|
47
|
-
return;
|
|
48
|
-
}
|
|
49
|
-
const raw = adapterContext.__raw_responses_payload;
|
|
50
|
-
if (!raw || typeof raw !== 'object' || Array.isArray(raw)) {
|
|
51
|
-
return;
|
|
52
|
-
}
|
|
53
|
-
try {
|
|
54
|
-
if (payload.required_action == null && raw.required_action != null) {
|
|
55
|
-
payload.required_action = JSON.parse(JSON.stringify(raw.required_action));
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
catch {
|
|
59
|
-
/* ignore clone errors */
|
|
60
|
-
}
|
|
61
|
-
const rawStatus = typeof raw.status === 'string' ? raw.status : undefined;
|
|
62
|
-
if (rawStatus === 'requires_action') {
|
|
63
|
-
payload.status = 'requires_action';
|
|
64
|
-
}
|
|
65
|
-
// 如果桥接后的 payload 没有 usage,而原始 Responses 载荷带有 usage,则回填原始 usage,
|
|
66
|
-
// 确保 token usage 不在工具/桥接路径中丢失。
|
|
67
|
-
const payloadUsage = payload.usage;
|
|
68
|
-
const rawUsage = raw.usage;
|
|
69
|
-
if ((payloadUsage == null || typeof payloadUsage !== 'object') && rawUsage && typeof rawUsage === 'object') {
|
|
70
|
-
try {
|
|
71
|
-
payload.usage = JSON.parse(JSON.stringify(rawUsage));
|
|
72
|
-
}
|
|
73
|
-
catch {
|
|
74
|
-
payload.usage = rawUsage;
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { runChatRequestToolFilters } from '../../shared/tool-filter-pipeline.js';
|
|
2
2
|
import { ToolGovernanceEngine } from '../tool-governance/index.js';
|
|
3
|
-
import {
|
|
3
|
+
import { ensureApplyPatchSchema } from '../../shared/tool-mapping.js';
|
|
4
4
|
const toolGovernanceEngine = new ToolGovernanceEngine();
|
|
5
5
|
export async function runHubChatProcess(options) {
|
|
6
6
|
const startTime = Date.now();
|
|
@@ -72,6 +72,12 @@ async function applyRequestToolGovernance(request, context) {
|
|
|
72
72
|
governanceTimestamp: Date.now()
|
|
73
73
|
}
|
|
74
74
|
};
|
|
75
|
+
// 清理历史图片:仅保留「最新一条 user 消息」中的图片分段,
|
|
76
|
+
// 避免历史对话中的图片在后续多轮工具 / 普通对话中继续作为多模态负载发给不支持图片的模型。
|
|
77
|
+
merged = {
|
|
78
|
+
...merged,
|
|
79
|
+
messages: stripHistoricalImageAttachments(merged.messages)
|
|
80
|
+
};
|
|
75
81
|
if (containsImageAttachment(merged.messages)) {
|
|
76
82
|
if (!merged.metadata) {
|
|
77
83
|
merged.metadata = {
|
|
@@ -205,6 +211,63 @@ function castSingleTool(tool) {
|
|
|
205
211
|
}
|
|
206
212
|
};
|
|
207
213
|
}
|
|
214
|
+
function stripHistoricalImageAttachments(messages) {
|
|
215
|
+
if (!Array.isArray(messages) || !messages.length) {
|
|
216
|
+
return messages;
|
|
217
|
+
}
|
|
218
|
+
// 找到最新一条 user 消息,仅允许该消息保留图片分段;
|
|
219
|
+
// 更早的 user 消息中若存在图片,则移除其 image* 分段,保留纯文本与非图片内容。
|
|
220
|
+
let latestUserIndex = -1;
|
|
221
|
+
for (let idx = messages.length - 1; idx >= 0; idx -= 1) {
|
|
222
|
+
const candidate = messages[idx];
|
|
223
|
+
if (candidate && typeof candidate === 'object' && candidate.role === 'user') {
|
|
224
|
+
latestUserIndex = idx;
|
|
225
|
+
break;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
if (latestUserIndex < 0) {
|
|
229
|
+
return messages;
|
|
230
|
+
}
|
|
231
|
+
let changed = false;
|
|
232
|
+
const next = messages.slice();
|
|
233
|
+
for (let idx = 0; idx < messages.length; idx += 1) {
|
|
234
|
+
if (idx === latestUserIndex) {
|
|
235
|
+
continue;
|
|
236
|
+
}
|
|
237
|
+
const message = messages[idx];
|
|
238
|
+
if (!message || typeof message !== 'object') {
|
|
239
|
+
continue;
|
|
240
|
+
}
|
|
241
|
+
if (message.role !== 'user') {
|
|
242
|
+
continue;
|
|
243
|
+
}
|
|
244
|
+
const content = message.content;
|
|
245
|
+
if (!Array.isArray(content) || !content.length) {
|
|
246
|
+
continue;
|
|
247
|
+
}
|
|
248
|
+
const filtered = [];
|
|
249
|
+
let removed = false;
|
|
250
|
+
for (const part of content) {
|
|
251
|
+
if (part && typeof part === 'object' && !Array.isArray(part)) {
|
|
252
|
+
const typeValue = part.type;
|
|
253
|
+
if (typeof typeValue === 'string' && typeValue.toLowerCase().includes('image')) {
|
|
254
|
+
removed = true;
|
|
255
|
+
continue;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
filtered.push(part);
|
|
259
|
+
}
|
|
260
|
+
if (removed) {
|
|
261
|
+
const cloned = {
|
|
262
|
+
...message,
|
|
263
|
+
content: filtered
|
|
264
|
+
};
|
|
265
|
+
next[idx] = cloned;
|
|
266
|
+
changed = true;
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
return changed ? next : messages;
|
|
270
|
+
}
|
|
208
271
|
function containsImageAttachment(messages) {
|
|
209
272
|
if (!Array.isArray(messages) || !messages.length) {
|
|
210
273
|
return false;
|
|
@@ -278,17 +341,7 @@ function castCustomTool(tool) {
|
|
|
278
341
|
function: {
|
|
279
342
|
name: 'apply_patch',
|
|
280
343
|
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
|
-
},
|
|
344
|
+
parameters: ensureApplyPatchSchema(),
|
|
292
345
|
strict: true
|
|
293
346
|
}
|
|
294
347
|
};
|
|
@@ -355,19 +408,10 @@ function maybeInjectWebSearchTool(request, metadata) {
|
|
|
355
408
|
: 'selective';
|
|
356
409
|
const intent = detectWebSearchIntent(request);
|
|
357
410
|
if (injectPolicy === 'selective') {
|
|
411
|
+
// 仅当当前这一轮用户输入明确表达“联网搜索”意图时才注入 web_search。
|
|
412
|
+
// 不再依赖上一轮工具分类(read/search/websearch),避免形成隐式续写语义。
|
|
358
413
|
if (!intent.hasIntent) {
|
|
359
|
-
|
|
360
|
-
// 如果上一轮 assistant 的工具调用已经属于搜索类(如 web_search),
|
|
361
|
-
// 则仍然视为 web_search 续写场景,强制注入 web_search 工具,
|
|
362
|
-
// 以便在后续路由中按 servertool 逻辑跳过不适配的 Provider(例如 serverToolsDisabled 的 crs)。
|
|
363
|
-
const assistantMessages = Array.isArray(request.messages)
|
|
364
|
-
? request.messages.filter((msg) => msg && msg.role === 'assistant')
|
|
365
|
-
: [];
|
|
366
|
-
const lastTool = detectLastAssistantToolCategory(assistantMessages);
|
|
367
|
-
const hasSearchToolContext = lastTool?.category === 'search';
|
|
368
|
-
if (!hasSearchToolContext) {
|
|
369
|
-
return request;
|
|
370
|
-
}
|
|
414
|
+
return request;
|
|
371
415
|
}
|
|
372
416
|
}
|
|
373
417
|
const existingTools = Array.isArray(request.tools) ? request.tools : [];
|
|
@@ -130,14 +130,6 @@ export async function convertProviderResponse(options) {
|
|
|
130
130
|
catch {
|
|
131
131
|
// ignore conversation capture errors
|
|
132
132
|
}
|
|
133
|
-
if (formatEnvelope.payload && typeof formatEnvelope.payload === 'object') {
|
|
134
|
-
try {
|
|
135
|
-
options.context.__raw_responses_payload = JSON.parse(JSON.stringify(formatEnvelope.payload));
|
|
136
|
-
}
|
|
137
|
-
catch {
|
|
138
|
-
/* best-effort clone */
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
133
|
}
|
|
142
134
|
formatEnvelope.payload = runRespInboundStageCompatResponse({
|
|
143
135
|
payload: formatEnvelope.payload,
|
|
@@ -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,14 @@ function buildGeminiRequestFromChat(chat, metadata) {
|
|
|
324
337
|
else {
|
|
325
338
|
argsStruct = fn.arguments ?? {};
|
|
326
339
|
}
|
|
327
|
-
|
|
340
|
+
let argsJson = cloneAsJsonValue(argsStruct);
|
|
341
|
+
// Gemini / Antigravity 期望 functionCall.args 为对象(Struct),
|
|
342
|
+
// 若顶层为数组或原始类型,则包装到 value 字段下,避免产生非法的 list 形状。
|
|
343
|
+
if (!argsJson || typeof argsJson !== 'object' || Array.isArray(argsJson)) {
|
|
344
|
+
argsJson = { value: argsJson };
|
|
345
|
+
}
|
|
346
|
+
const functionCall = { name, args: argsJson };
|
|
347
|
+
const part = { functionCall };
|
|
328
348
|
if (typeof tc.id === 'string') {
|
|
329
349
|
part.functionCall.id = tc.id;
|
|
330
350
|
}
|
|
@@ -409,7 +429,6 @@ function buildGeminiRequestFromChat(chat, metadata) {
|
|
|
409
429
|
}
|
|
410
430
|
// Apply claude-thinking compat directly at Gemini mapping time to ensure it is always active
|
|
411
431
|
// for antigravity.*.claude-sonnet-4-5-thinking, regardless of compatibilityProfile wiring.
|
|
412
|
-
const adapterContext = metadata?.context;
|
|
413
432
|
const compatRequest = applyClaudeThinkingToolSchemaCompat(request, adapterContext);
|
|
414
433
|
return compatRequest;
|
|
415
434
|
}
|