@co0ontty/wand 1.36.0 → 1.39.0

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.
@@ -12,6 +12,13 @@ interface CreateStructuredSessionOptions {
12
12
  model?: string;
13
13
  /** 用户预设的思考深度。留空 / null 视为 off。 */
14
14
  thinkingEffort?: SessionSnapshot["thinkingEffort"];
15
+ /**
16
+ * 恢复用的初始会话 id:
17
+ * - Codex:历史 thread id,首条消息即 `codex exec ... resume <id>` 续接。
18
+ * - Claude:历史 session id,首条消息即 `--resume` / SDK resume 续接。
19
+ * 留空表示新建会话。
20
+ */
21
+ claudeSessionId?: string;
15
22
  }
16
23
  /**
17
24
  * 把任意外部输入收敛到合法的 thinkingEffort 枚举值。`null` / 非法值都视为
@@ -150,6 +157,47 @@ export declare class StructuredSessionManager {
150
157
  private normalizeToolInput;
151
158
  private normalizeToolResultContent;
152
159
  private extractCodexText;
160
+ /**
161
+ * Merge one codex `item.*` event into `turnState.blocks`.
162
+ *
163
+ * 三种 phase 行为:
164
+ * - "started": 首次出现的 item,块直接 push(tool_result 走 upsert 配对)。
165
+ * text/thinking/TodoWrite 这种"靠 id 替换"的块记录到
166
+ * codexBlockIndex 里,方便后续 updated/completed 找回原位。
167
+ * - "updated": codex 重发完整 ThreadItem(不是 delta)。已记录过的块就
168
+ * 替换;新块按 started 路径处理。
169
+ * - "completed": 把"in_progress"卡片定型——text 同时更新 turnState.result
170
+ * 以便 result fallback 不为空;tool_use ↔ tool_result 通过
171
+ * 共享 id 配对到一起(包括 file_change 子项的 `${id}#i`)。
172
+ */
173
+ private applyCodexItem;
174
+ /**
175
+ * Map a codex `item.{started,updated,completed}` payload into wand's
176
+ * `ContentBlock[]` so the chat UI's existing tool/diff/todo cards just work.
177
+ *
178
+ * Codex `exec --json` emits 8 item.type values (see
179
+ * `codex-rs/exec/src/exec_events.rs`); below they're routed to whatever wand
180
+ * tool name reuses an existing renderer:
181
+ *
182
+ * agent_message → text
183
+ * reasoning → thinking
184
+ * command_execution → tool_use "Bash" + tool_result
185
+ * file_change → one tool_use per file, named Edit/Write/Bash by `kind`
186
+ * (codex does NOT carry old_string/new_string in the
187
+ * exec stream, only the path list; diff card body is
188
+ * empty but the file row + status still render)
189
+ * mcp_tool_call → tool_use named "<server>__<tool>" + tool_result
190
+ * web_search → tool_use "WebSearch" + tool_result (results not in stream)
191
+ * todo_list → tool_use "TodoWrite" (replaced in place on each update)
192
+ * error → text block prefixed with ❌
193
+ *
194
+ * Returns [] when there is nothing to emit yet (e.g. agent_message at
195
+ * `item.started` before any text has been produced).
196
+ *
197
+ * Callers handle in-place replacement for `item.updated` via
198
+ * `turnState.codexBlockIndex`; tool_use ↔ tool_result pairing still goes
199
+ * through `upsertCodexBlock` by matching ids.
200
+ */
153
201
  private extractCodexItemBlock;
154
202
  private upsertCodexBlock;
155
203
  /**
@@ -8,6 +8,7 @@ import { query as sdkQuery } from "@anthropic-ai/claude-agent-sdk";
8
8
  import { prepareSessionWorktree } from "./git-worktree.js";
9
9
  import { truncateMessagesForTransport } from "./message-truncator.js";
10
10
  import { buildChildEnv } from "./env-utils.js";
11
+ import { buildLanguageDirective } from "./language-prompt.js";
11
12
  function defaultStructuredRunner(provider) {
12
13
  return provider === "codex" ? "codex-cli-exec" : "claude-cli-print";
13
14
  }
@@ -389,9 +390,9 @@ function buildAppendSystemPromptParts(language, mode) {
389
390
  : "You are running in a fully managed, autonomous mode. The user may not be available to respond to questions or confirmations in a timely manner. You MUST make all decisions independently — choose the best approach yourself instead of asking the user for preferences, confirmations, or clarifications. If multiple approaches are viable, pick the one you judge most appropriate and proceed. Never block on user input unless the task is fundamentally ambiguous and cannot be reasonably inferred. Be decisive and self-directed.");
390
391
  }
391
392
  if (trimmedLanguage) {
392
- parts.push(isChinese
393
- ? "请使用中文回复。所有解释、注释和对话文本都使用中文。当你通过 Task 工具派发子代理(subagent)时,必须在传给子代理的 prompt 里明确要求它也使用中文回复,确保所有子代理的输出同样遵循该语言偏好。"
394
- : `Please respond in ${trimmedLanguage}. Use ${trimmedLanguage} for all your explanations, comments, and conversational text. When you dispatch a subagent via the Task tool, you MUST explicitly instruct the subagent in its prompt to also respond in ${trimmedLanguage}, so every subagent's output follows the same language preference.`);
393
+ const directive = buildLanguageDirective(trimmedLanguage);
394
+ if (directive)
395
+ parts.push(directive);
395
396
  }
396
397
  return parts;
397
398
  }
@@ -579,7 +580,7 @@ export class StructuredSessionManager {
579
580
  output: "",
580
581
  archived: false,
581
582
  archivedAt: null,
582
- claudeSessionId: null,
583
+ claudeSessionId: options.claudeSessionId?.trim() || null,
583
584
  messages: [],
584
585
  queuedMessages: [],
585
586
  structuredState: {
@@ -1160,6 +1161,7 @@ export class StructuredSessionManager {
1160
1161
  sessionId: session.claudeSessionId,
1161
1162
  model: session.selectedModel ?? session.structuredState?.model,
1162
1163
  usage: undefined,
1164
+ codexBlockIndex: new Map(),
1163
1165
  };
1164
1166
  let lineBuf = "";
1165
1167
  let stderr = "";
@@ -1231,23 +1233,24 @@ export class StructuredSessionManager {
1231
1233
  return;
1232
1234
  }
1233
1235
  if (parsed?.type === "item.started" && parsed.item) {
1234
- const block = this.extractCodexItemBlock(parsed.item, false);
1235
- if (block) {
1236
- turnState.blocks.push(block);
1237
- syncSnapshot();
1238
- scheduleEmit();
1239
- }
1236
+ this.applyCodexItem(turnState, parsed.item, "started");
1237
+ syncSnapshot();
1238
+ scheduleEmit();
1239
+ return;
1240
+ }
1241
+ if (parsed?.type === "item.updated" && parsed.item) {
1242
+ // codex `item.updated` 重新发送完整 ThreadItem(不是 delta)。
1243
+ // 对 text/thinking/TodoWrite 走 codexBlockIndex 替换;对 tool_use
1244
+ // 仍然按现有 id 复用,避免重复卡片。
1245
+ this.applyCodexItem(turnState, parsed.item, "updated");
1246
+ syncSnapshot();
1247
+ scheduleEmit();
1240
1248
  return;
1241
1249
  }
1242
1250
  if (parsed?.type === "item.completed" && parsed.item) {
1243
- const block = this.extractCodexItemBlock(parsed.item, true);
1244
- if (block) {
1245
- if (block.type === "text")
1246
- turnState.result = block.text;
1247
- this.upsertCodexBlock(turnState.blocks, block);
1248
- syncSnapshot();
1249
- scheduleEmit();
1250
- }
1251
+ this.applyCodexItem(turnState, parsed.item, "completed");
1252
+ syncSnapshot();
1253
+ scheduleEmit();
1251
1254
  return;
1252
1255
  }
1253
1256
  if (parsed?.type === "turn.completed") {
@@ -2562,16 +2565,88 @@ export class StructuredSessionManager {
2562
2565
  }
2563
2566
  return "";
2564
2567
  }
2568
+ /**
2569
+ * Merge one codex `item.*` event into `turnState.blocks`.
2570
+ *
2571
+ * 三种 phase 行为:
2572
+ * - "started": 首次出现的 item,块直接 push(tool_result 走 upsert 配对)。
2573
+ * text/thinking/TodoWrite 这种"靠 id 替换"的块记录到
2574
+ * codexBlockIndex 里,方便后续 updated/completed 找回原位。
2575
+ * - "updated": codex 重发完整 ThreadItem(不是 delta)。已记录过的块就
2576
+ * 替换;新块按 started 路径处理。
2577
+ * - "completed": 把"in_progress"卡片定型——text 同时更新 turnState.result
2578
+ * 以便 result fallback 不为空;tool_use ↔ tool_result 通过
2579
+ * 共享 id 配对到一起(包括 file_change 子项的 `${id}#i`)。
2580
+ */
2581
+ applyCodexItem(turnState, item, phase) {
2582
+ const completed = phase === "completed";
2583
+ const itemId = typeof item.id === "string" ? item.id : "";
2584
+ const blocks = this.extractCodexItemBlock(item, completed);
2585
+ if (blocks.length === 0)
2586
+ return;
2587
+ const index = turnState.codexBlockIndex ??= new Map();
2588
+ for (const block of blocks) {
2589
+ // text / thinking / TodoWrite tool_use 的卡片是"按 item id 整体替换"语义,
2590
+ // 否则一个 agent_message 在 updated/completed 时会被重复 push 多次。
2591
+ const replaceable = block.type === "text"
2592
+ || block.type === "thinking"
2593
+ || (block.type === "tool_use" && block.name === "TodoWrite");
2594
+ if (replaceable && itemId) {
2595
+ const existing = index.get(itemId);
2596
+ if (existing !== undefined && existing < turnState.blocks.length) {
2597
+ turnState.blocks[existing] = block;
2598
+ }
2599
+ else {
2600
+ index.set(itemId, turnState.blocks.length);
2601
+ turnState.blocks.push(block);
2602
+ }
2603
+ if (block.type === "text" && completed) {
2604
+ turnState.result = block.text;
2605
+ }
2606
+ continue;
2607
+ }
2608
+ // 其它块(tool_use 非 Todo / tool_result / 文件改动的多 sub-id 块)
2609
+ // 仍然走原有 upsert:tool_result 按 tool_use_id 配对,其余直接 push。
2610
+ this.upsertCodexBlock(turnState.blocks, block);
2611
+ }
2612
+ }
2613
+ /**
2614
+ * Map a codex `item.{started,updated,completed}` payload into wand's
2615
+ * `ContentBlock[]` so the chat UI's existing tool/diff/todo cards just work.
2616
+ *
2617
+ * Codex `exec --json` emits 8 item.type values (see
2618
+ * `codex-rs/exec/src/exec_events.rs`); below they're routed to whatever wand
2619
+ * tool name reuses an existing renderer:
2620
+ *
2621
+ * agent_message → text
2622
+ * reasoning → thinking
2623
+ * command_execution → tool_use "Bash" + tool_result
2624
+ * file_change → one tool_use per file, named Edit/Write/Bash by `kind`
2625
+ * (codex does NOT carry old_string/new_string in the
2626
+ * exec stream, only the path list; diff card body is
2627
+ * empty but the file row + status still render)
2628
+ * mcp_tool_call → tool_use named "<server>__<tool>" + tool_result
2629
+ * web_search → tool_use "WebSearch" + tool_result (results not in stream)
2630
+ * todo_list → tool_use "TodoWrite" (replaced in place on each update)
2631
+ * error → text block prefixed with ❌
2632
+ *
2633
+ * Returns [] when there is nothing to emit yet (e.g. agent_message at
2634
+ * `item.started` before any text has been produced).
2635
+ *
2636
+ * Callers handle in-place replacement for `item.updated` via
2637
+ * `turnState.codexBlockIndex`; tool_use ↔ tool_result pairing still goes
2638
+ * through `upsertCodexBlock` by matching ids.
2639
+ */
2565
2640
  extractCodexItemBlock(item, completed) {
2566
2641
  const id = typeof item.id === "string" ? item.id : randomUUID();
2567
2642
  const type = typeof item.type === "string" ? item.type : "unknown";
2568
2643
  if (type === "agent_message") {
2569
2644
  const text = this.extractCodexText(item);
2570
- return text ? { type: "text", text } : null;
2645
+ return text ? [{ type: "text", text }] : [];
2571
2646
  }
2572
2647
  if (type === "reasoning") {
2573
2648
  const text = this.extractCodexText(item);
2574
- return text ? { type: "thinking", thinking: text } : null;
2649
+ return text ? [{ type: "thinking", thinking: text }] : [];
2575
2650
  }
2576
2651
  if (type === "command_execution") {
2577
2652
  const command = typeof item.command === "string" ? item.command : "";
@@ -2579,28 +2654,213 @@ export class StructuredSessionManager {
2579
2654
  const exitCode = typeof item.exit_code === "number" ? item.exit_code : null;
2580
2655
  const status = typeof item.status === "string" ? item.status : completed ? "completed" : "in_progress";
2581
2656
  if (!completed) {
2657
+ return [{
2658
+ type: "tool_use",
2659
+ id,
2660
+ name: "Bash",
2661
+ input: { command, status },
2662
+ }];
2663
+ }
2664
+ // codex 的 status 可能是 declined(sandbox 拒了命令)/ failed(执行失败)—
2665
+ // 这时 exit_code 经常是 null,光靠 exitCode !== 0 判 is_error 会漏。
2666
+ const isError = status === "failed" || status === "declined"
2667
+ || (typeof exitCode === "number" && exitCode !== 0);
2668
+ const fallbackText = status === "declined"
2669
+ ? "command declined by sandbox"
2670
+ : (exitCode === null ? "" : `exit_code: ${exitCode}`);
2671
+ return [{
2672
+ type: "tool_result",
2673
+ tool_use_id: id,
2674
+ content: aggregatedOutput || fallbackText,
2675
+ is_error: isError,
2676
+ }];
2677
+ }
2678
+ if (type === "file_change") {
2679
+ // 注意:codex exec stream 没有 old_string/new_string——只给 path + kind。
2680
+ // 这里每个 file 一个 sub-id(`${item.id}#${i}`),这样如果 codex 一次给多
2681
+ // 个文件,每个文件能独立成卡片 + 独立 tool_result 状态。
2682
+ const rawChanges = Array.isArray(item.changes) ? item.changes : [];
2683
+ const status = typeof item.status === "string" ? item.status : completed ? "completed" : "in_progress";
2684
+ const isError = status === "failed";
2685
+ const blocks = [];
2686
+ rawChanges.forEach((entry, idx) => {
2687
+ if (!entry || typeof entry !== "object")
2688
+ return;
2689
+ const change = entry;
2690
+ const path = typeof change.path === "string" ? change.path : "";
2691
+ const kind = typeof change.kind === "string" ? change.kind : "update";
2692
+ const subId = `${id}#${idx}`;
2693
+ let toolName;
2694
+ let input;
2695
+ if (kind === "add") {
2696
+ toolName = "Write";
2697
+ input = { file_path: path, content: "" };
2698
+ }
2699
+ else if (kind === "delete") {
2700
+ // 复用 Bash 终端卡,rm 语义直观
2701
+ toolName = "Bash";
2702
+ input = { command: `rm ${path}`, description: `delete ${path}`, status };
2703
+ }
2704
+ else {
2705
+ toolName = "Edit";
2706
+ input = { file_path: path, old_string: "", new_string: "" };
2707
+ }
2708
+ if (!completed) {
2709
+ blocks.push({ type: "tool_use", id: subId, name: toolName, input });
2710
+ }
2711
+ else {
2712
+ blocks.push({ type: "tool_use", id: subId, name: toolName, input });
2713
+ blocks.push({
2714
+ type: "tool_result",
2715
+ tool_use_id: subId,
2716
+ content: isError ? `file change failed: ${path}` : "",
2717
+ is_error: isError,
2718
+ });
2719
+ }
2720
+ });
2721
+ return blocks;
2722
+ }
2723
+ if (type === "mcp_tool_call") {
2724
+ const server = typeof item.server === "string" ? item.server : "mcp";
2725
+ const tool = typeof item.tool === "string" ? item.tool : "tool";
2726
+ const args = item.arguments && typeof item.arguments === "object" ? item.arguments : {};
2727
+ const errObj = item.error && typeof item.error === "object" ? item.error : null;
2728
+ const status = typeof item.status === "string" ? item.status : completed ? "completed" : "in_progress";
2729
+ const isError = !!errObj || status === "failed";
2730
+ if (!completed) {
2731
+ return [{
2732
+ type: "tool_use",
2733
+ id,
2734
+ name: `${server}__${tool}`,
2735
+ input: args,
2736
+ }];
2737
+ }
2738
+ let resultText = "";
2739
+ if (errObj && typeof errObj.message === "string") {
2740
+ resultText = errObj.message;
2741
+ }
2742
+ else if (item.result && typeof item.result === "object") {
2743
+ const resultRec = item.result;
2744
+ const inner = this.extractCodexText(resultRec.content);
2745
+ resultText = inner || JSON.stringify(resultRec).slice(0, 4096);
2746
+ }
2747
+ return [{
2748
+ type: "tool_result",
2749
+ tool_use_id: id,
2750
+ content: resultText,
2751
+ is_error: isError,
2752
+ }];
2753
+ }
2754
+ if (type === "web_search") {
2755
+ const query = typeof item.query === "string" ? item.query : "";
2756
+ if (!completed) {
2757
+ return [{
2758
+ type: "tool_use",
2759
+ id,
2760
+ name: "WebSearch",
2761
+ input: { query },
2762
+ }];
2763
+ }
2764
+ return [{
2765
+ type: "tool_result",
2766
+ tool_use_id: id,
2767
+ // codex 不在 exec 流里回 search 结果,这里给个占位让 UI 卡片完成态。
2768
+ content: query ? `query: ${query}` : "",
2769
+ }];
2770
+ }
2771
+ if (type === "collab_tool_call") {
2772
+ // codex 的子-agent 编排(spawn_agent / send_input / wait / close_agent)。
2773
+ // 没有对应 Claude tool,所以名称用 "Codex/<op>" 让 UI 默认 tool 卡渲染时
2774
+ // 一眼能看出来是 codex 多 agent 操作。
2775
+ const tool = typeof item.tool === "string" ? item.tool : "collab";
2776
+ const prompt = typeof item.prompt === "string" ? item.prompt : "";
2777
+ const senderId = typeof item.sender_thread_id === "string" ? item.sender_thread_id : "";
2778
+ const receiverIds = Array.isArray(item.receiver_thread_ids)
2779
+ ? item.receiver_thread_ids.filter((v) => typeof v === "string")
2780
+ : [];
2781
+ const agentsStates = item.agents_states && typeof item.agents_states === "object"
2782
+ ? item.agents_states
2783
+ : {};
2784
+ const status = typeof item.status === "string" ? item.status : completed ? "completed" : "in_progress";
2785
+ const toolName = `Codex/${tool}`;
2786
+ const input = { tool };
2787
+ if (prompt)
2788
+ input.prompt = prompt;
2789
+ if (senderId)
2790
+ input.sender_thread_id = senderId;
2791
+ if (receiverIds.length > 0)
2792
+ input.receiver_thread_ids = receiverIds;
2793
+ if (Object.keys(agentsStates).length > 0)
2794
+ input.agents_states = agentsStates;
2795
+ if (!completed) {
2796
+ return [{ type: "tool_use", id, name: toolName, input }];
2797
+ }
2798
+ // 完成态:把每个 receiver agent 的最终状态汇总成可读 result。
2799
+ const summaryLines = [];
2800
+ for (const [tid, state] of Object.entries(agentsStates)) {
2801
+ if (!state || typeof state !== "object")
2802
+ continue;
2803
+ const rec = state;
2804
+ const s = typeof rec.status === "string" ? rec.status : "?";
2805
+ const msg = typeof rec.message === "string" && rec.message ? ` — ${rec.message}` : "";
2806
+ summaryLines.push(`${tid.slice(0, 8)}: ${s}${msg}`);
2807
+ }
2808
+ const isError = status === "failed"
2809
+ || summaryLines.some((l) => /errored|not_found|interrupted/.test(l));
2810
+ const content = summaryLines.length > 0
2811
+ ? summaryLines.join("\n")
2812
+ : (status === "completed" ? "ok" : status);
2813
+ return [
2814
+ { type: "tool_use", id, name: toolName, input },
2815
+ { type: "tool_result", tool_use_id: id, content, is_error: isError },
2816
+ ];
2817
+ }
2818
+ if (type === "todo_list") {
2819
+ // codex 的 todo: { items: [{ text, completed: bool }] }
2820
+ // wand UI(renderTodoWrite)读的是 block.input.todos = [{content, status, activeForm}]
2821
+ // 这里做形状翻译;in_progress 状态 codex 不区分,全部 pending → completed 二值。
2822
+ const rawItems = Array.isArray(item.items) ? item.items : [];
2823
+ const todos = rawItems.map((entry) => {
2824
+ const rec = (entry && typeof entry === "object") ? entry : {};
2825
+ const text = typeof rec.text === "string" ? rec.text : "";
2826
+ const done = rec.completed === true;
2582
2827
  return {
2828
+ content: text,
2829
+ status: done ? "completed" : "pending",
2830
+ activeForm: text,
2831
+ };
2832
+ });
2833
+ return [{
2583
2834
  type: "tool_use",
2584
2835
  id,
2585
- name: "Bash",
2586
- input: { command, status },
2587
- };
2588
- }
2589
- return {
2590
- type: "tool_result",
2591
- tool_use_id: id,
2592
- content: aggregatedOutput || (exitCode === null ? "" : `exit_code: ${exitCode}`),
2593
- is_error: typeof exitCode === "number" && exitCode !== 0,
2594
- };
2836
+ name: "TodoWrite",
2837
+ input: { todos },
2838
+ }];
2839
+ }
2840
+ if (type === "error") {
2841
+ // item-level error(不是 top-level error 事件,那个走 codexErrors / 退出报错路径)
2842
+ const message = this.extractCodexText(item) || "codex item error";
2843
+ return [{ type: "text", text: `❌ ${message}` }];
2595
2844
  }
2845
+ // unknown / 兜底:completed 时尝试取 text 字段免得 silently 丢
2596
2846
  if (completed) {
2597
2847
  const text = this.extractCodexText(item);
2598
2848
  if (text)
2599
- return { type: "text", text };
2849
+ return [{ type: "text", text }];
2600
2850
  }
2601
- return null;
2851
+ return [];
2602
2852
  }
2603
2853
  upsertCodexBlock(blocks, block) {
2854
+ // tool_use 按 id 去重——file_change 在 item.started 已经 push 过一份 tool_use,
2855
+ // 到 item.completed 还会再发一份相同 id 的(带 status 更新),不去重就出现
2856
+ // 两张同名卡片。command_execution 不受影响(它在 completed 只 emit tool_result)。
2857
+ if (block.type === "tool_use") {
2858
+ const existingIndex = blocks.findIndex((existing) => existing.type === "tool_use" && existing.id === block.id);
2859
+ if (existingIndex >= 0) {
2860
+ blocks[existingIndex] = block;
2861
+ return;
2862
+ }
2863
+ }
2604
2864
  if (block.type === "tool_result") {
2605
2865
  const toolUseIndex = blocks.findIndex((existing) => existing.type === "tool_use" && existing.id === block.tool_use_id);
2606
2866
  if (toolUseIndex >= 0) {
@@ -2724,8 +2984,12 @@ export class StructuredSessionManager {
2724
2984
  inputTokens: typeof source.input_tokens === "number" ? source.input_tokens : undefined,
2725
2985
  outputTokens: typeof source.output_tokens === "number" ? source.output_tokens : undefined,
2726
2986
  cacheReadInputTokens: typeof source.cached_input_tokens === "number" ? source.cached_input_tokens : undefined,
2987
+ reasoningOutputTokens: typeof source.reasoning_output_tokens === "number" ? source.reasoning_output_tokens : undefined,
2727
2988
  };
2728
- if (value.inputTokens === undefined && value.outputTokens === undefined && value.cacheReadInputTokens === undefined) {
2989
+ if (value.inputTokens === undefined
2990
+ && value.outputTokens === undefined
2991
+ && value.cacheReadInputTokens === undefined
2992
+ && value.reasoningOutputTokens === undefined) {
2729
2993
  return undefined;
2730
2994
  }
2731
2995
  return value;
package/dist/types.d.ts CHANGED
@@ -365,6 +365,8 @@ export interface ConversationTurn {
365
365
  outputTokens?: number;
366
366
  cacheReadInputTokens?: number;
367
367
  cacheCreationInputTokens?: number;
368
+ /** codex 专属:reasoning_output_tokens(GPT-5 等带思考模型,per-turn 计费)。 */
369
+ reasoningOutputTokens?: number;
368
370
  totalCostUsd?: number;
369
371
  };
370
372
  }