-
Notifications
You must be signed in to change notification settings - Fork 14
Support Reasoning Data #12
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -21,25 +21,50 @@ | |||||||||||||||||||||||||||||||||||||||||||
| // Lets grab the history up to now so that the AI has some context | ||||||||||||||||||||||||||||||||||||||||||||
| const history = await ctx.runQuery(internal.messages.getHistory); | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| // Lets kickoff a stream request to OpenAI | ||||||||||||||||||||||||||||||||||||||||||||
| const stream = await openai.chat.completions.create({ | ||||||||||||||||||||||||||||||||||||||||||||
| model: "gpt-4.1-mini", | ||||||||||||||||||||||||||||||||||||||||||||
| messages: [ | ||||||||||||||||||||||||||||||||||||||||||||
| // o4-mini works best with the Responses API for reasoning | ||||||||||||||||||||||||||||||||||||||||||||
| const response = await openai.responses.create({ | ||||||||||||||||||||||||||||||||||||||||||||
| model: "o4-mini", | ||||||||||||||||||||||||||||||||||||||||||||
| input: [ | ||||||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||||||
| role: "system", | ||||||||||||||||||||||||||||||||||||||||||||
| content: `You are a helpful assistant that can answer questions and help with tasks. | ||||||||||||||||||||||||||||||||||||||||||||
| Please provide your response in markdown format. | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| You are continuing a conversation. The conversation so far is found in the following JSON-formatted value:`, | ||||||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||||||
| ...history, | ||||||||||||||||||||||||||||||||||||||||||||
| ], | ||||||||||||||||||||||||||||||||||||||||||||
| reasoning: { | ||||||||||||||||||||||||||||||||||||||||||||
| effort: "medium", | ||||||||||||||||||||||||||||||||||||||||||||
| summary: "auto", // Get reasoning summary | ||||||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||||||
| stream: true, | ||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| // Append each chunk to the persistent stream as they come in from openai | ||||||||||||||||||||||||||||||||||||||||||||
| for await (const part of stream) | ||||||||||||||||||||||||||||||||||||||||||||
| await append(part.choices[0]?.delta?.content || ""); | ||||||||||||||||||||||||||||||||||||||||||||
| let currentReasoning = ""; | ||||||||||||||||||||||||||||||||||||||||||||
| let currentText = ""; | ||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+44
to
+45
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion | 🟠 Major Remove unused variables.
- let currentReasoning = "";
- let currentText = "";
-
// Process the streaming response
for await (const event of response) {🤖 Prompt for AI Agents
Comment on lines
+44
to
+45
|
||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| // Process the streaming response | ||||||||||||||||||||||||||||||||||||||||||||
| for await (const event of response) { | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| // Handle reasoning summary chunks | ||||||||||||||||||||||||||||||||||||||||||||
| if (event.type === "response.reasoning_summary_text.delta") { | ||||||||||||||||||||||||||||||||||||||||||||
| currentReasoning += event.delta || ""; | ||||||||||||||||||||||||||||||||||||||||||||
| await append({ | ||||||||||||||||||||||||||||||||||||||||||||
| text: "", | ||||||||||||||||||||||||||||||||||||||||||||
| reasoning: event.delta || "", | ||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| // Handle output text chunks | ||||||||||||||||||||||||||||||||||||||||||||
| if (event.type === "response.output_text.delta") { | ||||||||||||||||||||||||||||||||||||||||||||
| currentText += event.delta || ""; | ||||||||||||||||||||||||||||||||||||||||||||
| await append({ | ||||||||||||||||||||||||||||||||||||||||||||
| text: event.delta || "", | ||||||||||||||||||||||||||||||||||||||||||||
| reasoning: "", | ||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+60
to
+66
|
||||||||||||||||||||||||||||||||||||||||||||
| if (event.type === "response.output_text.delta") { | |
| currentText += event.delta || ""; | |
| await append({ | |
| text: event.delta || "", | |
| reasoning: "", | |
| }); | |
| } | |
| else if (event.type === "response.output_text.delta") { | |
| currentText += event.delta || ""; | |
| await append({ | |
| text: event.delta || "", | |
| reasoning: "", | |
| }); | |
| } | |
| // Handle unexpected event types | |
| else { | |
| console.warn( | |
| `Unhandled event type in streaming response: ${event.type}`, | |
| event | |
| ); | |
| } |
| Original file line number | Diff line number | Diff line change | ||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -17,7 +17,7 @@ export function ServerMessage({ | |||||||||||||
| stopStreaming: () => void; | ||||||||||||||
| scrollToBottom: () => void; | ||||||||||||||
| }) { | ||||||||||||||
| const { text, status } = useStream( | ||||||||||||||
| const { text, reasoning, status } = useStream( | ||||||||||||||
| api.streaming.getStreamBody, | ||||||||||||||
| new URL(`${getConvexSiteUrl()}/chat-stream`), | ||||||||||||||
| isDriven, | ||||||||||||||
|
|
@@ -42,7 +42,15 @@ export function ServerMessage({ | |||||||||||||
|
|
||||||||||||||
| return ( | ||||||||||||||
| <div className="md-answer"> | ||||||||||||||
| <Markdown>{text || "Thinking..."}</Markdown> | ||||||||||||||
| {reasoning && reasoning.length > 0 && ( | ||||||||||||||
| <div className="mb-3 pb-3 border-b border-gray-200"> | ||||||||||||||
| <div className="text-xs text-gray-500 mb-2">Reasoning:</div> | ||||||||||||||
| <div className="text-sm text-gray-600"> | ||||||||||||||
| <Markdown>{reasoning}</Markdown> | ||||||||||||||
| </div> | ||||||||||||||
| </div> | ||||||||||||||
| )} | ||||||||||||||
| <Markdown>{text}</Markdown> | ||||||||||||||
|
||||||||||||||
| <Markdown>{text}</Markdown> | |
| {isCurrentlyStreaming && !text ? ( | |
| <div className="text-gray-400 italic">Thinking...</div> | |
| ) : ( | |
| <Markdown>{text}</Markdown> | |
| )} |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -14,10 +14,12 @@ export type StreamId = string & { __isStreamId: true }; | |||||||||||||||||||||||||||
| export const StreamIdValidator = v.string(); | ||||||||||||||||||||||||||||
| export type StreamBody = { | ||||||||||||||||||||||||||||
| text: string; | ||||||||||||||||||||||||||||
| reasoning: string; | ||||||||||||||||||||||||||||
| status: StreamStatus; | ||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||
| /** | |
| * Represents a chunk of streamed data. | |
| * | |
| * Supports both the legacy string format and the new object format. | |
| * - Legacy format: `string` (e.g., "Hello world") | |
| * - Object format: `{ text: string; reasoning?: string }` | |
| * | |
| * The object format allows for optional reasoning metadata to be attached to each chunk. | |
| * This dual format supports API evolution: existing code using strings will continue to work, | |
| * while new code can migrate to the richer object format as needed. | |
| */ |
Copilot
AI
Dec 7, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[nitpick] Missing trailing comma. The other properties in this object have trailing commas (lines 87-88), so line 89 should also have one for consistency with the codebase style.
| status: status as StreamStatus | |
| status: status as StreamStatus, |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -63,7 +63,7 @@ export function useStream( | |
| getPersistentBody, | ||
| usePersistence && streamId ? { streamId } : "skip", | ||
| ); | ||
| const [streamBody, setStreamBody] = useState<string>(""); | ||
| const [streamBody, setStreamBody] = useState({ text: "", reasoning: "" }); | ||
|
|
||
| useEffect(() => { | ||
| if (driven && streamId && !streamStarted.current) { | ||
|
|
@@ -72,8 +72,11 @@ export function useStream( | |
| const success = await startStreaming( | ||
| streamUrl, | ||
| streamId, | ||
| (text) => { | ||
| setStreamBody((prev) => prev + text); | ||
| (chunk) => { | ||
| setStreamBody((prev) => ({ | ||
| text: prev.text + chunk.text, | ||
| reasoning: prev.reasoning + (chunk.reasoning || ""), | ||
| })); | ||
| }, | ||
| { | ||
| ...opts?.headers, | ||
|
|
@@ -110,12 +113,13 @@ export function useStream( | |
| } | ||
| let status: StreamStatus; | ||
| if (streamEnded === null) { | ||
| status = streamBody.length > 0 ? "streaming" : "pending"; | ||
| status = streamBody.text.length > 0 ? "streaming" : "pending"; | ||
| } else { | ||
| status = streamEnded ? "done" : "error"; | ||
| } | ||
| return { | ||
| text: streamBody, | ||
| text: streamBody.text, | ||
| reasoning: streamBody.reasoning, | ||
| status: status as StreamStatus, | ||
| }; | ||
| }, [persistentBody, streamBody, streamEnded]); | ||
|
|
@@ -139,7 +143,7 @@ export function useStream( | |
| async function startStreaming( | ||
| url: URL, | ||
| streamId: StreamId, | ||
| onUpdate: (text: string) => void, | ||
| onUpdate: (chunk: { text: string; reasoning?: string }) => void, | ||
| headers: Record<string, string>, | ||
| ) { | ||
| const response = await fetch(url, { | ||
|
|
@@ -162,18 +166,74 @@ async function startStreaming( | |
| console.error("No body in response", response); | ||
| return false; | ||
| } | ||
| const reader = response.body.getReader(); | ||
| while (true) { | ||
| try { | ||
| const { done, value } = await reader.read(); | ||
| if (done) { | ||
| onUpdate(new TextDecoder().decode(value)); | ||
| return true; | ||
|
|
||
| const reader = response.body.pipeThrough(new TextDecoderStream()).getReader(); | ||
|
|
||
| // Read first chunk to detect format | ||
| const firstRead = await reader.read(); | ||
| if (firstRead.done) { | ||
| return true; | ||
| } | ||
|
|
||
| const firstChunk = firstRead.value; | ||
| const isJsonMode = firstChunk.trimStart().startsWith("{"); | ||
|
||
|
|
||
| if (isJsonMode) { | ||
| // JSON mode: split by newlines and parse each line | ||
| let buffer = firstChunk; | ||
|
|
||
| const processBuffer = () => { | ||
| const lines = buffer.split("\n"); | ||
| buffer = lines.pop() || ""; | ||
| for (const line of lines) { | ||
| if (line.trim()) { | ||
| try { | ||
| onUpdate(JSON.parse(line)); | ||
| } catch { | ||
| onUpdate({ text: line }); | ||
|
Comment on lines
+192
to
+193
|
||
| } | ||
| } | ||
| } | ||
| }; | ||
|
|
||
| processBuffer(); | ||
|
|
||
| while (true) { | ||
| try { | ||
| const { done, value } = await reader.read(); | ||
| if (done) { | ||
| // Flush remaining buffer | ||
| if (buffer.trim()) { | ||
| try { | ||
| onUpdate(JSON.parse(buffer)); | ||
| } catch { | ||
| onUpdate({ text: buffer }); | ||
|
Comment on lines
+209
to
+210
|
||
| } | ||
| } | ||
| return true; | ||
| } | ||
| buffer += value; | ||
| processBuffer(); | ||
| } catch (e) { | ||
| console.error("Error reading stream", e); | ||
| return false; | ||
| } | ||
| } | ||
| } else { | ||
| // Plain text mode: pass chunks through directly | ||
| onUpdate({ text: firstChunk }); | ||
|
|
||
| while (true) { | ||
| try { | ||
| const { done, value } = await reader.read(); | ||
| if (done) { | ||
| return true; | ||
| } | ||
| onUpdate({ text: value }); | ||
| } catch (e) { | ||
| console.error("Error reading stream", e); | ||
| return false; | ||
| } | ||
| onUpdate(new TextDecoder().decode(value)); | ||
| } catch (e) { | ||
| console.error("Error reading stream", e); | ||
| return false; | ||
| } | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The parameter
inputis used instead ofmessages. The OpenAI Chat Completions API usesmessagesas the parameter name for the conversation history. If this is intended to use a different/future API, please document which version of the OpenAI SDK supports this interface.