Fix openai-compatible header filtering and reasoning_content mapping
This commit is contained in:
parent
b2ec2f1ad5
commit
59a0ec4fd1
4 changed files with 123 additions and 3 deletions
|
|
@ -113,6 +113,7 @@ The `model` field uses a protocol prefix format: `[protocol/]model-identifier`
|
||||||
| `api_base` | No | API endpoint URL |
|
| `api_base` | No | API endpoint URL |
|
||||||
| `api_key` | No* | API authentication key |
|
| `api_key` | No* | API authentication key |
|
||||||
| `proxy` | No | HTTP proxy URL |
|
| `proxy` | No | HTTP proxy URL |
|
||||||
|
| `headers` | No | Custom HTTP headers (`Authorization` and `Content-Type` are reserved and will be ignored) |
|
||||||
| `auth_method` | No | Authentication method: `oauth`, `token` |
|
| `auth_method` | No | Authentication method: `oauth`, `token` |
|
||||||
| `connect_mode` | No | Connection mode for CLI providers: `stdio`, `grpc` |
|
| `connect_mode` | No | Connection mode for CLI providers: `stdio`, `grpc` |
|
||||||
| `rpm` | No | Requests per minute limit |
|
| `rpm` | No | Requests per minute limit |
|
||||||
|
|
|
||||||
|
|
@ -464,7 +464,7 @@ type ModelConfig struct {
|
||||||
APIBase string `json:"api_base,omitempty"` // API endpoint URL
|
APIBase string `json:"api_base,omitempty"` // API endpoint URL
|
||||||
APIKey string `json:"api_key"` // API authentication key
|
APIKey string `json:"api_key"` // API authentication key
|
||||||
Proxy string `json:"proxy,omitempty"` // HTTP proxy URL
|
Proxy string `json:"proxy,omitempty"` // HTTP proxy URL
|
||||||
Headers map[string]string `json:"headers,omitempty"` // Custom HTTP headers
|
Headers map[string]string `json:"headers,omitempty"` // Custom HTTP headers (Authorization and Content-Type are reserved and will be ignored)
|
||||||
|
|
||||||
// Special providers (CLI-based, OAuth, etc.)
|
// Special providers (CLI-based, OAuth, etc.)
|
||||||
AuthMethod string `json:"auth_method,omitempty"` // Authentication method: oauth, token
|
AuthMethod string `json:"auth_method,omitempty"` // Authentication method: oauth, token
|
||||||
|
|
|
||||||
|
|
@ -184,6 +184,9 @@ func (p *Provider) Chat(
|
||||||
req.Header.Set("Authorization", "Bearer "+p.apiKey)
|
req.Header.Set("Authorization", "Bearer "+p.apiKey)
|
||||||
}
|
}
|
||||||
for key, value := range p.customHeaders {
|
for key, value := range p.customHeaders {
|
||||||
|
if k := http.CanonicalHeaderKey(key); k == "Authorization" || k == "Content-Type" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
req.Header.Set(key, value)
|
req.Header.Set(key, value)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -301,7 +304,7 @@ func parseResponse(body []byte) (*LLMResponse, error) {
|
||||||
type openaiMessage struct {
|
type openaiMessage struct {
|
||||||
Role string `json:"role"`
|
Role string `json:"role"`
|
||||||
Content string `json:"content"`
|
Content string `json:"content"`
|
||||||
Reasoning string `json:"reasoning_content,omitempty"`
|
ReasoningContent string `json:"reasoning_content,omitempty"`
|
||||||
ToolCalls []ToolCall `json:"tool_calls,omitempty"`
|
ToolCalls []ToolCall `json:"tool_calls,omitempty"`
|
||||||
ToolCallID string `json:"tool_call_id,omitempty"`
|
ToolCallID string `json:"tool_call_id,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
@ -315,7 +318,7 @@ func stripSystemParts(messages []Message) []openaiMessage {
|
||||||
out[i] = openaiMessage{
|
out[i] = openaiMessage{
|
||||||
Role: m.Role,
|
Role: m.Role,
|
||||||
Content: m.Content,
|
Content: m.Content,
|
||||||
Reasoning: m.ReasoningContent,
|
ReasoningContent: m.ReasoningContent,
|
||||||
ToolCalls: m.ToolCalls,
|
ToolCalls: m.ToolCalls,
|
||||||
ToolCallID: m.ToolCallID,
|
ToolCallID: m.ToolCallID,
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -361,3 +361,119 @@ func TestProvider_FunctionalOptionRequestTimeoutNonPositive(t *testing.T) {
|
||||||
t.Fatalf("http timeout = %v, want %v", p.httpClient.Timeout, defaultRequestTimeout)
|
t.Fatalf("http timeout = %v, want %v", p.httpClient.Timeout, defaultRequestTimeout)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestProviderChat_SendsCustomHeaders(t *testing.T) {
|
||||||
|
var gotHeader string
|
||||||
|
|
||||||
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
gotHeader = r.Header.Get("X-Custom-Key")
|
||||||
|
resp := map[string]any{
|
||||||
|
"choices": []map[string]any{
|
||||||
|
{
|
||||||
|
"message": map[string]any{"content": "ok"},
|
||||||
|
"finish_reason": "stop",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
json.NewEncoder(w).Encode(resp)
|
||||||
|
}))
|
||||||
|
defer server.Close()
|
||||||
|
|
||||||
|
p := NewProvider("key", server.URL, "", WithCustomHeaders(map[string]string{
|
||||||
|
"X-Custom-Key": "myvalue",
|
||||||
|
}))
|
||||||
|
_, err := p.Chat(t.Context(), []Message{{Role: "user", Content: "hi"}}, nil, "gpt-4o", nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Chat() error = %v", err)
|
||||||
|
}
|
||||||
|
if gotHeader != "myvalue" {
|
||||||
|
t.Fatalf("X-Custom-Key = %q, want %q", gotHeader, "myvalue")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestProviderChat_SkipsReservedHeaders(t *testing.T) {
|
||||||
|
var gotAuth, gotContentType string
|
||||||
|
|
||||||
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
gotAuth = r.Header.Get("Authorization")
|
||||||
|
gotContentType = r.Header.Get("Content-Type")
|
||||||
|
resp := map[string]any{
|
||||||
|
"choices": []map[string]any{
|
||||||
|
{
|
||||||
|
"message": map[string]any{"content": "ok"},
|
||||||
|
"finish_reason": "stop",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
json.NewEncoder(w).Encode(resp)
|
||||||
|
}))
|
||||||
|
defer server.Close()
|
||||||
|
|
||||||
|
p := NewProvider("mykey", server.URL, "", WithCustomHeaders(map[string]string{
|
||||||
|
"Authorization": "Bearer OVERRIDE",
|
||||||
|
"Content-Type": "text/plain",
|
||||||
|
"X-Extra": "allowed",
|
||||||
|
}))
|
||||||
|
_, err := p.Chat(t.Context(), []Message{{Role: "user", Content: "hi"}}, nil, "gpt-4o", nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Chat() error = %v", err)
|
||||||
|
}
|
||||||
|
if gotAuth != "Bearer mykey" {
|
||||||
|
t.Fatalf("Authorization = %q, want %q", gotAuth, "Bearer mykey")
|
||||||
|
}
|
||||||
|
if gotContentType != "application/json" {
|
||||||
|
t.Fatalf("Content-Type = %q, want %q", gotContentType, "application/json")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestProviderChat_RoundTripsReasoningContent(t *testing.T) {
|
||||||
|
var reqBody map[string]any
|
||||||
|
|
||||||
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if err := json.NewDecoder(r.Body).Decode(&reqBody); err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
resp := map[string]any{
|
||||||
|
"choices": []map[string]any{
|
||||||
|
{
|
||||||
|
"message": map[string]any{"content": "ok"},
|
||||||
|
"finish_reason": "stop",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
json.NewEncoder(w).Encode(resp)
|
||||||
|
}))
|
||||||
|
defer server.Close()
|
||||||
|
|
||||||
|
p := NewProvider("key", server.URL, "")
|
||||||
|
_, err := p.Chat(
|
||||||
|
t.Context(),
|
||||||
|
[]Message{
|
||||||
|
{Role: "user", Content: "1+1=?"},
|
||||||
|
{Role: "assistant", Content: "2", ReasoningContent: "let me think..."},
|
||||||
|
{Role: "user", Content: "thanks"},
|
||||||
|
},
|
||||||
|
nil,
|
||||||
|
"gpt-4o",
|
||||||
|
nil,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Chat() error = %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
msgs, ok := reqBody["messages"].([]any)
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("messages is not []any")
|
||||||
|
}
|
||||||
|
assistantMsg, ok := msgs[1].(map[string]any)
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("messages[1] is not map[string]any")
|
||||||
|
}
|
||||||
|
if got := assistantMsg["reasoning_content"]; got != "let me think..." {
|
||||||
|
t.Fatalf("reasoning_content = %v, want %q", got, "let me think...")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue