Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions relay/channel/openai/helper.go
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,36 @@ func HandleFinalResponse(c *gin.Context, info *relaycommon.RelayInfo, lastStream
for _, resp := range claudeResponses {
_ = helper.ClaudeData(c, *resp)
}

// Fallback: ensure closing events are emitted even if upstream never sent
// finish_reason / usage. Some OpenAI-compatible upstreams (e.g. LiteLLM)
// truncate the stream without proper terminators, and Claude Code hangs
// indefinitely without message_stop.
if !info.ClaudeConvertInfo.Done {
stopReason := service.StopReasonOpenAI2Claude(info.FinishReason)
if stopReason == "" {
stopReason = "end_turn"
}
if usage != nil {
_ = helper.ClaudeData(c, dto.ClaudeResponse{
Type: "message_delta",
Usage: service.BuildClaudeUsageFromOpenAIUsage(usage),
Delta: &dto.ClaudeMediaMessage{
StopReason: common.GetPointer[string](stopReason),
},
})
} else {
_ = helper.ClaudeData(c, dto.ClaudeResponse{
Type: "message_delta",
Delta: &dto.ClaudeMediaMessage{
StopReason: common.GetPointer[string](stopReason),
},
})
}
_ = helper.ClaudeData(c, dto.ClaudeResponse{
Type: "message_stop",
})
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
}
info.ClaudeConvertInfo.Done = true

case types.RelayFormatGemini:
Expand Down
45 changes: 42 additions & 3 deletions service/convert.go
Original file line number Diff line number Diff line change
Expand Up @@ -247,6 +247,13 @@ func buildClaudeUsageFromOpenAIUsage(oaiUsage *dto.Usage) *dto.ClaudeUsage {
return usage
}

// BuildClaudeUsageFromOpenAIUsage is the exported variant of
// buildClaudeUsageFromOpenAIUsage for cross-package callers (e.g. fallback
// closing events in relay/channel/openai/helper.go).
func BuildClaudeUsageFromOpenAIUsage(oaiUsage *dto.Usage) *dto.ClaudeUsage {
return buildClaudeUsageFromOpenAIUsage(oaiUsage)
}

func NormalizeCacheCreationSplit(totalTokens int, tokens5m int, tokens1h int) (int, int) {
remainder := lo.Max([]int{totalTokens - tokens5m - tokens1h, 0})
return tokens5m + remainder, tokens1h
Expand Down Expand Up @@ -468,10 +475,36 @@ func StreamResponseOpenAI2Claude(openAIResponse *dto.ChatCompletionsStreamRespon
oaiUsage := openAIResponse.Usage
if oaiUsage == nil {
oaiUsage = info.ClaudeConvertInfo.Usage
// Some upstreams emit finish_reason first, then send a final usage-only chunk.
// Defer closing until usage is available so the final message_delta carries it.
return claudeResponses
}
// Emit closing events immediately. Some OpenAI-compatible upstreams
// (e.g. LiteLLM) send finish_reason without usage and never follow up
// with a usage-only chunk; deferring would leave Claude Code hanging.
stopOpenBlocks()
stopReason := stopReasonOpenAI2Claude(info.FinishReason)
if stopReason == "" {
stopReason = "end_turn"
}
if oaiUsage != nil {
claudeResponses = append(claudeResponses, &dto.ClaudeResponse{
Type: "message_delta",
Usage: buildClaudeUsageFromOpenAIUsage(oaiUsage),
Delta: &dto.ClaudeMediaMessage{
StopReason: common.GetPointer[string](stopReason),
},
})
} else {
claudeResponses = append(claudeResponses, &dto.ClaudeResponse{
Type: "message_delta",
Delta: &dto.ClaudeMediaMessage{
StopReason: common.GetPointer[string](stopReason),
},
})
}
claudeResponses = append(claudeResponses, &dto.ClaudeResponse{
Type: "message_stop",
})
info.ClaudeConvertInfo.Done = true
return claudeResponses
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
}

var claudeResponse dto.ClaudeResponse
Expand Down Expand Up @@ -647,6 +680,12 @@ func stopReasonOpenAI2Claude(reason string) string {
return reasonmap.OpenAIFinishReasonToClaudeStopReason(reason)
}

// StopReasonOpenAI2Claude is the exported variant for cross-package callers
// (e.g. fallback closing events in relay/channel/openai/helper.go).
func StopReasonOpenAI2Claude(reason string) string {
return stopReasonOpenAI2Claude(reason)
}

func toJSONString(v interface{}) string {
b, err := json.Marshal(v)
if err != nil {
Expand Down
Loading