


























@@ -1494,12 +1494,21 @@ async function processOpenAICompletionsStream(
14941494continue;
14951495}
14961496if (choice.delta.content) {
1497-if (currentBlock?.type === "toolCall") {
1498-queuePostToolCallDelta({ kind: "text", text: choice.delta.content });
1499-} else {
1500-appendTextDelta(choice.delta.content);
1497+// Structured content can contain visible text and thinking blocks in the
1498+// same delta, so route each extracted block through the normal stream path.
1499+const contentDeltas = getCompletionsContentDeltas(choice.delta.content);
1500+for (const contentDelta of contentDeltas) {
1501+if (currentBlock?.type === "toolCall") {
1502+queuePostToolCallDelta(contentDelta);
1503+} else if (contentDelta.kind === "text") {
1504+appendTextDelta(contentDelta.text);
1505+} else {
1506+appendThinkingDelta(contentDelta);
1507+}
1508+}
1509+if (contentDeltas.length > 0) {
1510+continue;
15011511}
1502-continue;
15031512}
15041513const reasoningDeltas = getCompletionsReasoningDeltas(
15051514choice.delta as Record<string, unknown>,
@@ -1599,6 +1608,49 @@ type CompletionsReasoningDelta =
15991608text: string;
16001609};
160116101611+function getCompletionsContentDeltas(content: unknown): CompletionsReasoningDelta[] {
1612+if (typeof content === "string") {
1613+return content ? [{ kind: "text", text: content }] : [];
1614+}
1615+if (Array.isArray(content)) {
1616+return content.flatMap((item) => getCompletionsContentDeltas(item));
1617+}
1618+if (!content || typeof content !== "object") {
1619+return [];
1620+}
1621+const record = content as Record<string, unknown>;
1622+const type = typeof record.type === "string" ? record.type.toLowerCase() : "";
1623+// Some OpenAI-compatible providers, notably Mistral thinking models, stream
1624+// `delta.content` as typed objects. Never coerce those objects directly or
1625+// they become persisted visible text like "[object Object]".
1626+const extractText = (value: unknown): string => {
1627+if (typeof value === "string") {
1628+return value;
1629+}
1630+if (Array.isArray(value)) {
1631+return value.map((item) => extractText(item)).join("");
1632+}
1633+if (value && typeof value === "object") {
1634+const nested = value as Record<string, unknown>;
1635+return extractText(nested.text ?? nested.content ?? nested.thinking);
1636+}
1637+return "";
1638+};
1639+const text = extractText(record.text ?? record.content ?? record.thinking);
1640+if (!text) {
1641+return [];
1642+}
1643+// Preserve provider reasoning as OpenClaw thinking blocks so channel/UI
1644+// surfaces can decide whether to show it instead of leaking it as answer text.
1645+if (type.includes("thinking") || type.includes("reasoning")) {
1646+return [{ kind: "thinking", signature: "content", text }];
1647+}
1648+if (type === "text" || type === "output_text" || type.endsWith(".output_text")) {
1649+return [{ kind: "text", text }];
1650+}
1651+return [];
1652+}
1653+16021654function getCompletionsReasoningDeltas(
16031655delta: Record<string, unknown>,
16041656visibleReasoningDetailTypes: readonly string[],
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。