diff --git a/assets/config.schema.json b/assets/config.schema.json index d52a8efd2e..02a9c80660 100644 --- a/assets/config.schema.json +++ b/assets/config.schema.json @@ -85,6 +85,10 @@ "description": "Whether to use tool call middleware", "type": "boolean" }, + "useReasoningMiddleware": { + "description": "Whether to use reasoning middleware, which extracts reasoning wrapped in tags from the model output", + "type": "boolean" + }, "contentType": { "description": "The supported mime types model can handle", "type": "array", @@ -145,6 +149,10 @@ "description": "Whether to use tool call middleware", "type": "boolean" }, + "useReasoningMiddleware": { + "description": "Whether to use reasoning middleware, which extracts reasoning wrapped in tags from the model output", + "type": "boolean" + }, "contentType": { "description": "The supported mime types model can handle", "type": "array", @@ -206,6 +214,10 @@ "description": "Whether to use tool call middleware", "type": "boolean" }, + "useReasoningMiddleware": { + "description": "Whether to use reasoning middleware, which extracts reasoning wrapped in tags from the model output", + "type": "boolean" + }, "contentType": { "description": "The supported mime types model can handle", "type": "array", @@ -267,6 +279,10 @@ "description": "Whether to use tool call middleware", "type": "boolean" }, + "useReasoningMiddleware": { + "description": "Whether to use reasoning middleware, which extracts reasoning wrapped in tags from the model output", + "type": "boolean" + }, "contentType": { "description": "The supported mime types model can handle", "type": "array", @@ -397,6 +413,10 @@ "description": "Whether to use tool call middleware", "type": "boolean" }, + "useReasoningMiddleware": { + "description": "Whether to use reasoning middleware, which extracts reasoning wrapped in tags from the model output", + "type": "boolean" + }, "contentType": { "description": "The supported mime types model can handle", "type": "array", @@ -454,6 +474,10 @@ "description": "Whether to use tool call middleware", "type": "boolean" }, + "useReasoningMiddleware": { + "description": "Whether to use reasoning middleware, which extracts reasoning wrapped in tags from the model output", + "type": "boolean" + }, "contentType": { "description": "The supported mime types model can handle", "type": "array", diff --git a/packages/cli/src/cli.ts b/packages/cli/src/cli.ts index 06a325bb6f..7c583fda28 100755 --- a/packages/cli/src/cli.ts +++ b/packages/cli/src/cli.ts @@ -704,6 +704,7 @@ async function createLLMConfigWithVendors( contextWindow: options.contextWindow, useToolCallMiddleware: options.useToolCallMiddleware, + useReasoningMiddleware: options.useReasoningMiddleware, getModel: () => createModel(vendorId, { modelId, @@ -728,6 +729,7 @@ async function createLLMConfigWithPochi( contextWindow: pochiModelOptions.contextWindow, useToolCallMiddleware: pochiModelOptions.useToolCallMiddleware, + useReasoningMiddleware: pochiModelOptions.useReasoningMiddleware, getModel: () => createModel(vendorId, { modelId: model, @@ -778,6 +780,7 @@ async function createLLMConfigWithProviders( maxOutputTokens: modelSetting.maxTokens ?? constants.DefaultMaxOutputTokens, useToolCallMiddleware: modelSetting.useToolCallMiddleware, + useReasoningMiddleware: modelSetting.useReasoningMiddleware, contentType: modelSetting.contentType, }; } @@ -801,6 +804,7 @@ async function createLLMConfigWithProviders( maxOutputTokens: modelSetting.maxTokens ?? constants.DefaultMaxOutputTokens, useToolCallMiddleware: modelSetting.useToolCallMiddleware, + useReasoningMiddleware: modelSetting.useReasoningMiddleware, contentType: modelSetting.contentType, }; } diff --git a/packages/common/src/configuration/model.ts b/packages/common/src/configuration/model.ts index 724873a091..d7d432365f 100644 --- a/packages/common/src/configuration/model.ts +++ b/packages/common/src/configuration/model.ts @@ -26,6 +26,12 @@ const BaseModelSettings = z.object({ .boolean() .optional() .describe("Whether to use tool call middleware"), + useReasoningMiddleware: z + .boolean() + .optional() + .describe( + "Whether to use reasoning middleware, which extracts reasoning wrapped in tags from the model output", + ), contentType: z .array(z.string()) .optional() diff --git a/packages/common/src/vendor/types.ts b/packages/common/src/vendor/types.ts index 26b18939bc..3c05652113 100644 --- a/packages/common/src/vendor/types.ts +++ b/packages/common/src/vendor/types.ts @@ -4,6 +4,7 @@ export const ModelOptions = z.object({ label: z.string().optional(), contextWindow: z.number().optional(), useToolCallMiddleware: z.boolean().optional(), + useReasoningMiddleware: z.boolean().optional(), contentType: z.array(z.string()).optional(), }); diff --git a/packages/livekit/src/chat/__tests__/reasoning-middleware.test.ts b/packages/livekit/src/chat/__tests__/reasoning-middleware.test.ts new file mode 100644 index 0000000000..26875be181 --- /dev/null +++ b/packages/livekit/src/chat/__tests__/reasoning-middleware.test.ts @@ -0,0 +1,363 @@ +import type { + LanguageModelV3, + LanguageModelV3CallOptions, + LanguageModelV3StreamPart, + LanguageModelV3Usage, +} from '@ai-sdk/provider'; +import type { UIMessage } from 'ai'; +import { + convertToModelMessages, + readUIMessageStream, + streamText, + wrapLanguageModel, +} from 'ai'; +import { describe, expect, it } from 'vitest'; +import { + ReasoningTagMetadataKey, + createReasoningMiddleware, +} from '../middlewares/reasoning-middleware'; + +const emptyUsage: LanguageModelV3Usage = { + inputTokens: { + total: undefined, + noCache: undefined, + cacheRead: undefined, + cacheWrite: undefined, + }, + outputTokens: { + total: undefined, + text: undefined, + reasoning: undefined, + }, +}; + +function createTextStream(deltas: string[]) { + const chunks: LanguageModelV3StreamPart[] = [ + { type: 'stream-start', warnings: [] }, + { type: 'text-start', id: 'text-0' }, + ...deltas.map( + (delta): LanguageModelV3StreamPart => ({ + type: 'text-delta', + id: 'text-0', + delta, + }), + ), + { type: 'text-end', id: 'text-0' }, + { + type: 'finish', + finishReason: { unified: 'stop', raw: 'stop' }, + usage: emptyUsage, + }, + ]; + + return new ReadableStream({ + start(controller) { + for (const chunk of chunks) { + controller.enqueue(chunk); + } + controller.close(); + }, + }); +} + +function createModel( + deltas: string[], + calls: LanguageModelV3CallOptions[] = [], +): LanguageModelV3 { + return { + specificationVersion: 'v3', + provider: 'test', + modelId: 'test-model', + supportedUrls: {}, + doGenerate: async () => { + throw new Error('not implemented'); + }, + doStream: async (options) => { + calls.push(options); + return { stream: createTextStream(deltas) }; + }, + }; +} + +async function streamParts( + deltas: string[], + tag?: string, +): Promise { + const model = wrapLanguageModel({ + model: createModel(deltas), + middleware: createReasoningMiddleware(tag), + }); + + const { stream } = await model.doStream({ prompt: [] }); + const parts: LanguageModelV3StreamPart[] = []; + for await (const part of stream) { + parts.push(part); + } + return parts; +} + +function reasoningText(parts: LanguageModelV3StreamPart[]): string { + return parts + .filter( + (part): part is Extract => + part.type === 'reasoning-delta', + ) + .map((part) => part.delta) + .join(''); +} + +function text(parts: LanguageModelV3StreamPart[]): string { + return parts + .filter( + (part): part is Extract => + part.type === 'text-delta', + ) + .map((part) => part.delta) + .join(''); +} + +function reasoningStart(parts: LanguageModelV3StreamPart[]) { + return parts.find((part) => part.type === 'reasoning-start'); +} + +describe('createReasoningMiddleware', () => { + it('extracts reasoning from a plain tag', async () => { + const parts = await streamParts(['secretanswer']); + + expect(reasoningText(parts)).toBe('secret'); + expect(text(parts)).toBe('answer'); + expect(reasoningStart(parts)).toEqual({ + type: 'reasoning-start', + id: 'reasoning-1', + }); + }); + + it('keeps tag attributes in provider metadata', async () => { + const parts = await streamParts([ + 'secretanswer', + ]); + + expect(reasoningText(parts)).toBe('secret'); + expect(text(parts)).toBe('answer'); + expect(reasoningStart(parts)).toEqual({ + type: 'reasoning-start', + id: 'reasoning-1', + providerMetadata: { + [ReasoningTagMetadataKey]: { + tag: 'think', + attributes: ' signature="abc123"', + }, + }, + }); + }); + + it('handles tags with attributes split across deltas', async () => { + const parts = await streamParts([ + 'before sec', + 'retafter', + ]); + + expect(reasoningText(parts)).toBe('secret'); + expect(text(parts)).toBe('before after'); + expect(reasoningStart(parts)).toMatchObject({ + providerMetadata: { + [ReasoningTagMetadataKey]: { + tag: 'think', + attributes: ' signature="abc123"', + }, + }, + }); + }); + + it('supports multiple reasoning sections with different attributes', async () => { + const parts = await streamParts([ + 'firsta', + 'secondb', + ]); + + expect( + parts.filter((part) => part.type === 'reasoning-start'), + ).toEqual([ + { + type: 'reasoning-start', + id: 'reasoning-1', + providerMetadata: { + [ReasoningTagMetadataKey]: { + tag: 'think', + attributes: ' signature="one"', + }, + }, + }, + { + type: 'reasoning-start', + id: 'reasoning-2', + providerMetadata: { + [ReasoningTagMetadataKey]: { + tag: 'think', + attributes: ' signature="two"', + }, + }, + }, + ]); + expect(text(parts)).toBe('ab'); + }); + + it('does not treat tags sharing the prefix as reasoning', async () => { + const parts = await streamParts(['not reasoning']); + + expect(reasoningText(parts)).toBe(''); + expect(text(parts)).toBe('not reasoning'); + }); + + it('flushes an incomplete tag as text when the text section ends', async () => { + const parts = await streamParts(['answer { + const parts = await streamParts(['secret']); + + expect(reasoningText(parts)).toBe('secret'); + expect(parts.at(-2)).toEqual({ + type: 'reasoning-end', + id: 'reasoning-1', + }); + }); + + it('supports a custom tag name', async () => { + const parts = await streamParts( + ['secretanswer'], + 'reasoning', + ); + + expect(reasoningText(parts)).toBe('secret'); + expect(text(parts)).toBe('answer'); + expect(reasoningStart(parts)).toMatchObject({ + providerMetadata: { + [ReasoningTagMetadataKey]: { + tag: 'reasoning', + attributes: ' depth="2"', + }, + }, + }); + }); +}); + +/** + * Simulates a full turn: the model streams `deltas`, the UI message is rebuilt + * from the resulting UI stream, and the assistant message is returned. + */ +async function runTurn( + model: LanguageModelV3, + messages: UIMessage[], +): Promise { + const result = streamText({ + model, + messages: await convertToModelMessages(messages), + onError: ({ error }) => { + throw error; + }, + }); + + let assistantMessage: UIMessage | undefined; + for await (const message of readUIMessageStream({ + stream: result.toUIMessageStream(), + })) { + assistantMessage = message; + } + + if (!assistantMessage) { + throw new Error('No assistant message was produced'); + } + + return assistantMessage; +} + +describe('createReasoningMiddleware multi turn', () => { + it('echoes reasoning back in its original tag format', async () => { + const calls: LanguageModelV3CallOptions[] = []; + const model = wrapLanguageModel({ + model: createModel( + ['secretanswer'], + calls, + ), + middleware: createReasoningMiddleware(), + }); + + const userMessage: UIMessage = { + id: 'user-1', + role: 'user', + parts: [{ type: 'text', text: 'hi' }], + }; + const assistantMessage = await runTurn(model, [userMessage]); + + expect( + assistantMessage.parts.filter((part) => part.type !== 'step-start'), + ).toMatchObject([ + { + type: 'reasoning', + text: 'secret', + providerMetadata: { + [ReasoningTagMetadataKey]: { + tag: 'think', + attributes: ' signature="abc123"', + }, + }, + }, + { type: 'text', text: 'answer' }, + ]); + + // Second turn: the reasoning is sent back to the model verbatim. + await runTurn(model, [ + userMessage, + assistantMessage, + { + id: 'user-2', + role: 'user', + parts: [{ type: 'text', text: 'thanks' }], + }, + ]); + + expect(calls).toHaveLength(2); + expect(calls[1].prompt[1]).toEqual({ + role: 'assistant', + content: [ + { + type: 'text', + text: 'secret', + }, + { type: 'text', text: 'answer' }, + ], + }); + }); + + it('echoes reasoning without attributes back in a plain tag', async () => { + const calls: LanguageModelV3CallOptions[] = []; + const model = wrapLanguageModel({ + model: createModel(['secretanswer'], calls), + middleware: createReasoningMiddleware(), + }); + + const userMessage: UIMessage = { + id: 'user-1', + role: 'user', + parts: [{ type: 'text', text: 'hi' }], + }; + const assistantMessage = await runTurn(model, [userMessage]); + + await runTurn(model, [userMessage, assistantMessage]); + + expect(calls[1].prompt[1]).toMatchObject({ + role: 'assistant', + content: [ + { type: 'text', text: 'secret' }, + { type: 'text', text: 'answer' }, + ], + }); + }); +}); diff --git a/packages/livekit/src/chat/flexible-chat-transport.ts b/packages/livekit/src/chat/flexible-chat-transport.ts index 8a26ce8823..5de28670d8 100644 --- a/packages/livekit/src/chat/flexible-chat-transport.ts +++ b/packages/livekit/src/chat/flexible-chat-transport.ts @@ -267,7 +267,7 @@ export class FlexibleChatTransport implements ChatTransport { ); } - if ("modelId" in llm && isWellKnownReasoningModel(llm.modelId)) { + if (useReasoningMiddleware(llm)) { middlewares.push(createReasoningMiddleware()); } @@ -455,6 +455,19 @@ function prepareMessages(inputMessages: Message[]): Message[] { return convertDataReviewsToText(inputMessages); } +/** + * The reasoning middleware is opt-in via model configuration + * (`useReasoningMiddleware`), falling back to a well-known model list when the + * setting is not provided. + */ +function useReasoningMiddleware(llm: RequestData["llm"]): boolean { + if (llm.useReasoningMiddleware !== undefined) { + return llm.useReasoningMiddleware; + } + + return "modelId" in llm && isWellKnownReasoningModel(llm.modelId); +} + function isWellKnownReasoningModel(model?: string): boolean { if (!model) return false; diff --git a/packages/livekit/src/chat/middlewares/reasoning-middleware.ts b/packages/livekit/src/chat/middlewares/reasoning-middleware.ts index 40f6c1ac84..b4f3503f07 100644 --- a/packages/livekit/src/chat/middlewares/reasoning-middleware.ts +++ b/packages/livekit/src/chat/middlewares/reasoning-middleware.ts @@ -1,13 +1,30 @@ import type { LanguageModelV3Middleware, + LanguageModelV3Prompt, LanguageModelV3StreamPart, } from "@ai-sdk/provider"; import { getPotentialStartIndex } from "./utils"; +/** + * Provider metadata namespace used to remember how the reasoning tag was + * originally written by the model, so it can be echoed back verbatim in + * follow-up requests. + */ +export const ReasoningTagMetadataKey = "pochiReasoningTag"; + +export type ReasoningTagMetadata = { + /** The tag name, e.g. `think`. */ + tag: string; + /** The raw attributes of the opening tag, e.g. ` signature="abc"`. */ + attributes: string; +}; + export function createReasoningMiddleware( tag = "think", ): LanguageModelV3Middleware { - const tagStart = `<${tag}>`; + // The opening tag may carry attributes, e.g. ``. + const tagStartPrefix = `<${tag}`; + const tagStartRegex = new RegExp(`^<${escapeRegExp(tag)}(\\s[^>]*)?>$`); const tagEnd = ``; let countReasoning = 0; let textId = ""; @@ -24,6 +41,20 @@ export function createReasoningMiddleware( return { specificationVersion: "v3", + /** + * Models parsed by this middleware emit their reasoning as part of the text + * content, so the reasoning has to be sent back in the very same format: + * providers either drop reasoning parts or map them to a dedicated field + * the model never wrote itself. + */ + transformParams: async ({ params }) => ({ + ...params, + prompt: params.prompt.map((message) => + message.role === "assistant" + ? { ...message, content: serializeReasoning(message.content) } + : message, + ) as LanguageModelV3Prompt, + }), wrapStream: async ({ doStream }) => { const { stream, ...rest } = await doStream(); const transformedStream = stream.pipeThrough( @@ -39,6 +70,19 @@ export function createReasoningMiddleware( } if (chunk.type === "text-end") { + // Flush whatever is left in the buffer: it can hold an + // incomplete tag that will never be completed. + if (buffer.length > 0) { + publish(controller, buffer); + buffer = ""; + } + if (isReasoning) { + isReasoning = false; + controller.enqueue({ + type: "reasoning-end", + id: getReasoningId(), + }); + } textId = ""; // Skip entire text section if it's empty. if (pendingTextStart) { @@ -54,46 +98,16 @@ export function createReasoningMiddleware( buffer += chunk.delta; - function publish(text: string) { - const isEmptyText = text.trim().length === 0; - if (isReasoning) { - if (isFirstReasoning && isEmptyText) { - // Skip - } else { - controller.enqueue({ - id: getReasoningId(), - type: "reasoning-delta", - delta: text, - }); - } - isFirstReasoning = false; - } else { - if (pendingTextStart && isEmptyText) { - // Skip - } else { - if (pendingTextStart) { - controller.enqueue(pendingTextStart); - pendingTextStart = undefined; - } - controller.enqueue({ - id: textId, - type: "text-delta", - delta: text, - }); - } - } - } - do { if (isReasoning) { const endIndex = getPotentialStartIndex(buffer, tagEnd); if (endIndex === null) { - publish(buffer); + publish(controller, buffer); buffer = ""; break; } - publish(buffer.slice(0, endIndex)); + publish(controller, buffer.slice(0, endIndex)); const foundFullEndMatch = endIndex + tagEnd.length <= buffer.length; @@ -110,27 +124,60 @@ export function createReasoningMiddleware( break; } } else { - const startIndex = getPotentialStartIndex(buffer, tagStart); + const startIndex = getPotentialStartIndex( + buffer, + tagStartPrefix, + ); if (startIndex === null) { - publish(buffer); + publish(controller, buffer); buffer = ""; break; } - publish(buffer.slice(0, startIndex)); - const foundFullStartMatch = - startIndex + tagStart.length <= buffer.length; - if (foundFullStartMatch) { - buffer = buffer.slice(startIndex + tagStart.length); - isReasoning = true; - countReasoning++; - controller.enqueue({ - type: "reasoning-start", - id: getReasoningId(), - }); - } else { + + const foundFullStartPrefix = + startIndex + tagStartPrefix.length <= buffer.length; + if (!foundFullStartPrefix) { + publish(controller, buffer.slice(0, startIndex)); + buffer = buffer.slice(startIndex); + break; + } + + const tagEndIndex = buffer.indexOf(">", startIndex); + if (tagEndIndex < 0) { + // The opening tag is not complete yet, wait for more deltas. + publish(controller, buffer.slice(0, startIndex)); buffer = buffer.slice(startIndex); break; } + + const tagStart = buffer.slice(startIndex, tagEndIndex + 1); + const match = tagStart.match(tagStartRegex); + if (!match) { + // A different tag sharing the same prefix, e.g. ``. + publish(controller, buffer.slice(0, tagEndIndex + 1)); + buffer = buffer.slice(tagEndIndex + 1); + continue; + } + + publish(controller, buffer.slice(0, startIndex)); + buffer = buffer.slice(startIndex + tagStart.length); + isReasoning = true; + countReasoning++; + const attributes = match[1] ?? ""; + controller.enqueue({ + type: "reasoning-start", + id: getReasoningId(), + ...(attributes + ? { + providerMetadata: { + [ReasoningTagMetadataKey]: { + tag, + attributes, + } satisfies ReasoningTagMetadata, + }, + } + : {}), + }); } // biome-ignore lint/correctness/noConstantCondition: This loop intentionally runs indefinitely, processing the buffer in chunks until no more complete tags can be found. The loop breaks internally based on buffer content and parsing progress. @@ -144,4 +191,71 @@ export function createReasoningMiddleware( }; }, }; + + /** + * Renders assistant reasoning content back into the tag the model used, e.g. + * `...`. + */ + function serializeReasoning( + content: Extract< + LanguageModelV3Prompt[number], + { role: "assistant" } + >["content"], + ) { + return content.map((part) => { + if (part.type !== "reasoning") return part; + + const { [ReasoningTagMetadataKey]: metadata, ...providerOptions } = + part.providerOptions ?? {}; + const tagName = + typeof metadata?.tag === "string" && metadata.tag ? metadata.tag : tag; + const attributes = + typeof metadata?.attributes === "string" ? metadata.attributes : ""; + const separator = !attributes || attributes.startsWith(" ") ? "" : " "; + + return { + type: "text" as const, + text: `<${tagName}${separator}${attributes}>${part.text}`, + ...(Object.keys(providerOptions).length > 0 ? { providerOptions } : {}), + }; + }); + } + + function publish( + controller: TransformStreamDefaultController, + text: string, + ) { + if (text.length === 0) return; + const isEmptyText = text.trim().length === 0; + if (isReasoning) { + if (isFirstReasoning && isEmptyText) { + // Skip + } else { + controller.enqueue({ + id: getReasoningId(), + type: "reasoning-delta", + delta: text, + }); + } + isFirstReasoning = false; + } else { + if (pendingTextStart && isEmptyText) { + // Skip + } else { + if (pendingTextStart) { + controller.enqueue(pendingTextStart); + pendingTextStart = undefined; + } + controller.enqueue({ + id: textId, + type: "text-delta", + delta: text, + }); + } + } + } +} + +function escapeRegExp(value: string): string { + return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); } diff --git a/packages/livekit/src/types.ts b/packages/livekit/src/types.ts index 3ab9cb8ff7..285fa51221 100644 --- a/packages/livekit/src/types.ts +++ b/packages/livekit/src/types.ts @@ -88,6 +88,10 @@ const RequestData = z.object({ .boolean() .optional() .describe("Whether to use tool call middleware"), + useReasoningMiddleware: z + .boolean() + .optional() + .describe("Whether to use reasoning middleware"), contentType: z .array(z.string()) .optional() @@ -106,6 +110,10 @@ const RequestData = z.object({ .boolean() .optional() .describe("Whether to use tool call middleware"), + useReasoningMiddleware: z + .boolean() + .optional() + .describe("Whether to use reasoning middleware"), contentType: z .array(z.string()) .optional() @@ -124,6 +132,10 @@ const RequestData = z.object({ .boolean() .optional() .describe("Whether to use tool call middleware"), + useReasoningMiddleware: z + .boolean() + .optional() + .describe("Whether to use reasoning middleware"), contentType: z .array(z.string()) .optional() @@ -141,6 +153,10 @@ const RequestData = z.object({ .boolean() .optional() .describe("Whether to use tool call middleware"), + useReasoningMiddleware: z + .boolean() + .optional() + .describe("Whether to use reasoning middleware"), contentType: z .array(z.string()) .optional() @@ -158,6 +174,10 @@ const RequestData = z.object({ .boolean() .optional() .describe("Whether to use tool call middleware"), + useReasoningMiddleware: z + .boolean() + .optional() + .describe("Whether to use reasoning middleware"), contentType: z .array(z.string()) .optional() @@ -175,6 +195,10 @@ const RequestData = z.object({ .boolean() .optional() .describe("Whether to use tool call middleware"), + useReasoningMiddleware: z + .boolean() + .optional() + .describe("Whether to use reasoning middleware"), contentType: z .array(z.string()) .optional() @@ -192,6 +216,10 @@ const RequestData = z.object({ .boolean() .optional() .describe("Whether to use tool call middleware"), + useReasoningMiddleware: z + .boolean() + .optional() + .describe("Whether to use reasoning middleware"), getModel: z.custom<() => LanguageModelV3>(), contentType: z .array(z.string()) diff --git a/packages/vscode-webui/src/features/chat/lib/display-model-to-llm.ts b/packages/vscode-webui/src/features/chat/lib/display-model-to-llm.ts index 5aeb37c2b5..9d694c9438 100644 --- a/packages/vscode-webui/src/features/chat/lib/display-model-to-llm.ts +++ b/packages/vscode-webui/src/features/chat/lib/display-model-to-llm.ts @@ -18,6 +18,7 @@ export function displayModelToLLM(model: DisplayModel): LLMRequestData { type: "vendor", contextWindow: model.options.contextWindow, useToolCallMiddleware: model.options.useToolCallMiddleware, + useReasoningMiddleware: model.options.useReasoningMiddleware, getModel: () => createModel(model.vendorId, { modelId: model.modelId, @@ -39,6 +40,7 @@ export function displayModelToLLM(model: DisplayModel): LLMRequestData { contextWindow: model.options.contextWindow ?? constants.DefaultContextWindow, useToolCallMiddleware: model.options.useToolCallMiddleware, + useReasoningMiddleware: model.options.useReasoningMiddleware, contentType: model.contentType, }; } @@ -54,6 +56,7 @@ export function displayModelToLLM(model: DisplayModel): LLMRequestData { contextWindow: model.options.contextWindow ?? constants.DefaultContextWindow, useToolCallMiddleware: model.options.useToolCallMiddleware, + useReasoningMiddleware: model.options.useReasoningMiddleware, contentType: model.contentType, }; } @@ -76,6 +79,7 @@ export function displayModelToLLM(model: DisplayModel): LLMRequestData { contextWindow: model.options.contextWindow ?? constants.DefaultContextWindow, useToolCallMiddleware: model.options.useToolCallMiddleware, + useReasoningMiddleware: model.options.useReasoningMiddleware, contentType: model.contentType, }; }