diff --git a/README.md b/README.md index 2864bcd..d2f5113 100644 --- a/README.md +++ b/README.md @@ -7,9 +7,10 @@ A lightweight LLM proxy gateway written in Go that provides a unified API interf ## Purpose Simplify LLM integration by exposing a single, consistent API that routes requests to different providers: -- **Google Generative AI** (Gemini) -- **Anthropic** (Claude) - **OpenAI** (GPT models) +- **Azure OpenAI** (Azure-deployed models) +- **Anthropic** (Claude) +- **Google Generative AI** (Gemini) Instead of managing multiple SDK integrations in your application, call one endpoint and let the gateway handle provider-specific implementations. @@ -20,9 +21,10 @@ Client Request ↓ Go LLM Gateway (unified API) ↓ -├─→ Google Gen AI SDK +├─→ OpenAI SDK +├─→ Azure OpenAI (OpenAI SDK + Azure auth) ├─→ Anthropic SDK -└─→ OpenAI SDK +└─→ Google Gen AI SDK ``` ## Key Features @@ -43,13 +45,14 @@ Go LLM Gateway (unified API) ## 🎉 Status: **WORKING!** -✅ **All three providers integrated with official Go SDKs:** +✅ **All four providers integrated with official Go SDKs:** - OpenAI → `github.com/openai/openai-go` +- Azure OpenAI → `github.com/openai/openai-go` (with Azure auth) - Anthropic → `github.com/anthropics/anthropic-sdk-go` - Google → `google.golang.org/genai` ✅ **Compiles successfully** (36MB binary) -✅ **Provider auto-selection** (gpt→OpenAI, claude→Anthropic, gemini→Google) +✅ **Provider auto-selection** (gpt→Azure/OpenAI, claude→Anthropic, gemini→Google) ✅ **Configuration system** (YAML with env var support) ✅ **Streaming support** (Server-Sent Events for all providers) ✅ **OAuth2/OIDC authentication** (Google, Auth0, any OIDC provider) @@ -202,6 +205,28 @@ The gateway implements conversation tracking using `previous_response_id` from t See **[CONVERSATIONS.md](./CONVERSATIONS.md)** for details. +## Azure OpenAI + +The gateway supports Azure OpenAI with the same interface as standard OpenAI: + +```yaml +providers: + azureopenai: + api_key: "${AZURE_OPENAI_API_KEY}" + endpoint: "https://your-resource.openai.azure.com" + deployment_id: "your-deployment-name" +``` + +```bash +export AZURE_OPENAI_API_KEY="..." +export AZURE_OPENAI_ENDPOINT="https://your-resource.openai.azure.com" +export AZURE_OPENAI_DEPLOYMENT_ID="gpt-4o" + +./gateway +``` + +The gateway prefers Azure OpenAI for `gpt-*` models if configured. See **[AZURE_OPENAI.md](./AZURE_OPENAI.md)** for complete setup guide. + ## Authentication The gateway supports OAuth2/OIDC authentication. See **[AUTH.md](./AUTH.md)** for setup instructions. diff --git a/go.mod b/go.mod index 160dbd4..4f8ed23 100644 --- a/go.mod +++ b/go.mod @@ -14,22 +14,26 @@ require ( cloud.google.com/go v0.116.0 // indirect cloud.google.com/go/auth v0.9.3 // indirect cloud.google.com/go/compute/metadata v0.5.0 // indirect + github.com/Azure/azure-sdk-for-go/sdk/ai/azopenai v0.9.0 // indirect + github.com/Azure/azure-sdk-for-go/sdk/azcore v1.21.0 // indirect + github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.2 // indirect github.com/golang-jwt/jwt/v5 v5.3.1 // indirect github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect github.com/google/go-cmp v0.6.0 // indirect github.com/google/s2a-go v0.1.8 // indirect github.com/googleapis/enterprise-certificate-proxy v0.3.4 // indirect github.com/gorilla/websocket v1.5.3 // indirect + github.com/openai/openai-go/v3 v3.2.0 // indirect github.com/tidwall/gjson v1.18.0 // indirect github.com/tidwall/match v1.1.1 // indirect github.com/tidwall/pretty v1.2.1 // indirect github.com/tidwall/sjson v1.2.5 // indirect go.opencensus.io v0.24.0 // indirect - golang.org/x/crypto v0.40.0 // indirect - golang.org/x/net v0.41.0 // indirect - golang.org/x/sync v0.16.0 // indirect - golang.org/x/sys v0.34.0 // indirect - golang.org/x/text v0.27.0 // indirect + golang.org/x/crypto v0.47.0 // indirect + golang.org/x/net v0.49.0 // indirect + golang.org/x/sync v0.19.0 // indirect + golang.org/x/sys v0.40.0 // indirect + golang.org/x/text v0.33.0 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1 // indirect google.golang.org/grpc v1.66.2 // indirect google.golang.org/protobuf v1.34.2 // indirect diff --git a/go.sum b/go.sum index 18f27ce..be24977 100644 --- a/go.sum +++ b/go.sum @@ -5,6 +5,12 @@ cloud.google.com/go/auth v0.9.3 h1:VOEUIAADkkLtyfr3BLa3R8Ed/j6w1jTBmARx+wb5w5U= cloud.google.com/go/auth v0.9.3/go.mod h1:7z6VY+7h3KUdRov5F1i8NDP5ZzWKYmEPO842BgCsmTk= cloud.google.com/go/compute/metadata v0.5.0 h1:Zr0eK8JbFv6+Wi4ilXAR8FJ3wyNdpxHKJNPos6LTZOY= cloud.google.com/go/compute/metadata v0.5.0/go.mod h1:aHnloV2TPI38yx4s9+wAZhHykWvVCfu7hQbF+9CWoiY= +github.com/Azure/azure-sdk-for-go/sdk/ai/azopenai v0.9.0 h1:t/DLMixbb8ygU11RAHJ8quXwJD7FwlC7+u6XodmSi1w= +github.com/Azure/azure-sdk-for-go/sdk/ai/azopenai v0.9.0/go.mod h1:Bb4vy1c7tXIqFrypNxCO7I5xlDSbpQiOWu/XvF5htP8= +github.com/Azure/azure-sdk-for-go/sdk/azcore v1.21.0 h1:fou+2+WFTib47nS+nz/ozhEBnvU96bKHy6LjRsY4E28= +github.com/Azure/azure-sdk-for-go/sdk/azcore v1.21.0/go.mod h1:t76Ruy8AHvUAC8GfMWJMa0ElSbuIcO03NLpynfbgsPA= +github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.2 h1:9iefClla7iYpfYWdzPCRDozdmndjTm8DXdpCzPajMgA= +github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.2/go.mod h1:XtLgD3ZD34DAaVIIAyG3objl5DynM3CQ/vMcbBNJZGI= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/anthropics/anthropic-sdk-go v1.26.0 h1:oUTzFaUpAevfuELAP1sjL6CQJ9HHAfT7CoSYSac11PY= github.com/anthropics/anthropic-sdk-go v1.26.0/go.mod h1:qUKmaW+uuPB64iy1l+4kOSvaLqPXnHTTBKH6RVZ7q5Q= @@ -55,6 +61,8 @@ github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aN github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/openai/openai-go v1.12.0 h1:NBQCnXzqOTv5wsgNC36PrFEiskGfO5wccfCWDo9S1U0= github.com/openai/openai-go v1.12.0/go.mod h1:g461MYGXEXBVdV5SaR/5tNzNbSfwTBBefwc+LlDCK0Y= +github.com/openai/openai-go/v3 v3.2.0 h1:2AbqFUCsoW2pm/2pUtPRuwK89dnoGHaQokzWsfoQO/U= +github.com/openai/openai-go/v3 v3.2.0/go.mod h1:UOpNxkqC9OdNXNUfpNByKOtB4jAL0EssQXq5p8gO0Xs= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= @@ -66,6 +74,7 @@ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY= github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= @@ -82,6 +91,8 @@ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACk golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.40.0 h1:r4x+VvoG5Fm+eJcxMaY8CQM7Lb0l1lsmjGBQ6s8BfKM= golang.org/x/crypto v0.40.0/go.mod h1:Qr1vMER5WyS2dfPHAlsOj01wgLbsyWtFn/aY+5+ZdxY= +golang.org/x/crypto v0.47.0 h1:V6e3FRj+n4dbpw86FJ8Fv7XVOql7TEwpHapKoMJ/GO8= +golang.org/x/crypto v0.47.0/go.mod h1:ff3Y9VzzKbwSSEzWqJsJVBnWmRwRSHt/6Op5n9bQc4A= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= @@ -94,22 +105,30 @@ golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw= golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA= +golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o= +golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw= golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= +golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.34.0 h1:H5Y5sJ2L2JRdyv7ROF1he/lPdvFsd0mJHFw2ThKHxLA= golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ= +golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.27.0 h1:4fGWRpyh641NLlecmyl4LOe6yDdfaYNrGb2zdfo4JV4= golang.org/x/text v0.27.0/go.mod h1:1D28KMCvyooCX9hBiosv5Tz/+YLxj0j7XhWjpSUF7CU= +golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE= +golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= diff --git a/internal/config/config.go b/internal/config/config.go index c6c6ef0..eafdc44 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -28,9 +28,10 @@ type ServerConfig struct { // ProvidersConfig wraps supported provider settings. type ProvidersConfig struct { - Google ProviderConfig `yaml:"google"` - Anthropic ProviderConfig `yaml:"anthropic"` - OpenAI ProviderConfig `yaml:"openai"` + Google ProviderConfig `yaml:"google"` + Anthropic ProviderConfig `yaml:"anthropic"` + OpenAI ProviderConfig `yaml:"openai"` + AzureOpenAI AzureOpenAIConfig `yaml:"azureopenai"` } // ProviderConfig contains shared provider configuration fields. @@ -40,6 +41,14 @@ type ProviderConfig struct { Endpoint string `yaml:"endpoint"` } +// AzureOpenAIConfig contains Azure-specific settings. +type AzureOpenAIConfig struct { + APIKey string `yaml:"api_key"` + Endpoint string `yaml:"endpoint"` + DeploymentID string `yaml:"deployment_id"` + APIVersion string `yaml:"api_version"` +} + // Load reads and parses a YAML configuration file and applies env overrides. func Load(path string) (*Config, error) { data, err := os.ReadFile(path) @@ -60,6 +69,20 @@ func (cfg *Config) applyEnvOverrides() { overrideAPIKey(&cfg.Providers.Google, "GOOGLE_API_KEY") overrideAPIKey(&cfg.Providers.Anthropic, "ANTHROPIC_API_KEY") overrideAPIKey(&cfg.Providers.OpenAI, "OPENAI_API_KEY") + + // Azure OpenAI overrides + if v := os.Getenv("AZURE_OPENAI_API_KEY"); v != "" { + cfg.Providers.AzureOpenAI.APIKey = v + } + if v := os.Getenv("AZURE_OPENAI_ENDPOINT"); v != "" { + cfg.Providers.AzureOpenAI.Endpoint = v + } + if v := os.Getenv("AZURE_OPENAI_DEPLOYMENT_ID"); v != "" { + cfg.Providers.AzureOpenAI.DeploymentID = v + } + if v := os.Getenv("AZURE_OPENAI_API_VERSION"); v != "" { + cfg.Providers.AzureOpenAI.APIVersion = v + } } func overrideAPIKey(cfg *ProviderConfig, envKey string) { diff --git a/internal/providers/openai/openai.go b/internal/providers/openai/openai.go index 42018b6..ac55a01 100644 --- a/internal/providers/openai/openai.go +++ b/internal/providers/openai/openai.go @@ -6,6 +6,7 @@ import ( "time" "github.com/openai/openai-go" + "github.com/openai/openai-go/azure" "github.com/openai/openai-go/option" "github.com/yourusername/go-llm-gateway/internal/api" @@ -15,12 +16,14 @@ import ( const Name = "openai" // Provider implements the OpenAI SDK integration. +// It supports both direct OpenAI API and Azure-hosted endpoints. type Provider struct { cfg config.ProviderConfig client *openai.Client + azure bool } -// New constructs a Provider from configuration. +// New constructs a Provider for the direct OpenAI API. func New(cfg config.ProviderConfig) *Provider { var client *openai.Client if cfg.APIKey != "" { @@ -33,6 +36,32 @@ func New(cfg config.ProviderConfig) *Provider { } } +// NewAzure constructs a Provider targeting Azure OpenAI. +// Azure OpenAI uses the OpenAI SDK with the azure subpackage for proper +// endpoint routing, api-version query parameter, and API key header. +func NewAzure(azureCfg config.AzureOpenAIConfig) *Provider { + var client *openai.Client + if azureCfg.APIKey != "" && azureCfg.Endpoint != "" { + apiVersion := azureCfg.APIVersion + if apiVersion == "" { + apiVersion = "2024-12-01-preview" + } + c := openai.NewClient( + azure.WithEndpoint(azureCfg.Endpoint, apiVersion), + azure.WithAPIKey(azureCfg.APIKey), + ) + client = &c + } + return &Provider{ + cfg: config.ProviderConfig{ + APIKey: azureCfg.APIKey, + Model: azureCfg.DeploymentID, + }, + client: client, + azure: true, + } +} + // Name returns the provider identifier. func (p *Provider) Name() string { return Name } diff --git a/internal/providers/providers.go b/internal/providers/providers.go index 6b9f607..f8a6f29 100644 --- a/internal/providers/providers.go +++ b/internal/providers/providers.go @@ -34,7 +34,9 @@ func NewRegistry(cfg config.ProvidersConfig) (*Registry, error) { if cfg.Anthropic.APIKey != "" { reg.providers[anthropicprovider.Name] = anthropicprovider.New(cfg.Anthropic) } - if cfg.OpenAI.APIKey != "" { + if cfg.AzureOpenAI.APIKey != "" && cfg.AzureOpenAI.Endpoint != "" { + reg.providers[openaiprovider.Name] = openaiprovider.NewAzure(cfg.AzureOpenAI) + } else if cfg.OpenAI.APIKey != "" { reg.providers[openaiprovider.Name] = openaiprovider.New(cfg.OpenAI) } @@ -55,7 +57,7 @@ func (r *Registry) Get(name string) (Provider, bool) { func (r *Registry) Default(model string) (Provider, error) { if model != "" { switch { - case strings.HasPrefix(model, "gpt") || strings.HasPrefix(model, "o1"): + case strings.HasPrefix(model, "gpt") || strings.HasPrefix(model, "o1") || strings.HasPrefix(model, "o3"): if p, ok := r.providers[openaiprovider.Name]; ok { return p, nil }