@@ -54,6 +54,15 @@ export class ClaudeCodeHandler extends BaseProvider implements ApiHandler {
5454
5555 for await ( const chunk of claudeProcess ) {
5656 if ( typeof chunk === "string" ) {
57+ // Try to parse string chunks that might be JSON assistant messages
58+ const parsedChunk = this . attemptParseAssistantMessage ( chunk )
59+ if ( parsedChunk ) {
60+ // Process as assistant message
61+ yield * this . processAssistantMessage ( parsedChunk , usage , isPaidUsage )
62+ continue
63+ }
64+
65+ // If not a JSON message, yield as text
5766 yield {
5867 type : "text" ,
5968 text : chunk ,
@@ -69,64 +78,7 @@ export class ClaudeCodeHandler extends BaseProvider implements ApiHandler {
6978 }
7079
7180 if ( chunk . type === "assistant" && "message" in chunk ) {
72- const message = chunk . message
73-
74- if ( message . stop_reason !== null ) {
75- const content = "text" in message . content [ 0 ] ? message . content [ 0 ] : undefined
76-
77- const isError = content && content . text . startsWith ( `API Error` )
78- if ( isError ) {
79- // Error messages are formatted as: `API Error: <<status code>> <<json>>`
80- const errorMessageStart = content . text . indexOf ( "{" )
81- const errorMessage = content . text . slice ( errorMessageStart )
82-
83- const error = this . attemptParse ( errorMessage )
84- if ( ! error ) {
85- throw new Error ( content . text )
86- }
87-
88- if ( error . error . message . includes ( "Invalid model name" ) ) {
89- throw new Error (
90- content . text + `\n\n${ t ( "common:errors.claudeCode.apiKeyModelPlanMismatch" ) } ` ,
91- )
92- }
93-
94- throw new Error ( errorMessage )
95- }
96- }
97-
98- for ( const content of message . content ) {
99- switch ( content . type ) {
100- case "text" :
101- yield {
102- type : "text" ,
103- text : content . text ,
104- }
105- break
106- case "thinking" :
107- yield {
108- type : "reasoning" ,
109- text : content . thinking || "" ,
110- }
111- break
112- case "redacted_thinking" :
113- yield {
114- type : "reasoning" ,
115- text : "[Redacted thinking block]" ,
116- }
117- break
118- case "tool_use" :
119- console . error ( `tool_use is not supported yet. Received: ${ JSON . stringify ( content ) } ` )
120- break
121- }
122- }
123-
124- usage . inputTokens += message . usage . input_tokens
125- usage . outputTokens += message . usage . output_tokens
126- usage . cacheReadTokens = ( usage . cacheReadTokens || 0 ) + ( message . usage . cache_read_input_tokens || 0 )
127- usage . cacheWriteTokens =
128- ( usage . cacheWriteTokens || 0 ) + ( message . usage . cache_creation_input_tokens || 0 )
129-
81+ yield * this . processAssistantMessage ( chunk , usage , isPaidUsage )
13082 continue
13183 }
13284
@@ -172,4 +124,85 @@ export class ClaudeCodeHandler extends BaseProvider implements ApiHandler {
172124 return null
173125 }
174126 }
127+
128+ private * processAssistantMessage (
129+ chunk : any ,
130+ usage : ApiStreamUsageChunk ,
131+ isPaidUsage : boolean ,
132+ ) : Generator < any , void , unknown > {
133+ const message = chunk . message
134+
135+ if ( message . stop_reason !== null ) {
136+ const content = "text" in message . content [ 0 ] ? message . content [ 0 ] : undefined
137+
138+ const isError = content && content . text . startsWith ( `API Error` )
139+ if ( isError ) {
140+ // Error messages are formatted as: `API Error: <<status code>> <<json>>`
141+ const errorMessageStart = content . text . indexOf ( "{" )
142+ const errorMessage = content . text . slice ( errorMessageStart )
143+
144+ const error = this . attemptParse ( errorMessage )
145+ if ( ! error ) {
146+ throw new Error ( content . text )
147+ }
148+
149+ if ( error . error . message . includes ( "Invalid model name" ) ) {
150+ throw new Error ( content . text + `\n\n${ t ( "common:errors.claudeCode.apiKeyModelPlanMismatch" ) } ` )
151+ }
152+
153+ throw new Error ( errorMessage )
154+ }
155+ }
156+
157+ for ( const content of message . content ) {
158+ switch ( content . type ) {
159+ case "text" :
160+ yield {
161+ type : "text" ,
162+ text : content . text ,
163+ }
164+ break
165+ case "thinking" :
166+ yield {
167+ type : "reasoning" ,
168+ text : content . thinking || "" ,
169+ }
170+ break
171+ case "redacted_thinking" :
172+ yield {
173+ type : "reasoning" ,
174+ text : "[Redacted thinking block]" ,
175+ }
176+ break
177+ case "tool_use" :
178+ console . error ( `tool_use is not supported yet. Received: ${ JSON . stringify ( content ) } ` )
179+ break
180+ }
181+ }
182+
183+ usage . inputTokens += message . usage . input_tokens
184+ usage . outputTokens += message . usage . output_tokens
185+ usage . cacheReadTokens = ( usage . cacheReadTokens || 0 ) + ( message . usage . cache_read_input_tokens || 0 )
186+ usage . cacheWriteTokens = ( usage . cacheWriteTokens || 0 ) + ( message . usage . cache_creation_input_tokens || 0 )
187+ }
188+
189+ private attemptParseAssistantMessage ( str : string ) : any {
190+ // Only try to parse if it looks like a JSON assistant message
191+ if ( ! str . trim ( ) . startsWith ( '{"type":"assistant"' ) ) {
192+ return null
193+ }
194+
195+ try {
196+ const parsed = JSON . parse ( str )
197+ // Validate it has the expected structure
198+ if ( parsed . type === "assistant" && parsed . message ) {
199+ return parsed
200+ }
201+ return null
202+ } catch ( err ) {
203+ // If parsing fails, log the error for debugging but don't throw
204+ console . error ( "Failed to parse potential assistant message:" , err )
205+ return null
206+ }
207+ }
175208}
0 commit comments