Просмотр исходного кода

fix: use streaming in agent loop (required by some providers)

Some OpenAI-compatible proxies (e.g. codex-for.me) reject non-streaming
requests with 400. Switch agent loop to stream mode and reassemble
tool_calls from chunked deltas.
majx_mac 8 месяцев назад
Родитель
Сommit
af4faab138
1 измененных файлов с 53 добавлено и 10 удалено
  1. 53 10
      packages/core/src/pipeline/agent.ts

+ 53 - 10
packages/core/src/pipeline/agent.ts

@@ -176,32 +176,75 @@ export async function runAgentLoop(
   let lastAssistantMessage = "";
   let lastAssistantMessage = "";
 
 
   for (let turn = 0; turn < maxTurns; turn++) {
   for (let turn = 0; turn < maxTurns; turn++) {
-    const response = await config.client.chat.completions.create({
+    // Use streaming (some providers like codex-for.me require it)
+    const stream = await config.client.chat.completions.create({
       model: config.model,
       model: config.model,
       messages,
       messages,
       tools: TOOLS,
       tools: TOOLS,
       tool_choice: "auto",
       tool_choice: "auto",
+      stream: true,
     });
     });
 
 
-    const choice = response.choices[0];
-    const msg = choice.message;
+    // Accumulate streamed response into a complete message
+    let content = "";
+    const toolCalls: Map<number, { id: string; name: string; arguments: string }> = new Map();
 
 
-    // Add assistant message to history
-    messages.push(msg);
+    for await (const chunk of stream) {
+      const delta = chunk.choices[0]?.delta;
+      if (!delta) continue;
+
+      if (delta.content) {
+        content += delta.content;
+      }
+
+      if (delta.tool_calls) {
+        for (const tc of delta.tool_calls) {
+          const existing = toolCalls.get(tc.index);
+          if (existing) {
+            if (tc.function?.arguments) {
+              existing.arguments += tc.function.arguments;
+            }
+          } else {
+            toolCalls.set(tc.index, {
+              id: tc.id ?? "",
+              name: tc.function?.name ?? "",
+              arguments: tc.function?.arguments ?? "",
+            });
+          }
+        }
+      }
+    }
+
+    // Build the assistant message for history
+    const assembledToolCalls = toolCalls.size > 0
+      ? [...toolCalls.entries()]
+          .sort(([a], [b]) => a - b)
+          .map(([, tc]) => ({
+            id: tc.id,
+            type: "function" as const,
+            function: { name: tc.name, arguments: tc.arguments },
+          }))
+      : undefined;
+
+    messages.push({
+      role: "assistant" as const,
+      content: content || null,
+      ...(assembledToolCalls ? { tool_calls: assembledToolCalls } : {}),
+    } as OpenAI.Chat.Completions.ChatCompletionMessageParam);
 
 
     // If model produced text, emit it
     // If model produced text, emit it
-    if (msg.content) {
-      lastAssistantMessage = msg.content;
-      options?.onMessage?.(msg.content);
+    if (content) {
+      lastAssistantMessage = content;
+      options?.onMessage?.(content);
     }
     }
 
 
     // If no tool calls, we're done
     // If no tool calls, we're done
-    if (!msg.tool_calls || msg.tool_calls.length === 0) {
+    if (!assembledToolCalls || assembledToolCalls.length === 0) {
       break;
       break;
     }
     }
 
 
     // Execute tool calls
     // Execute tool calls
-    for (const toolCall of msg.tool_calls) {
+    for (const toolCall of assembledToolCalls) {
       const fn = toolCall.function;
       const fn = toolCall.function;
       const args = JSON.parse(fn.arguments) as Record<string, unknown>;
       const args = JSON.parse(fn.arguments) as Record<string, unknown>;