Repository: MartialBE/one-hub

Issue:
feat: support azure databricks
**例行检查**

[//]: # (方框内删除已有的空格，填 x 号)
+ [x] 我已确认目前没有类似 issue
+ [x] 我已确认我已升级到最新版本
+ [x] 我已完整查看过项目 README，已确定现有版本无法满足需求
+ [x] 我理解并愿意跟进此 issue，协助测试和提供反馈
+ [x] 我理解并认可上述内容，并理解项目维护者精力有限，**不遵循规则的 issue 可能会被无视或直接关闭**

**功能描述**

support azure databricks api

example:
```bash
curl https://adb-XXXXXXXXXXXXXXX.XX.azuredatabricks.net/serving-endpoints/databricks-claude-sonnet-4/invocations \
  -X POST \
  -u token:$DATABRICKS_TOKEN \
  -H "Content-Type: application/json" \
  -d '{"messages":[{"role":"user","content":"What is an LLM agent?"}]}'
```

Discussion:

Solution patch:
diff --git a/common/config/constants.go b/common/config/constants.go
index 860e7c85..1b5cd09e 100644
--- a/common/config/constants.go
+++ b/common/config/constants.go
@@ -301,6 +301,7 @@ const (
 	ChannelTypeRecraft      = 51
 	ChannelTypeReplicate    = 52
 	ChannelTypeKling        = 53
+	ChannelTypeAzureDatabricks = 54
 )
 
 const (
diff --git a/providers/azuredatabricks/main.go b/providers/azuredatabricks/main.go
new file mode 100644
index 00000000..6ca15401
--- /dev/null
+++ b/providers/azuredatabricks/main.go
@@ -0,0 +1,291 @@
+package azuredatabricks
+
+import (
+	"encoding/base64"
+	"encoding/json"
+	"fmt"
+	"io"
+	"net/http"
+	"strings"
+
+	"one-api/common"
+	"one-api/common/requester"
+	"one-api/model"
+	"one-api/providers/base"
+	"one-api/types"
+)
+
+type AzureDatabricksProviderFactory struct{}
+
+// Create returns an AzureDatabricksProvider
+func (f AzureDatabricksProviderFactory) Create(channel *model.Channel) base.ProviderInterface {
+	return &AzureDatabricksProvider{
+		BaseProvider: base.BaseProvider{
+			Channel:   channel,
+			Requester: requester.NewHTTPRequester(*channel.Proxy, requestErrorHandle),
+		},
+	}
+}
+
+type AzureDatabricksProvider struct {
+	base.BaseProvider
+}
+
+func (p *AzureDatabricksProvider) GetRequestHeaders() (headers map[string]string) {
+	headers = make(map[string]string)
+	// https://learn.microsoft.com/en-us/azure/databricks/dev-tools/api/latest/authentication
+	auth := base64.StdEncoding.EncodeToString([]byte("token:" + p.Channel.Key))
+	headers["Authorization"] = fmt.Sprintf("Basic %s", auth)
+	return headers
+}
+
+func (p *AzureDatabricksProvider) GetFullRequestURL(modelName string) string {
+	baseURL := strings.TrimSuffix(p.GetBaseURL(), "/")
+	return fmt.Sprintf("%s/serving-endpoints/%s/invocations", baseURL, modelName)
+}
+
+func (p *AzureDatabricksProvider) prepareRequest(request *types.ChatCompletionRequest) (*http.Response, *types.OpenAIErrorWithStatusCode) {
+	req, errWithCode := p.convertRequest(request)
+	if errWithCode != nil {
+		return nil, errWithCode
+	}
+
+	requestURL := p.GetFullRequestURL(request.Model)
+	httpResponse, errWithCode := p.doRequest(req, requestURL)
+	if errWithCode != nil {
+		return nil, errWithCode
+	}
+
+	return httpResponse, nil
+}
+
+func (p *AzureDatabricksProvider) CreateChatCompletion(request *types.ChatCompletionRequest) (*types.ChatCompletionResponse, *types.OpenAIErrorWithStatusCode) {
+	httpResponse, errWithCode := p.prepareRequest(request)
+	if errWithCode != nil {
+		return nil, errWithCode
+	}
+	defer httpResponse.Body.Close()
+
+	response, err := p.convertResponse(httpResponse)
+	if err != nil {
+		return nil, common.ErrorWrapper(err, "convert_response_failed", http.StatusInternalServerError)
+	}
+
+	return response, nil
+}
+
+func (p *AzureDatabricksProvider) doRequest(request any, requestURL string) (*http.Response, *types.OpenAIErrorWithStatusCode) {
+	req, err := p.Requester.NewRequest(http.MethodPost, requestURL,
+		p.Requester.WithBody(request),
+		p.Requester.WithHeader(p.GetRequestHeaders()),
+	)
+	if err != nil {
+		return nil, common.ErrorWrapper(err, "new_request_failed", http.StatusInternalServerError)
+	}
+	return p.Requester.SendRequestRaw(req)
+}
+
+func (p *AzureDatabricksProvider) convertRequest(request *types.ChatCompletionRequest) (*databricksChatRequest, *types.OpenAIErrorWithStatusCode) {
+	messages := make([]types.ChatCompletionMessage, 0)
+	for _, msg := range request.Messages {
+		messages = append(messages, types.ChatCompletionMessage{
+			Role:    msg.Role,
+			Content: msg.StringContent(),
+		})
+	}
+
+	databricksRequest := &databricksChatRequest{
+		Messages: messages,
+		Stream:   request.Stream,
+	}
+
+	if request.MaxTokens != 0 {
+		databricksRequest.MaxTokens = request.MaxTokens
+	}
+	if request.Temperature != nil {
+		databricksRequest.Temperature = float64(*request.Temperature)
+	}
+	if request.TopP != nil {
+		databricksRequest.TopP = float64(*request.TopP)
+	}
+	if request.N != nil {
+		databricksRequest.N = *request.N
+	}
+	if request.Stop != nil {
+		if stop, ok := request.Stop.([]string); ok {
+			databricksRequest.Stop = stop
+		}
+	}
+	if request.PresencePenalty != nil {
+		databricksRequest.PresencePenalty = float64(*request.PresencePenalty)
+	}
+	if request.FrequencyPenalty != nil {
+		databricksRequest.FrequencyPenalty = float64(*request.FrequencyPenalty)
+	}
+
+	if request.Reasoning != nil {
+		var opErr *types.OpenAIErrorWithStatusCode
+		databricksRequest.MaxTokens, databricksRequest.Thinking, opErr = getThinking(databricksRequest.MaxTokens, request.Reasoning)
+		if opErr != nil {
+			return nil, opErr
+		}
+	}
+
+	return databricksRequest, nil
+}
+
+func getThinking(maxTokens int, reasoning *types.ChatReasoning) (newMaxTokens int, thinking *Thinking, err *types.OpenAIErrorWithStatusCode) {
+	newMaxTokens = maxTokens
+	thinking = &Thinking{
+		Type: "enabled",
+	}
+	if reasoning == nil || (reasoning.MaxTokens == 0 && reasoning.Effort == "") {
+		thinking.BudgetTokens = int(float64(maxTokens) * 0.8)
+	} else if reasoning.MaxTokens > 0 {
+		if reasoning.MaxTokens > maxTokens {
+			err = common.StringErrorWrapper(fmt.Sprintf("budget_token cannot be greater than the max_token, max_token: %d, budget_token: %d", maxTokens, reasoning.MaxTokens), "budget_tokens_too_large", http.StatusBadRequest)
+			return
+		}
+		thinking.BudgetTokens = reasoning.MaxTokens
+	} else {
+		switch reasoning.Effort {
+		case "low":
+			thinking.BudgetTokens = int(float64(maxTokens) * 0.2)
+		case "medium":
+			thinking.BudgetTokens = int(float64(maxTokens) * 0.5)
+		default:
+			thinking.BudgetTokens = int(float64(maxTokens) * 0.8)
+		}
+	}
+	if thinking.BudgetTokens < 128 {
+		thinking.BudgetTokens = 128
+	}
+	if newMaxTokens <= thinking.BudgetTokens {
+		newMaxTokens = thinking.BudgetTokens + 128
+	}
+
+	return
+}
+
+func (p *AzureDatabricksProvider) convertResponse(resp *http.Response) (response *types.ChatCompletionResponse, err error) {
+	responseBody, err := io.ReadAll(resp.Body)
+	if err != nil {
+		return nil, err
+	}
+
+	var databricksResponse databricksChatResponse
+	err = json.Unmarshal(responseBody, &databricksResponse)
+	if err != nil {
+		return nil, err
+	}
+
+	response = &types.ChatCompletionResponse{
+		ID:      databricksResponse.ID,
+		Object:  "chat.completion",
+		Created: databricksResponse.Created,
+		Model:   databricksResponse.Model,
+		Choices: []types.ChatCompletionChoice{},
+	}
+
+	for _, choice := range databricksResponse.Choices {
+		response.Choices = append(response.Choices, types.ChatCompletionChoice{
+			Index: choice.Index,
+			Message: types.ChatCompletionMessage{
+				Role:    "assistant",
+				Content: choice.Message.Content,
+			},
+			FinishReason: choice.FinishReason,
+		})
+	}
+
+	if databricksResponse.Usage != nil {
+		response.Usage = &types.Usage{
+			PromptTokens:     databricksResponse.Usage.PromptTokens,
+			CompletionTokens: databricksResponse.Usage.CompletionTokens,
+			TotalTokens:      databricksResponse.Usage.TotalTokens,
+		}
+	}
+
+	return response, nil
+}
+
+func (p *AzureDatabricksProvider) CreateChatCompletionStream(request *types.ChatCompletionRequest) (requester.StreamReaderInterface[string], *types.OpenAIErrorWithStatusCode) {
+	httpResponse, errWithCode := p.prepareRequest(request)
+	if errWithCode != nil {
+		return nil, errWithCode
+	}
+
+	stream, err := requester.RequestStream(p.Requester, httpResponse, streamHandler)
+	if err != nil {
+		return nil, common.ErrorWrapper(err, "request_stream_failed", http.StatusInternalServerError)
+	}
+
+	return stream, nil
+}
+
+func streamHandler(rawLine *[]byte, dataChan chan string, errChan chan error) {
+	if !strings.HasPrefix(string(*rawLine), "data:") {
+		*rawLine = nil
+		return
+	}
+
+	*rawLine = (*rawLine)[5:]
+	*rawLine = []byte(strings.TrimSpace(string(*rawLine)))
+
+	if string(*rawLine) == "[DONE]" {
+		errChan <- io.EOF
+		*rawLine = requester.StreamClosed
+		return
+	}
+
+	dataChan <- string(*rawLine)
+}
+
+func requestErrorHandle(resp *http.Response) *types.OpenAIError {
+	var errorResponse types.OpenAIErrorResponse
+	err := json.NewDecoder(resp.Body).Decode(&errorResponse)
+	if err != nil {
+		return nil
+	}
+	return &errorResponse.Error
+}
+
+// databricksChatRequest is the request body for Azure Databricks
+type databricksChatRequest struct {
+	Messages         []types.ChatCompletionMessage `json:"messages"`
+	Stream           bool                          `json:"stream,omitempty"`
+	MaxTokens        int                           `json:"max_tokens,omitempty"`
+	Temperature      float64                       `json:"temperature,omitempty"`
+	TopP             float64                       `json:"top_p,omitempty"`
+	N                int                           `json:"n,omitempty"`
+	Stop             []string                      `json:"stop,omitempty"`
+	PresencePenalty  float64                       `json:"presence_penalty,omitempty"`
+	FrequencyPenalty float64                       `json:"frequency_penalty,omitempty"`
+	Thinking         *Thinking                     `json:"thinking,omitempty"`
+}
+
+type Thinking struct {
+	Type         string `json:"type,omitempty"`
+	BudgetTokens int    `json:"budget_tokens,omitempty"`
+}
+
+// databricksChatResponse is the response body for Azure Databricks
+type databricksChatResponse struct {
+	ID      string `json:"id"`
+	Object  string `json:"object"`
+	Created int64  `json:"created"`
+	Model   string `json:"model"`
+	Choices []struct {
+		Index   int `json:"index"`
+		Message struct {
+			Role    string `json:"role"`
+			Content string `json:"content"`
+		} `json:"message"`
+		FinishReason string `json:"finish_reason"`
+	} `json:"choices"`
+	Usage *struct {
+		PromptTokens     int `json:"prompt_tokens"`
+		CompletionTokens int `json:"completion_tokens"`
+		TotalTokens      int `json:"total_tokens"`
+	} `json:"usage,omitempty"`
+}
diff --git a/providers/openai/responses.go b/providers/openai/responses.go
index 5d13f196..b8c76cd4 100644
--- a/providers/openai/responses.go
+++ b/providers/openai/responses.go
@@ -6,16 +6,19 @@ import (
 	"one-api/common"
 	"one-api/common/config"
 	"one-api/common/requester"
+	"one-api/common/utils"
 	"one-api/types"
 	"strings"
 )
 
 type OpenAIResponsesStreamHandler struct {
-	Usage  *types.Usage
-	Prefix string
-	Model  string
+	Usage     *types.Usage
+	Prefix    string
+	Model     string
+	MessageID string
 
 	searchType string
+	toolIndex  int
 }
 
 func (p *OpenAIProvider) CreateResponses(request *types.OpenAIResponsesRequest) (openaiResponse *types.OpenAIResponsesResponses, errWithCode *types.OpenAIErrorWithStatusCode) {
@@ -65,14 +68,18 @@ func (p *OpenAIProvider) CreateResponsesStream(request *types.OpenAIResponsesReq
 
 	chatHandler := OpenAIResponsesStreamHandler{
 		Usage:  p.Usage,
-		Prefix: `data: {"type"`,
+		Prefix: `data: `,
 		Model:  request.Model,
 	}
 
-	return requester.RequestNoTrimStream(p.Requester, resp, chatHandler.HandlerChatStream)
+	if request.ConvertChat {
+		return requester.RequestStream(p.Requester, resp, chatHandler.HandlerChatStream)
+	}
+
+	return requester.RequestNoTrimStream(p.Requester, resp, chatHandler.HandlerResponsesStream)
 }
 
-func (h *OpenAIResponsesStreamHandler) HandlerChatStream(rawLine *[]byte, dataChan chan string, errChan chan error) {
+func (h *OpenAIResponsesStreamHandler) HandlerResponsesStream(rawLine *[]byte, dataChan chan string, errChan chan error) {
 	rawStr := string(*rawLine)
 
 	// 如果rawLine 前缀不为data:，则直接返回
@@ -125,8 +132,8 @@ func (h *OpenAIResponsesStreamHandler) HandlerChatStream(rawLine *[]byte, dataCh
 				h.Usage.IncExtraBilling(types.APITollTypeFileSearch, "")
 			}
 		}
-	case "response.completed":
-		if openaiResponse.Response != nil {
+	default:
+		if openaiResponse.Response != nil && openaiResponse.Response.Usage != nil {
 			usage := openaiResponse.Response.Usage
 			*h.Usage = *usage.ToOpenAIUsage()
 			getResponsesExtraBilling(openaiResponse.Response, h.Usage)
@@ -137,6 +144,179 @@ func (h *OpenAIResponsesStreamHandler) HandlerChatStream(rawLine *[]byte, dataCh
 	dataChan <- rawStr
 }
 
+func (h *OpenAIResponsesStreamHandler) HandlerChatStream(rawLine *[]byte, dataChan chan string, errChan chan error) {
+	// 如果rawLine 前缀不为data:，则直接返回
+	if !strings.HasPrefix(string(*rawLine), h.Prefix) {
+		*rawLine = nil
+		return
+	}
+
+	// 去除前缀
+	*rawLine = (*rawLine)[6:]
+
+	var openaiResponse types.OpenAIResponsesStreamResponses
+	err := json.Unmarshal(*rawLine, &openaiResponse)
+	if err != nil {
+		errChan <- common.ErrorToOpenAIError(err)
+		return
+	}
+
+	chatRes := types.ChatCompletionStreamResponse{
+		ID:      h.MessageID,
+		Object:  "chat.completion.chunk",
+		Created: utils.GetTimestamp(),
+		Model:   h.Model,
+		Choices: make([]types.ChatCompletionStreamChoice, 0),
+	}
+	needOutput := false
+
+	switch openaiResponse.Type {
+	case "response.created":
+		if openaiResponse.Response != nil {
+			if h.MessageID == "" {
+				h.MessageID = openaiResponse.Response.ID
+				chatRes.ID = h.MessageID
+			}
+		}
+		if len(openaiResponse.Response.Tools) > 0 {
+			for _, tool := range openaiResponse.Response.Tools {
+				if tool.Type == types.APITollTypeWebSearchPreview {
+					h.searchType = "medium"
+					if tool.SearchContextSize != "" {
+						h.searchType = tool.SearchContextSize
+					}
+				}
+			}
+		}
+		chatRes.Choices = append(chatRes.Choices, types.ChatCompletionStreamChoice{
+			Index: 0,
+			Delta: types.ChatCompletionStreamChoiceDelta{},
+		})
+		needOutput = true
+	case "response.output_text.delta": // 处理文本输出的增量
+		delta, ok := openaiResponse.Delta.(string)
+		if ok {
+			h.Usage.TextBuilder.WriteString(delta)
+		}
+		chatRes.Choices = append(chatRes.Choices, types.ChatCompletionStreamChoice{
+			Index: 0,
+			Delta: types.ChatCompletionStreamChoiceDelta{
+				Content: delta,
+			},
+		})
+		needOutput = true
+	case "response.reasoning_summary_text.delta": // 处理文本输出的增量
+		delta, ok := openaiResponse.Delta.(string)
+		if ok {
+			h.Usage.TextBuilder.WriteString(delta)
+		}
+		chatRes.Choices = append(chatRes.Choices, types.ChatCompletionStreamChoice{
+			Index: 0,
+			Delta: types.ChatCompletionStreamChoiceDelta{
+				ReasoningContent: delta,
+			},
+		})
+		needOutput = true
+	case "response.function_call_arguments.delta": // 处理函数调用参数的增量
+		delta, ok := openaiResponse.Delta.(string)
+		if ok {
+			h.Usage.TextBuilder.WriteString(delta)
+		}
+		chatRes.Choices = append(chatRes.Choices, types.ChatCompletionStreamChoice{
+			Index: 0,
+			Delta: types.ChatCompletionStreamChoiceDelta{
+				Role: types.ChatMessageRoleAssistant,
+				ToolCalls: []*types.ChatCompletionToolCalls{
+					{
+						Index: h.toolIndex,
+						Function: &types.ChatCompletionToolCallsFunction{
+							Arguments: delta,
+						},
+					},
+				},
+			},
+		})
+		needOutput = true
+	case "response.function_call_arguments.done":
+		h.toolIndex++
+	case "response.output_item.added":
+		if openaiResponse.Item != nil {
+			switch openaiResponse.Item.Type {
+			case types.InputTypeWebSearchCall:
+				if h.searchType == "" {
+					h.searchType = "medium"
+				}
+				h.Usage.IncExtraBilling(types.APITollTypeWebSearchPreview, h.searchType)
+			case types.InputTypeCodeInterpreterCall:
+				h.Usage.IncExtraBilling(types.APITollTypeCodeInterpreter, "")
+			case types.InputTypeFileSearchCall:
+				h.Usage.IncExtraBilling(types.APITollTypeFileSearch, "")
+
+			case types.InputTypeMessage, types.InputTypeReasoning:
+				chatRes.Choices = append(chatRes.Choices, types.ChatCompletionStreamChoice{
+					Index: 0,
+					Delta: types.ChatCompletionStreamChoiceDelta{
+						Role:    types.ChatMessageRoleAssistant,
+						Content: "",
+					},
+				})
+				needOutput = true
+			case types.InputTypeFunctionCall:
+				arguments := ""
+				if openaiResponse.Item.Arguments != nil {
+					arguments = *openaiResponse.Item.Arguments
+				}
+
+				chatRes.Choices = append(chatRes.Choices, types.ChatCompletionStreamChoice{
+					Index: 0,
+					Delta: types.ChatCompletionStreamChoiceDelta{
+						Role: types.ChatMessageRoleAssistant,
+						ToolCalls: []*types.ChatCompletionToolCalls{
+							{
+								Index: h.toolIndex,
+								Id:    openaiResponse.Item.CallID,
+								Type:  "function",
+								Function: &types.ChatCompletionToolCallsFunction{
+									Name:      openaiResponse.Item.Name,
+									Arguments: arguments,
+								},
+							},
+						},
+					},
+				})
+				needOutput = true
+			}
+		}
+	default:
+		if openaiResponse.Response != nil && openaiResponse.Response.Usage != nil {
+			usage := openaiResponse.Response.Usage
+			*h.Usage = *usage.ToOpenAIUsage()
+
+			getResponsesExtraBilling(openaiResponse.Response, h.Usage)
+			chatRes.Choices = append(chatRes.Choices, types.ChatCompletionStreamChoice{
+				Index:        0,
+				Delta:        types.ChatCompletionStreamChoiceDelta{},
+				FinishReason: types.ConvertResponsesStatusToChat(openaiResponse.Response.Status),
+			})
+			needOutput = true
+
+		}
+	}
+
+	if needOutput {
+		jsonData, err := json.Marshal(chatRes)
+		if err != nil {
+			errChan <- common.ErrorToOpenAIError(err)
+			return
+		}
+		dataChan <- string(jsonData)
+
+		return
+	}
+
+	*rawLine = nil
+}
+
 func getResponsesExtraBilling(response *types.OpenAIResponsesResponses, usage *types.Usage) {
 	if usage == nil {
 		return
diff --git a/providers/providers.go b/providers/providers.go
index f885fd0c..b874a2a3 100644
--- a/providers/providers.go
+++ b/providers/providers.go
@@ -5,6 +5,7 @@ import (
 	"one-api/model"
 	"one-api/providers/ali"
 	"one-api/providers/azure"
+	"one-api/providers/azuredatabricks"
 	azurespeech "one-api/providers/azureSpeech"
 	"one-api/providers/baichuan"
 	"one-api/providers/baidu"
@@ -87,6 +88,7 @@ func init() {
 		config.ChannelTypeRecraft:      recraftAI.RecraftProviderFactory{},
 		config.ChannelTypeReplicate:    replicate.ReplicateProviderFactory{},
 		config.ChannelTypeOpenRouter:   openrouter.OpenRouterProviderFactory{},
+		config.ChannelTypeAzureDatabricks: azuredatabricks.AzureDatabricksProviderFactory{},
 	}
 }
 
diff --git a/relay/chat.go b/relay/chat.go
index 1d2edf26..7ca37014 100644
--- a/relay/chat.go
+++ b/relay/chat.go
@@ -75,7 +75,26 @@ func (r *relayChat) getPromptTokens() (int, error) {
 	return common.CountTokenMessages(r.chatRequest.Messages, r.modelName, channel.PreCost), nil
 }
 
+var need2Response = map[string]bool{
+	"o3-pro-2025-06-10":                true,
+	"o3-pro":                           true,
+	"o1-pro-2025-03-19":                true,
+	"o1-pro":                           true,
+	"o3-deep-research-2025-06-26":      true,
+	"o3-deep-research":                 true,
+	"o4-mini-deep-research-2025-06-26": true,
+	"o4-mini-deep-research":            true,
+	"codex-mini-latest":                true,
+}
+
 func (r *relayChat) send() (err *types.OpenAIErrorWithStatusCode, done bool) {
+	if need2Response[r.modelName] {
+		resProvider, ok := r.provider.(providersBase.ResponsesInterface)
+		if ok {
+			return r.compatibleSend(resProvider)
+		}
+	}
+
 	chatProvider, ok := r.provider.(providersBase.ChatInterface)
 	if !ok {
 		err = common.StringErrorWrapperLocal("channel not implemented", "channel_error", http.StatusServiceUnavailable)
@@ -159,3 +178,45 @@ func (r *relayChat) getUsageResponse() string {
 
 	return ""
 }
+
+func (r *relayChat) compatibleSend(resProvider providersBase.ResponsesInterface) (err *types.OpenAIErrorWithStatusCode, done bool) {
+	resRequest := r.chatRequest.ToResponsesRequest()
+	resRequest.ConvertChat = true
+
+	if r.chatRequest.Stream {
+		var response requester.StreamReaderInterface[string]
+		response, err = resProvider.CreateResponsesStream(resRequest)
+		if err != nil {
+			return
+		}
+
+		if r.heartbeat != nil {
+			r.heartbeat.Stop()
+		}
+
+		doneStr := func() string {
+			return r.getUsageResponse()
+		}
+
+		var firstResponseTime time.Time
+		firstResponseTime, err = responseStreamClient(r.c, response, doneStr)
+		r.SetFirstResponseTime(firstResponseTime)
+	} else {
+		var response *types.OpenAIResponsesResponses
+		response, err = resProvider.CreateResponses(resRequest)
+		if err != nil {
+			return
+		}
+
+		if r.heartbeat != nil {
+			r.heartbeat.Stop()
+		}
+		err = responseJsonClient(r.c, response.ToChat())
+	}
+
+	if err != nil {
+		done = true
+	}
+
+	return
+}
diff --git a/relay/main.go b/relay/main.go
index df99ab69..bd7487aa 100644
--- a/relay/main.go
+++ b/relay/main.go
@@ -123,17 +123,15 @@ func RelayHandler(relay RelayBaseInterface) (err *types.OpenAIErrorWithStatusCod
 	}
 
 	err, done = relay.send()
-
-	if err != nil {
-		quota.Undo(relay.getContext())
-		return
-	}
-
 	// 最后处理流式中断时计算tokens
 	if usage.CompletionTokens == 0 && usage.TextBuilder.Len() > 0 {
 		usage.CompletionTokens = common.CountTokenText(usage.TextBuilder.String(), relay.getModelName())
 		usage.TotalTokens = usage.PromptTokens + usage.CompletionTokens
 	}
+	if err != nil {
+		quota.Undo(relay.getContext())
+		return
+	}
 
 	quota.SetFirstResponseTime(relay.GetFirstResponseTime())
 
diff --git a/types/chat.go b/types/chat.go
index b9960df4..6242113c 100644
--- a/types/chat.go
+++ b/types/chat.go
@@ -217,8 +217,9 @@ type ChatCompletionRequest struct {
 }
 
 type ChatReasoning struct {
-	MaxTokens int    `json:"max_tokens,omitempty"`
-	Effort    string `json:"effort,omitempty"`
+	MaxTokens int     `json:"max_tokens,omitempty"`
+	Effort    string  `json:"effort,omitempty"`
+	Summary   *string `json:"summary,omitempty"`
 }
 
 type WebSearchOptions struct {
@@ -277,7 +278,9 @@ type ChatCompletionFunction struct {
 
 type ChatCompletionTool struct {
 	Type     string                 `json:"type"`
-	Function ChatCompletionFunction `json:"function"`
+	Function ChatCompletionFunction `json:"function,omitzero"`
+
+	ResponsesTools
 }
 
 type ChatCompletionChoice struct {
@@ -452,3 +455,131 @@ type MultimediaData struct {
 	ID         string `json:"id,omitempty"`
 	Transcript string `json:"transcript,omitempty"`
 }
+
+func (c *ChatCompletionRequest) ToResponsesRequest() *OpenAIResponsesRequest {
+
+	res := &OpenAIResponsesRequest{
+		Model:             c.Model,
+		MaxOutputTokens:   c.MaxTokens,
+		ParallelToolCalls: c.ParallelToolCalls,
+		Stream:            c.Stream,
+		Temperature:       c.Temperature,
+		Text:              c.ResponseFormat,
+		ToolChoice:        c.ToolChoice,
+		TopP:              c.TopP,
+	}
+
+	if c.MaxCompletionTokens > 0 && c.MaxTokens == 0 {
+		res.MaxOutputTokens = c.MaxCompletionTokens
+	}
+
+	if c.Reasoning != nil {
+		res.Reasoning = &ReasoningEffort{
+			Summary: c.Reasoning.Summary,
+		}
+
+		if c.Reasoning.Effort != "" {
+			res.Reasoning.Effort = &c.Reasoning.Effort
+		}
+	}
+
+	if c.ReasoningEffort != nil && res.Reasoning == nil {
+		res.Reasoning = &ReasoningEffort{
+			Effort: c.ReasoningEffort,
+		}
+	}
+
+	if len(c.Tools) > 0 {
+		resTools := make([]ResponsesTools, 0)
+		for _, tool := range c.Tools {
+			if tool.Type == "function" && tool.Function.Name != "" {
+				resTools = append(resTools, ResponsesTools{
+					Type:        tool.Type,
+					Name:        tool.Function.Name,
+					Description: tool.Function.Description,
+					Parameters:  tool.Function.Parameters,
+					Strict:      tool.Function.Strict,
+				})
+				continue
+			}
+
+			tool.ResponsesTools.Type = tool.Type
+			resTools = append(resTools, tool.ResponsesTools)
+		}
+
+		if len(resTools) > 0 {
+			res.Tools = resTools
+		}
+	}
+
+	inputs := make([]InputResponses, 0)
+	for _, msg := range c.Messages {
+		// 处理ToolCalls
+		if len(msg.ToolCalls) > 0 {
+			for _, tool := range msg.ToolCalls {
+				if tool == nil || tool.Function == nil {
+					continue
+				}
+				inputs = append(inputs, InputResponses{
+					Type:      InputTypeFunctionCall,
+					CallID:    tool.Id,
+					Name:      tool.Function.Name,
+					Arguments: tool.Function.Arguments,
+				})
+			}
+
+			continue
+		}
+
+		if msg.ToolCallID != "" {
+			inputs = append(inputs, InputResponses{
+				Type:   InputTypeFunctionCallOutput,
+				CallID: msg.ToolCallID,
+				Output: msg.Content,
+			})
+
+			continue
+		}
+
+		input := InputResponses{
+			Type: InputTypeMessage,
+			Role: msg.Role,
+		}
+
+		inputContent := make([]ContentResponses, 0)
+
+		messges := msg.ParseContent()
+		for _, part := range messges {
+			switch part.Type {
+			case ContentTypeImageURL:
+				if part.ImageURL == nil {
+					continue
+				}
+				inputContent = append(inputContent, ContentResponses{
+					Type:     ContentTypeInputImage,
+					ImageUrl: part.ImageURL.URL,
+				})
+			case ContentTypeText:
+				roleType := ContentTypeInputText
+				if msg.Role == ChatMessageRoleAssistant {
+					roleType = ContentTypeOutputText
+				}
+				inputContent = append(inputContent, ContentResponses{
+					Type: roleType,
+					Text: part.Text,
+				})
+			}
+		}
+
+		if len(inputContent) > 0 {
+			input.Content = inputContent
+			inputs = append(inputs, input)
+		}
+	}
+
+	if len(inputs) > 0 {
+		res.Input = inputs
+	}
+
+	return res
+}
diff --git a/types/responses.go b/types/responses.go
index 059e4b6f..f72ef742 100644
--- a/types/responses.go
+++ b/types/responses.go
@@ -71,6 +71,8 @@ type OpenAIResponsesRequest struct {
 	Tools              []ResponsesTools              `json:"tools,omitempty"`
 	TopP               *float64                      `json:"top_p,omitempty"`
 	Truncation         string                        `json:"truncation,omitempty"`
+
+	ConvertChat bool `json:"-"`
 }
 
 func (r *OpenAIResponsesRequest) ToChatCompletionRequest() (*ChatCompletionRequest, error) {
@@ -493,6 +495,20 @@ func (m ResponsesOutput) StringContent() string {
 	return ""
 }
 
+func (m ResponsesOutput) GetSummaryString() string {
+	if m.Type != InputTypeReasoning {
+		return ""
+	}
+
+	summary := ""
+	for _, item := range m.Summary {
+		if item.Type == ContentTypeSummaryText {
+			summary += item.Text
+		}
+	}
+	return summary
+}
+
 type IncompleteDetail struct {
 	Reason string `json:"reason,omitempty"`
 }
@@ -629,16 +645,16 @@ func (u *Usage) ToResponsesUsage() *ResponsesUsage {
 	return responsesUsage
 }
 
-// func convertResponsesStatusToChat(status string) string {
-// 	switch status {
-// 	case ResponseStatusFailed:
-// 		return FinishReasonContentFilter
-// 	case ResponseStatusIncomplete:
-// 		return FinishReasonLength
-// 	default:
-// 		return FinishReasonStop
-// 	}
-// }
+func ConvertResponsesStatusToChat(status string) string {
+	switch status {
+	case ResponseStatusFailed:
+		return FinishReasonContentFilter
+	case ResponseStatusIncomplete:
+		return FinishReasonLength
+	default:
+		return FinishReasonStop
+	}
+}
 
 func ConvertChatStatusToResponses(status string) string {
 	switch status {
@@ -754,3 +770,56 @@ func (cc *ChatCompletionResponse) ToResponses(request *OpenAIResponsesRequest) *
 
 	return res
 }
+
+func (r *OpenAIResponsesResponses) ToChat() *ChatCompletionResponse {
+	resp := &ChatCompletionResponse{
+		Created: r.CreatedAt,
+		ID:      r.ID,
+		Model:   r.Model,
+		Object:  "chat.completion",
+		Usage:   r.Usage.ToOpenAIUsage(),
+		Choices: make([]ChatCompletionChoice, 0),
+	}
+
+	choice := ChatCompletionChoice{
+		Message: ChatCompletionMessage{
+			Role: ChatMessageRoleAssistant,
+		},
+		FinishReason: FinishReasonStop,
+	}
+
+	for _, output := range r.Output {
+		switch output.Type {
+		case InputTypeMessage:
+			choice.Message.Content = output.StringContent()
+		case InputTypeReasoning:
+			choice.Message.ReasoningContent = output.GetSummaryString()
+		case InputTypeFunctionCall:
+			if choice.Message.ToolCalls == nil {
+				choice.Message.ToolCalls = make([]*ChatCompletionToolCalls, 0)
+			}
+			arguments := ""
+			if output.Arguments != nil {
+				arguments = *output.Arguments
+			}
+			choice.Message.ToolCalls = append(choice.Message.ToolCalls, &ChatCompletionToolCalls{
+				Id:   output.CallID,
+				Type: "function",
+				Function: &ChatCompletionToolCallsFunction{
+					Name:      output.Name,
+					Arguments: arguments,
+				},
+			})
+			choice.FinishReason = FinishReasonToolCalls
+		}
+
+		if output.Status == ResponseStatusFailed || output.Status == ResponseStatusIncomplete {
+			choice.FinishReason = ConvertResponsesStatusToChat(output.Status)
+		}
+
+	}
+
+	resp.Choices = append(resp.Choices, choice)
+
+	return resp
+}



Test patch:
diff --git a/controller/channel-test.go b/controller/channel-test.go
index c9d87d01..e021b8f8 100644
--- a/controller/channel-test.go
+++ b/controller/channel-test.go
@@ -25,7 +25,8 @@ import (
 
 var (
 	embeddingsRegex = regexp.MustCompile(`(?:^text-|embed|Embed|rerank|davinci|babbage|bge-|e5-|LLM2Vec|retrieval|uae-|gte-|jina-clip|jina-embeddings)`)
-	imageRegex      = regexp.MustCompile(`flux|diffusion|stabilityai|sd-|dall|cogview|janus`)
+	imageRegex      = regexp.MustCompile(`flux|diffusion|stabilityai|sd-|dall|cogview|janus|image`)
+	responseRegex   = regexp.MustCompile(`(?:^o[1-9])`)
 	noSupportRegex  = regexp.MustCompile(`(?:^tts|rerank|whisper|speech|^mj_|^chirp)`)
 )
 
@@ -48,6 +49,8 @@ func testChannel(channel *model.Channel, testModel string) (openaiErr *types.Ope
 		url = "/v1/images/generations"
 	case "chat":
 		url = "/v1/chat/completions"
+	case "response":
+		url = "/v1/responses"
 	default:
 		return nil, errors.New("不支持的模型类型")
 	}
@@ -105,6 +108,19 @@ func testChannel(channel *model.Channel, testModel string) (openaiErr *types.Ope
 			N:      1,
 		}
 		response, openAIErrorWithStatusCode = imageProvider.CreateImageGenerations(testRequest)
+	case "response":
+		responseProvider, ok := provider.(providers_base.ResponsesInterface)
+		if !ok {
+			return nil, errors.New("channel not implemented")
+		}
+
+		testRequest := &types.OpenAIResponsesRequest{
+			Input:  "You just need to output 'hi' next.",
+			Model:  newModelName,
+			Stream: false,
+		}
+
+		response, openAIErrorWithStatusCode = responseProvider.CreateResponses(testRequest)
 	case "chat":
 		chatProvider, ok := provider.(providers_base.ChatInterface)
 		if !ok {
@@ -150,6 +166,10 @@ func getModelType(modelName string) string {
 		return "image"
 	}
 
+	if responseRegex.MatchString(modelName) {
+		return "response"
+	}
+
 	return "chat"
 }