mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-09-11 22:51:26 +00:00
fix: handle JSON parsing in Claude Code CLI responses
- Parse string chunks that contain JSON assistant messages - Extract and properly format text content from parsed messages - Handle malformed JSON gracefully by displaying as-is - Add comprehensive tests for new parsing behavior Fixes #6125
This commit is contained in:
parent
4042fb0fd0
commit
e28b9a932e
2 changed files with 221 additions and 58 deletions
|
|
@ -563,4 +563,134 @@ describe("ClaudeCodeHandler", () => {
|
|||
|
||||
consoleSpy.mockRestore()
|
||||
})
|
||||
|
||||
test("should parse string chunks that are JSON assistant messages", async () => {
|
||||
const systemPrompt = "You are a helpful assistant"
|
||||
const messages = [{ role: "user" as const, content: "Hello" }]
|
||||
|
||||
// Mock async generator that yields a string containing JSON assistant message
|
||||
const mockGenerator = async function* (): AsyncGenerator<ClaudeCodeMessage | string> {
|
||||
// Yield a string that's actually a JSON assistant message
|
||||
yield JSON.stringify({
|
||||
type: "assistant",
|
||||
message: {
|
||||
id: "msg_123",
|
||||
type: "message",
|
||||
role: "assistant",
|
||||
model: "claude-3-5-sonnet-20241022",
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: "This is a response from a JSON string",
|
||||
},
|
||||
],
|
||||
stop_reason: null,
|
||||
stop_sequence: null,
|
||||
usage: {
|
||||
input_tokens: 10,
|
||||
output_tokens: 20,
|
||||
},
|
||||
},
|
||||
session_id: "session_123",
|
||||
})
|
||||
}
|
||||
|
||||
mockRunClaudeCode.mockReturnValue(mockGenerator())
|
||||
|
||||
const stream = handler.createMessage(systemPrompt, messages)
|
||||
const results = []
|
||||
|
||||
for await (const chunk of stream) {
|
||||
results.push(chunk)
|
||||
}
|
||||
|
||||
// Should parse the JSON and yield the text content
|
||||
expect(results).toHaveLength(1)
|
||||
expect(results[0]).toEqual({
|
||||
type: "text",
|
||||
text: "This is a response from a JSON string",
|
||||
})
|
||||
})
|
||||
|
||||
test("should handle malformed JSON strings gracefully", async () => {
|
||||
const systemPrompt = "You are a helpful assistant"
|
||||
const messages = [{ role: "user" as const, content: "Hello" }]
|
||||
|
||||
// Mock async generator that yields malformed JSON
|
||||
const mockGenerator = async function* (): AsyncGenerator<ClaudeCodeMessage | string> {
|
||||
// Yield a malformed JSON string with escaped newlines
|
||||
yield '{"type":"assistant","message":{"id":"msg_123"\\n\\n\\n"content":[{"type":"text","text":"Malformed"}]}'
|
||||
}
|
||||
|
||||
mockRunClaudeCode.mockReturnValue(mockGenerator())
|
||||
|
||||
const stream = handler.createMessage(systemPrompt, messages)
|
||||
const results = []
|
||||
|
||||
for await (const chunk of stream) {
|
||||
results.push(chunk)
|
||||
}
|
||||
|
||||
// Should yield the malformed string as text since it can't be parsed
|
||||
expect(results).toHaveLength(1)
|
||||
expect(results[0]).toEqual({
|
||||
type: "text",
|
||||
text: '{"type":"assistant","message":{"id":"msg_123"\\n\\n\\n"content":[{"type":"text","text":"Malformed"}]}',
|
||||
})
|
||||
})
|
||||
|
||||
test("should handle string chunks with thinking content when parsed from JSON", async () => {
|
||||
const systemPrompt = "You are a helpful assistant"
|
||||
const messages = [{ role: "user" as const, content: "Hello" }]
|
||||
|
||||
// Mock async generator that yields a string containing JSON with thinking
|
||||
const mockGenerator = async function* (): AsyncGenerator<ClaudeCodeMessage | string> {
|
||||
yield JSON.stringify({
|
||||
type: "assistant",
|
||||
message: {
|
||||
id: "msg_123",
|
||||
type: "message",
|
||||
role: "assistant",
|
||||
model: "claude-3-5-sonnet-20241022",
|
||||
content: [
|
||||
{
|
||||
type: "thinking",
|
||||
thinking: "Let me think about this...",
|
||||
},
|
||||
{
|
||||
type: "text",
|
||||
text: "Here's my answer",
|
||||
},
|
||||
],
|
||||
stop_reason: null,
|
||||
stop_sequence: null,
|
||||
usage: {
|
||||
input_tokens: 10,
|
||||
output_tokens: 20,
|
||||
},
|
||||
},
|
||||
session_id: "session_123",
|
||||
})
|
||||
}
|
||||
|
||||
mockRunClaudeCode.mockReturnValue(mockGenerator())
|
||||
|
||||
const stream = handler.createMessage(systemPrompt, messages)
|
||||
const results = []
|
||||
|
||||
for await (const chunk of stream) {
|
||||
results.push(chunk)
|
||||
}
|
||||
|
||||
// Should parse and yield both thinking and text content
|
||||
expect(results).toHaveLength(2)
|
||||
expect(results[0]).toEqual({
|
||||
type: "reasoning",
|
||||
text: "Let me think about this...",
|
||||
})
|
||||
expect(results[1]).toEqual({
|
||||
type: "text",
|
||||
text: "Here's my answer",
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -54,6 +54,15 @@ export class ClaudeCodeHandler extends BaseProvider implements ApiHandler {
|
|||
|
||||
for await (const chunk of claudeProcess) {
|
||||
if (typeof chunk === "string") {
|
||||
// Try to parse string chunks that might be JSON assistant messages
|
||||
const parsedChunk = this.attemptParseAssistantMessage(chunk)
|
||||
if (parsedChunk) {
|
||||
// Process as assistant message
|
||||
yield* this.processAssistantMessage(parsedChunk, usage, isPaidUsage)
|
||||
continue
|
||||
}
|
||||
|
||||
// If not a JSON message, yield as text
|
||||
yield {
|
||||
type: "text",
|
||||
text: chunk,
|
||||
|
|
@ -69,64 +78,7 @@ export class ClaudeCodeHandler extends BaseProvider implements ApiHandler {
|
|||
}
|
||||
|
||||
if (chunk.type === "assistant" && "message" in chunk) {
|
||||
const message = chunk.message
|
||||
|
||||
if (message.stop_reason !== null) {
|
||||
const content = "text" in message.content[0] ? message.content[0] : undefined
|
||||
|
||||
const isError = content && content.text.startsWith(`API Error`)
|
||||
if (isError) {
|
||||
// Error messages are formatted as: `API Error: <<status code>> <<json>>`
|
||||
const errorMessageStart = content.text.indexOf("{")
|
||||
const errorMessage = content.text.slice(errorMessageStart)
|
||||
|
||||
const error = this.attemptParse(errorMessage)
|
||||
if (!error) {
|
||||
throw new Error(content.text)
|
||||
}
|
||||
|
||||
if (error.error.message.includes("Invalid model name")) {
|
||||
throw new Error(
|
||||
content.text + `\n\n${t("common:errors.claudeCode.apiKeyModelPlanMismatch")}`,
|
||||
)
|
||||
}
|
||||
|
||||
throw new Error(errorMessage)
|
||||
}
|
||||
}
|
||||
|
||||
for (const content of message.content) {
|
||||
switch (content.type) {
|
||||
case "text":
|
||||
yield {
|
||||
type: "text",
|
||||
text: content.text,
|
||||
}
|
||||
break
|
||||
case "thinking":
|
||||
yield {
|
||||
type: "reasoning",
|
||||
text: content.thinking || "",
|
||||
}
|
||||
break
|
||||
case "redacted_thinking":
|
||||
yield {
|
||||
type: "reasoning",
|
||||
text: "[Redacted thinking block]",
|
||||
}
|
||||
break
|
||||
case "tool_use":
|
||||
console.error(`tool_use is not supported yet. Received: ${JSON.stringify(content)}`)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
usage.inputTokens += message.usage.input_tokens
|
||||
usage.outputTokens += message.usage.output_tokens
|
||||
usage.cacheReadTokens = (usage.cacheReadTokens || 0) + (message.usage.cache_read_input_tokens || 0)
|
||||
usage.cacheWriteTokens =
|
||||
(usage.cacheWriteTokens || 0) + (message.usage.cache_creation_input_tokens || 0)
|
||||
|
||||
yield* this.processAssistantMessage(chunk, usage, isPaidUsage)
|
||||
continue
|
||||
}
|
||||
|
||||
|
|
@ -172,4 +124,85 @@ export class ClaudeCodeHandler extends BaseProvider implements ApiHandler {
|
|||
return null
|
||||
}
|
||||
}
|
||||
|
||||
private *processAssistantMessage(
|
||||
chunk: any,
|
||||
usage: ApiStreamUsageChunk,
|
||||
isPaidUsage: boolean,
|
||||
): Generator<any, void, unknown> {
|
||||
const message = chunk.message
|
||||
|
||||
if (message.stop_reason !== null) {
|
||||
const content = "text" in message.content[0] ? message.content[0] : undefined
|
||||
|
||||
const isError = content && content.text.startsWith(`API Error`)
|
||||
if (isError) {
|
||||
// Error messages are formatted as: `API Error: <<status code>> <<json>>`
|
||||
const errorMessageStart = content.text.indexOf("{")
|
||||
const errorMessage = content.text.slice(errorMessageStart)
|
||||
|
||||
const error = this.attemptParse(errorMessage)
|
||||
if (!error) {
|
||||
throw new Error(content.text)
|
||||
}
|
||||
|
||||
if (error.error.message.includes("Invalid model name")) {
|
||||
throw new Error(content.text + `\n\n${t("common:errors.claudeCode.apiKeyModelPlanMismatch")}`)
|
||||
}
|
||||
|
||||
throw new Error(errorMessage)
|
||||
}
|
||||
}
|
||||
|
||||
for (const content of message.content) {
|
||||
switch (content.type) {
|
||||
case "text":
|
||||
yield {
|
||||
type: "text",
|
||||
text: content.text,
|
||||
}
|
||||
break
|
||||
case "thinking":
|
||||
yield {
|
||||
type: "reasoning",
|
||||
text: content.thinking || "",
|
||||
}
|
||||
break
|
||||
case "redacted_thinking":
|
||||
yield {
|
||||
type: "reasoning",
|
||||
text: "[Redacted thinking block]",
|
||||
}
|
||||
break
|
||||
case "tool_use":
|
||||
console.error(`tool_use is not supported yet. Received: ${JSON.stringify(content)}`)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
usage.inputTokens += message.usage.input_tokens
|
||||
usage.outputTokens += message.usage.output_tokens
|
||||
usage.cacheReadTokens = (usage.cacheReadTokens || 0) + (message.usage.cache_read_input_tokens || 0)
|
||||
usage.cacheWriteTokens = (usage.cacheWriteTokens || 0) + (message.usage.cache_creation_input_tokens || 0)
|
||||
}
|
||||
|
||||
private attemptParseAssistantMessage(str: string): any {
|
||||
// Only try to parse if it looks like a JSON assistant message
|
||||
if (!str.trim().startsWith('{"type":"assistant"')) {
|
||||
return null
|
||||
}
|
||||
|
||||
try {
|
||||
const parsed = JSON.parse(str)
|
||||
// Validate it has the expected structure
|
||||
if (parsed.type === "assistant" && parsed.message) {
|
||||
return parsed
|
||||
}
|
||||
return null
|
||||
} catch (err) {
|
||||
// If parsing fails, log the error for debugging but don't throw
|
||||
console.error("Failed to parse potential assistant message:", err)
|
||||
return null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue