@jsonstudio/llms 0.6.473 → 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/claude-thinking-tools.d.ts +15 -0
- package/dist/conversion/compat/actions/claude-thinking-tools.js +72 -0
- 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/compat/profiles/chat-gemini.json +15 -14
- package/dist/conversion/compat/profiles/chat-glm.json +194 -194
- package/dist/conversion/compat/profiles/chat-iflow.json +199 -199
- package/dist/conversion/compat/profiles/chat-lmstudio.json +43 -43
- package/dist/conversion/compat/profiles/chat-qwen.json +20 -20
- package/dist/conversion/compat/profiles/responses-c4m.json +42 -42
- package/dist/conversion/compat/profiles/responses-output2choices-test.json +12 -0
- package/dist/conversion/hub/pipeline/compat/compat-pipeline-executor.js +6 -0
- package/dist/conversion/hub/pipeline/compat/compat-types.d.ts +2 -0
- package/dist/conversion/hub/pipeline/hub-pipeline.d.ts +6 -1
- package/dist/conversion/hub/pipeline/hub-pipeline.js +40 -13
- package/dist/conversion/hub/pipeline/stages/req_inbound/req_inbound_stage3_context_capture/index.js +15 -0
- package/dist/conversion/hub/process/chat-process.js +107 -26
- package/dist/conversion/hub/semantic-mappers/anthropic-mapper.js +8 -0
- package/dist/conversion/hub/semantic-mappers/gemini-mapper.js +28 -10
- package/dist/conversion/hub/semantic-mappers/responses-mapper.js +51 -2
- package/dist/conversion/hub/tool-session-compat.d.ts +26 -0
- package/dist/conversion/hub/tool-session-compat.js +299 -0
- package/dist/conversion/hub/types/chat-envelope.d.ts +1 -0
- package/dist/conversion/responses/responses-openai-bridge.d.ts +0 -1
- package/dist/conversion/responses/responses-openai-bridge.js +0 -71
- package/dist/conversion/shared/anthropic-message-utils.js +54 -0
- package/dist/conversion/shared/args-mapping.js +11 -3
- package/dist/conversion/shared/gemini-tool-utils.js +8 -0
- package/dist/conversion/shared/responses-output-builder.js +47 -88
- 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-governor.js +75 -4
- 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 -13
- package/dist/filters/index.d.ts +1 -0
- package/dist/filters/index.js +1 -0
- package/dist/filters/special/request-toolcalls-stringify.js +5 -55
- package/dist/filters/special/request-tools-normalize.js +14 -23
- 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 +70 -27
- 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 +22 -0
- package/dist/router/virtual-router/engine-health.js +423 -0
- package/dist/router/virtual-router/engine-logging.d.ts +20 -0
- package/dist/router/virtual-router/engine-logging.js +197 -0
- package/dist/router/virtual-router/engine-selection.d.ts +32 -0
- package/dist/router/virtual-router/engine-selection.js +649 -0
- package/dist/router/virtual-router/engine.d.ts +21 -14
- package/dist/router/virtual-router/engine.js +200 -523
- package/dist/router/virtual-router/message-utils.js +22 -0
- package/dist/router/virtual-router/routing-instructions.d.ts +8 -1
- package/dist/router/virtual-router/routing-instructions.js +137 -3
- 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/gemini-empty-reply-continue.d.ts +1 -0
- package/dist/servertool/handlers/gemini-empty-reply-continue.js +120 -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.d.ts +1 -0
- package/dist/servertool/handlers/stop-message-auto.js +204 -0
- package/dist/servertool/handlers/vision.js +105 -7
- package/dist/servertool/server-side-tools.d.ts +3 -0
- package/dist/servertool/server-side-tools.js +29 -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 +14 -5
- package/package.json +2 -2
|
@@ -15,6 +15,7 @@ import { applyGeminiWebSearchCompat } from '../../../compat/actions/gemini-web-s
|
|
|
15
15
|
import { applyIflowWebSearchRequestTransform } from '../../../compat/actions/iflow-web-search.js';
|
|
16
16
|
import { applyGlmImageContentTransform } from '../../../compat/actions/glm-image-content.js';
|
|
17
17
|
import { applyGlmVisionPromptTransform } from '../../../compat/actions/glm-vision-prompt.js';
|
|
18
|
+
import { applyClaudeThinkingToolSchemaCompat } from '../../../compat/actions/claude-thinking-tools.js';
|
|
18
19
|
const RATE_LIMIT_ERROR = 'ERR_COMPAT_RATE_LIMIT_DETECTED';
|
|
19
20
|
const INTERNAL_STATE = Symbol('compat.internal_state');
|
|
20
21
|
export function runRequestCompatPipeline(profileId, payload, options) {
|
|
@@ -177,6 +178,11 @@ function applyMapping(root, mapping, state) {
|
|
|
177
178
|
replaceRoot(root, applyIflowWebSearchRequestTransform(root, state.adapterContext));
|
|
178
179
|
}
|
|
179
180
|
break;
|
|
181
|
+
case 'claude_thinking_tool_schema':
|
|
182
|
+
if (state.direction === 'request') {
|
|
183
|
+
replaceRoot(root, applyClaudeThinkingToolSchemaCompat(root, state.adapterContext));
|
|
184
|
+
}
|
|
185
|
+
break;
|
|
180
186
|
case 'glm_image_content':
|
|
181
187
|
if (state.direction === 'request') {
|
|
182
188
|
replaceRoot(root, applyGlmImageContentTransform(root));
|
|
@@ -108,6 +108,8 @@ export type MappingInstruction = {
|
|
|
108
108
|
action: 'gemini_web_search_request';
|
|
109
109
|
} | {
|
|
110
110
|
action: 'iflow_web_search_request';
|
|
111
|
+
} | {
|
|
112
|
+
action: 'claude_thinking_tool_schema';
|
|
111
113
|
};
|
|
112
114
|
export type FilterInstruction = {
|
|
113
115
|
action: 'rate_limit_text';
|
|
@@ -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) => {
|
|
@@ -122,6 +124,8 @@ export class HubPipeline {
|
|
|
122
124
|
? normalizedMeta.responsesResume
|
|
123
125
|
: undefined;
|
|
124
126
|
const stdMetadata = workingRequest?.metadata;
|
|
127
|
+
const hasImageAttachment = (stdMetadata?.hasImageAttachment === true || stdMetadata?.hasImageAttachment === 'true') ||
|
|
128
|
+
(normalizedMeta?.hasImageAttachment === true || normalizedMeta?.hasImageAttachment === 'true');
|
|
125
129
|
const serverToolRequired = stdMetadata?.webSearchEnabled === true ||
|
|
126
130
|
stdMetadata?.serverToolRequired === true;
|
|
127
131
|
const sessionIdentifiers = extractSessionIdentifiersFromMetadata(normalized.metadata);
|
|
@@ -240,22 +244,26 @@ export class HubPipeline {
|
|
|
240
244
|
}
|
|
241
245
|
// 为响应侧 servertool/web_search 提供一次性 Chat 请求快照,便于在 Hub 内部实现
|
|
242
246
|
// 第三跳(将工具结果注入消息历史后重新调用主模型)。
|
|
247
|
+
//
|
|
248
|
+
// 注意:这里不再根据 processMode(passthrough/chat) 做分支判断——即使某些
|
|
249
|
+
// route 将 processMode 标记为 passthrough,我们仍然需要保留一次规范化后的
|
|
250
|
+
// Chat 请求快照,供 stopMessage / gemini_empty_reply_continue 等被动触发型
|
|
251
|
+
// servertool 在响应阶段使用。
|
|
243
252
|
let capturedChatRequest;
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
capturedChatRequest = undefined;
|
|
255
|
-
}
|
|
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;
|
|
256
263
|
}
|
|
257
264
|
const metadata = {
|
|
258
265
|
...normalized.metadata,
|
|
266
|
+
...(hasImageAttachment ? { hasImageAttachment: true } : {}),
|
|
259
267
|
...(capturedChatRequest ? { capturedChatRequest } : {}),
|
|
260
268
|
entryEndpoint: normalized.entryEndpoint,
|
|
261
269
|
providerProtocol: outboundProtocol,
|
|
@@ -382,6 +390,25 @@ export class HubPipeline {
|
|
|
382
390
|
adapterContext.serverToolFollowup = metadata
|
|
383
391
|
.serverToolFollowup;
|
|
384
392
|
}
|
|
393
|
+
const sessionId = typeof metadata.sessionId === 'string'
|
|
394
|
+
? metadata.sessionId.trim()
|
|
395
|
+
: '';
|
|
396
|
+
if (sessionId) {
|
|
397
|
+
adapterContext.sessionId = sessionId;
|
|
398
|
+
}
|
|
399
|
+
const conversationId = typeof metadata.conversationId === 'string'
|
|
400
|
+
? metadata.conversationId.trim()
|
|
401
|
+
: '';
|
|
402
|
+
if (conversationId) {
|
|
403
|
+
adapterContext.conversationId = conversationId;
|
|
404
|
+
}
|
|
405
|
+
const responsesResume = metadata.responsesResume &&
|
|
406
|
+
typeof metadata.responsesResume === 'object'
|
|
407
|
+
? metadata.responsesResume
|
|
408
|
+
: undefined;
|
|
409
|
+
if (responsesResume) {
|
|
410
|
+
adapterContext.responsesResume = responsesResume;
|
|
411
|
+
}
|
|
385
412
|
if (target?.compatibilityProfile && typeof target.compatibilityProfile === 'string') {
|
|
386
413
|
adapterContext.compatibilityProfile = target.compatibilityProfile;
|
|
387
414
|
}
|
package/dist/conversion/hub/pipeline/stages/req_inbound/req_inbound_stage3_context_capture/index.js
CHANGED
|
@@ -98,6 +98,21 @@ function collectToolOutputs(payload) {
|
|
|
98
98
|
if (!id) {
|
|
99
99
|
return;
|
|
100
100
|
}
|
|
101
|
+
// 针对 apply_patch 工具的失败结果做醒目日志,便于监控
|
|
102
|
+
try {
|
|
103
|
+
const name = typeof entry.name === 'string' ? entry.name.trim() : undefined;
|
|
104
|
+
const output = typeof entry.output === 'string' ? entry.output : undefined;
|
|
105
|
+
if (name === 'apply_patch' &&
|
|
106
|
+
output &&
|
|
107
|
+
output.toLowerCase().includes('apply_patch verification failed')) {
|
|
108
|
+
const firstLine = output.split('\n')[0] ?? output;
|
|
109
|
+
// eslint-disable-next-line no-console
|
|
110
|
+
console.error(`\x1b[31m[apply_patch][tool_error] tool_call_id=${id} ${firstLine}\x1b[0m`);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
catch {
|
|
114
|
+
// logging best-effort
|
|
115
|
+
}
|
|
101
116
|
if (seen.has(id)) {
|
|
102
117
|
return;
|
|
103
118
|
}
|
|
@@ -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,30 +212,114 @@ function castSingleTool(tool) {
|
|
|
205
212
|
}
|
|
206
213
|
};
|
|
207
214
|
}
|
|
208
|
-
function
|
|
209
|
-
if (!Array.isArray(messages)) {
|
|
210
|
-
return
|
|
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;
|
|
211
231
|
}
|
|
212
|
-
|
|
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];
|
|
213
239
|
if (!message || typeof message !== 'object') {
|
|
214
240
|
continue;
|
|
215
241
|
}
|
|
242
|
+
if (message.role !== 'user') {
|
|
243
|
+
continue;
|
|
244
|
+
}
|
|
216
245
|
const content = message.content;
|
|
217
|
-
if (!Array.isArray(content)) {
|
|
246
|
+
if (!Array.isArray(content) || !content.length) {
|
|
218
247
|
continue;
|
|
219
248
|
}
|
|
249
|
+
const filtered = [];
|
|
250
|
+
let removed = false;
|
|
220
251
|
for (const part of content) {
|
|
221
|
-
if (
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
}
|
|
228
|
-
const normalized = typeValue.toLowerCase();
|
|
229
|
-
if (normalized.includes('image')) {
|
|
230
|
-
return true;
|
|
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
|
+
}
|
|
231
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
|
+
}
|
|
272
|
+
function containsImageAttachment(messages) {
|
|
273
|
+
if (!Array.isArray(messages) || !messages.length) {
|
|
274
|
+
return false;
|
|
275
|
+
}
|
|
276
|
+
// 仅检查当前请求中「最新一条 user 消息」是否携带图片,避免历史对话中的图片导致后续轮次反复触发。
|
|
277
|
+
let latestUser;
|
|
278
|
+
for (let idx = messages.length - 1; idx >= 0; idx -= 1) {
|
|
279
|
+
const candidate = messages[idx];
|
|
280
|
+
if (candidate && typeof candidate === 'object' && candidate.role === 'user') {
|
|
281
|
+
latestUser = candidate;
|
|
282
|
+
break;
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
if (!latestUser) {
|
|
286
|
+
return false;
|
|
287
|
+
}
|
|
288
|
+
const content = latestUser.content;
|
|
289
|
+
if (!Array.isArray(content)) {
|
|
290
|
+
return false;
|
|
291
|
+
}
|
|
292
|
+
for (const part of content) {
|
|
293
|
+
if (!part || typeof part !== 'object') {
|
|
294
|
+
continue;
|
|
295
|
+
}
|
|
296
|
+
const typeValue = part.type;
|
|
297
|
+
if (typeof typeValue !== 'string') {
|
|
298
|
+
continue;
|
|
299
|
+
}
|
|
300
|
+
const normalized = typeValue.toLowerCase();
|
|
301
|
+
if (!normalized.includes('image')) {
|
|
302
|
+
continue;
|
|
303
|
+
}
|
|
304
|
+
const record = part;
|
|
305
|
+
let imageCandidate = '';
|
|
306
|
+
if (typeof record.image_url === 'string') {
|
|
307
|
+
imageCandidate = record.image_url;
|
|
308
|
+
}
|
|
309
|
+
else if (record.image_url && typeof record.image_url.url === 'string') {
|
|
310
|
+
imageCandidate = record.image_url.url ?? '';
|
|
311
|
+
}
|
|
312
|
+
else if (typeof record.url === 'string') {
|
|
313
|
+
imageCandidate = record.url;
|
|
314
|
+
}
|
|
315
|
+
else if (typeof record.uri === 'string') {
|
|
316
|
+
imageCandidate = record.uri;
|
|
317
|
+
}
|
|
318
|
+
else if (typeof record.data === 'string') {
|
|
319
|
+
imageCandidate = record.data;
|
|
320
|
+
}
|
|
321
|
+
if (imageCandidate.trim().length > 0) {
|
|
322
|
+
return true;
|
|
232
323
|
}
|
|
233
324
|
}
|
|
234
325
|
return false;
|
|
@@ -251,17 +342,7 @@ function castCustomTool(tool) {
|
|
|
251
342
|
function: {
|
|
252
343
|
name: 'apply_patch',
|
|
253
344
|
description,
|
|
254
|
-
parameters:
|
|
255
|
-
type: 'object',
|
|
256
|
-
properties: {
|
|
257
|
-
patch: {
|
|
258
|
-
type: 'string',
|
|
259
|
-
description: 'Unified diff patch content (FREEFORM, not JSON)'
|
|
260
|
-
}
|
|
261
|
-
},
|
|
262
|
-
required: ['patch'],
|
|
263
|
-
additionalProperties: false
|
|
264
|
-
},
|
|
345
|
+
parameters: ensureApplyPatchSchema(),
|
|
265
346
|
strict: true
|
|
266
347
|
}
|
|
267
348
|
};
|
|
@@ -200,6 +200,14 @@ export class AnthropicSemanticMapper {
|
|
|
200
200
|
return chatEnvelope;
|
|
201
201
|
}
|
|
202
202
|
async fromChat(chat, ctx) {
|
|
203
|
+
// Ensure tool_use / tool_result ordering and per-session history for /v1/messages style entrypoints.
|
|
204
|
+
try {
|
|
205
|
+
const { applyToolSessionCompat } = await import('../tool-session-compat.js');
|
|
206
|
+
await applyToolSessionCompat(chat, ctx);
|
|
207
|
+
}
|
|
208
|
+
catch {
|
|
209
|
+
// best-effort compat; do not block outbound mapping
|
|
210
|
+
}
|
|
203
211
|
const model = chat.parameters?.model;
|
|
204
212
|
if (typeof model !== 'string' || !model.trim()) {
|
|
205
213
|
throw new Error('ChatEnvelope.parameters.model is required for anthropic-messages outbound conversion');
|
|
@@ -6,6 +6,7 @@ import { encodeMetadataPassthrough, extractMetadataPassthrough } from '../../sha
|
|
|
6
6
|
import { mapBridgeToolsToChat, mapChatToolsToBridge } from '../../shared/tool-mapping.js';
|
|
7
7
|
import { prepareGeminiToolsForBridge, buildGeminiToolsFromBridge } from '../../shared/gemini-tool-utils.js';
|
|
8
8
|
import { ensureProtocolState, getProtocolState } from '../../shared/protocol-state.js';
|
|
9
|
+
import { applyClaudeThinkingToolSchemaCompat } from '../../compat/actions/claude-thinking-tools.js';
|
|
9
10
|
const GENERATION_CONFIG_KEYS = [
|
|
10
11
|
{ source: 'temperature', target: 'temperature' },
|
|
11
12
|
{ source: 'topP', target: 'top_p' },
|
|
@@ -285,6 +286,14 @@ function appendChatContentToGeminiParts(message, targetParts) {
|
|
|
285
286
|
function buildGeminiRequestFromChat(chat, metadata) {
|
|
286
287
|
const contents = [];
|
|
287
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';
|
|
288
297
|
for (const message of chat.messages) {
|
|
289
298
|
if (!message || typeof message !== 'object')
|
|
290
299
|
continue;
|
|
@@ -303,10 +312,15 @@ function buildGeminiRequestFromChat(chat, metadata) {
|
|
|
303
312
|
parts: []
|
|
304
313
|
};
|
|
305
314
|
appendChatContentToGeminiParts(message, entry.parts);
|
|
306
|
-
const toolCalls = Array.isArray(message.tool_calls)
|
|
315
|
+
const toolCalls = Array.isArray(message.tool_calls)
|
|
316
|
+
? message.tool_calls
|
|
317
|
+
: [];
|
|
307
318
|
for (const tc of toolCalls) {
|
|
308
319
|
if (!tc || typeof tc !== 'object')
|
|
309
320
|
continue;
|
|
321
|
+
if (omitFunctionCallPartsForCli) {
|
|
322
|
+
continue;
|
|
323
|
+
}
|
|
310
324
|
const fn = tc.function || {};
|
|
311
325
|
const name = typeof fn.name === 'string' ? fn.name : undefined;
|
|
312
326
|
if (!name)
|
|
@@ -323,7 +337,8 @@ function buildGeminiRequestFromChat(chat, metadata) {
|
|
|
323
337
|
else {
|
|
324
338
|
argsStruct = fn.arguments ?? {};
|
|
325
339
|
}
|
|
326
|
-
const
|
|
340
|
+
const functionCall = { name, args: cloneAsJsonValue(argsStruct) };
|
|
341
|
+
const part = { functionCall };
|
|
327
342
|
if (typeof tc.id === 'string') {
|
|
328
343
|
part.functionCall.id = tc.id;
|
|
329
344
|
}
|
|
@@ -385,13 +400,6 @@ function buildGeminiRequestFromChat(chat, metadata) {
|
|
|
385
400
|
if (Object.keys(generationConfig).length) {
|
|
386
401
|
request.generationConfig = generationConfig;
|
|
387
402
|
}
|
|
388
|
-
if (metadata?.extraFields && isJsonObject(metadata.extraFields)) {
|
|
389
|
-
for (const [key, value] of Object.entries(metadata.extraFields)) {
|
|
390
|
-
if (request[key] === undefined) {
|
|
391
|
-
request[key] = value;
|
|
392
|
-
}
|
|
393
|
-
}
|
|
394
|
-
}
|
|
395
403
|
if (metadata?.providerMetadata && isJsonObject(metadata.providerMetadata)) {
|
|
396
404
|
request.metadata = jsonClone(metadata.providerMetadata);
|
|
397
405
|
}
|
|
@@ -413,7 +421,10 @@ function buildGeminiRequestFromChat(chat, metadata) {
|
|
|
413
421
|
request.metadata[key] = value;
|
|
414
422
|
}
|
|
415
423
|
}
|
|
416
|
-
|
|
424
|
+
// Apply claude-thinking compat directly at Gemini mapping time to ensure it is always active
|
|
425
|
+
// for antigravity.*.claude-sonnet-4-5-thinking, regardless of compatibilityProfile wiring.
|
|
426
|
+
const compatRequest = applyClaudeThinkingToolSchemaCompat(request, adapterContext);
|
|
427
|
+
return compatRequest;
|
|
417
428
|
}
|
|
418
429
|
function buildGenerationConfigFromParameters(parameters) {
|
|
419
430
|
const config = {};
|
|
@@ -570,6 +581,13 @@ export class GeminiSemanticMapper {
|
|
|
570
581
|
};
|
|
571
582
|
}
|
|
572
583
|
async fromChat(chat, ctx) {
|
|
584
|
+
try {
|
|
585
|
+
const { applyToolSessionCompat } = await import('../tool-session-compat.js');
|
|
586
|
+
await applyToolSessionCompat(chat, ctx);
|
|
587
|
+
}
|
|
588
|
+
catch {
|
|
589
|
+
// best-effort compat; do not block outbound mapping
|
|
590
|
+
}
|
|
573
591
|
const envelopePayload = buildGeminiRequestFromChat(chat, chat.metadata);
|
|
574
592
|
try {
|
|
575
593
|
const bridgePolicy = resolveBridgePolicy({ protocol: 'gemini-chat' });
|
|
@@ -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',
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { ChatEnvelope } from './types/chat-envelope.js';
|
|
2
|
+
import type { AdapterContext } from './types/chat-envelope.js';
|
|
3
|
+
type ToolHistoryStatus = 'ok' | 'error' | 'unknown';
|
|
4
|
+
export interface ToolHistoryMessageRecord {
|
|
5
|
+
role: 'user' | 'assistant' | 'tool';
|
|
6
|
+
tool_use?: {
|
|
7
|
+
id: string;
|
|
8
|
+
name?: string;
|
|
9
|
+
};
|
|
10
|
+
tool_result?: {
|
|
11
|
+
id: string;
|
|
12
|
+
name?: string;
|
|
13
|
+
status: ToolHistoryStatus;
|
|
14
|
+
};
|
|
15
|
+
ts: string;
|
|
16
|
+
}
|
|
17
|
+
export interface ToolSessionHistory {
|
|
18
|
+
lastMessages: ToolHistoryMessageRecord[];
|
|
19
|
+
pendingToolUses: Record<string, {
|
|
20
|
+
name?: string;
|
|
21
|
+
ts: string;
|
|
22
|
+
}>;
|
|
23
|
+
updatedAt: string;
|
|
24
|
+
}
|
|
25
|
+
export declare function applyToolSessionCompat(chat: ChatEnvelope, ctx: AdapterContext): Promise<void>;
|
|
26
|
+
export {};
|