feat: add support for reasoning fields (OpenRouter) (#6206)
* add support for reasoning fields (OpenRouter) * reformat * fix linter * Update web-app/src/utils/reasoning.ts Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com> --------- Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com>
This commit is contained in:
parent
5ad3d282af
commit
2d486d7b3a
@ -6,6 +6,7 @@ import { EngineManager } from './EngineManager'
|
|||||||
export interface chatCompletionRequestMessage {
|
export interface chatCompletionRequestMessage {
|
||||||
role: 'system' | 'user' | 'assistant' | 'tool'
|
role: 'system' | 'user' | 'assistant' | 'tool'
|
||||||
content: string | null | Content[] // Content can be a string OR an array of content parts
|
content: string | null | Content[] // Content can be a string OR an array of content parts
|
||||||
|
reasoning?: string | null // Some models return reasoning in completed responses
|
||||||
name?: string
|
name?: string
|
||||||
tool_calls?: any[] // Simplified tool_call_id?: string
|
tool_calls?: any[] // Simplified tool_call_id?: string
|
||||||
}
|
}
|
||||||
|
|||||||
@ -29,6 +29,10 @@ import { updateSettings } from '@/services/providers'
|
|||||||
import { useContextSizeApproval } from './useModelContextApproval'
|
import { useContextSizeApproval } from './useModelContextApproval'
|
||||||
import { useModelLoad } from './useModelLoad'
|
import { useModelLoad } from './useModelLoad'
|
||||||
import { useGeneralSetting } from './useGeneralSetting'
|
import { useGeneralSetting } from './useGeneralSetting'
|
||||||
|
import {
|
||||||
|
ReasoningProcessor,
|
||||||
|
extractReasoningFromMessage,
|
||||||
|
} from '@/utils/reasoning'
|
||||||
|
|
||||||
export const useChat = () => {
|
export const useChat = () => {
|
||||||
const { prompt, setPrompt } = usePrompt()
|
const { prompt, setPrompt } = usePrompt()
|
||||||
@ -285,16 +289,25 @@ export const useChat = () => {
|
|||||||
const toolCalls: ChatCompletionMessageToolCall[] = []
|
const toolCalls: ChatCompletionMessageToolCall[] = []
|
||||||
try {
|
try {
|
||||||
if (isCompletionResponse(completion)) {
|
if (isCompletionResponse(completion)) {
|
||||||
accumulatedText =
|
const message = completion.choices[0]?.message
|
||||||
(completion.choices[0]?.message?.content as string) || ''
|
accumulatedText = (message?.content as string) || ''
|
||||||
if (completion.choices[0]?.message?.tool_calls) {
|
|
||||||
toolCalls.push(...completion.choices[0].message.tool_calls)
|
// Handle reasoning field if there is one
|
||||||
|
const reasoning = extractReasoningFromMessage(message)
|
||||||
|
if (reasoning) {
|
||||||
|
accumulatedText =
|
||||||
|
`<think>${reasoning}</think>` + accumulatedText
|
||||||
|
}
|
||||||
|
|
||||||
|
if (message?.tool_calls) {
|
||||||
|
toolCalls.push(...message.tool_calls)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// High-throughput scheduler: batch UI updates on rAF (requestAnimationFrame)
|
// High-throughput scheduler: batch UI updates on rAF (requestAnimationFrame)
|
||||||
let rafScheduled = false
|
let rafScheduled = false
|
||||||
let rafHandle: number | undefined
|
let rafHandle: number | undefined
|
||||||
let pendingDeltaCount = 0
|
let pendingDeltaCount = 0
|
||||||
|
const reasoningProcessor = new ReasoningProcessor()
|
||||||
const scheduleFlush = () => {
|
const scheduleFlush = () => {
|
||||||
if (rafScheduled) return
|
if (rafScheduled) return
|
||||||
rafScheduled = true
|
rafScheduled = true
|
||||||
@ -328,7 +341,10 @@ export const useChat = () => {
|
|||||||
}
|
}
|
||||||
const flushIfPending = () => {
|
const flushIfPending = () => {
|
||||||
if (!rafScheduled) return
|
if (!rafScheduled) return
|
||||||
if (typeof cancelAnimationFrame !== 'undefined' && rafHandle !== undefined) {
|
if (
|
||||||
|
typeof cancelAnimationFrame !== 'undefined' &&
|
||||||
|
rafHandle !== undefined
|
||||||
|
) {
|
||||||
cancelAnimationFrame(rafHandle)
|
cancelAnimationFrame(rafHandle)
|
||||||
} else if (rafHandle !== undefined) {
|
} else if (rafHandle !== undefined) {
|
||||||
clearTimeout(rafHandle)
|
clearTimeout(rafHandle)
|
||||||
@ -360,20 +376,30 @@ export const useChat = () => {
|
|||||||
: (JSON.stringify(part) ?? '')
|
: (JSON.stringify(part) ?? '')
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
const delta = part.choices[0]?.delta?.content || ''
|
|
||||||
|
|
||||||
if (part.choices[0]?.delta?.tool_calls) {
|
if (part.choices[0]?.delta?.tool_calls) {
|
||||||
extractToolCall(part, currentCall, toolCalls)
|
extractToolCall(part, currentCall, toolCalls)
|
||||||
// Schedule a flush to reflect tool update
|
// Schedule a flush to reflect tool update
|
||||||
scheduleFlush()
|
scheduleFlush()
|
||||||
}
|
}
|
||||||
if (delta) {
|
const deltaReasoning =
|
||||||
accumulatedText += delta
|
reasoningProcessor.processReasoningChunk(part)
|
||||||
|
if (deltaReasoning) {
|
||||||
|
accumulatedText += deltaReasoning
|
||||||
|
pendingDeltaCount += 1
|
||||||
|
// Schedule flush for reasoning updates
|
||||||
|
scheduleFlush()
|
||||||
|
}
|
||||||
|
const deltaContent = part.choices[0]?.delta?.content || ''
|
||||||
|
if (deltaContent) {
|
||||||
|
accumulatedText += deltaContent
|
||||||
pendingDeltaCount += 1
|
pendingDeltaCount += 1
|
||||||
// Batch UI update on next animation frame
|
// Batch UI update on next animation frame
|
||||||
scheduleFlush()
|
scheduleFlush()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// Finalize reasoning (close any open think tags)
|
||||||
|
accumulatedText += reasoningProcessor.finalize()
|
||||||
// Ensure any pending buffered content is rendered at the end
|
// Ensure any pending buffered content is rendered at the end
|
||||||
flushIfPending()
|
flushIfPending()
|
||||||
}
|
}
|
||||||
|
|||||||
74
web-app/src/utils/reasoning.ts
Normal file
74
web-app/src/utils/reasoning.ts
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
import { CompletionResponseChunk } from 'token.js'
|
||||||
|
import {
|
||||||
|
chatCompletionChunk,
|
||||||
|
ChatCompletionMessage,
|
||||||
|
chatCompletionRequestMessage,
|
||||||
|
} from '@janhq/core'
|
||||||
|
|
||||||
|
// Extract reasoning from a message (for completed responses)
|
||||||
|
export function extractReasoningFromMessage(
|
||||||
|
message: chatCompletionRequestMessage | ChatCompletionMessage
|
||||||
|
): string | null {
|
||||||
|
if (!message) return null
|
||||||
|
|
||||||
|
const extendedMessage = message as chatCompletionRequestMessage
|
||||||
|
return extendedMessage.reasoning || null
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract reasoning from a chunk (for streaming responses)
|
||||||
|
function extractReasoningFromChunk(
|
||||||
|
chunk: CompletionResponseChunk | chatCompletionChunk
|
||||||
|
): string | null {
|
||||||
|
if (!chunk.choices?.[0]?.delta) return null
|
||||||
|
|
||||||
|
const delta = chunk.choices[0].delta as chatCompletionRequestMessage
|
||||||
|
const reasoning = delta.reasoning
|
||||||
|
|
||||||
|
// Return null for falsy values, non-strings, or whitespace-only strings
|
||||||
|
if (!reasoning || typeof reasoning !== 'string' || !reasoning.trim())
|
||||||
|
return null
|
||||||
|
|
||||||
|
return reasoning
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tracks reasoning state and appends reasoning tokens with proper think tags
|
||||||
|
export class ReasoningProcessor {
|
||||||
|
private isReasoningActive = false
|
||||||
|
|
||||||
|
processReasoningChunk(
|
||||||
|
chunk: CompletionResponseChunk | chatCompletionChunk
|
||||||
|
): string {
|
||||||
|
const reasoning = extractReasoningFromChunk(chunk)
|
||||||
|
const chunkContent = chunk.choices?.[0]?.delta?.content || ''
|
||||||
|
|
||||||
|
// Handle reasoning tokens
|
||||||
|
if (reasoning) {
|
||||||
|
if (!this.isReasoningActive) {
|
||||||
|
this.isReasoningActive = true
|
||||||
|
return '<think>' + reasoning
|
||||||
|
}
|
||||||
|
return reasoning
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle reasoning end when content starts
|
||||||
|
if (this.isReasoningActive && chunkContent) {
|
||||||
|
this.isReasoningActive = false
|
||||||
|
return '</think>'
|
||||||
|
}
|
||||||
|
|
||||||
|
// No reasoning to process
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
|
||||||
|
finalize(): string {
|
||||||
|
if (this.isReasoningActive) {
|
||||||
|
this.isReasoningActive = false
|
||||||
|
return '</think>'
|
||||||
|
}
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
|
||||||
|
isReasoningInProgress(): boolean {
|
||||||
|
return this.isReasoningActive
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user