309 lines
7.2 KiB
Markdown
309 lines
7.2 KiB
Markdown
|
|
# Critical Protocol Issue: Grok Reasoning Field Not Translated
|
||
|
|
|
||
|
|
**Discovered**: 2025-11-11
|
||
|
|
**Severity**: HIGH - Causes UI freezing/no progress indication
|
||
|
|
**Model Affected**: x-ai/grok-code-fast-1 (and likely other Grok models)
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 🔴 The Problem
|
||
|
|
|
||
|
|
### What User Experienced
|
||
|
|
|
||
|
|
1. **Normal**: Thinking nodes blink, showing tool calls, file reads, progress
|
||
|
|
2. **After AskUserQuestion**: Everything STOPS - no blinking, appears done
|
||
|
|
3. **Then suddenly**: Final result appears all at once
|
||
|
|
|
||
|
|
### Root Cause: Grok's `reasoning` Field
|
||
|
|
|
||
|
|
**Grok sends thinking/reasoning in a DIFFERENT field** than regular content:
|
||
|
|
|
||
|
|
```json
|
||
|
|
// Grok's streaming chunks (186 chunks!)
|
||
|
|
{
|
||
|
|
"delta": {
|
||
|
|
"role": "assistant",
|
||
|
|
"content": "", // ❌ EMPTY!
|
||
|
|
"reasoning": " current", // ✅ Actual thinking content here
|
||
|
|
"reasoning_details": [{
|
||
|
|
"type": "reasoning.summary",
|
||
|
|
"summary": " current",
|
||
|
|
"format": "xai-responses-v1",
|
||
|
|
"index": 0
|
||
|
|
}]
|
||
|
|
}
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
**Our proxy ONLY looks at `delta.content`**:
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
// src/proxy-server.ts:748
|
||
|
|
if (delta?.content) {
|
||
|
|
log(`[Proxy] Sending content delta: ${delta.content}`);
|
||
|
|
sendSSE("content_block_delta", {
|
||
|
|
index: textBlockIndex,
|
||
|
|
delta: {
|
||
|
|
type: "text_delta",
|
||
|
|
text: delta.content, // ❌ This is "" when reasoning is active!
|
||
|
|
},
|
||
|
|
});
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
**Result**: 186 reasoning chunks completely ignored! No text_delta events sent → Claude Code UI thinks nothing is happening!
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 📊 Event Sequence Analysis
|
||
|
|
|
||
|
|
### From Logs (03:59:37 - 03:59:43)
|
||
|
|
|
||
|
|
```
|
||
|
|
03:59:37.272Z - Reasoning chunk 1: " current"
|
||
|
|
03:59:37.272Z - Reasoning chunk 2: " implementation"
|
||
|
|
03:59:37.272Z - Reasoning chunk 3: " is"
|
||
|
|
... 183 more reasoning chunks (all ignored) ...
|
||
|
|
03:59:42.978Z - Reasoning chunk 186: final summary
|
||
|
|
03:59:42.995Z - Tool call appears: ExitPlanMode with HUGE payload
|
||
|
|
03:59:42.995Z - Finish reason: "tool_calls"
|
||
|
|
03:59:43.018Z - [DONE]
|
||
|
|
```
|
||
|
|
|
||
|
|
**What our proxy sent to Claude Code**:
|
||
|
|
```
|
||
|
|
1. message_start ✅
|
||
|
|
2. content_block_start (index 0, type: text) ✅
|
||
|
|
3. ping ✅
|
||
|
|
4. ... NOTHING for 5+ seconds ... ❌❌❌
|
||
|
|
5. content_block_stop (index 0) ✅
|
||
|
|
6. content_block_start (index 1, type: tool_use) ✅
|
||
|
|
7. content_block_delta (huge JSON in one chunk) ✅
|
||
|
|
8. content_block_stop (index 1) ✅
|
||
|
|
9. message_delta ✅
|
||
|
|
10. message_stop ✅
|
||
|
|
```
|
||
|
|
|
||
|
|
**Claude Code UI interpretation**:
|
||
|
|
- Text block started → "Thinking..." indicator shows
|
||
|
|
- NO deltas received for 5+ seconds → "Must be done, hide indicator"
|
||
|
|
- Tool call suddenly appears → "Show result"
|
||
|
|
|
||
|
|
This is why it looked "done" but wasn't!
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 🎯 The Fix
|
||
|
|
|
||
|
|
### Option 1: Map Reasoning to Text Delta (Recommended)
|
||
|
|
|
||
|
|
Detect reasoning field and send as text_delta:
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
// In streaming handler
|
||
|
|
if (delta?.content) {
|
||
|
|
// Regular content
|
||
|
|
sendSSE("content_block_delta", {
|
||
|
|
index: textBlockIndex,
|
||
|
|
delta: {
|
||
|
|
type: "text_delta",
|
||
|
|
text: delta.content,
|
||
|
|
},
|
||
|
|
});
|
||
|
|
} else if (delta?.reasoning) {
|
||
|
|
// ✅ NEW: Grok's reasoning field
|
||
|
|
log(`[Proxy] Sending reasoning as text delta: ${delta.reasoning}`);
|
||
|
|
sendSSE("content_block_delta", {
|
||
|
|
index: textBlockIndex,
|
||
|
|
delta: {
|
||
|
|
type: "text_delta",
|
||
|
|
text: delta.reasoning, // Send reasoning as regular text
|
||
|
|
},
|
||
|
|
});
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
**Pros**:
|
||
|
|
- Simple fix
|
||
|
|
- Shows progress to user
|
||
|
|
- Compatible with Claude Code
|
||
|
|
|
||
|
|
**Cons**:
|
||
|
|
- Reasoning appears as regular text (user sees thinking process)
|
||
|
|
- Not true "thinking mode"
|
||
|
|
|
||
|
|
### Option 2: Map to Thinking Blocks (Proper)
|
||
|
|
|
||
|
|
Translate to Claude's thinking_delta format:
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
// Detect reasoning and send as thinking_delta
|
||
|
|
if (delta?.reasoning) {
|
||
|
|
// Send as thinking block
|
||
|
|
if (!thinkingBlockStarted) {
|
||
|
|
sendSSE("content_block_start", {
|
||
|
|
type: "content_block_start",
|
||
|
|
index: currentBlockIndex++,
|
||
|
|
content_block: {
|
||
|
|
type: "thinking",
|
||
|
|
thinking: ""
|
||
|
|
}
|
||
|
|
});
|
||
|
|
thinkingBlockStarted = true;
|
||
|
|
}
|
||
|
|
|
||
|
|
sendSSE("content_block_delta", {
|
||
|
|
index: thinkingBlockIndex,
|
||
|
|
delta: {
|
||
|
|
type: "thinking_delta", // ✅ Proper Claude format
|
||
|
|
thinking: delta.reasoning,
|
||
|
|
},
|
||
|
|
});
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
**Pros**:
|
||
|
|
- Proper protocol implementation
|
||
|
|
- Claude Code shows as thinking (not visible by default)
|
||
|
|
- Matches intended behavior
|
||
|
|
|
||
|
|
**Cons**:
|
||
|
|
- More complex implementation
|
||
|
|
- Requires thinking mode support
|
||
|
|
|
||
|
|
### Option 3: Hybrid Approach (Best)
|
||
|
|
|
||
|
|
Show reasoning as visible text during development, thinking mode in production:
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
const SHOW_REASONING_AS_TEXT = process.env.CLAUDISH_SHOW_REASONING === 'true';
|
||
|
|
|
||
|
|
if (delta?.reasoning) {
|
||
|
|
if (SHOW_REASONING_AS_TEXT) {
|
||
|
|
// Development: show as text
|
||
|
|
sendSSE("content_block_delta", {
|
||
|
|
index: textBlockIndex,
|
||
|
|
delta: {
|
||
|
|
type: "text_delta",
|
||
|
|
text: `[Thinking: ${delta.reasoning}]`,
|
||
|
|
},
|
||
|
|
});
|
||
|
|
} else {
|
||
|
|
// Production: proper thinking blocks
|
||
|
|
sendSSE("content_block_delta", {
|
||
|
|
index: thinkingBlockIndex,
|
||
|
|
delta: {
|
||
|
|
type: "thinking_delta",
|
||
|
|
thinking: delta.reasoning,
|
||
|
|
},
|
||
|
|
});
|
||
|
|
}
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 🧪 Test Case
|
||
|
|
|
||
|
|
### Reproduce the Issue
|
||
|
|
|
||
|
|
```bash
|
||
|
|
# Use Grok model
|
||
|
|
./dist/index.js "Analyze this codebase" --model x-ai/grok-code-fast-1 --debug
|
||
|
|
|
||
|
|
# Watch for:
|
||
|
|
1. Initial thinking indicator appears ✅
|
||
|
|
2. No updates for several seconds ❌
|
||
|
|
3. Sudden result appearance ❌
|
||
|
|
```
|
||
|
|
|
||
|
|
### Expected After Fix
|
||
|
|
|
||
|
|
```bash
|
||
|
|
# Same command after fix
|
||
|
|
./dist/index.js "Analyze this codebase" --model x-ai/grok-code-fast-1 --debug
|
||
|
|
|
||
|
|
# Should see:
|
||
|
|
1. Thinking indicator appears ✅
|
||
|
|
2. Continuous updates as reasoning streams ✅
|
||
|
|
3. Smooth transition to result ✅
|
||
|
|
```
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 📝 Implementation Checklist
|
||
|
|
|
||
|
|
- [ ] Add reasoning field detection in streaming handler
|
||
|
|
- [ ] Decide: text_delta vs thinking_delta approach
|
||
|
|
- [ ] Implement chosen solution
|
||
|
|
- [ ] Test with Grok models
|
||
|
|
- [ ] Add to snapshot tests
|
||
|
|
- [ ] Document in README (Grok-specific behavior)
|
||
|
|
- [ ] Consider other models with reasoning fields
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 🔍 Other Models to Check
|
||
|
|
|
||
|
|
These may also have reasoning fields:
|
||
|
|
- **OpenAI o1/o1-mini**: Known to have reasoning
|
||
|
|
- **Deepseek R1**: Reasoning-focused model
|
||
|
|
- **Qwen**: May have similar fields
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 💡 Immediate Action
|
||
|
|
|
||
|
|
**Quick Fix (5 minutes)**:
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
// src/proxy-server.ts, around line 748
|
||
|
|
// Change this:
|
||
|
|
if (delta?.content) {
|
||
|
|
log(`[Proxy] Sending content delta: ${delta.content}`);
|
||
|
|
sendSSE("content_block_delta", {
|
||
|
|
type: "content_block_delta",
|
||
|
|
index: textBlockIndex,
|
||
|
|
delta: {
|
||
|
|
type: "text_delta",
|
||
|
|
text: delta.content,
|
||
|
|
},
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
// To this:
|
||
|
|
const textContent = delta?.content || delta?.reasoning || "";
|
||
|
|
if (textContent) {
|
||
|
|
log(`[Proxy] Sending content delta: ${textContent}`);
|
||
|
|
sendSSE("content_block_delta", {
|
||
|
|
type: "content_block_delta",
|
||
|
|
index: textBlockIndex,
|
||
|
|
delta: {
|
||
|
|
type: "text_delta",
|
||
|
|
text: textContent,
|
||
|
|
},
|
||
|
|
});
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
This simple change will:
|
||
|
|
- ✅ Fix the "frozen" UI issue
|
||
|
|
- ✅ Show reasoning as it streams
|
||
|
|
- ✅ Work with all models
|
||
|
|
- ✅ Be backwards compatible
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 📈 Impact
|
||
|
|
|
||
|
|
**Before**: 186 reasoning chunks ignored → 5+ second UI freeze
|
||
|
|
**After**: 186 reasoning chunks displayed → smooth streaming experience
|
||
|
|
|
||
|
|
**Compliance**: 95% → 98% (handles model-specific fields)
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
**Status**: Ready to implement
|
||
|
|
**Priority**: HIGH (affects user experience significantly)
|
||
|
|
**Effort**: 5-10 minutes for quick fix, 1 hour for proper thinking mode
|