From 87a140bf67b83703a6b58e803b8ed2caf8e21e27 Mon Sep 17 00:00:00 2001 From: hgaol Date: Thu, 12 Feb 2026 11:28:55 +0800 Subject: [PATCH 1/6] fix: update AI provider configuration to include Azure OpenAI and adjust request handling --- internal/migrations/init_data.go | 2 +- internal/migrations/v31.go | 2 +- internal/schema/ai_config_schema.go | 20 +++++++++++-- internal/service/siteinfo/siteinfo_service.go | 29 +++++++++++++++++-- ui/src/pages/Admin/AiSettings/index.tsx | 3 ++ 5 files changed, 50 insertions(+), 6 deletions(-) diff --git a/internal/migrations/init_data.go b/internal/migrations/init_data.go index 5af41bbfc..43489cc10 100644 --- a/internal/migrations/init_data.go +++ b/internal/migrations/init_data.go @@ -353,7 +353,7 @@ var ( {ID: 128, Key: "rank.answer.undeleted", Value: `-1`}, {ID: 129, Key: "rank.question.undeleted", Value: `-1`}, {ID: 130, Key: "rank.tag.undeleted", Value: `-1`}, - {ID: 131, Key: "ai_config.provider", Value: `[{"default_api_host":"https://api.openai.com","display_name":"OpenAI","name":"openai"},{"default_api_host":"https://generativelanguage.googleapis.com","display_name":"Gemini","name":"gemini"},{"default_api_host":"https://api.anthropic.com","display_name":"Anthropic","name":"anthropic"}]`}, + {ID: 131, Key: "ai_config.provider", Value: `[{"default_api_host":"https://api.openai.com","display_name":"OpenAI","name":"openai"},{"default_api_host":"https://generativelanguage.googleapis.com","display_name":"Gemini","name":"gemini"},{"default_api_host":"https://api.anthropic.com","display_name":"Anthropic","name":"anthropic"},{"default_api_host":"https://{your-resource}.openai.azure.com","display_name":"Azure OpenAI","name":"azure_openai"}]`}, } defaultBadgeGroupTable = []*entity.BadgeGroup{ diff --git a/internal/migrations/v31.go b/internal/migrations/v31.go index 428c5828a..f83d57899 100644 --- a/internal/migrations/v31.go +++ b/internal/migrations/v31.go @@ -61,7 +61,7 @@ func addAPIKey(ctx context.Context, x *xorm.Engine) error { } defaultConfigTable := []*entity.Config{ - {ID: 131, Key: "ai_config.provider", Value: `[{"default_api_host":"https://api.openai.com","display_name":"OpenAI","name":"openai"},{"default_api_host":"https://generativelanguage.googleapis.com","display_name":"Gemini","name":"gemini"},{"default_api_host":"https://api.anthropic.com","display_name":"Anthropic","name":"anthropic"}]`}, + {ID: 131, Key: "ai_config.provider", Value: `[{"default_api_host":"https://api.openai.com","display_name":"OpenAI","name":"openai"},{"default_api_host":"https://generativelanguage.googleapis.com","display_name":"Gemini","name":"gemini"},{"default_api_host":"https://api.anthropic.com","display_name":"Anthropic","name":"anthropic"},{"default_api_host":"https://{your-resource}.openai.azure.com","display_name":"Azure OpenAI","name":"azure_openai"}]`}, } for _, c := range defaultConfigTable { exist, err := x.Context(ctx).Get(&entity.Config{Key: c.Key}) diff --git a/internal/schema/ai_config_schema.go b/internal/schema/ai_config_schema.go index 6ac686343..0d454a8d8 100644 --- a/internal/schema/ai_config_schema.go +++ b/internal/schema/ai_config_schema.go @@ -38,8 +38,9 @@ type GetAIModelsResp struct { } type GetAIModelsReq struct { - APIHost string `json:"api_host"` - APIKey string `json:"api_key"` + Provider string `json:"provider"` + APIHost string `json:"api_host"` + APIKey string `json:"api_key"` } // GetAIModelResp get AI model response @@ -49,3 +50,18 @@ type GetAIModelResp struct { Created int `json:"created"` OwnedBy string `json:"owned_by"` } + +// GetAzureDeploymentsResp get Azure OpenAI deployments response +type GetAzureDeploymentsResp struct { + Data []struct { + Id string `json:"id"` + Model string `json:"model"` + Owner string `json:"owner"` + Object string `json:"object"` + Status string `json:"status"` + CreatedAt int `json:"created_at"` + UpdatedAt int `json:"updated_at"` + } `json:"data"` +} + + diff --git a/internal/service/siteinfo/siteinfo_service.go b/internal/service/siteinfo/siteinfo_service.go index 1e25cbaa4..6214f9cc3 100644 --- a/internal/service/siteinfo/siteinfo_service.go +++ b/internal/service/siteinfo/siteinfo_service.go @@ -724,9 +724,20 @@ func (s *SiteInfoService) GetAIModels(ctx context.Context, req *schema.GetAIMode } r := resty.New() - r.SetHeader("Authorization", fmt.Sprintf("Bearer %s", req.APIKey)) r.SetHeader("Content-Type", "application/json") - respBody, err := r.R().Get(req.APIHost + "/v1/models") + + var respBody *resty.Response + apiHost := strings.TrimRight(req.APIHost, "/") + if req.Provider == "azure_openai" { + // Azure OpenAI uses api-key header and lists deployments + r.SetHeader("api-key", req.APIKey) + url := fmt.Sprintf("%s/openai/deployments?api-version=2022-12-01", apiHost) + respBody, err = r.R().Get(url) + } else { + // Standard OpenAI-compatible providers + r.SetHeader("Authorization", fmt.Sprintf("Bearer %s", req.APIKey)) + respBody, err = r.R().Get(apiHost + "/v1/models") + } if err != nil { log.Error(err) return resp, errors.BadRequest(fmt.Sprintf("failed to get AI models %s", err.Error())) @@ -736,6 +747,20 @@ func (s *SiteInfoService) GetAIModels(ctx context.Context, req *schema.GetAIMode return resp, errors.BadRequest(fmt.Sprintf("failed to get AI models, response: %s", respBody.String())) } + if req.Provider == "azure_openai" { + data := schema.GetAzureDeploymentsResp{} + _ = json.Unmarshal(respBody.Body(), &data) + for _, d := range data.Data { + resp = append(resp, &schema.GetAIModelResp{ + Id: d.Id, + Object: d.Object, + Created: d.CreatedAt, + OwnedBy: d.Model, + }) + } + return resp, nil + } + data := schema.GetAIModelsResp{} _ = json.Unmarshal(respBody.Body(), &data) diff --git a/ui/src/pages/Admin/AiSettings/index.tsx b/ui/src/pages/Admin/AiSettings/index.tsx index 2270aa5c5..55c3c28e2 100644 --- a/ui/src/pages/Admin/AiSettings/index.tsx +++ b/ui/src/pages/Admin/AiSettings/index.tsx @@ -84,6 +84,7 @@ const Index = () => { const checkAiConfigData = (data) => { const params = data || { + provider: formData.provider.value, api_host: formData.api_host.value || apiHostPlaceholder, api_key: formData.api_key.value, }; @@ -151,6 +152,7 @@ const Index = () => { const host = findHistoryProvider?.api_host || provider?.default_api_host; if (findHistoryProvider?.model) { checkAiConfigData({ + provider: value, api_host: host, api_key: findHistoryProvider.api_key, }); @@ -263,6 +265,7 @@ const Index = () => { ); const host = currentAiConfig.api_host || provider?.default_api_host; checkAiConfigData({ + provider: currentAiConfig.provider, api_host: host, api_key: currentAiConfig.api_key, }); From d91cddc6e9a712d30cc85d2a6348ba54d36fca66 Mon Sep 17 00:00:00 2001 From: hgaol Date: Thu, 12 Feb 2026 14:34:59 +0800 Subject: [PATCH 2/6] fix: update Azure AI Foundry configuration and deployment handling in AI models service --- internal/migrations/init_data.go | 2 +- internal/migrations/v31.go | 2 +- internal/schema/ai_config_schema.go | 18 ++++++++---------- internal/service/siteinfo/siteinfo_service.go | 16 +++++++++++----- 4 files changed, 21 insertions(+), 17 deletions(-) diff --git a/internal/migrations/init_data.go b/internal/migrations/init_data.go index 43489cc10..f3626f585 100644 --- a/internal/migrations/init_data.go +++ b/internal/migrations/init_data.go @@ -353,7 +353,7 @@ var ( {ID: 128, Key: "rank.answer.undeleted", Value: `-1`}, {ID: 129, Key: "rank.question.undeleted", Value: `-1`}, {ID: 130, Key: "rank.tag.undeleted", Value: `-1`}, - {ID: 131, Key: "ai_config.provider", Value: `[{"default_api_host":"https://api.openai.com","display_name":"OpenAI","name":"openai"},{"default_api_host":"https://generativelanguage.googleapis.com","display_name":"Gemini","name":"gemini"},{"default_api_host":"https://api.anthropic.com","display_name":"Anthropic","name":"anthropic"},{"default_api_host":"https://{your-resource}.openai.azure.com","display_name":"Azure OpenAI","name":"azure_openai"}]`}, + {ID: 131, Key: "ai_config.provider", Value: `[{"default_api_host":"https://api.openai.com","display_name":"OpenAI","name":"openai"},{"default_api_host":"https://generativelanguage.googleapis.com","display_name":"Gemini","name":"gemini"},{"default_api_host":"https://api.anthropic.com","display_name":"Anthropic","name":"anthropic"},{"default_api_host":"https://{your-project}.services.ai.azure.com","display_name":"Azure AI Foundry","name":"azure_openai"}]`}, } defaultBadgeGroupTable = []*entity.BadgeGroup{ diff --git a/internal/migrations/v31.go b/internal/migrations/v31.go index f83d57899..2ec47d5b5 100644 --- a/internal/migrations/v31.go +++ b/internal/migrations/v31.go @@ -61,7 +61,7 @@ func addAPIKey(ctx context.Context, x *xorm.Engine) error { } defaultConfigTable := []*entity.Config{ - {ID: 131, Key: "ai_config.provider", Value: `[{"default_api_host":"https://api.openai.com","display_name":"OpenAI","name":"openai"},{"default_api_host":"https://generativelanguage.googleapis.com","display_name":"Gemini","name":"gemini"},{"default_api_host":"https://api.anthropic.com","display_name":"Anthropic","name":"anthropic"},{"default_api_host":"https://{your-resource}.openai.azure.com","display_name":"Azure OpenAI","name":"azure_openai"}]`}, + {ID: 131, Key: "ai_config.provider", Value: `[{"default_api_host":"https://api.openai.com","display_name":"OpenAI","name":"openai"},{"default_api_host":"https://generativelanguage.googleapis.com","display_name":"Gemini","name":"gemini"},{"default_api_host":"https://api.anthropic.com","display_name":"Anthropic","name":"anthropic"},{"default_api_host":"https://{your-project}.services.ai.azure.com","display_name":"Azure AI Foundry","name":"azure_openai"}]`}, } for _, c := range defaultConfigTable { exist, err := x.Context(ctx).Get(&entity.Config{Key: c.Key}) diff --git a/internal/schema/ai_config_schema.go b/internal/schema/ai_config_schema.go index 0d454a8d8..8aba6d7da 100644 --- a/internal/schema/ai_config_schema.go +++ b/internal/schema/ai_config_schema.go @@ -51,17 +51,15 @@ type GetAIModelResp struct { OwnedBy string `json:"owned_by"` } -// GetAzureDeploymentsResp get Azure OpenAI deployments response +// GetAzureDeploymentsResp Azure OpenAI deployments response type GetAzureDeploymentsResp struct { Data []struct { - Id string `json:"id"` - Model string `json:"model"` - Owner string `json:"owner"` - Object string `json:"object"` - Status string `json:"status"` - CreatedAt int `json:"created_at"` - UpdatedAt int `json:"updated_at"` + Id string `json:"id"` + Model string `json:"model"` + Owner string `json:"owner"` + Object string `json:"object"` + Status string `json:"status"` + CreatedAt int `json:"created_at"` + UpdatedAt int `json:"updated_at"` } `json:"data"` } - - diff --git a/internal/service/siteinfo/siteinfo_service.go b/internal/service/siteinfo/siteinfo_service.go index 6214f9cc3..0385249fd 100644 --- a/internal/service/siteinfo/siteinfo_service.go +++ b/internal/service/siteinfo/siteinfo_service.go @@ -24,6 +24,7 @@ import ( "encoding/json" errpkg "errors" "fmt" + "net/url" "strings" "github.com/apache/answer/internal/base/constant" @@ -728,11 +729,16 @@ func (s *SiteInfoService) GetAIModels(ctx context.Context, req *schema.GetAIMode var respBody *resty.Response apiHost := strings.TrimRight(req.APIHost, "/") - if req.Provider == "azure_openai" { - // Azure OpenAI uses api-key header and lists deployments + if req.Provider == "azure_ai" { + // Azure AI: parse resource name from apiHost and list deployments via *.openai.azure.com r.SetHeader("api-key", req.APIKey) - url := fmt.Sprintf("%s/openai/deployments?api-version=2022-12-01", apiHost) - respBody, err = r.R().Get(url) + parsedURL, parseErr := url.Parse(apiHost) + if parseErr != nil || parsedURL.Host == "" { + return resp, errors.BadRequest("invalid api_host URL") + } + resourceName := strings.Split(parsedURL.Hostname(), ".")[0] + deploymentsURL := fmt.Sprintf("https://%s.openai.azure.com/openai/deployments?api-version=2022-12-01", resourceName) + respBody, err = r.R().Get(deploymentsURL) } else { // Standard OpenAI-compatible providers r.SetHeader("Authorization", fmt.Sprintf("Bearer %s", req.APIKey)) @@ -747,7 +753,7 @@ func (s *SiteInfoService) GetAIModels(ctx context.Context, req *schema.GetAIMode return resp, errors.BadRequest(fmt.Sprintf("failed to get AI models, response: %s", respBody.String())) } - if req.Provider == "azure_openai" { + if req.Provider == "azure_ai" { data := schema.GetAzureDeploymentsResp{} _ = json.Unmarshal(respBody.Body(), &data) for _, d := range data.Data { From 27ac47f251a56d6444d4cff82051a9917b3e4c20 Mon Sep 17 00:00:00 2001 From: hgaol Date: Thu, 12 Feb 2026 14:46:05 +0800 Subject: [PATCH 3/6] fix: refactor model input handling in AI settings form for improved validation and feedback --- ui/src/pages/Admin/AiSettings/index.tsx | 54 +++++++------------------ 1 file changed, 14 insertions(+), 40 deletions(-) diff --git a/ui/src/pages/Admin/AiSettings/index.tsx b/ui/src/pages/Admin/AiSettings/index.tsx index 55c3c28e2..04f48e21d 100644 --- a/ui/src/pages/Admin/AiSettings/index.tsx +++ b/ui/src/pages/Admin/AiSettings/index.tsx @@ -427,35 +427,12 @@ const Index = () => { -
- - {/* - handleValueChange({ - model: { - value: e.target.value, - errorMsg: '', - isInvalid: false, - }, - }) - }> - {modelsData?.map((model) => { - return ( - - ); - })} - */} - + {t('model.label')} + handleValueChange({ @@ -467,18 +444,15 @@ const Index = () => { }) } /> - - {modelsData?.map((model) => { - return ( - - ); - })} + + {modelsData?.map((model) => ( + - -
{formData.model.errorMsg}
-
+ + {formData.model.errorMsg} + + From 913b56cd08c5a3550c67fe7530cdab27af9847a1 Mon Sep 17 00:00:00 2001 From: hgaol Date: Thu, 12 Feb 2026 16:36:31 +0800 Subject: [PATCH 4/6] feat: implement Azure OpenAI client support and update provider configuration --- internal/controller/ai_controller.go | 38 +++++++++++++++++++ internal/migrations/init_data.go | 2 +- internal/migrations/v31.go | 2 +- internal/service/siteinfo/siteinfo_service.go | 10 +---- 4 files changed, 42 insertions(+), 10 deletions(-) diff --git a/internal/controller/ai_controller.go b/internal/controller/ai_controller.go index e020ed30e..80f0c55e1 100644 --- a/internal/controller/ai_controller.go +++ b/internal/controller/ai_controller.go @@ -292,6 +292,10 @@ func (c *AIController) createOpenAIClient() *openai.Client { aiProvider := aiConfig.GetProvider() + if aiProvider.Provider == "azure_ai" { + return c.createAzureAIClient(aiProvider) + } + config = openai.DefaultConfig(aiProvider.APIKey) config.BaseURL = aiProvider.APIHost if !strings.HasSuffix(config.BaseURL, "/v1") { @@ -300,6 +304,36 @@ func (c *AIController) createOpenAIClient() *openai.Client { return openai.NewClientWithConfig(config) } +// createAzureAIClient creates an OpenAI client configured for Azure AI. +// Uses the Azure OpenAI compatibility endpoint: https://{resource}.openai.azure.com/openai/v1 +func (c *AIController) createAzureAIClient(aiProvider *schema.SiteAIProvider) *openai.Client { + azureBaseURL := strings.TrimRight(aiProvider.APIHost, "/") + "/openai/v1" + + config := openai.DefaultConfig(aiProvider.APIKey) + config.BaseURL = azureBaseURL + config.HTTPClient = &http.Client{ + Transport: &azureAPIKeyTransport{ + apiKey: aiProvider.APIKey, + transport: http.DefaultTransport, + }, + } + return openai.NewClientWithConfig(config) +} + +// azureAPIKeyTransport is an http.RoundTripper that replaces the Authorization +// header with the Azure-style api-key header for Azure OpenAI requests. +type azureAPIKeyTransport struct { + apiKey string + transport http.RoundTripper +} + +func (t *azureAPIKeyTransport) RoundTrip(req *http.Request) (*http.Response, error) { + req = req.Clone(req.Context()) + req.Header.Del("Authorization") + req.Header.Set("api-key", t.apiKey) + return t.transport.RoundTrip(req) +} + // getPromptByLanguage func (c *AIController) getPromptByLanguage(language i18n.Language, question string) string { aiConfig, err := c.siteInfoService.GetSiteAI(context.Background()) @@ -497,6 +531,10 @@ func (c *AIController) processAIStream( break } + if len(response.Choices) == 0 { + continue + } + choice := response.Choices[0] if len(choice.Delta.ToolCalls) > 0 { diff --git a/internal/migrations/init_data.go b/internal/migrations/init_data.go index f3626f585..1fd9c5222 100644 --- a/internal/migrations/init_data.go +++ b/internal/migrations/init_data.go @@ -353,7 +353,7 @@ var ( {ID: 128, Key: "rank.answer.undeleted", Value: `-1`}, {ID: 129, Key: "rank.question.undeleted", Value: `-1`}, {ID: 130, Key: "rank.tag.undeleted", Value: `-1`}, - {ID: 131, Key: "ai_config.provider", Value: `[{"default_api_host":"https://api.openai.com","display_name":"OpenAI","name":"openai"},{"default_api_host":"https://generativelanguage.googleapis.com","display_name":"Gemini","name":"gemini"},{"default_api_host":"https://api.anthropic.com","display_name":"Anthropic","name":"anthropic"},{"default_api_host":"https://{your-project}.services.ai.azure.com","display_name":"Azure AI Foundry","name":"azure_openai"}]`}, + {ID: 131, Key: "ai_config.provider", Value: `[{"default_api_host":"https://api.openai.com","display_name":"OpenAI","name":"openai"},{"default_api_host":"https://generativelanguage.googleapis.com","display_name":"Gemini","name":"gemini"},{"default_api_host":"https://api.anthropic.com","display_name":"Anthropic","name":"anthropic"},{"default_api_host":"https://{your-resource}.openai.azure.com","display_name":"Azure AI","name":"azure_ai"}]`}, } defaultBadgeGroupTable = []*entity.BadgeGroup{ diff --git a/internal/migrations/v31.go b/internal/migrations/v31.go index 2ec47d5b5..d2be0c9f5 100644 --- a/internal/migrations/v31.go +++ b/internal/migrations/v31.go @@ -61,7 +61,7 @@ func addAPIKey(ctx context.Context, x *xorm.Engine) error { } defaultConfigTable := []*entity.Config{ - {ID: 131, Key: "ai_config.provider", Value: `[{"default_api_host":"https://api.openai.com","display_name":"OpenAI","name":"openai"},{"default_api_host":"https://generativelanguage.googleapis.com","display_name":"Gemini","name":"gemini"},{"default_api_host":"https://api.anthropic.com","display_name":"Anthropic","name":"anthropic"},{"default_api_host":"https://{your-project}.services.ai.azure.com","display_name":"Azure AI Foundry","name":"azure_openai"}]`}, + {ID: 131, Key: "ai_config.provider", Value: `[{"default_api_host":"https://api.openai.com","display_name":"OpenAI","name":"openai"},{"default_api_host":"https://generativelanguage.googleapis.com","display_name":"Gemini","name":"gemini"},{"default_api_host":"https://api.anthropic.com","display_name":"Anthropic","name":"anthropic"},{"default_api_host":"https://{your-resource}.openai.azure.com","display_name":"Azure AI","name":"azure_ai"}]`}, } for _, c := range defaultConfigTable { exist, err := x.Context(ctx).Get(&entity.Config{Key: c.Key}) diff --git a/internal/service/siteinfo/siteinfo_service.go b/internal/service/siteinfo/siteinfo_service.go index 0385249fd..e80e50c5e 100644 --- a/internal/service/siteinfo/siteinfo_service.go +++ b/internal/service/siteinfo/siteinfo_service.go @@ -24,7 +24,6 @@ import ( "encoding/json" errpkg "errors" "fmt" - "net/url" "strings" "github.com/apache/answer/internal/base/constant" @@ -730,14 +729,9 @@ func (s *SiteInfoService) GetAIModels(ctx context.Context, req *schema.GetAIMode var respBody *resty.Response apiHost := strings.TrimRight(req.APIHost, "/") if req.Provider == "azure_ai" { - // Azure AI: parse resource name from apiHost and list deployments via *.openai.azure.com + // Azure AI: list deployments via the Azure OpenAI endpoint r.SetHeader("api-key", req.APIKey) - parsedURL, parseErr := url.Parse(apiHost) - if parseErr != nil || parsedURL.Host == "" { - return resp, errors.BadRequest("invalid api_host URL") - } - resourceName := strings.Split(parsedURL.Hostname(), ".")[0] - deploymentsURL := fmt.Sprintf("https://%s.openai.azure.com/openai/deployments?api-version=2022-12-01", resourceName) + deploymentsURL := apiHost + "/openai/deployments?api-version=2022-12-01" respBody, err = r.R().Get(deploymentsURL) } else { // Standard OpenAI-compatible providers From 95291ec0caee4ed361e8a1bce92264901f1f69c3 Mon Sep 17 00:00:00 2001 From: hgaol Date: Thu, 12 Feb 2026 16:50:22 +0800 Subject: [PATCH 5/6] update --- ui/src/pages/Admin/AiSettings/index.tsx | 54 ++++++++++++++++++------- 1 file changed, 40 insertions(+), 14 deletions(-) diff --git a/ui/src/pages/Admin/AiSettings/index.tsx b/ui/src/pages/Admin/AiSettings/index.tsx index 04f48e21d..55c3c28e2 100644 --- a/ui/src/pages/Admin/AiSettings/index.tsx +++ b/ui/src/pages/Admin/AiSettings/index.tsx @@ -427,12 +427,35 @@ const Index = () => { - - {t('model.label')} - + + {/* + handleValueChange({ + model: { + value: e.target.value, + errorMsg: '', + isInvalid: false, + }, + }) + }> + {modelsData?.map((model) => { + return ( + + ); + })} + */} + handleValueChange({ @@ -444,15 +467,18 @@ const Index = () => { }) } /> - - {modelsData?.map((model) => ( - + +
{formData.model.errorMsg}
+ From 9916bfe7dd089ca313300b5a7e69fb9cf492bba3 Mon Sep 17 00:00:00 2001 From: hgaol Date: Thu, 12 Feb 2026 17:09:01 +0800 Subject: [PATCH 6/6] fix lint issue --- internal/controller/ai_controller.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/controller/ai_controller.go b/internal/controller/ai_controller.go index 80f0c55e1..41b0afa9a 100644 --- a/internal/controller/ai_controller.go +++ b/internal/controller/ai_controller.go @@ -330,7 +330,7 @@ type azureAPIKeyTransport struct { func (t *azureAPIKeyTransport) RoundTrip(req *http.Request) (*http.Response, error) { req = req.Clone(req.Context()) req.Header.Del("Authorization") - req.Header.Set("api-key", t.apiKey) + req.Header.Set("Api-Key", t.apiKey) return t.transport.RoundTrip(req) }