Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions core/llm/autodetect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,9 @@ function modelSupportsReasoning(
) {
return true;
}
if (model.model.includes("command-a-reasoning")) {
return true;
}
if (model.model.includes("deepseek-r")) {
return true;
}
Expand Down
98 changes: 60 additions & 38 deletions core/llm/llms/Cohere.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,10 @@ import {
Chunk,
CompletionOptions,
LLMOptions,
MessageContent,
} from "../../index.js";
import { renderChatMessage, stripImages } from "../../util/messageContent.js";
import { BaseLLM } from "../index.js";
import { DEFAULT_REASONING_TOKENS } from "../constants.js";

class Cohere extends BaseLLM {
static providerName = "cohere";
Expand All @@ -19,7 +19,6 @@ class Cohere extends BaseLLM {

private _convertMessages(msgs: ChatMessage[]): any[] {
const messages = [];
let lastToolPlan: MessageContent | undefined;
for (const m of msgs) {
if (!m.content) {
continue;
Expand Down Expand Up @@ -48,36 +47,44 @@ class Cohere extends BaseLLM {
});
break;
case "thinking":
lastToolPlan = m.content;
messages.push({
role: "assistant",
content: [
{
type: "thinking",
thinking: m.content,
},
],
});
break;
case "assistant":
if (m.toolCalls) {
if (!lastToolPlan) {
throw new Error("No tool plan found");
}
messages.push({
let msg: any;
if (messages.at(-1)?.content[0]?.thinking) {
msg = messages.pop();
} else {
msg = {
role: m.role,
tool_calls: m.toolCalls.map((toolCall) => ({
id: toolCall.id,
type: "function",
function: {
name: toolCall.function?.name,
arguments: toolCall.function?.arguments,
},
})),
// Ideally the tool plan would be in this message, but it is
// split in another, usually the previous, this one's content is
// a space.
// tool_plan: m.content,
tool_plan: lastToolPlan,
content: [],
};
}

if (m.toolCalls) {
msg.tool_calls = m.toolCalls.map((toolCall) => ({
id: toolCall.id,
type: "function",
function: {
name: toolCall.function?.name,
arguments: toolCall.function?.arguments,
},
}));
} else {
msg.content.push({
type: "text",
text: m.content,
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot Nov 4, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wrapping assistant content in a {type: "text"} block assigns arrays of MessageParts to the text field, corrupting structured assistant replies when they are already multi-part.

Prompt for AI agents
Address the following comment on core/llm/llms/Cohere.ts at line 83:

<comment>Wrapping assistant content in a `{type: &quot;text&quot;}` block assigns arrays of `MessagePart`s to the `text` field, corrupting structured assistant replies when they are already multi-part.</comment>

<file context>
@@ -48,36 +47,44 @@ class Cohere extends BaseLLM {
+          } else {
+            msg.content.push({
+              type: &quot;text&quot;,
+              text: m.content,
             });
-            lastToolPlan = undefined;
</file context>
Fix with Cubic

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@maxbrunet could you check this feedback?

});
lastToolPlan = undefined;
break;
}
messages.push({
role: m.role,
content: m.content,
});

messages.push(msg);
break;
case "system":
messages.push({
Expand Down Expand Up @@ -110,6 +117,15 @@ class Cohere extends BaseLLM {
stop_sequences: options.stop?.slice(0, Cohere.maxStopSequences),
frequency_penalty: options.frequencyPenalty,
presence_penalty: options.presencePenalty,
thinking: options.reasoning
? {
type: "enabled" as const,
token_budget:
options.reasoningBudgetTokens ?? DEFAULT_REASONING_TOKENS,
}
: // Reasoning is enabled by default for models that support it.
// https://docs.cohere.com/reference/chat-stream#request.body.thinking
{ type: "disabled" as const },
tools: options.tools?.map((tool) => ({
type: "function",
function: {
Expand Down Expand Up @@ -159,14 +175,17 @@ class Cohere extends BaseLLM {

if (options.stream === false) {
const data = await resp.json();
for (const content of data.message.content) {
if (content.thinking) {
yield { role: "thinking", content: content.thinking };
continue;
}
yield { role: "assistant", content: content.text };
}
if (data.message.tool_calls) {
yield {
// Use the "thinking" role for `tool_plan`, since there is no such
// role in the Cohere API at the moment and it is a "a
// chain-of-thought style reflection".
role: "thinking",
content: data.message.tool_plan,
};
if (data.message.tool_plan) {
yield { role: "thinking", content: data.message.tool_plan };
}
yield {
role: "assistant",
content: "",
Expand All @@ -181,7 +200,6 @@ class Cohere extends BaseLLM {
};
return;
}
yield { role: "assistant", content: data.message.content[0].text };
return;
}

Expand All @@ -192,16 +210,20 @@ class Cohere extends BaseLLM {
switch (value.type) {
// https://docs.cohere.com/v2/docs/streaming#content-delta
case "content-delta":
if (value.delta.message.content.thinking) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@maxbrunet just to double check can content deltas also have text that would be lost here? or is it thinking OR text?

yield {
role: "thinking",
content: value.delta.message.content.thinking,
};
break;
}
yield {
role: "assistant",
content: value.delta.message.content.text,
};
break;
// https://docs.cohere.com/reference/chat-stream#request.body.messages.assistant.tool_plan
case "tool-plan-delta":
// Use the "thinking" role for `tool_plan`, since there is no such
// role in the Cohere API at the moment and it is a "a
// chain-of-thought style reflection".
yield {
role: "thinking",
content: value.delta.message.tool_plan,
Expand Down
Loading
Loading