Repository: sashabaranov/go-openai
Pull request #934 (2025-02-09 10:46:00)

feat: add Anthropic API support with custom version header
**Describe the change**
Add support for Anthropic models.

**Provide documentation link**
Anthropic API requests require a custom header "anthropic-version". More details [here](https://docs.anthropic.com/en/api/versioning)

**Describe your solution**
Added new APITypeAnthropic with APIVersion = "2023-06-01" and baseURL of "https://api.anthropic.com/v1"

**Tests**
Checking correct APIVersion and default baseURL.

**Additional context**
Currently, trying to override the default config with Anthropic values, fails with a 404 not found error (because there's no request header "anthropic-version":"2023-06-01").



Patch:
diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml
index a41fff9..ea0c327 100644
--- a/.github/workflows/pr.yml
+++ b/.github/workflows/pr.yml
@@ -18,9 +18,9 @@ jobs:
       run: |
         go vet .
     - name: Run golangci-lint
-      uses: golangci/golangci-lint-action@v4
+      uses: golangci/golangci-lint-action@v6
       with:
-        version: latest
+        version: v1.63.4
     - name: Run tests
       run: go test -race -covermode=atomic -coverprofile=coverage.out -v .
     - name: Upload coverage reports to Codecov
diff --git a/chat.go b/chat.go
index ce24fa3..995860c 100644
--- a/chat.go
+++ b/chat.go
@@ -14,6 +14,7 @@ const (
 	ChatMessageRoleAssistant = "assistant"
 	ChatMessageRoleFunction  = "function"
 	ChatMessageRoleTool      = "tool"
+	ChatMessageRoleDeveloper = "developer"
 )
 
 const chatCompletionsSuffix = "/chat/completions"
diff --git a/client.go b/client.go
index ed8595e..cef3753 100644
--- a/client.go
+++ b/client.go
@@ -182,13 +182,21 @@ func sendRequestStream[T streamable](client *Client, req *http.Request) (*stream
 
 func (c *Client) setCommonHeaders(req *http.Request) {
 	// https://learn.microsoft.com/en-us/azure/cognitive-services/openai/reference#authentication
-	// Azure API Key authentication
-	if c.config.APIType == APITypeAzure || c.config.APIType == APITypeCloudflareAzure {
+	switch c.config.APIType {
+	case APITypeAzure, APITypeCloudflareAzure:
+		// Azure API Key authentication
 		req.Header.Set(AzureAPIKeyHeader, c.config.authToken)
-	} else if c.config.authToken != "" {
-		// OpenAI or Azure AD authentication
-		req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", c.config.authToken))
+	case APITypeAnthropic:
+		// https://docs.anthropic.com/en/api/versioning
+		req.Header.Set("anthropic-version", c.config.APIVersion)
+	case APITypeOpenAI, APITypeAzureAD:
+		fallthrough
+	default:
+		if c.config.authToken != "" {
+			req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", c.config.authToken))
+		}
 	}
+
 	if c.config.OrgID != "" {
 		req.Header.Set("OpenAI-Organization", c.config.OrgID)
 	}
diff --git a/config.go b/config.go
index 8a91835..4788ba6 100644
--- a/config.go
+++ b/config.go
@@ -11,6 +11,8 @@ const (
 
 	azureAPIPrefix         = "openai"
 	azureDeploymentsPrefix = "deployments"
+
+	AnthropicAPIVersion = "2023-06-01"
 )
 
 type APIType string
@@ -20,6 +22,7 @@ const (
 	APITypeAzure           APIType = "AZURE"
 	APITypeAzureAD         APIType = "AZURE_AD"
 	APITypeCloudflareAzure APIType = "CLOUDFLARE_AZURE"
+	APITypeAnthropic       APIType = "ANTHROPIC"
 )
 
 const AzureAPIKeyHeader = "api-key"
@@ -37,7 +40,7 @@ type ClientConfig struct {
 	BaseURL              string
 	OrgID                string
 	APIType              APIType
-	APIVersion           string // required when APIType is APITypeAzure or APITypeAzureAD
+	APIVersion           string // required when APIType is APITypeAzure or APITypeAzureAD or APITypeAnthropic
 	AssistantVersion     string
 	AzureModelMapperFunc func(model string) string // replace model to azure deployment name func
 	HTTPClient           HTTPDoer
@@ -76,6 +79,23 @@ func DefaultAzureConfig(apiKey, baseURL string) ClientConfig {
 	}
 }
 
+func DefaultAnthropicConfig(apiKey, baseURL string) ClientConfig {
+	if baseURL == "" {
+		baseURL = "https://api.anthropic.com/v1"
+	}
+	return ClientConfig{
+		authToken:  apiKey,
+		BaseURL:    baseURL,
+		OrgID:      "",
+		APIType:    APITypeAnthropic,
+		APIVersion: AnthropicAPIVersion,
+
+		HTTPClient: &http.Client{},
+
+		EmptyMessagesLimit: defaultEmptyMessagesLimit,
+	}
+}
+
 func (ClientConfig) String() string {
 	return "<OpenAI API ClientConfig>"
 }
diff --git a/messages.go b/messages.go
index 9023639..3852d2e 100644
--- a/messages.go
+++ b/messages.go
@@ -41,6 +41,7 @@ type MessageContent struct {
 	Type      string       `json:"type"`
 	Text      *MessageText `json:"text,omitempty"`
 	ImageFile *ImageFile   `json:"image_file,omitempty"`
+	ImageURL  *ImageURL    `json:"image_url,omitempty"`
 }
 type MessageText struct {
 	Value       string `json:"value"`
@@ -51,6 +52,11 @@ type ImageFile struct {
 	FileID string `json:"file_id"`
 }
 
+type ImageURL struct {
+	URL    string `json:"url"`
+	Detail string `json:"detail"`
+}
+
 type MessageRequest struct {
 	Role        string             `json:"role"`
 	Content     string             `json:"content"`
diff --git a/reasoning_validator.go b/reasoning_validator.go
index 42a9fbd..040d6b4 100644
--- a/reasoning_validator.go
+++ b/reasoning_validator.go
@@ -28,15 +28,6 @@ var (
 	ErrReasoningModelLimitationsOther    = errors.New("this model has beta-limitations, temperature, top_p and n are fixed at 1, while presence_penalty and frequency_penalty are fixed at 0") //nolint:lll
 )
 
-var unsupportedToolsForO1Models = map[ToolType]struct{}{
-	ToolTypeFunction: {},
-}
-
-var availableMessageRoleForO1Models = map[string]struct{}{
-	ChatMessageRoleUser:      {},
-	ChatMessageRoleAssistant: {},
-}
-
 // ReasoningValidator handles validation for o-series model requests.
 type ReasoningValidator struct{}
 
@@ -58,12 +49,6 @@ func (v *ReasoningValidator) Validate(request ChatCompletionRequest) error {
 		return err
 	}
 
-	if o1Series {
-		if err := v.validateO1Specific(request); err != nil {
-			return err
-		}
-	}
-
 	return nil
 }
 
@@ -93,19 +78,3 @@ func (v *ReasoningValidator) validateReasoningModelParams(request ChatCompletion
 
 	return nil
 }
-
-// validateO1Specific checks O1-specific limitations.
-func (v *ReasoningValidator) validateO1Specific(request ChatCompletionRequest) error {
-	for _, m := range request.Messages {
-		if _, found := availableMessageRoleForO1Models[m.Role]; !found {
-			return ErrO1BetaLimitationsMessageTypes
-		}
-	}
-
-	for _, t := range request.Tools {
-		if _, found := unsupportedToolsForO1Models[t.Type]; found {
-			return ErrO1BetaLimitationsTools
-		}
-	}
-	return nil
-}


Test patch:
diff --git a/chat_test.go b/chat_test.go
index fc6c4a9..e90142d 100644
--- a/chat_test.go
+++ b/chat_test.go
@@ -106,40 +106,6 @@ func TestO1ModelsChatCompletionsBetaLimitations(t *testing.T) {
 			},
 			expectedError: openai.ErrReasoningModelLimitationsLogprobs,
 		},
-		{
-			name: "message_type_unsupported",
-			in: openai.ChatCompletionRequest{
-				MaxCompletionTokens: 1000,
-				Model:               openai.O1Mini,
-				Messages: []openai.ChatCompletionMessage{
-					{
-						Role: openai.ChatMessageRoleSystem,
-					},
-				},
-			},
-			expectedError: openai.ErrO1BetaLimitationsMessageTypes,
-		},
-		{
-			name: "tool_unsupported",
-			in: openai.ChatCompletionRequest{
-				MaxCompletionTokens: 1000,
-				Model:               openai.O1Mini,
-				Messages: []openai.ChatCompletionMessage{
-					{
-						Role: openai.ChatMessageRoleUser,
-					},
-					{
-						Role: openai.ChatMessageRoleAssistant,
-					},
-				},
-				Tools: []openai.Tool{
-					{
-						Type: openai.ToolTypeFunction,
-					},
-				},
-			},
-			expectedError: openai.ErrO1BetaLimitationsTools,
-		},
 		{
 			name: "set_temperature_unsupported",
 			in: openai.ChatCompletionRequest{
diff --git a/client_test.go b/client_test.go
index 2ed82f1..3219714 100644
--- a/client_test.go
+++ b/client_test.go
@@ -39,6 +39,21 @@ func TestClient(t *testing.T) {
 	}
 }
 
+func TestSetCommonHeadersAnthropic(t *testing.T) {
+	config := DefaultAnthropicConfig("mock-token", "")
+	client := NewClientWithConfig(config)
+	req, err := http.NewRequest("GET", "http://example.com", nil)
+	if err != nil {
+		t.Fatalf("Failed to create request: %v", err)
+	}
+
+	client.setCommonHeaders(req)
+
+	if got := req.Header.Get("anthropic-version"); got != AnthropicAPIVersion {
+		t.Errorf("Expected anthropic-version header to be %q, got %q", AnthropicAPIVersion, got)
+	}
+}
+
 func TestDecodeResponse(t *testing.T) {
 	stringInput := ""
 
diff --git a/config_test.go b/config_test.go
index 3e528c3..145c260 100644
--- a/config_test.go
+++ b/config_test.go
@@ -60,3 +60,43 @@ func TestGetAzureDeploymentByModel(t *testing.T) {
 		})
 	}
 }
+
+func TestDefaultAnthropicConfig(t *testing.T) {
+	apiKey := "test-key"
+	baseURL := "https://api.anthropic.com/v1"
+
+	config := openai.DefaultAnthropicConfig(apiKey, baseURL)
+
+	if config.APIType != openai.APITypeAnthropic {
+		t.Errorf("Expected APIType to be %v, got %v", openai.APITypeAnthropic, config.APIType)
+	}
+
+	if config.APIVersion != openai.AnthropicAPIVersion {
+		t.Errorf("Expected APIVersion to be 2023-06-01, got %v", config.APIVersion)
+	}
+
+	if config.BaseURL != baseURL {
+		t.Errorf("Expected BaseURL to be %v, got %v", baseURL, config.BaseURL)
+	}
+
+	if config.EmptyMessagesLimit != 300 {
+		t.Errorf("Expected EmptyMessagesLimit to be 300, got %v", config.EmptyMessagesLimit)
+	}
+}
+
+func TestDefaultAnthropicConfigWithEmptyValues(t *testing.T) {
+	config := openai.DefaultAnthropicConfig("", "")
+
+	if config.APIType != openai.APITypeAnthropic {
+		t.Errorf("Expected APIType to be %v, got %v", openai.APITypeAnthropic, config.APIType)
+	}
+
+	if config.APIVersion != openai.AnthropicAPIVersion {
+		t.Errorf("Expected APIVersion to be %s, got %v", openai.AnthropicAPIVersion, config.APIVersion)
+	}
+
+	expectedBaseURL := "https://api.anthropic.com/v1"
+	if config.BaseURL != expectedBaseURL {
+		t.Errorf("Expected BaseURL to be %v, got %v", expectedBaseURL, config.BaseURL)
+	}
+}