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 {
|
||||
role: 'system' | 'user' | 'assistant' | 'tool'
|
||||
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
|
||||
tool_calls?: any[] // Simplified tool_call_id?: string
|
||||
}
|
||||
|
||||
@ -29,6 +29,10 @@ import { updateSettings } from '@/services/providers'
|
||||
import { useContextSizeApproval } from './useModelContextApproval'
|
||||
import { useModelLoad } from './useModelLoad'
|
||||
import { useGeneralSetting } from './useGeneralSetting'
|
||||
import {
|
||||
ReasoningProcessor,
|
||||
extractReasoningFromMessage,
|
||||
} from '@/utils/reasoning'
|
||||
|
||||
export const useChat = () => {
|
||||
const { prompt, setPrompt } = usePrompt()
|
||||
@ -285,16 +289,25 @@ export const useChat = () => {
|
||||
const toolCalls: ChatCompletionMessageToolCall[] = []
|
||||
try {
|
||||
if (isCompletionResponse(completion)) {
|
||||
accumulatedText =
|
||||
(completion.choices[0]?.message?.content as string) || ''
|
||||
if (completion.choices[0]?.message?.tool_calls) {
|
||||
toolCalls.push(...completion.choices[0].message.tool_calls)
|
||||
const message = completion.choices[0]?.message
|
||||
accumulatedText = (message?.content as string) || ''
|
||||
|
||||
// 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 {
|
||||
// High-throughput scheduler: batch UI updates on rAF (requestAnimationFrame)
|
||||
let rafScheduled = false
|
||||
let rafHandle: number | undefined
|
||||
let pendingDeltaCount = 0
|
||||
const reasoningProcessor = new ReasoningProcessor()
|
||||
const scheduleFlush = () => {
|
||||
if (rafScheduled) return
|
||||
rafScheduled = true
|
||||
@ -328,7 +341,10 @@ export const useChat = () => {
|
||||
}
|
||||
const flushIfPending = () => {
|
||||
if (!rafScheduled) return
|
||||
if (typeof cancelAnimationFrame !== 'undefined' && rafHandle !== undefined) {
|
||||
if (
|
||||
typeof cancelAnimationFrame !== 'undefined' &&
|
||||
rafHandle !== undefined
|
||||
) {
|
||||
cancelAnimationFrame(rafHandle)
|
||||
} else if (rafHandle !== undefined) {
|
||||
clearTimeout(rafHandle)
|
||||
@ -360,20 +376,30 @@ export const useChat = () => {
|
||||
: (JSON.stringify(part) ?? '')
|
||||
)
|
||||
}
|
||||
const delta = part.choices[0]?.delta?.content || ''
|
||||
|
||||
if (part.choices[0]?.delta?.tool_calls) {
|
||||
extractToolCall(part, currentCall, toolCalls)
|
||||
// Schedule a flush to reflect tool update
|
||||
scheduleFlush()
|
||||
}
|
||||
if (delta) {
|
||||
accumulatedText += delta
|
||||
const deltaReasoning =
|
||||
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
|
||||
// Batch UI update on next animation frame
|
||||
scheduleFlush()
|
||||
}
|
||||
}
|
||||
// Finalize reasoning (close any open think tags)
|
||||
accumulatedText += reasoningProcessor.finalize()
|
||||
// Ensure any pending buffered content is rendered at the end
|
||||
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