feat: Add reasoning token support with expandable thinking blocks

- Add reasoning field to AssistantMessage type
- Create ReasoningBlock component with truncate/expand functionality
- Add reasoning-capable models (Qwen QwQ, DeepSeek R1, Qwen 3 32B)
- Implement reasoning event handling in stream processing
- Enable reasoning format for Groq and Anthropic models
- Style reasoning blocks with grey border and reduced font size
- Display reasoning above assistant messages when available

The reasoning blocks show truncated thinking tokens by default and
expand on click to show full reasoning process from models like
Qwen3-32b that support step-by-step reasoning output.
This commit is contained in:
Glen Maddern 2025-06-24 16:41:56 +10:00
commit 62000dcdac
6 changed files with 384 additions and 300 deletions

View file

@ -5,6 +5,7 @@ import rehypeHighlight from "rehype-highlight";
import { type Message } from "../types";
import ToolCallMessage from "./ToolCallMessage";
import ToolResultMessage from "./ToolResultMessage";
import ReasoningBlock from "./ReasoningBlock";
interface ChatMessageProps {
message: Message;
@ -37,6 +38,11 @@ const ChatMessage: React.FC<ChatMessageProps> = ({
: " text-zinc-900 w-full"
}`}
>
{/* Show reasoning block for assistant messages with reasoning */}
{message.role === "assistant" && "reasoning" in message && message.reasoning && (
<ReasoningBlock reasoning={message.reasoning} />
)}
<div className="prose prose-zinc">
<ReactMarkdown
remarkPlugins={[remarkGfm]}

View file

@ -168,7 +168,7 @@ const ConversationThread: React.FC<ConversationThreadProps> = ({
}
}, [conversations])
console.log({ currentConversation })
// console.log({ currentConversation })
return (
<div className={`flex flex-col min-h-screen w-full ${currentConversation.messages.length === 0 ? 'justify-center' : ''}`}>
@ -233,7 +233,7 @@ const ConversationThread: React.FC<ConversationThreadProps> = ({
<span className="text-lg">🧠</span>
<span className="text-sm text-zinc-500">
{hasApiKey(selectedModel.provider.id) ? (
selectedModel.name.toLowerCase()
selectedModel.name.toLowerCase() + ' (' + selectedModel.provider.name.toLowerCase() + ')'
) : (
"none"
)}

View file

@ -0,0 +1,54 @@
import React, { useState } from "react";
interface ReasoningBlockProps {
reasoning: string;
}
const ReasoningBlock: React.FC<ReasoningBlockProps> = ({ reasoning }) => {
const [isExpanded, setIsExpanded] = useState(false);
if (!reasoning || reasoning.trim().length === 0) {
return null;
}
const toggleExpanded = () => {
setIsExpanded(!isExpanded);
};
// Truncate the reasoning text for the collapsed state
const maxLength = 120;
const shouldTruncate = reasoning.length > maxLength;
const displayText = isExpanded || !shouldTruncate
? reasoning
: reasoning.substring(0, maxLength) + "...";
return (
<div className="mb-3">
<div
className={`text-sm text-zinc-600 border border-zinc-200 rounded-lg p-3 bg-zinc-50 cursor-pointer hover:bg-zinc-100 transition-colors ${shouldTruncate && !isExpanded ? "line-clamp-1" : ""
}`}
onClick={toggleExpanded}
>
<div className="flex items-start gap-2">
<span className="text-zinc-400 text-xs mt-0.5 flex-shrink-0">💭</span>
<div className={`${shouldTruncate && !isExpanded ? "truncate" : "whitespace-pre-wrap"}`}>
{displayText}
</div>
{shouldTruncate && (
<button
className="text-zinc-400 hover:text-zinc-600 text-xs ml-auto flex-shrink-0"
onClick={(e) => {
e.stopPropagation();
toggleExpanded();
}}
>
{isExpanded ? "↑" : "↓"}
</button>
)}
</div>
</div>
</div>
);
};
export default ReasoningBlock;

View file

@ -9,329 +9,340 @@ import { type Tool } from 'use-mcp/react'
// Debug logging helper
const debugLog = (...args: any[]) => {
if (typeof window !== 'undefined' && localStorage.getItem('USE_MCP_DEBUG') === 'true') {
console.log(...args)
}
if (typeof window !== 'undefined' && localStorage.getItem('USE_MCP_DEBUG') === 'true') {
console.log(...args)
}
}
// Type guard for messages with content
const hasContent = (message: Message): message is UserMessage | AssistantMessage | SystemMessage => {
return 'content' in message
return 'content' in message
}
interface UseStreamResponseProps {
conversationId?: number
setConversations: React.Dispatch<React.SetStateAction<Conversation[]>>
scrollToBottom: () => void
selectedModel: Model
onApiKeyRequired: (model: Model) => Promise<boolean>
mcpTools: Tool[]
conversationId?: number
setConversations: React.Dispatch<React.SetStateAction<Conversation[]>>
scrollToBottom: () => void
selectedModel: Model
onApiKeyRequired: (model: Model) => Promise<boolean>
mcpTools: Tool[]
}
export const useStreamResponse = ({
conversationId,
setConversations,
scrollToBottom,
selectedModel,
onApiKeyRequired,
mcpTools,
conversationId,
setConversations,
scrollToBottom,
selectedModel,
onApiKeyRequired,
mcpTools,
}: UseStreamResponseProps) => {
const [isLoading, setIsLoading] = useState(false)
const [streamStarted, setStreamStarted] = useState(false)
const [controller, setController] = useState(new AbortController())
const aiResponseRef = useRef<string>('')
const [isLoading, setIsLoading] = useState(false)
const [streamStarted, setStreamStarted] = useState(false)
const [controller, setController] = useState(new AbortController())
const aiResponseRef = useRef<string>('')
// Convert MCP tools to the format expected by the 'ai' package
const convertMcpToolsToAiTools = (mcpTools: Tool[]) => {
const aiTools: Record<string, any> = {}
debugLog(`[useStreamResponse] Converting ${mcpTools.length} MCP tools to AI format`)
mcpTools.forEach((mcpTool) => {
const schema = mcpTool.inputSchema || { type: 'object', properties: {} }
debugLog(`[useStreamResponse] Converting tool: ${mcpTool.name}`, { description: mcpTool.description, schema })
aiTools[mcpTool.name] = tool({
description: mcpTool.description,
parameters: jsonSchema(schema as any),
execute: async (args: unknown, options?: any) => {
debugLog(`[useStreamResponse] Tool "${mcpTool.name}" execute called with args:`, args)
debugLog(`[useStreamResponse] Tool execute options:`, options)
try {
// Call the MCP tool using the callTool method attached to the tool
if ('callTool' in mcpTool && typeof mcpTool.callTool === 'function') {
debugLog(`[useStreamResponse] Calling MCP tool "${mcpTool.name}"...`)
const result = await mcpTool.callTool(args as Record<string, unknown>)
debugLog(`[useStreamResponse] Tool "${mcpTool.name}" returned result:`, result)
return result
} else {
throw new Error(`Tool ${mcpTool.name} does not have a callable implementation`)
}
} catch (error) {
console.error(`[useStreamResponse] Error calling MCP tool ${mcpTool.name}:`, error)
throw error
}
},
})
})
debugLog(`[useStreamResponse] Converted tools:`, Object.keys(aiTools))
return aiTools
}
// Convert MCP tools to the format expected by the 'ai' package
const convertMcpToolsToAiTools = (mcpTools: Tool[]) => {
const aiTools: Record<string, any> = {}
const getModelInstance = (model: Model, apiKey: string) => {
switch (model.provider.id) {
case 'groq': {
const groqProvider = createGroq({ apiKey })
return groqProvider(model.modelId)
}
case 'anthropic': {
const anthropicProvider = createAnthropic({
apiKey,
headers: {
'anthropic-dangerous-direct-browser-access': 'true',
},
})
return anthropicProvider(model.modelId)
}
default:
throw new Error(`Unsupported provider: ${model.provider.id}`)
}
}
debugLog(`[useStreamResponse] Converting ${mcpTools.length} MCP tools to AI format`)
const streamResponse = async (messages: Message[]) => {
let aiResponse = ''
let assistantMessageIndex = -1 // Track the index of our assistant message
let assistantMessageCreated = false // Track if we've created the assistant message yet
debugLog(`[useStreamResponse] streamResponse called with ${messages.length} messages`)
debugLog(`[useStreamResponse] Messages:`, messages)
debugLog(`[useStreamResponse] Current conversationId:`, conversationId)
debugLog(`[useStreamResponse] MCP tools available:`, mcpTools.length)
mcpTools.forEach((mcpTool) => {
const schema = mcpTool.inputSchema || { type: 'object', properties: {} }
debugLog(`[useStreamResponse] Converting tool: ${mcpTool.name}`, { description: mcpTool.description, schema })
// Check if API key is available
let apiKey = getApiKey(selectedModel.provider.id)
if (!apiKey) {
const keyProvided = await onApiKeyRequired(selectedModel)
if (!keyProvided) {
return // User cancelled
}
apiKey = getApiKey(selectedModel.provider.id)
if (!apiKey) {
throw new Error('No API key provided')
}
}
aiTools[mcpTool.name] = tool({
description: mcpTool.description,
parameters: jsonSchema(schema as any),
execute: async (args: unknown, options?: any) => {
debugLog(`[useStreamResponse] Tool "${mcpTool.name}" execute called with args:`, args)
debugLog(`[useStreamResponse] Tool execute options:`, options)
try {
const modelInstance = getModelInstance(selectedModel, apiKey)
debugLog(`[useStreamResponse] Setting stream started to true`)
setStreamStarted(true)
// Convert MCP tools to AI package format
const aiTools = convertMcpToolsToAiTools(mcpTools)
debugLog(`[useStreamResponse] Starting streamText with ${Object.keys(aiTools).length} tools available`)
debugLog(`[useStreamResponse] Messages:`, messages.map(m => ({
role: m.role,
content: hasContent(m) ? m.content.substring(0, 100) + '...' : 'no content'
})))
const result = await streamText({
model: modelInstance,
messages: messages.filter(msg =>
['user', 'assistant', 'system'].includes(msg.role) && hasContent(msg)
).map((msg) => {
const contentMsg = msg as UserMessage | AssistantMessage | SystemMessage
return {
role: contentMsg.role,
content: contentMsg.content,
}
}),
tools: Object.keys(aiTools).length > 0 ? aiTools : undefined,
maxSteps: 5, // Allow up to 5 steps for tool calling
abortSignal: controller.signal,
})
debugLog(`[useStreamResponse] streamText result obtained, starting to process fullStream`)
// Use fullStream to get all events including tool calls, results, and text
const processedEvents = new Set<string>() // Track processed events to avoid duplicates
for await (const event of result.fullStream) {
try {
debugLog(`[useStreamResponse] Full stream event:`, event.type, event)
if (event.type === 'tool-call') {
const eventKey = `tool-call-${event.toolCallId}`
if (processedEvents.has(eventKey)) {
debugLog(`[useStreamResponse] Skipping duplicate tool call:`, eventKey)
continue
}
processedEvents.add(eventKey)
debugLog(`[useStreamResponse] Tool call event:`, event)
if (event.toolName) {
setConversations((prev) => {
const updated = [...prev]
const conv = updated.find((c) => c.id === conversationId)
if (conv) {
// Check if this tool call already exists
const existingToolCall = conv.messages.find(
(msg) => msg.role === 'tool-call' && 'callId' in msg && msg.callId === event.toolCallId
)
if (!existingToolCall) {
conv.messages.push({
role: 'tool-call',
toolName: event.toolName,
toolArgs: event.args || {},
callId: event.toolCallId || 'unknown',
})
}
}
return updated
})
scrollToBottom()
} else {
console.warn('[useStreamResponse] Tool call event missing toolName:', event)
}
} else if (event.type === 'tool-result') {
const eventKey = `tool-result-${event.toolCallId}`
if (processedEvents.has(eventKey)) {
debugLog(`[useStreamResponse] Skipping duplicate tool result:`, eventKey)
continue
}
processedEvents.add(eventKey)
debugLog(`[useStreamResponse] Tool result event:`, event)
if (event.toolName) {
setConversations((prev) => {
const updated = [...prev]
const conv = updated.find((c) => c.id === conversationId)
if (conv) {
// Check if this tool result already exists
const existingToolResult = conv.messages.find(
(msg) => msg.role === 'tool-result' && 'callId' in msg && msg.callId === event.toolCallId
)
if (!existingToolResult) {
conv.messages.push({
role: 'tool-result',
toolName: event.toolName,
toolArgs: event.args || {},
toolResult: event.result,
callId: event.toolCallId || 'unknown',
})
}
}
return updated
})
scrollToBottom()
} else {
console.warn('[useStreamResponse] Tool result event missing toolName:', event)
}
} else if (event.type === 'text-delta') {
debugLog(`[useStreamResponse] Text delta:`, event.textDelta)
// Create assistant message on first text delta (after all tool calls/results)
if (!assistantMessageCreated) {
debugLog(`[useStreamResponse] Creating assistant message after tool calls/results`)
setConversations((prev) => {
const updated = [...prev]
const conv = updated.find((c) => c.id === conversationId)
if (conv) {
conv.messages.push({ role: 'assistant', content: '' })
assistantMessageIndex = conv.messages.length - 1
debugLog(`[useStreamResponse] Created assistant message at index ${assistantMessageIndex}`)
}
return updated
})
assistantMessageCreated = true
}
aiResponse += event.textDelta
aiResponseRef.current = aiResponse
//custom extraction of <chat-title> tag
const titleMatch = aiResponse.match(/<chat-title>(.*?)<\/chat-title>/)
if (titleMatch) {
const title = titleMatch[1].trim()
debugLog(`[useStreamResponse] Extracted title: ${title}`)
setConversations((prev) => {
const updated = [...prev]
const conv = updated.find((c) => c.id === conversationId)
if (conv) {
conv.title = title
}
return updated
})
aiResponse = aiResponse.replace(/<chat-title>.*?<\/chat-title>/, '').trim()
}
setConversations((prev) => {
const updated = [...prev]
const conv = updated.find((c) => c.id === conversationId)
if (conv) {
debugLog(`[useStreamResponse] Updating conversation message content (length: ${aiResponse.length})`)
// Update the specific assistant message we created, not just the last message
const assistantMessage = conv.messages[assistantMessageIndex]
if (assistantMessage && hasContent(assistantMessage) && assistantMessage.role === 'assistant') {
assistantMessage.content = aiResponse
debugLog(`[useStreamResponse] Updated assistant message at index ${assistantMessageIndex}`)
} else {
debugLog(`[useStreamResponse] Could not find assistant message at index ${assistantMessageIndex}`, assistantMessage)
}
} else {
debugLog(`[useStreamResponse] Could not find conversation with id: ${conversationId}`)
}
return updated
try {
// Call the MCP tool using the callTool method attached to the tool
if ('callTool' in mcpTool && typeof mcpTool.callTool === 'function') {
debugLog(`[useStreamResponse] Calling MCP tool "${mcpTool.name}"...`)
const result = await mcpTool.callTool(args as Record<string, unknown>)
debugLog(`[useStreamResponse] Tool "${mcpTool.name}" returned result:`, result)
return result
} else {
throw new Error(`Tool ${mcpTool.name} does not have a callable implementation`)
}
} catch (error) {
console.error(`[useStreamResponse] Error calling MCP tool ${mcpTool.name}:`, error)
throw error
}
},
})
})
scrollToBottom()
}
} catch (e) {
console.error('[useStreamResponse] Error in full stream processing:', e)
}
}
debugLog(`[useStreamResponse] Finished processing full stream. Final response length: ${aiResponse.length}`)
} catch (error: unknown) {
if (controller.signal.aborted) {
debugLog('[useStreamResponse] Stream aborted')
} else {
console.error('[useStreamResponse] Error generating response:', error)
// Log additional error details
if (error instanceof Error) {
console.error('[useStreamResponse] Error stack:', error.stack)
console.error('[useStreamResponse] Error message:', error.message)
}
// Remove the assistant message if there was an error and we created one
if (assistantMessageCreated) {
setConversations((prev) => {
const updated = [...prev]
const conv = updated.find((c) => c.id === conversationId)
if (conv && assistantMessageIndex >= 0 && conv.messages[assistantMessageIndex]?.role === 'assistant') {
debugLog('[useStreamResponse] Removing assistant message due to error')
conv.messages.splice(assistantMessageIndex, 1)
}
return updated
})
}
}
} finally {
debugLog('[useStreamResponse] Cleaning up stream state')
setStreamStarted(false)
setController(new AbortController())
debugLog(`[useStreamResponse] Converted tools:`, Object.keys(aiTools))
return aiTools
}
return aiResponse
}
return {
isLoading,
setIsLoading,
streamStarted,
controller,
streamResponse,
aiResponseRef,
}
// Check if a model supports reasoning
const supportsReasoning = (model: Model) => {
const reasoningModels = [
'qwen/qwen3-32b',
'qwen-qwq-32b',
'deepseek-r1-distill-llama-70b',
'claude-4-opus-20250514',
'claude-4-sonnet-20250514',
'claude-3-7-sonnet-20250219'
]
return reasoningModels.includes(model.modelId)
}
const getModelInstance = (model: Model, apiKey: string) => {
switch (model.provider.id) {
case 'groq': {
const groqProvider = createGroq({ apiKey })
return groqProvider(model.modelId)
}
case 'anthropic': {
const anthropicProvider = createAnthropic({
apiKey,
headers: {
'anthropic-dangerous-direct-browser-access': 'true',
},
})
return anthropicProvider(model.modelId)
}
default:
throw new Error(`Unsupported provider: ${model.provider.id}`)
}
}
const streamResponse = async (messages: Message[]) => {
let aiResponse = ''
let reasoning = ''
let assistantMessageIndex = -1 // Track the index of our assistant message
let assistantMessageCreated = false // Track if we've created the assistant message yet
debugLog(`[useStreamResponse] streamResponse called with ${messages.length} messages`)
debugLog(`[useStreamResponse] Messages:`, messages)
debugLog(`[useStreamResponse] Current conversationId:`, conversationId)
debugLog(`[useStreamResponse] MCP tools available:`, mcpTools.length)
// Check if API key is available
let apiKey = getApiKey(selectedModel.provider.id)
if (!apiKey) {
const keyProvided = await onApiKeyRequired(selectedModel)
if (!keyProvided) {
return // User cancelled
}
apiKey = getApiKey(selectedModel.provider.id)
if (!apiKey) {
throw new Error('No API key provided')
}
}
try {
const modelInstance = getModelInstance(selectedModel, apiKey)
debugLog(`[useStreamResponse] Setting stream started to true`)
setStreamStarted(true)
// Convert MCP tools to AI package format
const aiTools = convertMcpToolsToAiTools(mcpTools)
debugLog(`[useStreamResponse] Starting streamText with ${Object.keys(aiTools).length} tools available`)
debugLog(`[useStreamResponse] Messages:`, messages.map(m => ({
role: m.role,
content: hasContent(m) ? m.content.substring(0, 100) + '...' : 'no content'
})))
// Prepare stream options
const streamOptions: any = {
model: modelInstance,
messages: messages.filter(msg =>
['user', 'assistant', 'system'].includes(msg.role) && hasContent(msg)
).map((msg) => {
const contentMsg = msg as UserMessage | AssistantMessage | SystemMessage
return {
role: contentMsg.role,
content: contentMsg.content,
}
}),
tools: Object.keys(aiTools).length > 0 ? aiTools : undefined,
maxSteps: 5, // Allow up to 5 steps for tool calling
abortSignal: controller.signal,
}
// Add reasoning parameters for supported models
if (supportsReasoning(selectedModel)) {
debugLog(`[useStreamResponse] Enabling reasoning for model: ${selectedModel.modelId}`)
if (selectedModel.provider.id === 'groq') {
// For Groq models, use reasoning_format parameter
streamOptions.experimental_providerOptions = {
groq: {
reasoning_format: 'parsed'
}
}
} else if (selectedModel.provider.id === 'anthropic') {
// For Anthropic models, enable thinking
streamOptions.experimental_providerOptions = {
anthropic: {
thinking: { type: 'enabled', budgetTokens: 12000 }
}
}
}
}
const result = await streamText(streamOptions)
debugLog(`[useStreamResponse] streamText result obtained, starting to process fullStream`)
// Use fullStream to get all events including tool calls, results, and text
const processedEvents = new Set<string>() // Track processed events to avoid duplicates
for await (const event of result.fullStream) {
try {
debugLog(`[useStreamResponse] Full stream event:`, event.type, event)
if (event.type === 'reasoning') {
debugLog(`[useStreamResponse] Reasoning event:`, event)
reasoning += (event as any).textDelta || ''
} else if (event.type === 'tool-call') {
const eventKey = `tool-call-${event.toolCallId}`
if (processedEvents.has(eventKey)) {
debugLog(`[useStreamResponse] Skipping duplicate tool call:`, eventKey)
continue
}
processedEvents.add(eventKey)
debugLog(`[useStreamResponse] Tool call event:`, event)
if (event.toolName) {
setConversations((prev) => {
const updated = [...prev]
const conv = updated.find((c) => c.id === conversationId)
if (conv) {
// Check if this tool call already exists
const existingToolCall = conv.messages.find(
(msg) => msg.role === 'tool-call' && 'callId' in msg && msg.callId === event.toolCallId
)
if (!existingToolCall) {
conv.messages.push({
role: 'tool-call',
toolName: event.toolName,
toolArgs: event.args || {},
callId: event.toolCallId || 'unknown',
})
}
}
return updated
})
scrollToBottom()
} else {
console.warn('[useStreamResponse] Tool call event missing toolName:', event)
}
} else if (event.type === 'text-delta') {
debugLog(`[useStreamResponse] Text delta:`, event.textDelta)
// Create assistant message on first text delta (after all tool calls/results)
if (!assistantMessageCreated) {
debugLog(`[useStreamResponse] Creating assistant message after tool calls/results`)
setConversations((prev) => {
const updated = [...prev]
const conv = updated.find((c) => c.id === conversationId)
if (conv) {
conv.messages.push({ role: 'assistant', content: '', reasoning: reasoning || undefined })
assistantMessageIndex = conv.messages.length - 1
debugLog(`[useStreamResponse] Created assistant message at index ${assistantMessageIndex}`)
}
return updated
})
assistantMessageCreated = true
}
aiResponse += event.textDelta
aiResponseRef.current = aiResponse
//custom extraction of <chat-title> tag
const titleMatch = aiResponse.match(/<chat-title>(.*?)<\/chat-title>/)
if (titleMatch) {
const title = titleMatch[1].trim()
debugLog(`[useStreamResponse] Extracted title: ${title}`)
setConversations((prev) => {
const updated = [...prev]
const conv = updated.find((c) => c.id === conversationId)
if (conv) {
conv.title = title
}
return updated
})
aiResponse = aiResponse.replace(/<chat-title>.*?<\/chat-title>/, '').trim()
}
setConversations((prev) => {
const updated = [...prev]
const conv = updated.find((c) => c.id === conversationId)
if (conv) {
debugLog(`[useStreamResponse] Updating conversation message content (length: ${aiResponse.length})`)
// Update the specific assistant message we created, not just the last message
const assistantMessage = conv.messages[assistantMessageIndex]
if (assistantMessage && hasContent(assistantMessage) && assistantMessage.role === 'assistant') {
assistantMessage.content = aiResponse
// Update reasoning if available
if (reasoning) {
assistantMessage.reasoning = reasoning
}
debugLog(`[useStreamResponse] Updated assistant message at index ${assistantMessageIndex}`)
} else {
debugLog(`[useStreamResponse] Could not find assistant message at index ${assistantMessageIndex}`, assistantMessage)
}
} else {
debugLog(`[useStreamResponse] Could not find conversation with id: ${conversationId}`)
}
return updated
})
scrollToBottom()
}
} catch (e) {
console.error('[useStreamResponse] Error in full stream processing:', e)
}
}
debugLog(`[useStreamResponse] Finished processing full stream. Final response length: ${aiResponse.length}`)
} catch (error: unknown) {
if (controller.signal.aborted) {
debugLog('[useStreamResponse] Stream aborted')
} else {
console.error('[useStreamResponse] Error generating response:', error)
// Log additional error details
if (error instanceof Error) {
console.error('[useStreamResponse] Error stack:', error.stack)
console.error('[useStreamResponse] Error message:', error.message)
}
// Remove the assistant message if there was an error and we created one
if (assistantMessageCreated) {
setConversations((prev) => {
const updated = [...prev]
const conv = updated.find((c) => c.id === conversationId)
if (conv && assistantMessageIndex >= 0 && conv.messages[assistantMessageIndex]?.role === 'assistant') {
debugLog('[useStreamResponse] Removing assistant message due to error')
conv.messages.splice(assistantMessageIndex, 1)
}
return updated
})
}
}
} finally {
debugLog('[useStreamResponse] Cleaning up stream state')
setStreamStarted(false)
setController(new AbortController())
}
return aiResponse
}
return {
isLoading,
setIsLoading,
streamStarted,
controller,
streamResponse,
aiResponseRef,
}
}

View file

@ -12,6 +12,7 @@ export interface SystemMessage extends BaseMessage {
export interface AssistantMessage extends BaseMessage {
role: "assistant";
reasoning?: string;
}
export interface ToolCallMessage {

View file

@ -42,10 +42,22 @@ export const availableModels: Model[] = [
},
{
id: 'qwen-3-32b',
name: 'Qwen 3 32B',
name: 'Qwen 3 32B (Reasoning)',
provider: providers.groq,
modelId: 'qwen/qwen3-32b'
},
{
id: 'qwen-qwq-32b',
name: 'Qwen QwQ 32B (Reasoning)',
provider: providers.groq,
modelId: 'qwen-qwq-32b'
},
{
id: 'deepseek-r1-distill-llama-70b',
name: 'DeepSeek R1 Distil Llama 70B (Reasoning)',
provider: providers.groq,
modelId: 'deepseek-r1-distill-llama-70b'
},
{
id: 'claude-3-5-sonnet-20241022',
name: 'Claude 4 Sonnet',