feat(provider): support chat completions for openai providers

This commit is contained in:
lpf
2026-05-11 18:14:43 +08:00
parent c1cbec551b
commit 78d546989c
12 changed files with 106 additions and 5 deletions

View File

@@ -31,6 +31,7 @@ type HTTPProvider struct {
apiBase string
defaultModel string
supportsResponsesCompact bool
responsesAPI string
authMode string
timeout time.Duration
httpClient *http.Client
@@ -48,6 +49,7 @@ func NewHTTPProvider(providerName, apiKey, apiBase, defaultModel string, support
apiBase: normalizedBase,
defaultModel: strings.TrimSpace(defaultModel),
supportsResponsesCompact: supportsResponsesCompact,
responsesAPI: "responses",
authMode: authMode,
timeout: timeout,
httpClient: &http.Client{Timeout: timeout},
@@ -79,7 +81,7 @@ func (p *HTTPProvider) Chat(ctx context.Context, messages []Message, tools []Too
if !json.Valid(body) {
return nil, fmt.Errorf("API error (status %d, content-type %q): non-JSON response: %s", statusCode, contentType, previewResponseBody(body))
}
if p.useOpenAICompatChatUpstream() {
if p.useOpenAICompatChatUpstream() || p.useConfiguredOpenAICompatChat() {
return parseOpenAICompatResponse(body)
}
return parseResponsesAPIResponse(body)
@@ -102,7 +104,7 @@ func (p *HTTPProvider) ChatStream(ctx context.Context, messages []Message, tools
if !json.Valid(body) {
return nil, fmt.Errorf("API error (status %d, content-type %q): non-JSON response: %s", status, ctype, previewResponseBody(body))
}
if p.useOpenAICompatChatUpstream() {
if p.useOpenAICompatChatUpstream() || p.useConfiguredOpenAICompatChat() {
return parseOpenAICompatResponse(body)
}
return parseResponsesAPIResponse(body)

View File

@@ -112,6 +112,18 @@ func (p *HTTPProvider) useOpenAICompatChatUpstream() bool {
}
}
func (p *HTTPProvider) useConfiguredOpenAICompatChat() bool {
if p == nil {
return false
}
switch strings.ToLower(strings.TrimSpace(p.responsesAPI)) {
case "chat_completions":
return true
default:
return false
}
}
func (p *HTTPProvider) compatBase() string {
switch p.oauthProvider() {
case defaultQwenOAuthProvider:

View File

@@ -1,6 +1,7 @@
package providers
import (
"encoding/json"
"net/http"
"net/http/httptest"
"strings"
@@ -180,3 +181,37 @@ func TestBuildOpenAICompatChatRequestStripsKimiPrefixAndSuffix(t *testing.T) {
t.Fatalf("reasoning_effort = %#v, want auto", got)
}
}
func TestHTTPProviderChatUsesConfiguredChatCompletionsAPI(t *testing.T) {
var gotPath string
var gotBody map[string]interface{}
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
gotPath = r.URL.Path
if err := json.NewDecoder(r.Body).Decode(&gotBody); err != nil {
t.Fatalf("decode request: %v", err)
}
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write([]byte(`{"choices":[{"message":{"content":"hello from chat"},"finish_reason":"stop"}],"usage":{"prompt_tokens":1,"completion_tokens":2,"total_tokens":3}}`))
}))
defer server.Close()
provider := NewHTTPProvider("openai", "token", server.URL+"/v1", "gpt-5", false, "api_key", 5*time.Second, nil)
provider.responsesAPI = "chat_completions"
resp, err := provider.Chat(t.Context(), []Message{{Role: "user", Content: "hi"}}, nil, "gpt-5", nil)
if err != nil {
t.Fatalf("Chat error: %v", err)
}
if gotPath != "/v1/chat/completions" {
t.Fatalf("path = %q, want /v1/chat/completions", gotPath)
}
if gotBody["model"] != "gpt-5" {
t.Fatalf("model = %#v, want gpt-5", gotBody["model"])
}
if resp.Content != "hello from chat" {
t.Fatalf("content = %q, want hello from chat", resp.Content)
}
if resp.Usage == nil || resp.Usage.TotalTokens != 3 {
t.Fatalf("usage = %#v, want total_tokens=3", resp.Usage)
}
}

View File

@@ -116,7 +116,11 @@ func CreateProviderByName(cfg *config.Config, name string) (LLMProvider, error)
if oauthProvider == defaultIFlowOAuthProvider || strings.EqualFold(routeName, defaultIFlowOAuthProvider) {
return NewIFlowProvider(routeName, pc.APIKey, pc.APIBase, defaultModel, pc.SupportsResponsesCompact, pc.Auth, time.Duration(pc.TimeoutSec)*time.Second, oauth), nil
}
return NewHTTPProvider(routeName, pc.APIKey, pc.APIBase, defaultModel, pc.SupportsResponsesCompact, pc.Auth, time.Duration(pc.TimeoutSec)*time.Second, oauth), nil
provider := NewHTTPProvider(routeName, pc.APIKey, pc.APIBase, defaultModel, pc.SupportsResponsesCompact, pc.Auth, time.Duration(pc.TimeoutSec)*time.Second, oauth)
if api := strings.TrimSpace(pc.Responses.API); api != "" {
provider.responsesAPI = api
}
return provider, nil
}
func ProviderSupportsResponsesCompact(cfg *config.Config, name string) bool {

View File

@@ -44,7 +44,7 @@ func (p *HTTPProvider) callResponses(ctx context.Context, messages []Message, to
if prevID, ok := stringOption(options, "responses_previous_response_id"); ok && prevID != "" {
requestBody["previous_response_id"] = prevID
}
if p.useOpenAICompatChatUpstream() {
if p.useOpenAICompatChatUpstream() || p.useConfiguredOpenAICompatChat() {
chatBody := p.buildOpenAICompatChatRequest(messages, tools, model, options)
return p.postJSON(ctx, endpointFor(p.compatBase(), "/chat/completions"), chatBody)
}
@@ -309,7 +309,7 @@ func (p *HTTPProvider) callResponsesStream(ctx context.Context, messages []Messa
if streamOpts, ok := mapOption(options, "responses_stream_options"); ok && len(streamOpts) > 0 {
requestBody["stream_options"] = streamOpts
}
if p.useOpenAICompatChatUpstream() {
if p.useOpenAICompatChatUpstream() || p.useConfiguredOpenAICompatChat() {
chatBody := p.buildOpenAICompatChatRequest(messages, tools, model, options)
chatBody["stream"] = true
streamOptions := map[string]interface{}{"include_usage": true}