Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
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
5 changes: 5 additions & 0 deletions constant/context_key.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,11 @@ const (
// It is not returned to end users, but can be persisted into consume/error logs for debugging.
ContextKeyAdminRejectReason ContextKey = "admin_reject_reason"

// ContextKeyClaudeResponsesStreamState holds the chat-to-responses stream
// state when routing /v1/responses through a Claude-family adaptor. It
// persists across streaming chunks and into the final flush.
ContextKeyClaudeResponsesStreamState ContextKey = "claude_responses_stream_state"

// ContextKeyLanguage stores the user's language preference for i18n
ContextKeyLanguage ContextKey = "language"
ContextKeyIsStream ContextKey = "is_stream"
Expand Down
21 changes: 12 additions & 9 deletions dto/openai_response.go
Original file line number Diff line number Diff line change
Expand Up @@ -413,17 +413,20 @@ const (

// ResponsesStreamResponse 用于处理 /v1/responses 流式响应
type ResponsesStreamResponse struct {
Type string `json:"type"`
Response *OpenAIResponsesResponse `json:"response,omitempty"`
Delta string `json:"delta,omitempty"`
Item *ResponsesOutput `json:"item,omitempty"`
Type string `json:"type"`
ResponseID string `json:"response_id,omitempty"`
Response *OpenAIResponsesResponse `json:"response,omitempty"`
Delta string `json:"delta,omitempty"`
Text string `json:"text,omitempty"`
Arguments string `json:"arguments,omitempty"`
Item *ResponsesOutput `json:"item,omitempty"`
// - response.function_call_arguments.delta
// - response.function_call_arguments.done
OutputIndex *int `json:"output_index,omitempty"`
ContentIndex *int `json:"content_index,omitempty"`
SummaryIndex *int `json:"summary_index,omitempty"`
ItemID string `json:"item_id,omitempty"`
Part *ResponsesReasoningSummaryPart `json:"part,omitempty"`
OutputIndex *int `json:"output_index,omitempty"`
ContentIndex *int `json:"content_index,omitempty"`
SummaryIndex *int `json:"summary_index,omitempty"`
ItemID string `json:"item_id,omitempty"`
Part any `json:"part,omitempty"`
}

// GetOpenAIError 从动态错误类型中提取OpenAIError结构
Expand Down
8 changes: 6 additions & 2 deletions relay/channel/claude/adaptor.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
"github.com/QuantumNous/new-api/dto"
"github.com/QuantumNous/new-api/relay/channel"
relaycommon "github.com/QuantumNous/new-api/relay/common"
"github.com/QuantumNous/new-api/service"
"github.com/QuantumNous/new-api/service/relayconvert"
"github.com/QuantumNous/new-api/setting/model_setting"
"github.com/QuantumNous/new-api/types"
Expand Down Expand Up @@ -113,8 +114,11 @@ func (a *Adaptor) ConvertEmbeddingRequest(c *gin.Context, info *relaycommon.Rela
}

func (a *Adaptor) ConvertOpenAIResponsesRequest(c *gin.Context, info *relaycommon.RelayInfo, request dto.OpenAIResponsesRequest) (any, error) {
// TODO implement me
return nil, errors.New("not implemented")
chatReq, err := service.ResponsesRequestToChatCompletionsRequest(&request)
if err != nil {
return nil, err
}
return a.ConvertOpenAIRequest(c, info, chatReq)
}

func (a *Adaptor) DoRequest(c *gin.Context, info *relaycommon.RelayInfo, requestBody io.Reader) (any, error) {
Expand Down
46 changes: 46 additions & 0 deletions relay/channel/claude/relay-claude.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (
relaycommon "github.com/QuantumNous/new-api/relay/common"
"github.com/QuantumNous/new-api/relay/helper"
"github.com/QuantumNous/new-api/service"
openaicompat "github.com/QuantumNous/new-api/service/openaicompat"
"github.com/QuantumNous/new-api/service/relayconvert"
"github.com/QuantumNous/new-api/setting/model_setting"
"github.com/QuantumNous/new-api/types"
Expand Down Expand Up @@ -42,6 +43,15 @@ func ResponseClaude2OpenAI(claudeResponse *dto.ClaudeResponse) *dto.OpenAITextRe

type ClaudeResponseInfo = relayconvert.ClaudeResponseInfo

func claudeResponsesStreamState(c *gin.Context, claudeInfo *ClaudeResponseInfo) *openaicompat.ChatToResponsesStreamState {
if state, ok := common.GetContextKeyType[*openaicompat.ChatToResponsesStreamState](c, constant.ContextKeyClaudeResponsesStreamState); ok && state != nil {
return state
}
state := openaicompat.NewChatToResponsesStreamState(claudeInfo.ResponseId, claudeInfo.Created, claudeInfo.Model)
common.SetContextKey(c, constant.ContextKeyClaudeResponsesStreamState, state)
return state
}

func cacheCreationTokensForOpenAIUsage(usage *dto.Usage) int {
if usage == nil {
return 0
Expand Down Expand Up @@ -126,6 +136,20 @@ func HandleStreamResponseData(c *gin.Context, info *relaycommon.RelayInfo, claud
if err != nil {
logger.LogError(c, "send_stream_response_failed: "+err.Error())
}
} else if info.RelayFormat == types.RelayFormatOpenAIResponses {
response := StreamResponseClaude2OpenAI(&claudeResponse)
if !FormatClaudeResponseInfo(&claudeResponse, response, claudeInfo) {
return nil
}
streamState := claudeResponsesStreamState(c, claudeInfo)
for _, event := range streamState.HandleChatChunk(response) {
jsonData, marshalErr := common.Marshal(event)
if marshalErr != nil {
logger.LogError(c, "send_stream_response_failed: "+marshalErr.Error())
continue
}
helper.ResponseChunkData(c, event, string(jsonData))
}
}
return nil
}
Expand Down Expand Up @@ -168,6 +192,17 @@ func HandleStreamFinalResponse(c *gin.Context, info *relaycommon.RelayInfo, clau
}
}
helper.Done(c)
} else if info.RelayFormat == types.RelayFormatOpenAIResponses {
streamState := claudeResponsesStreamState(c, claudeInfo)
for _, event := range streamState.FinalEvents(claudeInfo.Usage) {
jsonData, err := common.Marshal(event)
if err != nil {
common.SysLog("send final response failed: " + err.Error())
continue
}
helper.ResponseChunkData(c, event, string(jsonData))
}
helper.Done(c)
}
}

Expand Down Expand Up @@ -227,6 +262,17 @@ func HandleClaudeResponseData(c *gin.Context, info *relaycommon.RelayInfo, claud
if err != nil {
return types.NewError(err, types.ErrorCodeBadResponseBody)
}
case types.RelayFormatOpenAIResponses:
openaiResponse := ResponseClaude2OpenAI(&claudeResponse)
openaiResponse.Usage = buildOpenAIStyleUsageFromClaudeUsage(claudeInfo.Usage)
responsesResp, _, convErr := service.ChatCompletionsResponseToResponsesResponse(openaiResponse, info.UpstreamModelName)
if convErr != nil {
return types.NewError(convErr, types.ErrorCodeBadResponseBody)
}
responseData, err = common.Marshal(responsesResp)
if err != nil {
return types.NewError(err, types.ErrorCodeBadResponseBody)
}
case types.RelayFormatClaude:
responseData = data
}
Expand Down
18 changes: 18 additions & 0 deletions relay/responses_handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,24 @@ func ResponsesHelper(c *gin.Context, info *relaycommon.RelayInfo) (newAPIError *
return types.NewError(err, types.ErrorCodeChannelModelMappedError, types.ErrOptionWithSkipRetry())
}

// Image generation models may not be supported via /v1/responses on
// upstream proxies. Convert to /v1/chat/completions and convert the
// response back to Responses format.
if !model_setting.GetGlobalSettings().PassThroughRequestEnabled &&
!info.ChannelSetting.PassThroughBodyEnabled &&
shouldResponsesUseChatCompletions(info) {
adaptor := GetAdaptor(info.ApiType)
if adaptor != nil {
adaptor.Init(info)
usage, newApiErr := responsesViaChatCompletions(c, info, adaptor, request)
if newApiErr != nil {
return newApiErr
}
service.PostTextConsumeQuota(c, info, usage, nil)
return nil
}
}

adaptor := GetAdaptor(info.ApiType)
if adaptor == nil {
return types.NewError(fmt.Errorf("invalid api type: %d", info.ApiType), types.ErrorCodeInvalidApiType, types.ErrOptionWithSkipRetry())
Expand Down
Loading