@@ -17,6 +17,7 @@ import (
1717 "github.com/Wei-Shaw/sub2api/internal/pkg/logger"
1818 "github.com/Wei-Shaw/sub2api/internal/util/responseheaders"
1919 "github.com/gin-gonic/gin"
20+ "github.com/tidwall/gjson"
2021 "go.uber.org/zap"
2122)
2223
@@ -171,26 +172,48 @@ func (s *GatewayService) ForwardAsChatCompletions(
171172 return nil , fmt .Errorf ("upstream error: %d %s" , resp .StatusCode , upstreamMsg )
172173 }
173174
174- // 13. Handle normal response
175+ // 13. Extract reasoning effort from CC request body
176+ reasoningEffort := extractCCReasoningEffortFromBody (body )
177+
178+ // 14. Handle normal response
175179 // Read Anthropic SSE → convert to Responses events → convert to CC format
176180 var result * ForwardResult
177181 var handleErr error
178182 if clientStream {
179- result , handleErr = s .handleCCStreamingFromAnthropic (resp , c , originalModel , mappedModel , startTime , includeUsage )
183+ result , handleErr = s .handleCCStreamingFromAnthropic (resp , c , originalModel , mappedModel , reasoningEffort , startTime , includeUsage )
180184 } else {
181- result , handleErr = s .handleCCBufferedFromAnthropic (resp , c , originalModel , mappedModel , startTime )
185+ result , handleErr = s .handleCCBufferedFromAnthropic (resp , c , originalModel , mappedModel , reasoningEffort , startTime )
182186 }
183187
184188 return result , handleErr
185189}
186190
191+ // extractCCReasoningEffortFromBody reads reasoning effort from a Chat Completions
192+ // request body. It checks both nested (reasoning.effort) and flat (reasoning_effort)
193+ // formats used by OpenAI-compatible clients.
194+ func extractCCReasoningEffortFromBody (body []byte ) * string {
195+ raw := strings .TrimSpace (gjson .GetBytes (body , "reasoning.effort" ).String ())
196+ if raw == "" {
197+ raw = strings .TrimSpace (gjson .GetBytes (body , "reasoning_effort" ).String ())
198+ }
199+ if raw == "" {
200+ return nil
201+ }
202+ normalized := normalizeOpenAIReasoningEffort (raw )
203+ if normalized == "" {
204+ return nil
205+ }
206+ return & normalized
207+ }
208+
187209// handleCCBufferedFromAnthropic reads Anthropic SSE events, assembles the full
188210// response, then converts Anthropic → Responses → Chat Completions.
189211func (s * GatewayService ) handleCCBufferedFromAnthropic (
190212 resp * http.Response ,
191213 c * gin.Context ,
192214 originalModel string ,
193215 mappedModel string ,
216+ reasoningEffort * string ,
194217 startTime time.Time ,
195218) (* ForwardResult , error ) {
196219 requestID := resp .Header .Get ("x-request-id" )
@@ -225,18 +248,16 @@ func (s *GatewayService) handleCCBufferedFromAnthropic(
225248 continue
226249 }
227250
251+ // message_start carries the initial response structure and cache usage
228252 if event .Type == "message_start" && event .Message != nil {
229253 finalResp = event .Message
254+ mergeAnthropicUsage (& usage , event .Message .Usage )
230255 }
256+
257+ // message_delta carries final usage and stop_reason
231258 if event .Type == "message_delta" {
232259 if event .Usage != nil {
233- usage = ClaudeUsage {
234- InputTokens : event .Usage .InputTokens ,
235- OutputTokens : event .Usage .OutputTokens ,
236- }
237- if event .Usage .CacheReadInputTokens > 0 {
238- usage .CacheReadInputTokens = event .Usage .CacheReadInputTokens
239- }
260+ mergeAnthropicUsage (& usage , * event .Usage )
240261 }
241262 if event .Delta != nil && event .Delta .StopReason != "" && finalResp != nil {
242263 finalResp .StopReason = event .Delta .StopReason
@@ -274,10 +295,13 @@ func (s *GatewayService) handleCCBufferedFromAnthropic(
274295 return nil , fmt .Errorf ("upstream stream ended without response" )
275296 }
276297
298+ // Update usage from accumulated delta
277299 if usage .InputTokens > 0 || usage .OutputTokens > 0 {
278300 finalResp .Usage = apicompat.AnthropicUsage {
279- InputTokens : usage .InputTokens ,
280- OutputTokens : usage .OutputTokens ,
301+ InputTokens : usage .InputTokens ,
302+ OutputTokens : usage .OutputTokens ,
303+ CacheCreationInputTokens : usage .CacheCreationInputTokens ,
304+ CacheReadInputTokens : usage .CacheReadInputTokens ,
281305 }
282306 }
283307
@@ -291,12 +315,13 @@ func (s *GatewayService) handleCCBufferedFromAnthropic(
291315 c .JSON (http .StatusOK , ccResp )
292316
293317 return & ForwardResult {
294- RequestID : requestID ,
295- Usage : usage ,
296- Model : originalModel ,
297- UpstreamModel : mappedModel ,
298- Stream : false ,
299- Duration : time .Since (startTime ),
318+ RequestID : requestID ,
319+ Usage : usage ,
320+ Model : originalModel ,
321+ UpstreamModel : mappedModel ,
322+ ReasoningEffort : reasoningEffort ,
323+ Stream : false ,
324+ Duration : time .Since (startTime ),
300325 }, nil
301326}
302327
@@ -307,6 +332,7 @@ func (s *GatewayService) handleCCStreamingFromAnthropic(
307332 c * gin.Context ,
308333 originalModel string ,
309334 mappedModel string ,
335+ reasoningEffort * string ,
310336 startTime time.Time ,
311337 includeUsage bool ,
312338) (* ForwardResult , error ) {
@@ -341,13 +367,14 @@ func (s *GatewayService) handleCCStreamingFromAnthropic(
341367
342368 resultWithUsage := func () * ForwardResult {
343369 return & ForwardResult {
344- RequestID : requestID ,
345- Usage : usage ,
346- Model : originalModel ,
347- UpstreamModel : mappedModel ,
348- Stream : true ,
349- Duration : time .Since (startTime ),
350- FirstTokenMs : firstTokenMs ,
370+ RequestID : requestID ,
371+ Usage : usage ,
372+ Model : originalModel ,
373+ UpstreamModel : mappedModel ,
374+ ReasoningEffort : reasoningEffort ,
375+ Stream : true ,
376+ Duration : time .Since (startTime ),
377+ FirstTokenMs : firstTokenMs ,
351378 }
352379 }
353380
@@ -369,18 +396,13 @@ func (s *GatewayService) handleCCStreamingFromAnthropic(
369396 firstTokenMs = & ms
370397 }
371398
372- // Extract usage
399+ // Extract usage from message_delta
373400 if event .Type == "message_delta" && event .Usage != nil {
374- usage = ClaudeUsage {
375- InputTokens : event .Usage .InputTokens ,
376- OutputTokens : event .Usage .OutputTokens ,
377- }
378- if event .Usage .CacheReadInputTokens > 0 {
379- usage .CacheReadInputTokens = event .Usage .CacheReadInputTokens
380- }
401+ mergeAnthropicUsage (& usage , * event .Usage )
381402 }
382- if event .Type == "message_start" && event .Message != nil && event .Message .Usage .InputTokens > 0 {
383- usage .InputTokens = event .Message .Usage .InputTokens
403+ // Also capture usage from message_start (carries cache fields)
404+ if event .Type == "message_start" && event .Message != nil {
405+ mergeAnthropicUsage (& usage , event .Message .Usage )
384406 }
385407
386408 // Chain: Anthropic event → Responses events → CC chunks
0 commit comments