Repository navigation
Conversation
1. **Layer 1: Universal Response Reasoning Extraction (Zero Config)**
- Created [`lib/chat/stream/reasoning-stream-parser.ts`](./lib/chat/stream/reasoning-stream-parser.ts):
- **Multi-tag state machine:** Automatically parses `<think>`, `<thinking>`, `<thought>`, `<reasoning>`, `[THINK]`, and `◁think▷` across split SSE chunks, ignoring tags inside markdown code fences (` ``` `).
- **Structured delta normalizer:** Extracts `delta.reasoning_content` (DeepSeek, Qwen, vLLM), `delta.reasoning` (OpenRouter), and `delta.reasoning_details` (OpenRouter/Claude/Gemini array format).
- **Normalizing `fetch` wrapper:** Injected into `buildSdkProvider` so OpenAI-compatible SSE streams normalize structured reasoning deltas into standard events without deadlocking the AI SDK.
- Updated [`lib/inngest/chat-response/chat-stream-consumer.ts`](./lib/inngest/chat-response/chat-stream-consumer.ts) to emit reasoning deltas via Inngest Realtime for instant frontend streaming into `ThinkingDisplay`.
2. **Layer 2: Model-Level Reasoning Traits & Request Activation**
- **Database & Schemas:** Added `reasoningEffortFormat` (`"none" | "openai" | "openrouter" | "anthropic"`) and `modelOptions` (arbitrary JSON overrides) to [`drizzle/schemas/ai-model-schema.ts`](./drizzle/schemas/ai-model-schema.ts), generated migration [`0054_ai_model_reasoning_effort.sql`](./drizzle/migrations/0054_ai_model_reasoning_effort.sql), and updated [`schemas/providers/provider-registry.ts`](./schemas/providers/provider-registry.ts).
- **Model Actions & Auto-Detection:** Updated model creation, updates, export, import, and sync actions ([`actions/models/sync-provider-models.ts`](./actions/models/sync-provider-models.ts)) to auto-detect reasoning traits during provider sync based on `supported_parameters` or model IDs (`o1`, `o3`, `r1`, `qwq`, etc.).
- **Settings UI:** Added progressive disclosure controls in [`components/settings/providers/model-form-dialog.tsx`](./components/settings/providers/model-form-dialog.tsx) to configure format and custom JSON options when `capReasoning` is checked.
3. **User Toolbar & End-to-End Reasoning Effort Dispatch**
- Created [`components/chat/input/reasoning-effort-selector.tsx`](./components/chat/input/reasoning-effort-selector.tsx) with a compact `Brain` dropdown (`Off`, `Low`, `Medium`, `High`, default `Medium`).
- Wired in [`components/chat/input/chat-input-toolbar.tsx`](./components/chat/input/chat-input-toolbar.tsx), showing only when the selected model supports reasoning and has a format configured.
- Threaded effort end-to-end through [`app/api/chat/route.ts`](./app/api/chat/route.ts) -> Inngest `chat/response.generate` -> [`lib/inngest/chat-response/chat-execution-config.ts`](./lib/inngest/chat-response/chat-execution-config.ts) & fallback synthesis pass -> message metadata.
- Using slider instead of dropdown as it is more intuitive - Made button more consistent with other controls - Removed icon
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
1. High-Level Overview
Goal
Add universal thinking/reasoning support for any generic OpenAI-compatible
/v1/chat/completionsprovider without hardcoding vendor URLs or installing vendor-specific SDKs. The design uses a 2-Layer Protocol-Trait Architecture (inspired by VS Code'schatLanguageModels.json) plus a per-turn Reasoning Effort Selector (Off,Low,Medium,High) in the chat toolbar:delta.reasoning_content(DeepSeek, Qwen, vLLM, Kimi, llama.cpp),delta.reasoning(OpenRouter), anddelta.reasoning_details(Array<{ text?: string; summary?: string }>) from/v1/chat/completionsresponses before@ai-sdk/openai's strict Zod schema can strip them.<think>...</think>,<thinking>...</thinking>,<thought>...</thought>,<reasoning>...</reasoning>,[THINK]...[/THINK], and◁think▷...◁/think▷blocks from streamingtext-deltachunks (handling arbitrary chunk splits and mid-thought stream aborts) and emits them asreasoning-deltaevents into the existingReasoningTraceStepUI.ai_model(drizzle/schemas/ai-model-schema.ts) and the import/export provider registry (schemas/providers/provider-registry.ts) with two dedicated columns alongside the existingcap_reasoningboolean:reasoning_effort_format:"none" | "openai" | "openrouter" | "anthropic"(default"none").model_options: nullable JSON string (Record<string, unknown>) for custom pass-through body parameters (e.g.{"enable_thinking": true}).Off,Low,Medium,High, defaulting toMediumand persisted in turn metadata) inChatInputToolbarwhen the active model hascapReasoning === trueandreasoningEffortFormat !== "none".Architecture & Data Flow
flowchart LR subgraph Client["Frontend (Next.js / React 19)"] Toolbar["ChatInputToolbar\n(Effort: Off | Low | Medium | High)"] Route["POST /api/chat"] Timeline["ResponseTimeline &\nReasoningTraceStep"] end subgraph Inngest["Inngest Durable Chat Pipeline"] Orch["generateChatResponse\n(chat-response.ts)"] Resolve["buildResolvedProvider &\nbuildSdkProvider"] Consumer["consumeModelStream &\ncreateInlineReasoningParser"] end subgraph Transport["Custom Fetch Wrapper (build-sdk-provider.ts)"] ReqTransform["Request Body Transformer\n(reasoning_effort / reasoning / thinking + modelOptions)"] SseTransform["SSE & JSON Response Normalizer\n(delta.reasoning* -> <think>...</think>)"] end API["Generic OpenAI-Compatible Endpoint\n(/v1/chat/completions)"] Toolbar -->|"reasoningEffort"| Route Route -->|"chat/response.generate"| Orch Orch --> Resolve Resolve --> ReqTransform ReqTransform -->|"HTTP POST"| API API -->|"SSE / JSON"| SseTransform SseTransform -->|"@ai-sdk/openai stream"| Consumer Consumer -->|"reasoning-delta / text-delta"| TimelineGlobal Constraints & Invariants
reasoningEffortFormat("none" | "openai" | "openrouter" | "anthropic") andmodelOptions. Incoming extraction is always active for all models.fetchSSE Normalization Is Required for@ai-sdk/openaiv4:@ai-sdk/openai(node_modules/@ai-sdk/openai/dist/internal/index.js),OpenAIChatLanguageModel.doStream()pipes the HTTP response throughthrowIfOpenAIStreamErrorBeforeOutput(which buffers the entire stream in memory untilisOpenAIChatOutputChunkseesdelta.content.length > 0,delta.tool_calls, ordelta.refusal) and validates every SSE event withopenaiChatChunkSchema(which only allowsrole,content,refusal,tool_calls,annotationsondeltaand stripsreasoning_content,reasoning, andreasoning_details).delta.reasoning_content,delta.reasoning, anddelta.reasoning_detailsinto<think>...</think>insidedelta.contentat the customfetchSSE layer unblocksisOpenAIChatOutputChunkon the very first reasoning token and preserves structured reasoning across@ai-sdk/openai's Zod parser, whilecreateInlineReasoningParserinconsumeModelStreamextracts all inline tag formats intoreasoning-delta..partial()Invariant: Per.agents/learnings.md(Refactoring & Code Cleanup #28),updateModelSchemauses.partial(). Fields with.default()inside.partial()replaceundefinedwith the default value during partial updates. Therefore,createModelSchemaapplies defaults on creation, whileupdateModelSchemakeepsreasoningEffortFormatandmodelOptionsoptional without inner.default().2. Alternatives Evaluated
Approach 1: 2-Layer Protocol-Trait Pipeline — Custom
fetchRequest/SSE Normalizer + Streaming Multi-Tag Parser + Dedicatedai_modelColumns (Recommended)buildSdkProvidersupplies a customfetchthat transforms/chat/completionsPOST bodies usingreasoningEffortFormat+reasoningEffortand mergesmodelOptions.fetchnormalizes SSEdelta.reasoning_content,delta.reasoning, anddelta.reasoning_detailsinto<think>...</think>, andconsumeModelStreampipestext-deltachunks throughcreateInlineReasoningParser(handling<think>,<thinking>,<thought>,<reasoning>,[THINK],◁think▷) + checks raw structured delta fields if passed directly.reasoning_effort_formatandmodel_optionscolumns onai_model.@ai-sdk/openai's early-error stream buffering freeze and Zod field stripping without patchingnode_modules.ai_model's existing flat capability columns.buildSdkProviderand a tag state machine inlib/chat/stream/reasoning-stream-parser.ts.Approach 2: Chaining Built-In AI SDK
extractReasoningMiddlewaresdkProvider.chat(modelId)with 6 nestedextractReasoningMiddleware({ tagName })calls from"ai".delta.reasoning_content,delta.reasoning,delta.reasoning_details) because@ai-sdk/openaistrips them beforewrapStreamruns.TransformStreaminstances per request, each delayingtext-startand buffering independently.reasoning_effort,reasoning.effort,thinking.budget_tokens,modelOptions).Approach 3: Vendor-Specific AI SDK Packages (
@ai-sdk/deepseek,@openrouter/ai-sdk-provider,@ai-sdk/anthropic)baseUrlor provider type.3. Edge Cases, Failure Modes & Mitigations
"<thi", chunk 2 starts with"nk>Hello")createInlineReasoningParserchecks if the tail of the current buffer is a prefix of any valid opening/closing tag (getPotentialTagPrefixIndex). It holds only the matching tail inbufferuntil the next chunk confirms or disproves the tag.<think>tag on user Stop ormax_tokensparser.flush()emits all remaining buffered text as"reasoning"so partial thoughts appear inReasoningTraceStepand never leak into the assistant's markdown answer.<think>tags" and model writes```xml\n<think>...</think>\n```)```) when in normal text mode; do not enter thinking state for tags that appear inside fenced code blocks or inline backticks.@ai-sdk/openaiearly-error stream buffering & Zod strippingcreateReasoningNormalizingFetchtransforms SSEdelta.reasoning_content,delta.reasoning, anddelta.reasoning_detailsinto<think>...</think>insidedelta.contentbefore@ai-sdk/openaireads the stream, satisfyingisOpenAIChatOutputChunkimmediately on chunk #1.reasoningEffort: "off"semantics across formats"openai": setsreasoning_effort: "none"(or omits ifmodelOptionsoverrides it).Wait: many OpenAI-compatible servers (vLLM, DeepSeek, Groq, older OpenAI o1/o3) rejectreasoning_effort: "none"with HTTP 400 and only accept"low" | "medium" | "high", whereas omittingreasoning_effort(or settingreasoning: { enabled: false }for OpenRouter /thinking: { type: "disabled" }for Anthropic) is universally safe! Let's omitreasoning_effortwhen"off"in"openai"format (or allowmodelOptionsoverride), sendreasoning: { enabled: false }for"openrouter", and sendthinking: { type: "disabled" }for"anthropic".delta.reasoning_contentAND inline<think>returned by a proxydelta.reasoning_contentitself begins with<think>,createInlineReasoningParseravoids nested/duplicate<think>tags and strips inner tags cleanly.modelOptionsin UI or DBModelFormDialogvalidates thatmodelOptionsparses as a plain JSON object (!Array.isArray(parsed)) before form submission;buildResolvedProviderwrapsJSON.parseintry/catchand falls back tonullwith a warning log.