Skip to content
Open
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
67 changes: 65 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@

English|[中文](https://www.cnblogs.com/stulzq/p/17271937.html)

Azure OpenAI Service Proxy, convert OpenAI official API request to Azure OpenAI API request, support all models, support GPT-4,Embeddings.
>Eliminate the differences between OpenAI and Azure OpenAI, acting as a bridge connecting them, OpenAI ecosystem accesses Azure OpenAI at zero cost.
Azure OpenAI Service Proxy, convert OpenAI official API request to Azure OpenAI API request, support all models, support GPT-4,Embeddings. Also supports [MiniMax](https://www.minimaxi.com/) as an alternative backend provider.
>Eliminate the differences between OpenAI and Azure OpenAI, acting as a bridge connecting them, OpenAI ecosystem accesses Azure OpenAI at zero cost. Now also supports MiniMax cloud models (MiniMax-M2.7, MiniMax-M2.5, MiniMax-M2.5-highspeed) as backend.

![aoai-proxy.jpg](assets/images/aoai-proxy.jpg)

Expand Down Expand Up @@ -259,5 +259,68 @@ azure-openai:
- chatgpt-ns
````

### Use MiniMax as Backend

[MiniMax](https://www.minimaxi.com/) provides an OpenAI-compatible API. You can use this proxy to route requests to MiniMax models alongside or instead of Azure OpenAI.

**Environment Variables:**

| Name | Desc | Default |
| --------------------- | ------------------------------------------------------------ | ---------------------- |
| MINIMAX_API_KEY | Your MiniMax API key. Setting this enables MiniMax support. | N |
| MINIMAX_ENDPOINT | MiniMax API endpoint | https://api.minimax.io/v1 |
| MINIMAX_MODEL_MAPPER | Comma-separated model mappings (optional). If not set, default models are registered: MiniMax-M2.7, MiniMax-M2.5, MiniMax-M2.5-highspeed | N |

**Example with Docker:**

````shell
docker run -d -p 8080:8080 --name=azure-openai-proxy \
--env MINIMAX_API_KEY=your_minimax_api_key \
stulzq/azure-openai-proxy:latest
````

**Call MiniMax API through the proxy:**

````shell
curl --location --request POST 'localhost:8080/v1/chat/completions' \
-H 'Authorization: Bearer <MiniMax API Key>' \
-H 'Content-Type: application/json' \
-d '{
"max_tokens": 1000,
"model": "MiniMax-M2.7",
"temperature": 0.8,
"messages": [
{
"role": "user",
"content": "Hello"
}
]
}'
````

**Use Config File with MiniMax:**

````yaml
api_base: "/v1"
deployment_config:
# Azure OpenAI deployments
- deployment_name: "gpt-4-deployment"
model_name: "gpt-4"
endpoint: "https://xxx.openai.azure.com/"
api_key: "azure-key"
api_version: "2024-02-01"
# MiniMax deployments
- model_name: "MiniMax-M2.7"
endpoint: "https://api.minimax.io/v1"
api_key: "your-minimax-key"
provider_type: "minimax"
- model_name: "MiniMax-M2.5-highspeed"
endpoint: "https://api.minimax.io/v1"
api_key: "your-minimax-key"
provider_type: "minimax"
````

You can mix Azure OpenAI and MiniMax models in the same configuration. The proxy automatically selects the correct backend based on the `provider_type` field.



57 changes: 57 additions & 0 deletions azure/init.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,9 @@ func Init() error {
}
}

// Initialize MiniMax from environment variables
InitMiniMaxFromEnv()

// ensure apiBase likes /v1
viper.SetDefault("api_base", "/v1")
apiBase := viper.GetString("api_base")
Expand Down Expand Up @@ -91,6 +94,60 @@ func InitFromEnvironmentVariables(apiVersion, endpoint, openaiModelMapper string
}
}

// InitMiniMaxFromEnv initializes MiniMax provider from environment variables.
// Set MINIMAX_API_KEY to enable MiniMax support.
// Optionally set MINIMAX_ENDPOINT (default: https://api.minimax.io/v1) and
// MINIMAX_MODEL_MAPPER (e.g. MiniMax-M2.7=MiniMax-M2.7,MiniMax-M2.5-highspeed=MiniMax-M2.5-highspeed).
// If MINIMAX_MODEL_MAPPER is not set, default models (MiniMax-M2.7, MiniMax-M2.5, MiniMax-M2.5-highspeed) are registered.
func InitMiniMaxFromEnv() {
apiKey := viper.GetString(constant.ENV_MINIMAX_API_KEY)
if apiKey == "" {
return
}

log.Println("Init MiniMax from environment variables")

endpoint := viper.GetString(constant.ENV_MINIMAX_ENDPOINT)
if endpoint == "" {
endpoint = "https://api.minimax.io/v1"
}

modelMapper := viper.GetString(constant.ENV_MINIMAX_MODEL_MAPPER)

u, err := url.Parse(endpoint)
if err != nil {
log.Fatalf("parse MiniMax endpoint error: %s", err.Error())
}

if modelMapper != "" {
for _, pair := range strings.Split(modelMapper, ",") {
info := strings.Split(pair, "=")
modelName := strings.TrimSpace(info[0])
ModelDeploymentConfig[modelName] = DeploymentConfig{
DeploymentName: modelName,
ModelName: modelName,
Endpoint: endpoint,
EndpointUrl: u,
ApiKey: apiKey,
ProviderType: "minimax",
}
}
} else {
// Register default MiniMax models
defaultModels := []string{"MiniMax-M2.7", "MiniMax-M2.5", "MiniMax-M2.5-highspeed"}
for _, model := range defaultModels {
ModelDeploymentConfig[model] = DeploymentConfig{
DeploymentName: model,
ModelName: model,
Endpoint: endpoint,
EndpointUrl: u,
ApiKey: apiKey,
ProviderType: "minimax",
}
}
}
}

func InitFromConfigFile() error {
log.Println("Init from config file")

Expand Down
209 changes: 209 additions & 0 deletions azure/minimax_integration_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,209 @@
package azure

import (
"encoding/json"
"io"
"net/http"
"net/http/httptest"
"net/url"
"strings"
"testing"

"github.com/gin-gonic/gin"
"github.com/stretchr/testify/assert"
)

func setupTestRouter() *gin.Engine {
gin.SetMode(gin.TestMode)
r := gin.New()
return r
}

// closeNotifierRecorder wraps httptest.ResponseRecorder to implement http.CloseNotifier,
// which gin's reverse proxy requires.
type closeNotifierRecorder struct {
*httptest.ResponseRecorder
closeCh chan bool
}

func newCloseNotifierRecorder() *closeNotifierRecorder {
return &closeNotifierRecorder{
ResponseRecorder: httptest.NewRecorder(),
closeCh: make(chan bool, 1),
}
}

func (c *closeNotifierRecorder) CloseNotify() <-chan bool {
return c.closeCh
}

// TestIntegrationMiniMaxChatCompletions verifies that a chat/completions request
// for a MiniMax model is proxied to the MiniMax endpoint with correct auth.
func TestIntegrationMiniMaxChatCompletions(t *testing.T) {
// Create a mock MiniMax API server
mockMiniMax := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Verify the request is properly formatted
assert.Equal(t, "/v1/chat/completions", r.URL.Path)
assert.Equal(t, "Bearer test-minimax-key", r.Header.Get("Authorization"))
assert.Empty(t, r.Header.Get("api-key"), "api-key header should not be present for MiniMax")
assert.Empty(t, r.URL.Query().Get("api-version"), "api-version should not be present for MiniMax")

// Return a mock response
resp := map[string]interface{}{
"id": "chatcmpl-mock",
"object": "chat.completion",
"model": "MiniMax-M2.7",
"choices": []map[string]interface{}{{"message": map[string]string{"role": "assistant", "content": "Hello from MiniMax!"}}},
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(resp)
}))
defer mockMiniMax.Close()

// Save original state
originalConfig := make(map[string]DeploymentConfig)
for k, v := range ModelDeploymentConfig {
originalConfig[k] = v
}
defer func() {
ModelDeploymentConfig = originalConfig
}()

// Configure MiniMax deployment pointing to mock server
mockURL, _ := url.Parse(mockMiniMax.URL)
ModelDeploymentConfig = map[string]DeploymentConfig{
"MiniMax-M2.7": {
ModelName: "MiniMax-M2.7",
Endpoint: mockMiniMax.URL,
EndpointUrl: mockURL,
ApiKey: "test-minimax-key",
ProviderType: "minimax",
},
}

// Set up the route
r := setupTestRouter()
stripPrefixConverter := NewStripPrefixConverter("/v1")
r.POST("/v1/chat/completions", ProxyWithConverter(stripPrefixConverter))

// Make the request using closeNotifierRecorder
body := `{"model":"MiniMax-M2.7","messages":[{"role":"user","content":"Hello"}],"temperature":0.7}`
req := httptest.NewRequest(http.MethodPost, "/v1/chat/completions", strings.NewReader(body))
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", "Bearer test-minimax-key")

w := newCloseNotifierRecorder()
r.ServeHTTP(w, req)

assert.Equal(t, http.StatusOK, w.Code)
var respBody map[string]interface{}
err := json.Unmarshal(w.Body.Bytes(), &respBody)
assert.NoError(t, err)
assert.Equal(t, "MiniMax-M2.7", respBody["model"])
}

// TestIntegrationMiniMaxModelList verifies that /v1/models returns MiniMax models.
func TestIntegrationMiniMaxModelList(t *testing.T) {
// Save original state
originalConfig := make(map[string]DeploymentConfig)
for k, v := range ModelDeploymentConfig {
originalConfig[k] = v
}
defer func() {
ModelDeploymentConfig = originalConfig
}()

// Configure only MiniMax models (no Azure)
endpointURL, _ := url.Parse("https://api.minimax.io/v1")
ModelDeploymentConfig = map[string]DeploymentConfig{
"MiniMax-M2.7": {
ModelName: "MiniMax-M2.7",
Endpoint: "https://api.minimax.io/v1",
EndpointUrl: endpointURL,
ApiKey: "test-key",
ProviderType: "minimax",
},
"MiniMax-M2.5-highspeed": {
ModelName: "MiniMax-M2.5-highspeed",
Endpoint: "https://api.minimax.io/v1",
EndpointUrl: endpointURL,
ApiKey: "test-key",
ProviderType: "minimax",
},
}

r := setupTestRouter()
r.GET("/v1/models", ModelProxy)

req := httptest.NewRequest(http.MethodGet, "/v1/models", nil)
w := httptest.NewRecorder()
r.ServeHTTP(w, req)

assert.Equal(t, http.StatusOK, w.Code)

var respBody DeploymentInfo
err := json.Unmarshal(w.Body.Bytes(), &respBody)
assert.NoError(t, err)
assert.Equal(t, "list", respBody.Object)
assert.Len(t, respBody.Data, 2)

// Verify MiniMax models are present
modelIDs := make(map[string]bool)
for _, model := range respBody.Data {
modelIDs[model["id"].(string)] = true
assert.Equal(t, "minimax", model["owned_by"])
}
assert.True(t, modelIDs["MiniMax-M2.7"])
assert.True(t, modelIDs["MiniMax-M2.5-highspeed"])
}

// TestIntegrationMiniMaxStreamingProxy verifies that streaming requests to MiniMax work.
func TestIntegrationMiniMaxStreamingProxy(t *testing.T) {
mockMiniMax := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
assert.Equal(t, "/v1/chat/completions", r.URL.Path)
assert.Equal(t, "Bearer test-key", r.Header.Get("Authorization"))

// Read request body to verify stream=true
bodyBytes, _ := io.ReadAll(r.Body)
assert.Contains(t, string(bodyBytes), `"stream":true`)

w.Header().Set("Content-Type", "text/event-stream")
w.WriteHeader(http.StatusOK)
w.Write([]byte("data: {\"choices\":[{\"delta\":{\"content\":\"Hi\"}}]}\n\n"))
w.Write([]byte("data: [DONE]\n\n"))
}))
defer mockMiniMax.Close()

originalConfig := make(map[string]DeploymentConfig)
for k, v := range ModelDeploymentConfig {
originalConfig[k] = v
}
defer func() {
ModelDeploymentConfig = originalConfig
}()

mockURL, _ := url.Parse(mockMiniMax.URL)
ModelDeploymentConfig = map[string]DeploymentConfig{
"MiniMax-M2.7": {
ModelName: "MiniMax-M2.7",
Endpoint: mockMiniMax.URL,
EndpointUrl: mockURL,
ApiKey: "test-key",
ProviderType: "minimax",
},
}

r := setupTestRouter()
r.POST("/v1/chat/completions", ProxyWithConverter(NewStripPrefixConverter("/v1")))

body := `{"model":"MiniMax-M2.7","messages":[{"role":"user","content":"Hello"}],"stream":true}`
req := httptest.NewRequest(http.MethodPost, "/v1/chat/completions", strings.NewReader(body))
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", "Bearer test-key")

w := newCloseNotifierRecorder()
r.ServeHTTP(w, req)

assert.Equal(t, http.StatusOK, w.Code)
assert.Contains(t, w.Body.String(), "data:")
}
Loading