u-foo 2.5.7 → 2.5.9

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.
@@ -1,5 +1,9 @@
1
1
  const { randomUUID } = require("crypto");
2
2
  const { loadConfig, defaultAgentModelForProvider, sameModelProvider } = require("../config");
3
+ const {
4
+ readKimiAccessToken,
5
+ resolveKimiUpstreamCredentials,
6
+ } = require("../agents/providers/credentials/kimi");
3
7
  const { runToolCall } = require("./dispatch");
4
8
  const { getReadToolDescription } = require("../agents/prompts/native/toolDescriptions/read");
5
9
  const { getWriteToolDescription } = require("../agents/prompts/native/toolDescriptions/write");
@@ -9,6 +13,8 @@ const { getBashToolDescription } = require("../agents/prompts/native/toolDescrip
9
13
  const CORE_TOOL_NAMES = new Set(["read", "write", "edit", "bash"]);
10
14
  const DEFAULT_OPENAI_BASE_URL = "https://api.openai.com/v1";
11
15
  const DEFAULT_ANTHROPIC_BASE_URL = "https://api.anthropic.com/v1";
16
+ const DEFAULT_KIMI_BASE_URL = "https://api.kimi.com/coding/v1";
17
+ const DEFAULT_KIMI_MODEL = "k3";
12
18
  // Claude Code SDK defaults to no turn limit; built-in agents cap at 30 (DreamTask)
13
19
  // to 200 (fork). We count individual tool calls (not turns), so 100 leaves headroom
14
20
  // for non-trivial tasks while still catching runaway loops. Override via env.
@@ -107,6 +113,7 @@ function normalizeProvider(value = "") {
107
113
  if (!text) return "";
108
114
  if (text === "codex" || text === "codex-cli" || text === "codex-code") return "openai";
109
115
  if (text === "claude" || text === "claude-cli" || text === "claude-code") return "anthropic";
116
+ if (text === "kimi" || text === "kimi-code" || text === "moonshot") return "kimi";
110
117
  if (text === "openai" || text === "anthropic") return text;
111
118
  return text;
112
119
  }
@@ -116,6 +123,7 @@ function resolveTransport({ provider = "", baseUrl = "" } = {}) {
116
123
  const url = String(baseUrl || "").trim().toLowerCase();
117
124
 
118
125
  if (normalizedProvider === "anthropic") return "anthropic-messages";
126
+ if (normalizedProvider === "kimi") return "openai-chat";
119
127
  if (url.includes("anthropic.com")) return "anthropic-messages";
120
128
  if (/\/messages(?:$|[/?#])/.test(url) && !/\/chat\/completions(?:$|[/?#])/.test(url)) {
121
129
  return "anthropic-messages";
@@ -141,12 +149,14 @@ function resolveRuntimeConfig({ workspaceRoot = process.cwd(), provider = "", mo
141
149
  model
142
150
  || process.env.UFOO_UCODE_MODEL
143
151
  || configuredModel
144
- || defaultAgentModelForProvider(selectedProvider)
152
+ || (selectedProvider === "kimi" ? DEFAULT_KIMI_MODEL : defaultAgentModelForProvider(selectedProvider))
145
153
  ).trim();
146
154
 
147
155
  const defaultBaseUrl = selectedProvider === "anthropic"
148
156
  ? String(process.env.ANTHROPIC_BASE_URL || DEFAULT_ANTHROPIC_BASE_URL)
149
- : String(process.env.OPENAI_BASE_URL || DEFAULT_OPENAI_BASE_URL);
157
+ : selectedProvider === "kimi"
158
+ ? DEFAULT_KIMI_BASE_URL
159
+ : String(process.env.OPENAI_BASE_URL || DEFAULT_OPENAI_BASE_URL);
150
160
 
151
161
  const baseUrl = String(
152
162
  process.env.UFOO_UCODE_BASE_URL
@@ -154,19 +164,38 @@ function resolveRuntimeConfig({ workspaceRoot = process.cwd(), provider = "", mo
154
164
  || defaultBaseUrl
155
165
  ).trim();
156
166
 
157
- const apiKey = String(
167
+ const explicitApiKey = String(
158
168
  process.env.UFOO_UCODE_API_KEY
159
169
  || config.ucodeApiKey
160
- || (selectedProvider === "openai" ? process.env.OPENAI_API_KEY : "")
161
- || (selectedProvider === "anthropic" ? process.env.ANTHROPIC_API_KEY : "")
162
170
  || ""
163
171
  ).trim();
172
+ let apiKey = explicitApiKey;
173
+ let apiKeySource = explicitApiKey ? "explicit" : "";
174
+ let kimiCredentialState = "";
175
+ if (!apiKey && selectedProvider === "kimi") {
176
+ const credential = readKimiAccessToken({ env: process.env });
177
+ if (credential && credential.accessToken) {
178
+ apiKey = String(credential.accessToken).trim();
179
+ apiKeySource = "kimi-credential";
180
+ kimiCredentialState = String(credential.state || "");
181
+ }
182
+ }
183
+ if (!apiKey) {
184
+ apiKey = String(
185
+ (selectedProvider === "openai" ? process.env.OPENAI_API_KEY : "")
186
+ || (selectedProvider === "anthropic" ? process.env.ANTHROPIC_API_KEY : "")
187
+ || ""
188
+ ).trim();
189
+ if (apiKey) apiKeySource = "env";
190
+ }
164
191
 
165
192
  return {
166
193
  provider: selectedProvider,
167
194
  model: selectedModel,
168
195
  baseUrl,
169
196
  apiKey,
197
+ apiKeySource,
198
+ kimiCredentialState,
170
199
  transport: resolveTransport({ provider: selectedProvider, baseUrl }),
171
200
  };
172
201
  }
@@ -437,34 +466,22 @@ function emitPhase(callback, event = {}) {
437
466
  }
438
467
  }
439
468
 
440
- async function runOpenAiLikeTurn({
469
+ // Shared SSE transport skeleton: POST the payload, then read the stream as
470
+ // SSE blocks, dispatch each non-[DONE] block to onEvent, and stop after the
471
+ // batch that carried [DONE]. Timeout/cancel translation and request cleanup
472
+ // live here so each protocol turn only declares its event handling.
473
+ async function runSseRequest({
441
474
  url = "",
442
- apiKey = "",
443
- model = "",
444
- messages = [],
445
- onTextDelta = null,
446
- onThinkingDelta = null,
447
- onPhase = null,
475
+ headers = {},
476
+ payload = {},
448
477
  signal = null,
449
478
  timeoutMs = 300000,
479
+ onPhase = null,
480
+ onNonStream,
481
+ onEvent,
482
+ onTail = null,
483
+ buildResult,
450
484
  } = {}) {
451
- const payload = {
452
- model,
453
- max_tokens: resolveMaxTokens(DEFAULT_OPENAI_MAX_TOKENS),
454
- messages,
455
- tools: buildCoreToolSpecs(),
456
- tool_choice: "auto",
457
- stream: true,
458
- temperature: 0,
459
- };
460
-
461
- const headers = {
462
- "content-type": "application/json",
463
- };
464
- if (apiKey) {
465
- headers.authorization = `Bearer ${apiKey}`;
466
- }
467
-
468
485
  const request = createRequestController({ signal, timeoutMs });
469
486
 
470
487
  emitPhase(onPhase, { type: "request_start" });
@@ -484,28 +501,12 @@ async function runOpenAiLikeTurn({
484
501
 
485
502
  if (!response.body || typeof response.body.getReader !== "function") {
486
503
  const data = await response.json();
487
- const message = data && data.choices && data.choices[0] && data.choices[0].message
488
- ? data.choices[0].message
489
- : {};
490
- const text = typeof message.content === "string" ? message.content : "";
491
- const toolCalls = Array.isArray(message.tool_calls) ? message.tool_calls : [];
492
- if (text && typeof onTextDelta === "function") {
493
- onTextDelta(text);
494
- }
495
- return {
496
- text,
497
- toolCalls,
498
- };
504
+ return onNonStream(data);
499
505
  }
500
506
 
501
507
  const reader = response.body.getReader();
502
508
  const decoder = new TextDecoder();
503
- const toolCallMap = new Map();
504
509
  let rawBuffer = "";
505
- let responseText = "";
506
- const announcedToolNames = new Set();
507
- let nextSyntheticIndex = 0;
508
- let lastSyntheticIndex = -1;
509
510
  let sawDone = false;
510
511
 
511
512
  while (true) {
@@ -517,96 +518,180 @@ async function runOpenAiLikeTurn({
517
518
  rawBuffer = parsed.rest;
518
519
 
519
520
  for (const block of parsed.blocks) {
520
- const payloadText = parseSseDataBlock(block);
521
- if (!payloadText) continue;
522
- if (payloadText === "[DONE]") {
523
- // Stop reading after this batch, but keep the buffered tail and
524
- // finish the blocks already parsed alongside [DONE] instead of
525
- // silently dropping them.
521
+ const { event, data } = parseSseEventBlock(block);
522
+ if (!data) continue;
523
+ if (data === "[DONE]") {
524
+ // Stop reading after this batch instead of waiting for the server
525
+ // to close the connection, but keep the buffered tail and finish
526
+ // the blocks already parsed alongside [DONE] instead of silently
527
+ // dropping them.
526
528
  sawDone = true;
527
529
  continue;
528
530
  }
529
531
 
530
- const chunk = parseJsonSafe(payloadText, null);
531
- if (!chunk || typeof chunk !== "object") continue;
532
+ onEvent({ event, data });
533
+ }
532
534
 
533
- const choice = chunk.choices && chunk.choices[0] ? chunk.choices[0] : null;
534
- if (!choice || typeof choice !== "object") continue;
535
+ if (sawDone) break;
536
+ }
535
537
 
536
- const delta = choice.delta && typeof choice.delta === "object" ? choice.delta : {};
538
+ if (typeof onTail === "function") {
539
+ onTail(rawBuffer);
540
+ }
537
541
 
538
- const reasoningChunk = typeof delta.reasoning_content === "string"
539
- ? delta.reasoning_content
540
- : (typeof delta.reasoning === "string" ? delta.reasoning : "");
541
- if (reasoningChunk) {
542
- emitPhase(onPhase, { type: "thinking_delta", text: reasoningChunk });
543
- if (typeof onThinkingDelta === "function") {
544
- onThinkingDelta(reasoningChunk);
545
- }
542
+ return buildResult();
543
+ } catch (err) {
544
+ if (request.timedOut()) {
545
+ const timeoutError = new Error(`CLI timeout (${normalizeTimeoutMs(timeoutMs)}ms)`);
546
+ timeoutError.code = "timeout";
547
+ throw timeoutError;
548
+ }
549
+ if (signal && typeof signal === "object" && signal.aborted) {
550
+ const cancelError = new Error("CLI cancelled");
551
+ cancelError.code = "cancelled";
552
+ throw cancelError;
553
+ }
554
+ throw err;
555
+ } finally {
556
+ request.cleanup();
557
+ }
558
+ }
559
+
560
+ async function runOpenAiLikeTurn({
561
+ url = "",
562
+ apiKey = "",
563
+ model = "",
564
+ provider = "",
565
+ messages = [],
566
+ onTextDelta = null,
567
+ onThinkingDelta = null,
568
+ onPhase = null,
569
+ signal = null,
570
+ timeoutMs = 300000,
571
+ } = {}) {
572
+ const payload = {
573
+ model,
574
+ max_tokens: resolveMaxTokens(DEFAULT_OPENAI_MAX_TOKENS),
575
+ messages,
576
+ tools: buildCoreToolSpecs(),
577
+ tool_choice: "auto",
578
+ stream: true,
579
+ // Kimi k3 rejects any temperature other than 1.
580
+ temperature: normalizeProvider(provider) === "kimi" ? 1 : 0,
581
+ };
582
+
583
+ const headers = {
584
+ "content-type": "application/json",
585
+ };
586
+ if (apiKey) {
587
+ headers.authorization = `Bearer ${apiKey}`;
588
+ }
589
+
590
+ const toolCallMap = new Map();
591
+ const announcedToolNames = new Set();
592
+ let responseText = "";
593
+ let nextSyntheticIndex = 0;
594
+ let lastSyntheticIndex = -1;
595
+
596
+ return runSseRequest({
597
+ url,
598
+ headers,
599
+ payload,
600
+ signal,
601
+ timeoutMs,
602
+ onPhase,
603
+ onNonStream: (data) => {
604
+ const message = data && data.choices && data.choices[0] && data.choices[0].message
605
+ ? data.choices[0].message
606
+ : {};
607
+ const text = typeof message.content === "string" ? message.content : "";
608
+ const toolCalls = Array.isArray(message.tool_calls) ? message.tool_calls : [];
609
+ if (text && typeof onTextDelta === "function") {
610
+ onTextDelta(text);
611
+ }
612
+ return {
613
+ text,
614
+ toolCalls,
615
+ };
616
+ },
617
+ onEvent: ({ data }) => {
618
+ const chunk = parseJsonSafe(data, null);
619
+ if (!chunk || typeof chunk !== "object") return;
620
+
621
+ const choice = chunk.choices && chunk.choices[0] ? chunk.choices[0] : null;
622
+ if (!choice || typeof choice !== "object") return;
623
+
624
+ const delta = choice.delta && typeof choice.delta === "object" ? choice.delta : {};
625
+
626
+ const reasoningChunk = typeof delta.reasoning_content === "string"
627
+ ? delta.reasoning_content
628
+ : (typeof delta.reasoning === "string" ? delta.reasoning : "");
629
+ if (reasoningChunk) {
630
+ emitPhase(onPhase, { type: "thinking_delta", text: reasoningChunk });
631
+ if (typeof onThinkingDelta === "function") {
632
+ onThinkingDelta(reasoningChunk);
546
633
  }
634
+ }
547
635
 
548
- if (typeof delta.content === "string" && delta.content) {
549
- responseText += delta.content;
550
- emitPhase(onPhase, { type: "text_delta", text: delta.content });
551
- if (typeof onTextDelta === "function") {
552
- onTextDelta(delta.content);
553
- }
636
+ if (typeof delta.content === "string" && delta.content) {
637
+ responseText += delta.content;
638
+ emitPhase(onPhase, { type: "text_delta", text: delta.content });
639
+ if (typeof onTextDelta === "function") {
640
+ onTextDelta(delta.content);
554
641
  }
642
+ }
555
643
 
556
- if (Array.isArray(delta.tool_calls)) {
557
- for (const callPart of delta.tool_calls) {
558
- let index;
559
- if (Number.isFinite(callPart.index)) {
560
- index = callPart.index;
561
- } else if (typeof callPart.id === "string" && callPart.id) {
562
- // Provider omitted index: a chunk carrying an id starts a new
563
- // call, so give it its own synthetic index instead of
564
- // collapsing every call into slot 0.
565
- while (toolCallMap.has(nextSyntheticIndex)) nextSyntheticIndex += 1;
566
- index = nextSyntheticIndex;
567
- nextSyntheticIndex += 1;
568
- lastSyntheticIndex = index;
569
- } else if (lastSyntheticIndex >= 0) {
570
- // No index and no id: continuation of the latest synthetic call.
571
- index = lastSyntheticIndex;
572
- } else {
573
- index = 0;
574
- }
575
- const previous = toolCallMap.get(index) || {
576
- id: "",
577
- type: "function",
578
- function: {
579
- name: "",
580
- arguments: "",
581
- },
582
- };
644
+ if (Array.isArray(delta.tool_calls)) {
645
+ for (const callPart of delta.tool_calls) {
646
+ let index;
647
+ if (Number.isFinite(callPart.index)) {
648
+ index = callPart.index;
649
+ } else if (typeof callPart.id === "string" && callPart.id) {
650
+ // Provider omitted index: a chunk carrying an id starts a new
651
+ // call, so give it its own synthetic index instead of
652
+ // collapsing every call into slot 0.
653
+ while (toolCallMap.has(nextSyntheticIndex)) nextSyntheticIndex += 1;
654
+ index = nextSyntheticIndex;
655
+ nextSyntheticIndex += 1;
656
+ lastSyntheticIndex = index;
657
+ } else if (lastSyntheticIndex >= 0) {
658
+ // No index and no id: continuation of the latest synthetic call.
659
+ index = lastSyntheticIndex;
660
+ } else {
661
+ index = 0;
662
+ }
663
+ const previous = toolCallMap.get(index) || {
664
+ id: "",
665
+ type: "function",
666
+ function: {
667
+ name: "",
668
+ arguments: "",
669
+ },
670
+ };
583
671
 
584
- if (typeof callPart.id === "string" && callPart.id) previous.id = callPart.id;
585
- if (callPart.function && typeof callPart.function === "object") {
586
- if (typeof callPart.function.name === "string" && callPart.function.name) {
587
- previous.function.name = callPart.function.name;
588
- }
589
- if (typeof callPart.function.arguments === "string" && callPart.function.arguments) {
590
- previous.function.arguments += callPart.function.arguments;
591
- }
672
+ if (typeof callPart.id === "string" && callPart.id) previous.id = callPart.id;
673
+ if (callPart.function && typeof callPart.function === "object") {
674
+ if (typeof callPart.function.name === "string" && callPart.function.name) {
675
+ previous.function.name = callPart.function.name;
676
+ }
677
+ if (typeof callPart.function.arguments === "string" && callPart.function.arguments) {
678
+ previous.function.arguments += callPart.function.arguments;
592
679
  }
680
+ }
593
681
 
594
- toolCallMap.set(index, previous);
682
+ toolCallMap.set(index, previous);
595
683
 
596
- const toolName = previous.function.name;
597
- const announceKey = `${index}:${toolName}`;
598
- if (toolName && !announcedToolNames.has(announceKey)) {
599
- announcedToolNames.add(announceKey);
600
- emitPhase(onPhase, { type: "tool_request", name: toolName });
601
- }
684
+ const toolName = previous.function.name;
685
+ const announceKey = `${index}:${toolName}`;
686
+ if (toolName && !announcedToolNames.has(announceKey)) {
687
+ announcedToolNames.add(announceKey);
688
+ emitPhase(onPhase, { type: "tool_request", name: toolName });
602
689
  }
603
690
  }
604
691
  }
605
-
606
- if (sawDone) break;
607
- }
608
-
609
- if (rawBuffer.trim()) {
692
+ },
693
+ onTail: (rawBuffer) => {
694
+ if (!rawBuffer.trim()) return;
610
695
  const fallbackBlock = parseSseDataBlock(rawBuffer);
611
696
  if (fallbackBlock && fallbackBlock !== "[DONE]") {
612
697
  const chunk = parseJsonSafe(fallbackBlock, null);
@@ -618,29 +703,14 @@ async function runOpenAiLikeTurn({
618
703
  }
619
704
  }
620
705
  }
621
- }
622
-
623
- return {
706
+ },
707
+ buildResult: () => ({
624
708
  text: responseText,
625
709
  toolCalls: Array.from(toolCallMap.entries())
626
710
  .sort((a, b) => a[0] - b[0])
627
711
  .map((entry) => entry[1]),
628
- };
629
- } catch (err) {
630
- if (request.timedOut()) {
631
- const timeoutError = new Error(`CLI timeout (${normalizeTimeoutMs(timeoutMs)}ms)`);
632
- timeoutError.code = "timeout";
633
- throw timeoutError;
634
- }
635
- if (signal && typeof signal === "object" && signal.aborted) {
636
- const cancelError = new Error("CLI cancelled");
637
- cancelError.code = "cancelled";
638
- throw cancelError;
639
- }
640
- throw err;
641
- } finally {
642
- request.cleanup();
643
- }
712
+ }),
713
+ });
644
714
  }
645
715
 
646
716
  function normalizeAnthropicMessageContent(raw = []) {
@@ -713,25 +783,19 @@ async function runAnthropicTurn({
713
783
  headers["x-api-key"] = apiKey;
714
784
  }
715
785
 
716
- const request = createRequestController({ signal, timeoutMs });
717
-
718
- emitPhase(onPhase, { type: "request_start" });
719
-
720
- try {
721
- const response = await fetch(url, {
722
- method: "POST",
723
- headers,
724
- body: JSON.stringify(payload),
725
- signal: request.signal,
726
- });
727
-
728
- if (!response.ok) {
729
- const body = await response.text().catch(() => "");
730
- throw new Error(`provider request failed (${response.status}): ${clipText(body, 500)}`);
731
- }
732
-
733
- if (!response.body || typeof response.body.getReader !== "function") {
734
- const data = await response.json();
786
+ const blockMap = new Map();
787
+ let responseText = "";
788
+ let nextSyntheticBlockIndex = 0;
789
+ let lastBlockIndex = -1;
790
+
791
+ return runSseRequest({
792
+ url,
793
+ headers,
794
+ payload,
795
+ signal,
796
+ timeoutMs,
797
+ onPhase,
798
+ onNonStream: (data) => {
735
799
  const content = normalizeAnthropicMessageContent(data && data.content);
736
800
  const text = content
737
801
  .filter((item) => item.type === "text")
@@ -745,270 +809,181 @@ async function runAnthropicTurn({
745
809
  assistantContent: content,
746
810
  toolCalls: extractAnthropicToolCalls(content),
747
811
  };
748
- }
749
-
750
- const reader = response.body.getReader();
751
- const decoder = new TextDecoder();
752
- const blockMap = new Map();
753
- let rawBuffer = "";
754
- let responseText = "";
755
- let nextSyntheticBlockIndex = 0;
756
- let lastBlockIndex = -1;
757
- let sawDone = false;
758
-
759
- while (true) {
760
- const { done, value } = await reader.read();
761
- if (done) break;
762
-
763
- rawBuffer += decoder.decode(value, { stream: true });
764
- const parsed = parseSseBlocks(rawBuffer);
765
- rawBuffer = parsed.rest;
766
-
767
- for (const rawBlock of parsed.blocks) {
768
- const { event, data } = parseSseEventBlock(rawBlock);
769
- if (!data) continue;
770
- if (data === "[DONE]") {
771
- // Stop reading after this batch instead of waiting for the server
772
- // to close the connection, mirroring the OpenAI transport.
773
- sawDone = true;
774
- continue;
775
- }
776
-
777
- const payloadChunk = parseJsonSafe(data, null);
778
- if (!payloadChunk || typeof payloadChunk !== "object") continue;
812
+ },
813
+ onEvent: ({ event, data }) => {
814
+ const payloadChunk = parseJsonSafe(data, null);
815
+ if (!payloadChunk || typeof payloadChunk !== "object") return;
816
+
817
+ if (event === "error") {
818
+ const errMsg = payloadChunk.error && payloadChunk.error.message
819
+ ? String(payloadChunk.error.message)
820
+ : "anthropic stream error";
821
+ throw new Error(errMsg);
822
+ }
779
823
 
780
- if (event === "error") {
781
- const errMsg = payloadChunk.error && payloadChunk.error.message
782
- ? String(payloadChunk.error.message)
783
- : "anthropic stream error";
784
- throw new Error(errMsg);
824
+ if (event === "content_block_start") {
825
+ let index;
826
+ if (Number.isFinite(payloadChunk.index)) {
827
+ index = payloadChunk.index;
828
+ } else {
829
+ // Provider omitted index: each start opens a new block, so give
830
+ // it its own synthetic index instead of collapsing every block
831
+ // into slot 0.
832
+ while (blockMap.has(nextSyntheticBlockIndex)) nextSyntheticBlockIndex += 1;
833
+ index = nextSyntheticBlockIndex;
834
+ nextSyntheticBlockIndex += 1;
785
835
  }
786
-
787
- if (event === "content_block_start") {
788
- let index;
789
- if (Number.isFinite(payloadChunk.index)) {
790
- index = payloadChunk.index;
791
- } else {
792
- // Provider omitted index: each start opens a new block, so give
793
- // it its own synthetic index instead of collapsing every block
794
- // into slot 0.
795
- while (blockMap.has(nextSyntheticBlockIndex)) nextSyntheticBlockIndex += 1;
796
- index = nextSyntheticBlockIndex;
797
- nextSyntheticBlockIndex += 1;
798
- }
799
- lastBlockIndex = index;
800
- const contentBlock = payloadChunk.content_block && typeof payloadChunk.content_block === "object"
801
- ? payloadChunk.content_block
802
- : {};
803
-
804
- if (contentBlock.type === "text") {
805
- blockMap.set(index, {
806
- order: index,
807
- type: "text",
808
- text: String(contentBlock.text || ""),
809
- });
810
- } else if (contentBlock.type === "thinking") {
811
- blockMap.set(index, {
812
- order: index,
813
- type: "thinking",
814
- text: String(contentBlock.thinking || ""),
815
- });
816
- } else if (contentBlock.type === "tool_use") {
817
- blockMap.set(index, {
818
- order: index,
819
- type: "tool_use",
820
- id: String(contentBlock.id || ""),
821
- name: String(contentBlock.name || ""),
822
- input: contentBlock.input && typeof contentBlock.input === "object" && !Array.isArray(contentBlock.input)
823
- ? { ...contentBlock.input }
824
- : {},
825
- inputJson: "",
826
- });
827
- const toolName = String(contentBlock.name || "");
828
- if (toolName) {
829
- emitPhase(onPhase, { type: "tool_request", name: toolName });
830
- }
836
+ lastBlockIndex = index;
837
+ const contentBlock = payloadChunk.content_block && typeof payloadChunk.content_block === "object"
838
+ ? payloadChunk.content_block
839
+ : {};
840
+
841
+ if (contentBlock.type === "text") {
842
+ blockMap.set(index, {
843
+ order: index,
844
+ type: "text",
845
+ text: String(contentBlock.text || ""),
846
+ });
847
+ } else if (contentBlock.type === "thinking") {
848
+ blockMap.set(index, {
849
+ order: index,
850
+ type: "thinking",
851
+ text: String(contentBlock.thinking || ""),
852
+ });
853
+ } else if (contentBlock.type === "tool_use") {
854
+ blockMap.set(index, {
855
+ order: index,
856
+ type: "tool_use",
857
+ id: String(contentBlock.id || ""),
858
+ name: String(contentBlock.name || ""),
859
+ input: contentBlock.input && typeof contentBlock.input === "object" && !Array.isArray(contentBlock.input)
860
+ ? { ...contentBlock.input }
861
+ : {},
862
+ inputJson: "",
863
+ });
864
+ const toolName = String(contentBlock.name || "");
865
+ if (toolName) {
866
+ emitPhase(onPhase, { type: "tool_request", name: toolName });
831
867
  }
832
- continue;
833
868
  }
869
+ return;
870
+ }
834
871
 
835
- if (event === "content_block_delta") {
836
- let index;
837
- if (Number.isFinite(payloadChunk.index)) {
838
- index = payloadChunk.index;
839
- } else if (lastBlockIndex >= 0) {
840
- // No index: continuation of the most recently started block.
841
- index = lastBlockIndex;
842
- } else {
843
- index = 0;
844
- }
845
- const delta = payloadChunk.delta && typeof payloadChunk.delta === "object"
846
- ? payloadChunk.delta
847
- : {};
848
- const current = blockMap.get(index) || { order: index, type: "text", text: "" };
849
-
850
- if (delta.type === "text_delta") {
851
- const deltaText = String(delta.text || "");
852
- current.type = "text";
853
- current.text = `${String(current.text || "")}${deltaText}`;
854
- blockMap.set(index, current);
855
- if (deltaText) {
856
- responseText += deltaText;
857
- emitPhase(onPhase, { type: "text_delta", text: deltaText });
858
- if (typeof onTextDelta === "function") {
859
- onTextDelta(deltaText);
860
- }
872
+ if (event === "content_block_delta") {
873
+ let index;
874
+ if (Number.isFinite(payloadChunk.index)) {
875
+ index = payloadChunk.index;
876
+ } else if (lastBlockIndex >= 0) {
877
+ // No index: continuation of the most recently started block.
878
+ index = lastBlockIndex;
879
+ } else {
880
+ index = 0;
881
+ }
882
+ const delta = payloadChunk.delta && typeof payloadChunk.delta === "object"
883
+ ? payloadChunk.delta
884
+ : {};
885
+ const current = blockMap.get(index) || { order: index, type: "text", text: "" };
886
+
887
+ if (delta.type === "text_delta") {
888
+ const deltaText = String(delta.text || "");
889
+ current.type = "text";
890
+ current.text = `${String(current.text || "")}${deltaText}`;
891
+ blockMap.set(index, current);
892
+ if (deltaText) {
893
+ responseText += deltaText;
894
+ emitPhase(onPhase, { type: "text_delta", text: deltaText });
895
+ if (typeof onTextDelta === "function") {
896
+ onTextDelta(deltaText);
861
897
  }
862
- continue;
863
898
  }
899
+ return;
900
+ }
864
901
 
865
- if (delta.type === "thinking_delta") {
866
- const deltaText = String(delta.thinking || "");
867
- current.type = "thinking";
868
- current.text = `${String(current.text || "")}${deltaText}`;
869
- blockMap.set(index, current);
870
- if (deltaText) {
871
- emitPhase(onPhase, { type: "thinking_delta", text: deltaText });
872
- if (typeof onThinkingDelta === "function") {
873
- onThinkingDelta(deltaText);
874
- }
902
+ if (delta.type === "thinking_delta") {
903
+ const deltaText = String(delta.thinking || "");
904
+ current.type = "thinking";
905
+ current.text = `${String(current.text || "")}${deltaText}`;
906
+ blockMap.set(index, current);
907
+ if (deltaText) {
908
+ emitPhase(onPhase, { type: "thinking_delta", text: deltaText });
909
+ if (typeof onThinkingDelta === "function") {
910
+ onThinkingDelta(deltaText);
875
911
  }
876
- continue;
877
912
  }
913
+ return;
914
+ }
878
915
 
879
- if (delta.type === "input_json_delta") {
880
- current.type = "tool_use";
881
- current.inputJson = `${String(current.inputJson || "")}${String(delta.partial_json || "")}`;
882
- blockMap.set(index, current);
883
- continue;
884
- }
916
+ if (delta.type === "input_json_delta") {
917
+ current.type = "tool_use";
918
+ current.inputJson = `${String(current.inputJson || "")}${String(delta.partial_json || "")}`;
919
+ blockMap.set(index, current);
920
+ return;
885
921
  }
886
922
  }
923
+ },
924
+ buildResult: () => {
925
+ const assistantContent = Array.from(blockMap.values())
926
+ .sort((a, b) => a.order - b.order)
927
+ .filter((item) => item.type !== "thinking")
928
+ .map((item) => {
929
+ if (item.type === "text") {
930
+ return {
931
+ type: "text",
932
+ text: String(item.text || ""),
933
+ };
934
+ }
887
935
 
888
- if (sawDone) break;
889
- }
890
-
891
- const assistantContent = Array.from(blockMap.values())
892
- .sort((a, b) => a.order - b.order)
893
- .filter((item) => item.type !== "thinking")
894
- .map((item) => {
895
- if (item.type === "text") {
936
+ const inputFromDelta = normalizeToolCallArgs(item.inputJson || "");
937
+ const mergedInput = {
938
+ ...(item.input && typeof item.input === "object" ? item.input : {}),
939
+ ...(inputFromDelta && typeof inputFromDelta === "object" ? inputFromDelta : {}),
940
+ };
896
941
  return {
897
- type: "text",
898
- text: String(item.text || ""),
942
+ type: "tool_use",
943
+ id: String(item.id || `tool_${randomUUID()}`),
944
+ name: String(item.name || ""),
945
+ input: mergedInput,
899
946
  };
900
- }
901
-
902
- const inputFromDelta = normalizeToolCallArgs(item.inputJson || "");
903
- const mergedInput = {
904
- ...(item.input && typeof item.input === "object" ? item.input : {}),
905
- ...(inputFromDelta && typeof inputFromDelta === "object" ? inputFromDelta : {}),
906
- };
907
- return {
908
- type: "tool_use",
909
- id: String(item.id || `tool_${randomUUID()}`),
910
- name: String(item.name || ""),
911
- input: mergedInput,
912
- };
913
- });
947
+ });
914
948
 
915
- if (!responseText) {
916
- responseText = assistantContent
917
- .filter((item) => item.type === "text")
918
- .map((item) => item.text)
919
- .join("");
920
- }
949
+ if (!responseText) {
950
+ responseText = assistantContent
951
+ .filter((item) => item.type === "text")
952
+ .map((item) => item.text)
953
+ .join("");
954
+ }
921
955
 
922
- return {
923
- text: responseText,
924
- assistantContent,
925
- toolCalls: extractAnthropicToolCalls(assistantContent),
926
- };
927
- } catch (err) {
928
- if (request.timedOut()) {
929
- const timeoutError = new Error(`CLI timeout (${normalizeTimeoutMs(timeoutMs)}ms)`);
930
- timeoutError.code = "timeout";
931
- throw timeoutError;
932
- }
933
- if (signal && typeof signal === "object" && signal.aborted) {
934
- const cancelError = new Error("CLI cancelled");
935
- cancelError.code = "cancelled";
936
- throw cancelError;
937
- }
938
- throw err;
939
- } finally {
940
- request.cleanup();
941
- }
956
+ return {
957
+ text: responseText,
958
+ assistantContent,
959
+ toolCalls: extractAnthropicToolCalls(assistantContent),
960
+ };
961
+ },
962
+ });
942
963
  }
943
964
 
944
- async function runNativeLoopOpenAi({
945
- workspaceRoot = process.cwd(),
946
- prompt = "",
947
- systemPrompt = "",
948
- historyMessages = [],
949
- model = "",
950
- baseUrl = "",
951
- apiKey = "",
952
- timeoutMs = 300000,
953
- onStreamDelta = null,
954
- onThinkingDelta = null,
955
- onPhase = null,
956
- onToolEvent = null,
957
- signal = null,
958
- guards,
959
- } = {}) {
960
- const requestModel = String(model || "").trim();
961
- if (!requestModel) {
962
- throw new Error("ucode model is not configured");
963
- }
964
-
965
- const requestUrl = resolveCompletionUrl(baseUrl);
966
- if (!requestUrl) {
967
- throw new Error("ucode baseUrl is not configured");
968
- }
969
-
970
- const messages = cloneMessageList(historyMessages);
971
- const systemText = String(systemPrompt || "").trim();
972
- const hasSystem = messages.some((entry) => String(entry.role || "").trim() === "system");
973
- if (systemText && !hasSystem) {
974
- messages.unshift({ role: "system", content: systemText });
975
- }
976
- messages.push({ role: "user", content: String(prompt || "") });
977
-
978
- let aggregated = "";
979
- let streamed = false;
980
- let toolCallsExecuted = 0;
981
- let toolErrors = 0;
982
- const toolBudget = resolveNativeToolBudget();
983
-
984
- while (true) {
985
- guards.ensureActive();
986
-
987
- const turnResult = await runOpenAiLikeTurn({
988
- url: requestUrl,
989
- apiKey,
990
- model: requestModel,
991
- messages,
992
- signal,
993
- timeoutMs,
994
- onPhase,
995
- onThinkingDelta,
996
- onTextDelta: (chunk) => {
997
- const text = String(chunk || "");
998
- if (!text) return;
999
- aggregated += text;
1000
- if (typeof onStreamDelta === "function") {
1001
- streamed = true;
1002
- onStreamDelta(text);
1003
- }
1004
- },
1005
- });
1006
-
1007
- const toolCalls = Array.isArray(turnResult.toolCalls)
1008
- ? turnResult.toolCalls.filter((call) => call && call.function && typeof call.function === "object")
1009
- : [];
1010
-
1011
- if (toolCalls.length === 0) {
965
+ // Transport descriptors: everything the shared native loop needs that differs
966
+ // between the OpenAI chat-completions and Anthropic messages protocols —
967
+ // request URL resolution, initial message shaping, turn execution, and
968
+ // assistant/tool-result message formatting.
969
+ const TRANSPORTS = {
970
+ "openai-chat": {
971
+ resolveUrl: resolveCompletionUrl,
972
+ prepareMessages({ messages, systemPrompt, prompt }) {
973
+ const systemText = String(systemPrompt || "").trim();
974
+ const hasSystem = messages.some((entry) => String(entry.role || "").trim() === "system");
975
+ if (systemText && !hasSystem) {
976
+ messages.unshift({ role: "system", content: systemText });
977
+ }
978
+ messages.push({ role: "user", content: String(prompt || "") });
979
+ },
980
+ runTurn: runOpenAiLikeTurn,
981
+ getToolCalls(turnResult) {
982
+ return Array.isArray(turnResult.toolCalls)
983
+ ? turnResult.toolCalls.filter((call) => call && call.function && typeof call.function === "object")
984
+ : [];
985
+ },
986
+ appendFinalAssistantMessage({ messages, turnResult }) {
1012
987
  const text = String(turnResult.text || "").trim();
1013
988
  if (text) {
1014
989
  messages.push({
@@ -1016,78 +991,114 @@ async function runNativeLoopOpenAi({
1016
991
  content: text,
1017
992
  });
1018
993
  }
1019
- if (!aggregated.trim() && text) {
1020
- aggregated = text;
994
+ },
995
+ prepareToolCalls({ messages, toolCalls }) {
996
+ const assistantToolCalls = [];
997
+ for (const call of toolCalls) {
998
+ const callId = String(call.id || `call_${randomUUID()}`);
999
+ const name = normalizeToolName(call.function.name || "");
1000
+ const args = normalizeToolCallArgs(call.function.arguments || "");
1001
+
1002
+ assistantToolCalls.push({
1003
+ id: callId,
1004
+ type: "function",
1005
+ function: {
1006
+ name: name || String(call.function.name || ""),
1007
+ arguments: toJsonString(args),
1008
+ },
1009
+ });
1021
1010
  }
1022
- return {
1023
- text: aggregated,
1024
- streamed,
1025
- toolCallsExecuted,
1026
- messages,
1027
- };
1028
- }
1029
1011
 
1030
- const assistantToolCalls = [];
1031
- for (const call of toolCalls) {
1032
- const callId = String(call.id || `call_${randomUUID()}`);
1033
- const name = normalizeToolName(call.function.name || "");
1034
- const args = normalizeToolCallArgs(call.function.arguments || "");
1035
-
1036
- assistantToolCalls.push({
1037
- id: callId,
1038
- type: "function",
1039
- function: {
1040
- name: name || String(call.function.name || ""),
1041
- arguments: toJsonString(args),
1042
- },
1043
- });
1044
- }
1045
-
1046
- if (assistantToolCalls.length === 0) {
1047
- return {
1048
- text: aggregated,
1049
- streamed,
1050
- toolCallsExecuted,
1051
- messages,
1052
- };
1053
- }
1012
+ if (assistantToolCalls.length === 0) return null;
1054
1013
 
1055
- messages.push({
1056
- role: "assistant",
1057
- content: null,
1058
- tool_calls: assistantToolCalls,
1059
- });
1014
+ messages.push({
1015
+ role: "assistant",
1016
+ content: null,
1017
+ tool_calls: assistantToolCalls,
1018
+ });
1060
1019
 
1061
- for (const toolCall of assistantToolCalls) {
1062
- const toolResult = runCoreTool({
1063
- tool: toolCall.function.name,
1020
+ return assistantToolCalls.map((toolCall) => ({
1021
+ name: toolCall.function.name,
1064
1022
  args: normalizeToolCallArgs(toolCall.function.arguments),
1065
- workspaceRoot,
1066
- onToolEvent,
1067
- });
1068
- toolCallsExecuted += 1;
1069
- if (!toolResult || toolResult.ok === false) {
1070
- toolErrors += 1;
1071
- }
1072
- enforceNativeToolBudget({
1073
- toolCallsExecuted,
1074
- toolErrors,
1075
- maxToolCalls: toolBudget.maxToolCalls,
1076
- maxToolErrors: toolBudget.maxToolErrors,
1077
- lastTool: toolCall.function.name,
1078
- lastError: toolResult && toolResult.error ? String(toolResult.error) : "",
1079
- });
1023
+ source: toolCall,
1024
+ }));
1025
+ },
1026
+ appendToolResult({ messages, call, toolResult }) {
1080
1027
  messages.push({
1081
1028
  role: "tool",
1082
- tool_call_id: toolCall.id,
1029
+ tool_call_id: call.source.id,
1083
1030
  content: clipText(toJsonString(toolResult), 12000),
1084
1031
  });
1085
- }
1086
- }
1032
+ },
1033
+ },
1034
+ "anthropic-messages": {
1035
+ resolveUrl: resolveAnthropicMessagesUrl,
1036
+ prepareMessages({ messages, prompt }) {
1037
+ messages.push({
1038
+ role: "user",
1039
+ content: String(prompt || ""),
1040
+ });
1041
+ },
1042
+ runTurn: runAnthropicTurn,
1043
+ getToolCalls(turnResult) {
1044
+ return Array.isArray(turnResult.toolCalls) ? turnResult.toolCalls : [];
1045
+ },
1046
+ appendFinalAssistantMessage({ messages, turnResult }) {
1047
+ const assistantContent = Array.isArray(turnResult.assistantContent)
1048
+ ? turnResult.assistantContent
1049
+ : [];
1050
+ if (assistantContent.length > 0) {
1051
+ messages.push({
1052
+ role: "assistant",
1053
+ content: assistantContent,
1054
+ });
1055
+ } else if (String(turnResult.text || "").trim()) {
1056
+ messages.push({
1057
+ role: "assistant",
1058
+ content: [
1059
+ {
1060
+ type: "text",
1061
+ text: String(turnResult.text || ""),
1062
+ },
1063
+ ],
1064
+ });
1065
+ }
1066
+ },
1067
+ prepareToolCalls({ messages, turnResult, toolCalls }) {
1068
+ const assistantContent = Array.isArray(turnResult.assistantContent)
1069
+ ? turnResult.assistantContent
1070
+ : [];
1087
1071
 
1088
- }
1072
+ messages.push({
1073
+ role: "assistant",
1074
+ content: assistantContent,
1075
+ });
1089
1076
 
1090
- async function runNativeLoopAnthropic({
1077
+ return toolCalls.map((call) => ({
1078
+ name: call.name,
1079
+ args: call.args,
1080
+ source: call,
1081
+ }));
1082
+ },
1083
+ appendToolResult({ collected, call, toolResult }) {
1084
+ collected.push({
1085
+ type: "tool_result",
1086
+ tool_use_id: String(call.source.id || ""),
1087
+ content: clipText(toJsonString(toolResult), 12000),
1088
+ is_error: Boolean(!toolResult || toolResult.ok === false),
1089
+ });
1090
+ },
1091
+ flushToolResults({ messages, collected }) {
1092
+ messages.push({
1093
+ role: "user",
1094
+ content: collected,
1095
+ });
1096
+ },
1097
+ },
1098
+ };
1099
+
1100
+ async function runNativeLoop({
1101
+ transport,
1091
1102
  workspaceRoot = process.cwd(),
1092
1103
  prompt = "",
1093
1104
  systemPrompt = "",
@@ -1095,6 +1106,7 @@ async function runNativeLoopAnthropic({
1095
1106
  model = "",
1096
1107
  baseUrl = "",
1097
1108
  apiKey = "",
1109
+ provider = "",
1098
1110
  timeoutMs = 300000,
1099
1111
  onStreamDelta = null,
1100
1112
  onThinkingDelta = null,
@@ -1108,16 +1120,13 @@ async function runNativeLoopAnthropic({
1108
1120
  throw new Error("ucode model is not configured");
1109
1121
  }
1110
1122
 
1111
- const requestUrl = resolveAnthropicMessagesUrl(baseUrl);
1123
+ const requestUrl = transport.resolveUrl(baseUrl);
1112
1124
  if (!requestUrl) {
1113
1125
  throw new Error("ucode baseUrl is not configured");
1114
1126
  }
1115
1127
 
1116
1128
  const messages = cloneMessageList(historyMessages);
1117
- messages.push({
1118
- role: "user",
1119
- content: String(prompt || ""),
1120
- });
1129
+ transport.prepareMessages({ messages, systemPrompt, prompt });
1121
1130
 
1122
1131
  let aggregated = "";
1123
1132
  let streamed = false;
@@ -1128,10 +1137,11 @@ async function runNativeLoopAnthropic({
1128
1137
  while (true) {
1129
1138
  guards.ensureActive();
1130
1139
 
1131
- const turnResult = await runAnthropicTurn({
1140
+ const turnResult = await transport.runTurn({
1132
1141
  url: requestUrl,
1133
1142
  apiKey,
1134
1143
  model: requestModel,
1144
+ provider,
1135
1145
  systemPrompt,
1136
1146
  messages,
1137
1147
  signal,
@@ -1149,28 +1159,10 @@ async function runNativeLoopAnthropic({
1149
1159
  },
1150
1160
  });
1151
1161
 
1152
- const toolCalls = Array.isArray(turnResult.toolCalls) ? turnResult.toolCalls : [];
1162
+ const toolCalls = transport.getToolCalls(turnResult);
1153
1163
 
1154
1164
  if (toolCalls.length === 0) {
1155
- const assistantContent = Array.isArray(turnResult.assistantContent)
1156
- ? turnResult.assistantContent
1157
- : [];
1158
- if (assistantContent.length > 0) {
1159
- messages.push({
1160
- role: "assistant",
1161
- content: assistantContent,
1162
- });
1163
- } else if (String(turnResult.text || "").trim()) {
1164
- messages.push({
1165
- role: "assistant",
1166
- content: [
1167
- {
1168
- type: "text",
1169
- text: String(turnResult.text || ""),
1170
- },
1171
- ],
1172
- });
1173
- }
1165
+ transport.appendFinalAssistantMessage({ messages, turnResult });
1174
1166
  const text = String(turnResult.text || "").trim();
1175
1167
  if (!aggregated.trim() && text) {
1176
1168
  aggregated = text;
@@ -1183,20 +1175,21 @@ async function runNativeLoopAnthropic({
1183
1175
  };
1184
1176
  }
1185
1177
 
1186
- const assistantContent = Array.isArray(turnResult.assistantContent)
1187
- ? turnResult.assistantContent
1188
- : [];
1189
-
1190
- messages.push({
1191
- role: "assistant",
1192
- content: assistantContent,
1193
- });
1178
+ const pendingCalls = transport.prepareToolCalls({ messages, turnResult, toolCalls });
1179
+ if (!pendingCalls) {
1180
+ return {
1181
+ text: aggregated,
1182
+ streamed,
1183
+ toolCallsExecuted,
1184
+ messages,
1185
+ };
1186
+ }
1194
1187
 
1195
- const toolResults = [];
1196
- for (const call of toolCalls) {
1188
+ const collectedResults = [];
1189
+ for (const pending of pendingCalls) {
1197
1190
  const toolResult = runCoreTool({
1198
- tool: call.name,
1199
- args: call.args,
1191
+ tool: pending.name,
1192
+ args: pending.args,
1200
1193
  workspaceRoot,
1201
1194
  onToolEvent,
1202
1195
  });
@@ -1209,23 +1202,21 @@ async function runNativeLoopAnthropic({
1209
1202
  toolErrors,
1210
1203
  maxToolCalls: toolBudget.maxToolCalls,
1211
1204
  maxToolErrors: toolBudget.maxToolErrors,
1212
- lastTool: call.name,
1205
+ lastTool: pending.name,
1213
1206
  lastError: toolResult && toolResult.error ? String(toolResult.error) : "",
1214
1207
  });
1215
- toolResults.push({
1216
- type: "tool_result",
1217
- tool_use_id: String(call.id || ""),
1218
- content: clipText(toJsonString(toolResult), 12000),
1219
- is_error: Boolean(!toolResult || toolResult.ok === false),
1208
+ transport.appendToolResult({
1209
+ messages,
1210
+ collected: collectedResults,
1211
+ call: pending,
1212
+ toolResult,
1220
1213
  });
1221
1214
  }
1222
1215
 
1223
- messages.push({
1224
- role: "user",
1225
- content: toolResults,
1226
- });
1216
+ if (typeof transport.flushToolResults === "function") {
1217
+ transport.flushToolResults({ messages, collected: collectedResults });
1218
+ }
1227
1219
  }
1228
-
1229
1220
  }
1230
1221
 
1231
1222
  async function runNativeAgentTask({
@@ -1277,11 +1268,27 @@ async function runNativeAgentTask({
1277
1268
  model,
1278
1269
  });
1279
1270
 
1280
- const loopRunner = runtime.transport === "anthropic-messages"
1281
- ? runNativeLoopAnthropic
1282
- : runNativeLoopOpenAi;
1271
+ // Kimi tokens expire; resolveRuntimeConfig reads the credential file
1272
+ // synchronously, so refresh it here (async) when the key came from that
1273
+ // file and the token is outside the fresh window.
1274
+ if (
1275
+ runtime.provider === "kimi"
1276
+ && runtime.apiKeySource === "kimi-credential"
1277
+ && runtime.kimiCredentialState !== "fresh"
1278
+ ) {
1279
+ try {
1280
+ const credential = await resolveKimiUpstreamCredentials({ env: process.env });
1281
+ const token = String(credential && credential.accessToken || "").trim();
1282
+ if (token) runtime.apiKey = token;
1283
+ } catch {
1284
+ // Keep the file token; the request itself will surface auth failures.
1285
+ }
1286
+ }
1287
+
1288
+ const transport = TRANSPORTS[runtime.transport] || TRANSPORTS["openai-chat"];
1283
1289
 
1284
- const runResult = await loopRunner({
1290
+ const runResult = await runNativeLoop({
1291
+ transport,
1285
1292
  workspaceRoot,
1286
1293
  prompt: promptText,
1287
1294
  systemPrompt,
@@ -1289,6 +1296,7 @@ async function runNativeAgentTask({
1289
1296
  model: runtime.model,
1290
1297
  baseUrl: runtime.baseUrl,
1291
1298
  apiKey: runtime.apiKey,
1299
+ provider: runtime.provider,
1292
1300
  timeoutMs,
1293
1301
  onStreamDelta: trackingStreamDelta,
1294
1302
  onThinkingDelta,