@jsonstudio/llms 0.6.938 → 0.6.1164

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.
Files changed (131) hide show
  1. package/dist/conversion/hub/operation-table/operation-table-runner.d.ts +18 -0
  2. package/dist/conversion/hub/operation-table/operation-table-runner.js +158 -0
  3. package/dist/conversion/hub/operation-table/semantic-mappers/anthropic-mapper.d.ts +8 -0
  4. package/dist/conversion/hub/operation-table/semantic-mappers/anthropic-mapper.js +303 -0
  5. package/dist/conversion/hub/operation-table/semantic-mappers/chat-mapper.d.ts +8 -0
  6. package/dist/conversion/hub/operation-table/semantic-mappers/chat-mapper.js +413 -0
  7. package/dist/conversion/hub/operation-table/semantic-mappers/gemini-mapper.d.ts +7 -0
  8. package/dist/conversion/hub/operation-table/semantic-mappers/gemini-mapper.js +841 -0
  9. package/dist/conversion/hub/operation-table/semantic-mappers/responses-mapper.d.ts +21 -0
  10. package/dist/conversion/hub/operation-table/semantic-mappers/responses-mapper.js +535 -0
  11. package/dist/conversion/hub/ops/operations.d.ts +19 -0
  12. package/dist/conversion/hub/ops/operations.js +126 -0
  13. package/dist/conversion/hub/pipeline/hub-pipeline.d.ts +9 -0
  14. package/dist/conversion/hub/pipeline/hub-pipeline.js +533 -24
  15. package/dist/conversion/hub/pipeline/stages/req_inbound/req_inbound_stage2_semantic_map/index.js +6 -0
  16. package/dist/conversion/hub/pipeline/stages/req_inbound/req_inbound_stage3_context_capture/index.js +6 -3
  17. package/dist/conversion/hub/pipeline/stages/req_outbound/req_outbound_stage1_semantic_map/index.js +11 -0
  18. package/dist/conversion/hub/policy/policy-engine.js +41 -9
  19. package/dist/conversion/hub/policy/protocol-spec.d.ts +25 -0
  20. package/dist/conversion/hub/policy/protocol-spec.js +73 -23
  21. package/dist/conversion/hub/process/chat-process.js +252 -41
  22. package/dist/conversion/hub/response/provider-response.js +175 -2
  23. package/dist/conversion/hub/response/response-runtime.js +1 -1
  24. package/dist/conversion/hub/semantic-mappers/anthropic-mapper.d.ts +1 -8
  25. package/dist/conversion/hub/semantic-mappers/anthropic-mapper.js +1 -365
  26. package/dist/conversion/hub/semantic-mappers/chat-mapper.d.ts +1 -8
  27. package/dist/conversion/hub/semantic-mappers/chat-mapper.js +1 -436
  28. package/dist/conversion/hub/semantic-mappers/gemini-mapper.d.ts +1 -7
  29. package/dist/conversion/hub/semantic-mappers/gemini-mapper.js +1 -894
  30. package/dist/conversion/hub/semantic-mappers/responses-mapper.d.ts +1 -21
  31. package/dist/conversion/hub/semantic-mappers/responses-mapper.js +1 -593
  32. package/dist/conversion/hub/tool-surface/tool-surface-engine.d.ts +18 -0
  33. package/dist/conversion/hub/tool-surface/tool-surface-engine.js +571 -0
  34. package/dist/conversion/responses/responses-openai-bridge.js +14 -2
  35. package/dist/conversion/shared/bridge-message-utils.js +2 -8
  36. package/dist/conversion/shared/bridge-policies.js +5 -105
  37. package/dist/conversion/shared/gemini-tool-utils.js +121 -4
  38. package/dist/conversion/shared/protocol-field-allowlists.d.ts +7 -0
  39. package/dist/conversion/shared/protocol-field-allowlists.js +145 -0
  40. package/dist/conversion/shared/reasoning-tool-normalizer.js +4 -2
  41. package/dist/conversion/shared/snapshot-hooks.js +166 -3
  42. package/dist/conversion/shared/text-markup-normalizer.d.ts +2 -0
  43. package/dist/conversion/shared/text-markup-normalizer.js +345 -9
  44. package/dist/conversion/shared/thought-signature-validator.d.ts +52 -0
  45. package/dist/conversion/shared/thought-signature-validator.js +170 -0
  46. package/dist/conversion/shared/tool-argument-repairer.d.ts +39 -0
  47. package/dist/conversion/shared/tool-argument-repairer.js +56 -0
  48. package/dist/conversion/shared/tool-call-id-manager.d.ts +113 -0
  49. package/dist/conversion/shared/tool-call-id-manager.js +231 -0
  50. package/dist/conversion/shared/tool-canonicalizer.js +2 -11
  51. package/dist/router/virtual-router/bootstrap.js +54 -5
  52. package/dist/router/virtual-router/engine-selection.js +132 -42
  53. package/dist/router/virtual-router/engine.d.ts +3 -0
  54. package/dist/router/virtual-router/engine.js +142 -33
  55. package/dist/router/virtual-router/health-weighted.d.ts +25 -0
  56. package/dist/router/virtual-router/health-weighted.js +63 -0
  57. package/dist/router/virtual-router/load-balancer.d.ts +2 -0
  58. package/dist/router/virtual-router/load-balancer.js +45 -16
  59. package/dist/router/virtual-router/routing-instructions.js +17 -1
  60. package/dist/router/virtual-router/sticky-session-store.js +136 -24
  61. package/dist/router/virtual-router/stop-message-file-resolver.d.ts +1 -0
  62. package/dist/router/virtual-router/stop-message-file-resolver.js +74 -0
  63. package/dist/router/virtual-router/stop-message-state-sync.d.ts +15 -0
  64. package/dist/router/virtual-router/stop-message-state-sync.js +57 -0
  65. package/dist/router/virtual-router/types.d.ts +70 -0
  66. package/dist/servertool/clock/config.d.ts +7 -0
  67. package/dist/servertool/clock/config.js +27 -0
  68. package/dist/servertool/clock/daemon.d.ts +3 -0
  69. package/dist/servertool/clock/daemon.js +79 -0
  70. package/dist/servertool/clock/io.d.ts +2 -0
  71. package/dist/servertool/clock/io.js +13 -0
  72. package/dist/servertool/clock/paths.d.ts +4 -0
  73. package/dist/servertool/clock/paths.js +25 -0
  74. package/dist/servertool/clock/session-store.d.ts +3 -0
  75. package/dist/servertool/clock/session-store.js +56 -0
  76. package/dist/servertool/clock/state.d.ts +5 -0
  77. package/dist/servertool/clock/state.js +62 -0
  78. package/dist/servertool/clock/task-store.d.ts +5 -0
  79. package/dist/servertool/clock/task-store.js +4 -0
  80. package/dist/servertool/clock/tasks.d.ts +17 -0
  81. package/dist/servertool/clock/tasks.js +221 -0
  82. package/dist/servertool/clock/types.d.ts +36 -0
  83. package/dist/servertool/clock/types.js +1 -0
  84. package/dist/servertool/engine.d.ts +2 -0
  85. package/dist/servertool/engine.js +164 -8
  86. package/dist/servertool/followup-shadow.d.ts +16 -0
  87. package/dist/servertool/followup-shadow.js +145 -0
  88. package/dist/servertool/handlers/apply-patch-guard.js +1 -265
  89. package/dist/servertool/handlers/clock-auto.d.ts +1 -0
  90. package/dist/servertool/handlers/clock-auto.js +160 -0
  91. package/dist/servertool/handlers/clock.d.ts +1 -0
  92. package/dist/servertool/handlers/clock.js +197 -0
  93. package/dist/servertool/handlers/exec-command-guard.js +7 -555
  94. package/dist/servertool/handlers/followup-request-builder.d.ts +15 -7
  95. package/dist/servertool/handlers/followup-request-builder.js +248 -28
  96. package/dist/servertool/handlers/gemini-empty-reply-continue.js +62 -169
  97. package/dist/servertool/handlers/iflow-model-error-retry.js +18 -28
  98. package/dist/servertool/handlers/recursive-detection-guard.d.ts +1 -0
  99. package/dist/servertool/handlers/recursive-detection-guard.js +333 -0
  100. package/dist/servertool/handlers/stop-message-auto.js +47 -175
  101. package/dist/servertool/handlers/vision.d.ts +7 -1
  102. package/dist/servertool/handlers/vision.js +61 -117
  103. package/dist/servertool/handlers/web-search.d.ts +7 -1
  104. package/dist/servertool/handlers/web-search.js +122 -105
  105. package/dist/servertool/reenter-backend.d.ts +23 -0
  106. package/dist/servertool/reenter-backend.js +18 -0
  107. package/dist/servertool/server-side-tools.d.ts +3 -2
  108. package/dist/servertool/server-side-tools.js +64 -10
  109. package/dist/servertool/types.d.ts +92 -3
  110. package/dist/sse/json-to-sse/event-generators/responses.js +3 -21
  111. package/dist/sse/shared/serializers/responses-event-serializer.d.ts +8 -0
  112. package/dist/sse/shared/serializers/responses-event-serializer.js +19 -0
  113. package/dist/sse/shared/writer.js +24 -7
  114. package/dist/tools/apply-patch/execution-capturer.js +3 -1
  115. package/dist/tools/apply-patch/json/parse-loose.d.ts +3 -0
  116. package/dist/tools/apply-patch/json/parse-loose.js +139 -0
  117. package/dist/tools/apply-patch/patch-text/context-diff.d.ts +1 -0
  118. package/dist/tools/apply-patch/patch-text/context-diff.js +173 -0
  119. package/dist/tools/apply-patch/patch-text/git-diff.d.ts +1 -0
  120. package/dist/tools/apply-patch/patch-text/git-diff.js +138 -0
  121. package/dist/tools/apply-patch/patch-text/looks-like-patch.d.ts +1 -0
  122. package/dist/tools/apply-patch/patch-text/looks-like-patch.js +13 -0
  123. package/dist/tools/apply-patch/patch-text/normalize.d.ts +3 -0
  124. package/dist/tools/apply-patch/patch-text/normalize.js +262 -0
  125. package/dist/tools/apply-patch/structured/coercion.d.ts +3 -0
  126. package/dist/tools/apply-patch/structured/coercion.js +82 -0
  127. package/dist/tools/apply-patch/validation/shared.d.ts +3 -0
  128. package/dist/tools/apply-patch/validation/shared.js +6 -0
  129. package/dist/tools/apply-patch/validator.d.ts +2 -2
  130. package/dist/tools/apply-patch/validator.js +6 -556
  131. package/package.json +1 -1
@@ -1,6 +1,6 @@
1
- import { buildAnthropicRequestFromOpenAIChat } from '../../conversion/codecs/anthropic-openai-codec.js';
2
- import { buildChatRequestFromResponses, buildResponsesRequestFromChat, captureResponsesContext } from '../../conversion/responses/responses-openai-bridge.js';
1
+ import { buildChatRequestFromResponses, captureResponsesContext } from '../../conversion/responses/responses-openai-bridge.js';
3
2
  import { cloneJson } from '../server-side-tools.js';
3
+ import { trimOpenAiMessagesForFollowup } from './followup-message-trimmer.js';
4
4
  function extractResponsesTopLevelParameters(record) {
5
5
  const allowed = new Set([
6
6
  'temperature',
@@ -79,32 +79,6 @@ export function normalizeFollowupParameters(value) {
79
79
  delete cloned.stream;
80
80
  return Object.keys(cloned).length ? cloned : undefined;
81
81
  }
82
- export function buildEntryAwareFollowupPayload(args) {
83
- const chatPayload = {
84
- ...(args.model ? { model: args.model } : {}),
85
- messages: args.messages,
86
- ...(args.tools ? { tools: args.tools } : {})
87
- };
88
- const normalizedEntry = typeof args.entryEndpoint === 'string' ? args.entryEndpoint.trim().toLowerCase() : '';
89
- if (normalizedEntry.includes('/v1/responses')) {
90
- return buildResponsesRequestFromChat(chatPayload, {
91
- stream: false,
92
- ...(args.parameters ? { parameters: args.parameters } : {})
93
- }).request;
94
- }
95
- if (normalizedEntry.includes('/v1/messages')) {
96
- const anthropicChatPayload = {
97
- ...chatPayload,
98
- ...(args.parameters ? args.parameters : {})
99
- };
100
- return buildAnthropicRequestFromOpenAIChat(anthropicChatPayload);
101
- }
102
- const openaiChatPayload = {
103
- ...chatPayload,
104
- ...(args.parameters ? args.parameters : {})
105
- };
106
- return openaiChatPayload;
107
- }
108
82
  export function dropToolByFunctionName(tools, dropName) {
109
83
  const name = typeof dropName === 'string' ? dropName.trim() : '';
110
84
  if (!tools || !tools.length || !name) {
@@ -120,3 +94,249 @@ export function dropToolByFunctionName(tools, dropName) {
120
94
  return toolName !== name;
121
95
  });
122
96
  }
97
+ function extractAssistantMessageFromChatLike(chatResponse) {
98
+ if (!chatResponse || typeof chatResponse !== 'object') {
99
+ return null;
100
+ }
101
+ const choices = Array.isArray(chatResponse.choices)
102
+ ? chatResponse.choices
103
+ : [];
104
+ if (choices.length > 0) {
105
+ const first = choices[0] && typeof choices[0] === 'object' && !Array.isArray(choices[0])
106
+ ? choices[0]
107
+ : null;
108
+ const msg = first &&
109
+ first.message &&
110
+ typeof first.message === 'object' &&
111
+ !Array.isArray(first.message)
112
+ ? first.message
113
+ : null;
114
+ if (msg) {
115
+ return cloneJson(msg);
116
+ }
117
+ }
118
+ // Responses-like fallback: try output_text.
119
+ const outputText = chatResponse.output_text;
120
+ if (typeof outputText === 'string' && outputText.trim().length) {
121
+ return { role: 'assistant', content: outputText.trim() };
122
+ }
123
+ return null;
124
+ }
125
+ function buildToolMessagesFromToolOutputs(chatResponse) {
126
+ const toolOutputs = Array.isArray(chatResponse.tool_outputs)
127
+ ? chatResponse.tool_outputs
128
+ : [];
129
+ const messages = [];
130
+ for (const entry of toolOutputs) {
131
+ if (!entry || typeof entry !== 'object' || Array.isArray(entry))
132
+ continue;
133
+ const record = entry;
134
+ const toolCallId = typeof record.tool_call_id === 'string' ? record.tool_call_id : undefined;
135
+ if (!toolCallId)
136
+ continue;
137
+ const name = typeof record.name === 'string' && record.name.trim() ? record.name.trim() : 'tool';
138
+ const rawContent = record.content;
139
+ let contentText;
140
+ if (typeof rawContent === 'string') {
141
+ contentText = rawContent;
142
+ }
143
+ else {
144
+ try {
145
+ contentText = JSON.stringify(rawContent ?? {});
146
+ }
147
+ catch {
148
+ contentText = String(rawContent ?? '');
149
+ }
150
+ }
151
+ messages.push({
152
+ role: 'tool',
153
+ tool_call_id: toolCallId,
154
+ name,
155
+ content: contentText
156
+ });
157
+ }
158
+ return messages;
159
+ }
160
+ function injectVisionSummaryIntoMessages(source, summary) {
161
+ const messages = Array.isArray(source) ? cloneJson(source) : [];
162
+ let injected = false;
163
+ for (const message of messages) {
164
+ if (!message || typeof message !== 'object')
165
+ continue;
166
+ const content = message.content;
167
+ if (!Array.isArray(content))
168
+ continue;
169
+ const nextParts = [];
170
+ let removed = false;
171
+ for (const part of content) {
172
+ if (part && typeof part === 'object') {
173
+ const typeValue = typeof part.type === 'string'
174
+ ? String(part.type).toLowerCase()
175
+ : '';
176
+ if (typeValue.includes('image')) {
177
+ removed = true;
178
+ continue;
179
+ }
180
+ }
181
+ nextParts.push(part);
182
+ }
183
+ if (removed) {
184
+ nextParts.push({
185
+ type: 'text',
186
+ text: `[Vision] ${summary}`
187
+ });
188
+ message.content = nextParts;
189
+ injected = true;
190
+ }
191
+ }
192
+ if (!injected) {
193
+ for (let i = messages.length - 1; i >= 0; i -= 1) {
194
+ const msg = messages[i];
195
+ if (!msg || typeof msg !== 'object')
196
+ continue;
197
+ const role = typeof msg.role === 'string'
198
+ ? String(msg.role).toLowerCase()
199
+ : '';
200
+ if (role !== 'user')
201
+ continue;
202
+ const content = msg.content;
203
+ if (Array.isArray(content)) {
204
+ content.push({
205
+ type: 'text',
206
+ text: `[Vision] ${summary}`
207
+ });
208
+ injected = true;
209
+ break;
210
+ }
211
+ if (typeof content === 'string' && content.length) {
212
+ msg.content = `${content}\n[Vision] ${summary}`;
213
+ }
214
+ else {
215
+ msg.content = `[Vision] ${summary}`;
216
+ }
217
+ injected = true;
218
+ break;
219
+ }
220
+ }
221
+ if (!injected) {
222
+ messages.push({
223
+ role: 'user',
224
+ content: `[Vision] ${summary}`
225
+ });
226
+ }
227
+ return messages;
228
+ }
229
+ function injectSystemTextIntoMessages(source, text) {
230
+ const messages = Array.isArray(source) ? cloneJson(source) : [];
231
+ const content = typeof text === 'string' ? text : '';
232
+ if (!content.trim().length) {
233
+ return messages;
234
+ }
235
+ const sys = { role: 'system', content };
236
+ let insertAt = 0;
237
+ while (insertAt < messages.length) {
238
+ const msg = messages[insertAt];
239
+ const role = msg && typeof msg === 'object' && !Array.isArray(msg) && typeof msg.role === 'string'
240
+ ? String(msg.role).trim().toLowerCase()
241
+ : '';
242
+ if (role === 'system') {
243
+ insertAt += 1;
244
+ continue;
245
+ }
246
+ break;
247
+ }
248
+ messages.splice(insertAt, 0, sys);
249
+ return messages;
250
+ }
251
+ /**
252
+ * Build a canonical followup request body from injection ops.
253
+ *
254
+ * Important: this returns a protocol-agnostic "chat-like" payload:
255
+ * { model, messages, tools?, parameters? }
256
+ *
257
+ * The followup is expected to re-enter HubPipeline at the chat-process entry,
258
+ * so we must not convert to /v1/responses or /v1/messages here.
259
+ */
260
+ export function buildServerToolFollowupChatPayloadFromInjection(args) {
261
+ const captured = args.adapterContext && typeof args.adapterContext === 'object'
262
+ ? args.adapterContext.capturedChatRequest
263
+ : undefined;
264
+ const seed = extractCapturedChatSeed(captured);
265
+ if (!seed) {
266
+ return null;
267
+ }
268
+ if (!seed.model || typeof seed.model !== 'string' || !seed.model.trim()) {
269
+ return null;
270
+ }
271
+ let messages = Array.isArray(seed.messages) ? cloneJson(seed.messages) : [];
272
+ let tools = seed.tools ? cloneJson(seed.tools) : undefined;
273
+ const parameters = seed.parameters ? cloneJson(seed.parameters) : undefined;
274
+ const ops = Array.isArray(args.injection?.ops) ? args.injection.ops : [];
275
+ for (const op of ops) {
276
+ if (!op || typeof op !== 'object')
277
+ continue;
278
+ if (op.op === 'trim_openai_messages') {
279
+ const maxNonSystemMessages = typeof op.maxNonSystemMessages === 'number'
280
+ ? op.maxNonSystemMessages
281
+ : 16;
282
+ messages = trimOpenAiMessagesForFollowup(messages, { maxNonSystemMessages });
283
+ continue;
284
+ }
285
+ if (op.op === 'append_assistant_message') {
286
+ const required = op.required !== false;
287
+ const msg = extractAssistantMessageFromChatLike(args.chatResponse);
288
+ if (!msg) {
289
+ if (required)
290
+ return null;
291
+ continue;
292
+ }
293
+ messages.push(msg);
294
+ continue;
295
+ }
296
+ if (op.op === 'append_tool_messages_from_tool_outputs') {
297
+ const required = op.required !== false;
298
+ const toolMessages = buildToolMessagesFromToolOutputs(args.chatResponse);
299
+ if (!toolMessages.length) {
300
+ if (required)
301
+ return null;
302
+ continue;
303
+ }
304
+ messages.push(...toolMessages);
305
+ continue;
306
+ }
307
+ if (op.op === 'inject_system_text') {
308
+ const text = typeof op.text === 'string' ? String(op.text) : '';
309
+ if (text.trim().length) {
310
+ messages = injectSystemTextIntoMessages(messages, text.trim());
311
+ }
312
+ continue;
313
+ }
314
+ if (op.op === 'append_user_text') {
315
+ const text = typeof op.text === 'string' ? String(op.text) : '';
316
+ if (text.trim().length) {
317
+ messages.push({ role: 'user', content: text });
318
+ }
319
+ continue;
320
+ }
321
+ if (op.op === 'drop_tool_by_name') {
322
+ const name = typeof op.name === 'string' ? String(op.name) : '';
323
+ if (name.trim().length) {
324
+ tools = dropToolByFunctionName(tools, name.trim());
325
+ }
326
+ continue;
327
+ }
328
+ if (op.op === 'inject_vision_summary') {
329
+ const summary = typeof op.summary === 'string' ? String(op.summary) : '';
330
+ if (summary.trim().length) {
331
+ messages = injectVisionSummaryIntoMessages(messages, summary.trim());
332
+ }
333
+ continue;
334
+ }
335
+ }
336
+ return {
337
+ model: seed.model,
338
+ messages,
339
+ ...(tools ? { tools } : {}),
340
+ ...(parameters ? { parameters } : {})
341
+ };
342
+ }
@@ -1,10 +1,9 @@
1
1
  import { registerServerToolHandler } from '../registry.js';
2
- import { cloneJson } from '../server-side-tools.js';
3
2
  import { isCompactionRequest } from './compaction-detect.js';
4
- import { buildResponsesRequestFromChat, captureResponsesContext, buildChatRequestFromResponses } from '../../conversion/responses/responses-openai-bridge.js';
3
+ import { extractCapturedChatSeed } from './followup-request-builder.js';
5
4
  const FLOW_ID = 'gemini_empty_reply_continue';
6
5
  const handler = async (ctx) => {
7
- if (!ctx.options.reenterPipeline) {
6
+ if (!ctx.capabilities.reenterPipeline) {
8
7
  return null;
9
8
  }
10
9
  // 避免在 followup 请求里再次触发,防止循环。
@@ -17,10 +16,10 @@ const handler = async (ctx) => {
17
16
  return null;
18
17
  }
19
18
  // 仅针对 gemini-chat 协议 + antigravity.* providerKey 的 /v1/responses 路径启用。
20
- if (ctx.options.providerProtocol !== 'gemini-chat') {
19
+ if (ctx.providerProtocol !== 'gemini-chat') {
21
20
  return null;
22
21
  }
23
- const entryEndpoint = (ctx.options.entryEndpoint || '').toLowerCase();
22
+ const entryEndpoint = (ctx.entryEndpoint || '').toLowerCase();
24
23
  if (!entryEndpoint.includes('/v1/responses')) {
25
24
  return null;
26
25
  }
@@ -53,6 +52,10 @@ const handler = async (ctx) => {
53
52
  if (isCompactionRequest(captured)) {
54
53
  return null;
55
54
  }
55
+ const seed = extractCapturedChatSeed(captured);
56
+ if (!seed) {
57
+ return null;
58
+ }
56
59
  // 超过最多 3 次空回复:返回一个 HTTP_HANDLER_ERROR 形状的错误,交由上层错误中心处理。
57
60
  if (nextCount > 3) {
58
61
  const errorChat = {
@@ -66,34 +69,37 @@ const handler = async (ctx) => {
66
69
  }
67
70
  };
68
71
  return {
69
- chatResponse: errorChat,
70
- execution: {
71
- flowId: FLOW_ID
72
- }
72
+ flowId: FLOW_ID,
73
+ finalize: async () => ({
74
+ chatResponse: errorChat,
75
+ execution: {
76
+ flowId: FLOW_ID
77
+ }
78
+ })
73
79
  };
74
80
  }
75
- const assistantMessage = extractAssistantMessage(ctx.base);
76
- const followupPayload = buildContinueFollowupPayload(captured, assistantMessage);
77
- if (!followupPayload) {
78
- return null;
79
- }
81
+ const assistantMessage = extractAssistantMessageForFollowup(ctx.base);
80
82
  return {
81
- chatResponse: ctx.base,
82
- execution: {
83
- flowId: FLOW_ID,
84
- followup: {
85
- requestIdSuffix: ':continue',
86
- payload: followupPayload,
87
- metadata: {
88
- serverToolFollowup: true,
89
- stream: false,
90
- preserveRouteHint: false,
91
- disableStickyRoutes: true,
92
- serverToolOriginalEntryEndpoint: ctx.options.entryEndpoint,
93
- geminiEmptyReplyCount: nextCount
83
+ flowId: FLOW_ID,
84
+ finalize: async () => ({
85
+ chatResponse: ctx.base,
86
+ execution: {
87
+ flowId: FLOW_ID,
88
+ followup: {
89
+ requestIdSuffix: ':continue',
90
+ entryEndpoint: ctx.entryEndpoint,
91
+ injection: {
92
+ ops: [
93
+ { op: 'append_assistant_message', required: false },
94
+ { op: 'append_user_text', text: '继续执行' }
95
+ ]
96
+ },
97
+ metadata: {
98
+ geminiEmptyReplyCount: nextCount
99
+ }
94
100
  }
95
101
  }
96
- }
102
+ })
97
103
  };
98
104
  };
99
105
  registerServerToolHandler('gemini_empty_reply_continue', handler, { trigger: 'auto' });
@@ -153,6 +159,34 @@ function decideEmptyReply(base) {
153
159
  // 允许 output 为空或仅包含空消息:视作空回复,触发自动续写。
154
160
  return { shouldTrigger: true };
155
161
  }
162
+ function extractAssistantMessageForFollowup(chatResponse) {
163
+ if (!chatResponse || typeof chatResponse !== 'object' || Array.isArray(chatResponse)) {
164
+ return null;
165
+ }
166
+ const choices = Array.isArray(chatResponse.choices)
167
+ ? chatResponse.choices
168
+ : [];
169
+ if (!choices.length) {
170
+ return null;
171
+ }
172
+ const first = choices[0];
173
+ if (!first || typeof first !== 'object' || Array.isArray(first)) {
174
+ return null;
175
+ }
176
+ const message = first.message;
177
+ if (!message || typeof message !== 'object' || Array.isArray(message)) {
178
+ return null;
179
+ }
180
+ const role = typeof message.role === 'string' ? String(message.role) : '';
181
+ if (role && role.toLowerCase() !== 'assistant') {
182
+ return null;
183
+ }
184
+ const content = message.content;
185
+ if (typeof content !== 'string' || !content.trim()) {
186
+ return null;
187
+ }
188
+ return { role: 'assistant', content: content.trim() };
189
+ }
156
190
  function extractResponsesOutputText(base) {
157
191
  const raw = base.output_text;
158
192
  if (typeof raw === 'string') {
@@ -216,112 +250,6 @@ function getCapturedRequest(adapterContext) {
216
250
  }
217
251
  return captured;
218
252
  }
219
- function extractChatSeedFromCapturedRequest(source) {
220
- const model = typeof source.model === 'string' && source.model.trim()
221
- ? source.model.trim()
222
- : undefined;
223
- const rawMessages = Array.isArray(source.messages)
224
- ? source.messages
225
- : null;
226
- if (rawMessages) {
227
- const tools = Array.isArray(source.tools)
228
- ? cloneJson(source.tools)
229
- : undefined;
230
- return {
231
- ...(model ? { model } : {}),
232
- messages: cloneJson(rawMessages),
233
- ...(tools ? { tools } : {})
234
- };
235
- }
236
- const rawInput = Array.isArray(source.input)
237
- ? source.input
238
- : null;
239
- if (rawInput) {
240
- try {
241
- const ctx = captureResponsesContext(source);
242
- if (!ctx.isResponsesPayload) {
243
- return null;
244
- }
245
- const rebuilt = buildChatRequestFromResponses(source, ctx).request;
246
- const rebuiltModel = typeof rebuilt.model === 'string' && rebuilt.model.trim().length ? String(rebuilt.model).trim() : model;
247
- const rebuiltMessages = Array.isArray(rebuilt.messages) ? rebuilt.messages : [];
248
- const rebuiltTools = Array.isArray(rebuilt.tools) ? rebuilt.tools : undefined;
249
- return {
250
- ...(rebuiltModel ? { model: rebuiltModel } : {}),
251
- messages: cloneJson(rebuiltMessages),
252
- ...(rebuiltTools ? { tools: cloneJson(rebuiltTools) } : {})
253
- };
254
- }
255
- catch {
256
- return null;
257
- }
258
- }
259
- return null;
260
- }
261
- function buildContinueFollowupPayload(source, assistant) {
262
- if (!source || typeof source !== 'object') {
263
- return null;
264
- }
265
- const chatSeed = extractChatSeedFromCapturedRequest(source);
266
- const model = chatSeed?.model;
267
- const originalMessages = chatSeed?.messages ? cloneJson(chatSeed.messages) : [];
268
- const originalTools = chatSeed?.tools ? cloneJson(chatSeed.tools) : undefined;
269
- const originalParameters = (() => {
270
- const direct = source.parameters;
271
- if (direct && typeof direct === 'object' && !Array.isArray(direct)) {
272
- return cloneJson(direct);
273
- }
274
- // Backward/compat: captured request might be a raw `/v1/responses` payload with
275
- // top-level parameters (max_output_tokens, temperature, ...), not nested under `parameters`.
276
- const record = source;
277
- const allowed = new Set([
278
- 'temperature',
279
- 'top_p',
280
- 'max_output_tokens',
281
- 'seed',
282
- 'logit_bias',
283
- 'user',
284
- 'parallel_tool_calls',
285
- 'tool_choice',
286
- 'response_format',
287
- 'stream'
288
- ]);
289
- const out = {};
290
- if (record.max_output_tokens === undefined && record.max_tokens !== undefined) {
291
- out.max_output_tokens = record.max_tokens;
292
- }
293
- for (const key of Object.keys(record)) {
294
- if (!allowed.has(key))
295
- continue;
296
- out[key] = record[key];
297
- }
298
- return Object.keys(out).length ? cloneJson(out) : undefined;
299
- })();
300
- const parametersForFollowup = originalParameters
301
- ? (() => {
302
- const cloned = cloneJson(originalParameters);
303
- delete cloned.stream;
304
- return cloned;
305
- })()
306
- : undefined;
307
- const messages = [...originalMessages];
308
- if (assistant && typeof assistant === 'object' && !Array.isArray(assistant)) {
309
- messages.push(cloneJson(assistant));
310
- }
311
- messages.push({
312
- role: 'user',
313
- content: '继续执行'
314
- });
315
- const chatPayload = {
316
- ...(model ? { model } : {}),
317
- messages,
318
- ...(originalTools ? { tools: originalTools } : {})
319
- };
320
- return buildResponsesRequestFromChat(chatPayload, {
321
- stream: false,
322
- ...(parametersForFollowup ? { parameters: parametersForFollowup } : {})
323
- }).request;
324
- }
325
253
  function hasCompactionFlag(record) {
326
254
  const flag = record.compactionRequest;
327
255
  if (flag === true) {
@@ -332,38 +260,3 @@ function hasCompactionFlag(record) {
332
260
  }
333
261
  return false;
334
262
  }
335
- function extractAssistantMessage(baseResponse) {
336
- if (!baseResponse || typeof baseResponse !== 'object' || Array.isArray(baseResponse)) {
337
- return null;
338
- }
339
- const base = baseResponse;
340
- // OpenAI Chat shape
341
- const choices = Array.isArray(base.choices) ? base.choices : [];
342
- if (choices.length > 0) {
343
- const first = choices[0] && typeof choices[0] === 'object' && !Array.isArray(choices[0])
344
- ? choices[0]
345
- : null;
346
- const msg = first &&
347
- first.message &&
348
- typeof first.message === 'object' &&
349
- !Array.isArray(first.message)
350
- ? first.message
351
- : null;
352
- if (msg) {
353
- const content = typeof msg.content === 'string' ? msg.content.trim() : '';
354
- if (!content) {
355
- return null;
356
- }
357
- return cloneJson(msg);
358
- }
359
- }
360
- // OpenAI Responses shape
361
- const outputText = extractResponsesOutputText(base);
362
- if (outputText.length > 0) {
363
- return {
364
- role: 'assistant',
365
- content: outputText
366
- };
367
- }
368
- return null;
369
- }
@@ -1,9 +1,9 @@
1
1
  import { registerServerToolHandler } from '../registry.js';
2
2
  import { isCompactionRequest } from './compaction-detect.js';
3
- import { buildEntryAwareFollowupPayload, extractCapturedChatSeed } from './followup-request-builder.js';
3
+ import { extractCapturedChatSeed } from './followup-request-builder.js';
4
4
  const FLOW_ID = 'iflow_model_error_retry';
5
5
  const handler = async (ctx) => {
6
- if (!ctx.options.reenterPipeline) {
6
+ if (!ctx.capabilities.reenterPipeline) {
7
7
  return null;
8
8
  }
9
9
  const adapterRecord = ctx.adapterContext;
@@ -16,10 +16,10 @@ const handler = async (ctx) => {
16
16
  return null;
17
17
  }
18
18
  // 仅针对 openai-chat 协议 + iflow.* providerKey 的 /v1/responses 路径启用。
19
- if (ctx.options.providerProtocol !== 'openai-chat') {
19
+ if (ctx.providerProtocol !== 'openai-chat') {
20
20
  return null;
21
21
  }
22
- const entryEndpoint = (ctx.options.entryEndpoint || '').toLowerCase();
22
+ const entryEndpoint = (ctx.entryEndpoint || '').toLowerCase();
23
23
  if (!entryEndpoint.includes('/v1/responses')) {
24
24
  return null;
25
25
  }
@@ -46,22 +46,25 @@ const handler = async (ctx) => {
46
46
  if (isCompactionRequest(captured)) {
47
47
  return null;
48
48
  }
49
- const followupPayload = buildRetryFollowupPayload(captured, ctx.options.entryEndpoint || ctx.adapterContext?.entryEndpoint || '/v1/chat/completions');
50
- if (!followupPayload) {
49
+ const seed = extractCapturedChatSeed(captured);
50
+ if (!seed) {
51
51
  return null;
52
52
  }
53
53
  return {
54
- chatResponse: ctx.base,
55
- execution: {
56
- flowId: FLOW_ID,
57
- followup: {
58
- requestIdSuffix: ':retry',
59
- payload: followupPayload,
60
- metadata: {
61
- serverToolFollowup: true
54
+ flowId: FLOW_ID,
55
+ finalize: async () => ({
56
+ chatResponse: ctx.base,
57
+ execution: {
58
+ flowId: FLOW_ID,
59
+ followup: {
60
+ requestIdSuffix: ':retry',
61
+ entryEndpoint: ctx.entryEndpoint,
62
+ injection: {
63
+ ops: []
64
+ }
62
65
  }
63
66
  }
64
- }
67
+ })
65
68
  };
66
69
  };
67
70
  registerServerToolHandler('iflow_model_error_retry', handler, { trigger: 'auto' });
@@ -75,19 +78,6 @@ function getCapturedRequest(adapterContext) {
75
78
  }
76
79
  return captured;
77
80
  }
78
- function buildRetryFollowupPayload(source, entryEndpoint) {
79
- const seed = extractCapturedChatSeed(source);
80
- if (!seed) {
81
- return null;
82
- }
83
- return buildEntryAwareFollowupPayload({
84
- entryEndpoint,
85
- model: seed.model,
86
- messages: seed.messages,
87
- ...(seed.tools ? { tools: seed.tools } : {}),
88
- ...(seed.parameters ? { parameters: seed.parameters } : {})
89
- });
90
- }
91
81
  function hasCompactionFlag(record) {
92
82
  const flag = record.compactionRequest;
93
83
  if (flag === true) {