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
5 changes: 5 additions & 0 deletions .changeset/clear-jeans-drive.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"braintrust": patch
---

fix(openrouter): Capture reasoning
48 changes: 48 additions & 0 deletions e2e/scenarios/openrouter-instrumentation/assertions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
CHAT_MODEL,
EMBEDDING_MODEL,
RERANK_MODEL,
REASONING_MODEL,
ROOT_NAME,
SCENARIO_NAME,
} from "./constants.mjs";
Expand All @@ -21,7 +22,10 @@ const CHAT_MODEL_NAME = CHAT_MODEL.split("/").at(-1) ?? CHAT_MODEL;
const EMBEDDING_MODEL_NAME =
EMBEDDING_MODEL.split("/").at(-1) ?? EMBEDDING_MODEL;
const RERANK_MODEL_NAME = RERANK_MODEL.split("/").at(-1) ?? RERANK_MODEL;
const REASONING_MODEL_NAME =
REASONING_MODEL.split("/").at(-1) ?? REASONING_MODEL;
const OPENROUTER_MODEL_PROVIDER = "openai";
const OPENROUTER_REASONING_PROVIDER = "deepseek";
const OPENROUTER_RERANK_PROVIDER = "cohere";

type RunOpenRouterScenario = (harness: {
Expand Down Expand Up @@ -216,6 +220,50 @@ export function defineOpenRouterTraceAssertions(options: {
},
);

test(
"captures reasoning fields for a streamed reasoning chat completion",
testConfig,
() => {
const root = findLatestSpan(events, ROOT_NAME);
const operation = findLatestSpan(
events,
"openrouter-chat-reasoning-stream-operation",
);
const span = findOpenRouterSpan(events, operation?.span.id, [
"openrouter.chat.send",
]);
const output = span?.output as
| Array<{
message?: {
reasoning?: string;
reasoning_content?: string;
reasoning_details?: unknown[];
};
}>
| undefined;
const message = output?.[0]?.message;
const reasoning = message?.reasoning ?? message?.reasoning_content;
const hasReasoningText =
typeof reasoning === "string" && reasoning.length > 0;
const hasReasoningDetails =
Array.isArray(message?.reasoning_details) &&
message.reasoning_details.length > 0;

expect(operation).toBeDefined();
expect(span).toBeDefined();
expect(operation?.span.parentIds).toEqual([root?.span.id ?? ""]);
expect(span?.row.metadata).toMatchObject({
provider: OPENROUTER_REASONING_PROVIDER,
});
expect(span?.row.metadata?.model).toBe(REASONING_MODEL_NAME);
expect(span?.metrics?.time_to_first_token).toEqual(expect.any(Number));
expect(span?.metrics?.completion_reasoning_tokens).toEqual(
expect.any(Number),
);
expect(hasReasoningText || hasReasoningDetails).toBe(true);
},
);

test("captures trace for client.embeddings.generate()", testConfig, () => {
const root = findLatestSpan(events, ROOT_NAME);
const operation = findLatestSpan(
Expand Down
10 changes: 9 additions & 1 deletion e2e/scenarios/openrouter-instrumentation/constants.mjs
Original file line number Diff line number Diff line change
@@ -1,7 +1,15 @@
const CHAT_MODEL = "openai/gpt-4o-mini-2024-07-18";
const EMBEDDING_MODEL = "openai/text-embedding-3-small";
const RERANK_MODEL = "cohere/rerank-v3.5";
const REASONING_MODEL = "deepseek/deepseek-r1";
const ROOT_NAME = "openrouter-root";
const SCENARIO_NAME = "openrouter-instrumentation";

export { CHAT_MODEL, EMBEDDING_MODEL, RERANK_MODEL, ROOT_NAME, SCENARIO_NAME };
export {
CHAT_MODEL,
EMBEDDING_MODEL,
RERANK_MODEL,
REASONING_MODEL,
ROOT_NAME,
SCENARIO_NAME,
};
31 changes: 31 additions & 0 deletions e2e/scenarios/openrouter-instrumentation/scenario.impl.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
CHAT_MODEL,
EMBEDDING_MODEL,
RERANK_MODEL,
REASONING_MODEL,
ROOT_NAME,
SCENARIO_NAME,
} from "./constants.mjs";
Expand Down Expand Up @@ -90,6 +91,36 @@ async function runOpenRouterInstrumentationScenario(
},
);

await runOperation(
"openrouter-chat-reasoning-stream-operation",
"chat-reasoning-stream",
async () => {
const stream = await client.chat.send(
withCompatibleChatRequest({
model: REASONING_MODEL,
messages: [
{
role: "user",
content:
"Think briefly, then answer with exactly the number 4.",
},
],
maxTokens: 256,
reasoning: {
enabled: true,
exclude: false,
},
stream: true,
streamOptions: {
includeUsage: true,
},
temperature: 0,
}),
);
await collectAsync(stream);
},
);

await runOperation(
"openrouter-embeddings-operation",
"embeddings",
Expand Down
65 changes: 65 additions & 0 deletions js/src/instrumentation/plugins/openrouter-agent-plugin.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -335,6 +335,71 @@ describe("OpenRouter Agent Plugin", () => {
metrics: {},
});
});

it("should aggregate reasoning fields from streaming deltas", () => {
expect(
aggregateOpenRouterChatChunks([
{
choices: [
{
delta: {
role: "assistant",
reasoning: "First, ",
reasoning_details: [
{
type: "reasoning.text",
text: "First, ",
},
],
},
},
],
},
{
choices: [
{
delta: {
reasoning_content: "check the answer.",
content: "OK",
reasoning_details: [
{
type: "reasoning.summary",
summary: "Checked the answer.",
},
],
},
finish_reason: "stop",
},
],
},
]),
).toEqual({
output: [
{
index: 0,
message: {
role: "assistant",
content: "OK",
reasoning: "First, check the answer.",
reasoning_content: "check the answer.",
reasoning_details: [
{
type: "reasoning.text",
text: "First, ",
},
{
type: "reasoning.summary",
summary: "Checked the answer.",
},
],
},
logprobs: null,
finish_reason: "stop",
},
],
metrics: {},
});
});
});

describe("callModel tool patching", () => {
Expand Down
28 changes: 28 additions & 0 deletions js/src/instrumentation/plugins/openrouter-agent-plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -633,6 +633,11 @@ export function aggregateOpenRouterChatChunks(
} {
let role: string | undefined;
let content = "";
let reasoning = "";
let hasReasoning = false;
let reasoningContent = "";
let hasReasoningContent = false;
let reasoningDetails: unknown[] | undefined;
let toolCalls:
| Array<{
index?: number;
Expand Down Expand Up @@ -670,6 +675,24 @@ export function aggregateOpenRouterChatChunks(
content += delta.content;
}

if (typeof delta.reasoning === "string") {
reasoning += delta.reasoning;
hasReasoning = true;
}

if (typeof delta.reasoning_content === "string") {
reasoning += delta.reasoning_content;
reasoningContent += delta.reasoning_content;
hasReasoningContent = true;
}

if (Array.isArray(delta.reasoning_details)) {
reasoningDetails = [
...(reasoningDetails || []),
...delta.reasoning_details,
];
}

const choiceFinishReason =
choice?.finishReason ?? choice?.finish_reason ?? undefined;
const deltaFinishReason =
Expand Down Expand Up @@ -740,6 +763,11 @@ export function aggregateOpenRouterChatChunks(
message: {
role,
content: content || undefined,
...(hasReasoning || hasReasoningContent ? { reasoning } : {}),
...(hasReasoningContent
? { reasoning_content: reasoningContent }
: {}),
...(reasoningDetails ? { reasoning_details: reasoningDetails } : {}),
...(toolCalls ? { tool_calls: toolCalls } : {}),
},
logprobs: null,
Expand Down
65 changes: 65 additions & 0 deletions js/src/instrumentation/plugins/openrouter-plugin.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -282,6 +282,71 @@ describe("OpenRouter Plugin", () => {
metrics: {},
});
});

it("should aggregate reasoning fields from streaming deltas", () => {
expect(
aggregateOpenRouterChatChunks([
{
choices: [
{
delta: {
role: "assistant",
reasoning: "First, ",
reasoning_details: [
{
type: "reasoning.text",
text: "First, ",
},
],
},
},
],
},
{
choices: [
{
delta: {
reasoning_content: "check the answer.",
content: "OK",
reasoning_details: [
{
type: "reasoning.summary",
summary: "Checked the answer.",
},
],
},
finish_reason: "stop",
},
],
},
]),
).toEqual({
output: [
{
index: 0,
message: {
role: "assistant",
content: "OK",
reasoning: "First, check the answer.",
reasoning_content: "check the answer.",
reasoning_details: [
{
type: "reasoning.text",
text: "First, ",
},
{
type: "reasoning.summary",
summary: "Checked the answer.",
},
],
},
logprobs: null,
finish_reason: "stop",
},
],
metrics: {},
});
});
});

describe("aggregateOpenRouterResponseStreamEvents", () => {
Expand Down
28 changes: 28 additions & 0 deletions js/src/instrumentation/plugins/openrouter-plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -815,6 +815,11 @@ export function aggregateOpenRouterChatChunks(
} {
let role: string | undefined;
let content = "";
let reasoning = "";
let hasReasoning = false;
let reasoningContent = "";
let hasReasoningContent = false;
let reasoningDetails: unknown[] | undefined;
let toolCalls:
| Array<{
index?: number;
Expand Down Expand Up @@ -852,6 +857,24 @@ export function aggregateOpenRouterChatChunks(
content += delta.content;
}

if (typeof delta.reasoning === "string") {
reasoning += delta.reasoning;
hasReasoning = true;
}

if (typeof delta.reasoning_content === "string") {
reasoning += delta.reasoning_content;
reasoningContent += delta.reasoning_content;
hasReasoningContent = true;
}

if (Array.isArray(delta.reasoning_details)) {
reasoningDetails = [
...(reasoningDetails || []),
...delta.reasoning_details,
];
}

const choiceFinishReason =
choice?.finishReason ?? choice?.finish_reason ?? undefined;
const deltaFinishReason =
Expand Down Expand Up @@ -922,6 +945,11 @@ export function aggregateOpenRouterChatChunks(
message: {
role,
content: content || undefined,
...(hasReasoning || hasReasoningContent ? { reasoning } : {}),
...(hasReasoningContent
? { reasoning_content: reasoningContent }
: {}),
...(reasoningDetails ? { reasoning_details: reasoningDetails } : {}),
...(toolCalls ? { tool_calls: toolCalls } : {}),
},
logprobs: null,
Expand Down
6 changes: 6 additions & 0 deletions js/src/vendor-sdk-types/openrouter-agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@ export type OpenRouterAgentChatChoice = {
message?: {
role?: string;
content?: string | null;
reasoning?: string | null;
reasoning_content?: string | null;
reasoning_details?: unknown[];
tool_calls?: unknown;
};
logprobs?: unknown;
Expand All @@ -26,6 +29,9 @@ export type OpenRouterAgentChatCompletionChunk = {
delta?: {
role?: string;
content?: string;
reasoning?: string;
reasoning_content?: string;
reasoning_details?: unknown[];
tool_calls?: OpenRouterAgentChatToolCallDelta[];
toolCalls?: OpenRouterAgentChatToolCallDelta[];
finish_reason?: string | null;
Expand Down
Loading
Loading