fix(openai-gateway): address PR review — probe URL /v1 prefix, Create trigger, tests

Fix four issues flagged by copilot-pull-request-reviewer on PR #2143:

1. Probe URL missing /v1 prefix (openai_apikey_responses_probe.go)
   Replaced bare TrimSuffix + "/responses" with buildOpenAIResponsesURL(),
   which handles bare domain → /v1/responses correctly. Affected:
   - ProbeOpenAIAPIKeyResponsesSupport (probe URL)
   - TestAccount endpoint (apiURL for APIKey accounts)

2. Create endpoint not triggering probe (account_handler.go)
   Capture created account from idempotent closure and call
   scheduleOpenAIResponsesProbe after success, same pattern as
   BatchCreate and Update.

3. Tests (openai_gateway_chat_completions_raw_test.go)
   Added TestBuildOpenAIChatCompletionsURL (7 cases covering
   bare domain, /v1 suffix, trailing slash, third-party domains,
   whitespace) and TestBuildOpenAIResponsesURL_ProbeURL (6 cases
   locking the probe URL construction for bare-domain inputs).

All unit tests pass; go build ./cmd/server/ clean.
This commit is contained in:
alfadb
2026-04-30 21:46:46 +08:00
parent 4d145300c3
commit adf01ac880
4 changed files with 77 additions and 2 deletions

View File

@@ -528,6 +528,10 @@ func (h *AccountHandler) Create(c *gin.Context) {
// 确定是否跳过混合渠道检查
skipCheck := req.ConfirmMixedChannelRisk != nil && *req.ConfirmMixedChannelRisk
// 捕获闭包内创建的账号引用,用于创建成功后触发异步探测。
// 幂等重放时闭包不会执行 → createdAccount 为 nil → 不重复调度。
var createdAccount *service.Account
result, err := executeAdminIdempotent(c, "admin.accounts.create", req, service.DefaultWriteIdempotencyTTL(), func(ctx context.Context) (any, error) {
account, execErr := h.adminService.CreateAccount(ctx, &service.CreateAccountInput{
Name: req.Name,
@@ -549,6 +553,7 @@ func (h *AccountHandler) Create(c *gin.Context) {
if execErr != nil {
return nil, execErr
}
createdAccount = account
// Antigravity OAuth: 新账号直接设置隐私
h.adminService.ForceAntigravityPrivacy(ctx, account)
// OpenAI OAuth: 新账号直接设置隐私
@@ -577,6 +582,9 @@ func (h *AccountHandler) Create(c *gin.Context) {
if result != nil && result.Replayed {
c.Header("X-Idempotency-Replayed", "true")
}
// OpenAI APIKey 账号创建后异步探测上游 /v1/responses 能力。
// 探测失败不影响账号创建响应。
h.scheduleOpenAIResponsesProbe(createdAccount)
response.Success(c, result.Data)
}

View File

@@ -564,7 +564,7 @@ func (s *AccountTestService) testOpenAIAccountConnection(c *gin.Context, account
"账号本身可正常使用,但当前测试接口仅支持 Responses API 路径。请直接通过实际 API 调用验证。",
)
}
apiURL = strings.TrimSuffix(normalizedBaseURL, "/") + "/responses"
apiURL = buildOpenAIResponsesURL(normalizedBaseURL)
} else {
return s.sendErrorAndEnd(c, fmt.Sprintf("Unsupported account type: %s", account.Type))
}

View File

@@ -85,7 +85,7 @@ func (s *AccountTestService) ProbeOpenAIAPIKeyResponsesSupport(ctx context.Conte
return
}
probeURL := strings.TrimSuffix(normalizedBaseURL, "/") + "/responses"
probeURL := buildOpenAIResponsesURL(normalizedBaseURL)
probeCtx, cancel := context.WithTimeout(ctx, openaiResponsesProbeTimeout)
defer cancel()

View File

@@ -0,0 +1,67 @@
//go:build unit
package service
import (
"testing"
"github.com/stretchr/testify/require"
)
func TestBuildOpenAIChatCompletionsURL(t *testing.T) {
t.Parallel()
tests := []struct {
name string
base string
want string
}{
// 已是 /chat/completions原样返回
{"already chat/completions", "https://api.openai.com/v1/chat/completions", "https://api.openai.com/v1/chat/completions"},
// 以 /v1 结尾:追加 /chat/completions
{"bare /v1", "https://api.openai.com/v1", "https://api.openai.com/v1/chat/completions"},
// 其他情况:追加 /v1/chat/completions
{"bare domain", "https://api.openai.com", "https://api.openai.com/v1/chat/completions"},
{"domain with trailing slash", "https://api.openai.com/", "https://api.openai.com/v1/chat/completions"},
// 第三方上游常见形式
{"third-party bare domain", "https://api.deepseek.com", "https://api.deepseek.com/v1/chat/completions"},
{"third-party with path prefix", "https://api.gptgod.online/api", "https://api.gptgod.online/api/v1/chat/completions"},
// 带空白字符
{"whitespace trimmed", " https://api.openai.com/v1 ", "https://api.openai.com/v1/chat/completions"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
got := buildOpenAIChatCompletionsURL(tt.base)
require.Equal(t, tt.want, got)
})
}
}
// TestBuildOpenAIResponsesURL_ProbeURL 锁定 probe/测试端点使用的 URL 构建逻辑,
// 确保 buildOpenAIResponsesURL 对标准 OpenAI base_url 格式均拼出 `/v1/responses`。
func TestBuildOpenAIResponsesURL_ProbeURL(t *testing.T) {
t.Parallel()
tests := []struct {
name string
base string
want string
}{
{"bare domain", "https://api.openai.com", "https://api.openai.com/v1/responses"},
{"domain trailing slash", "https://api.openai.com/", "https://api.openai.com/v1/responses"},
{"bare /v1", "https://api.openai.com/v1", "https://api.openai.com/v1/responses"},
{"already /responses", "https://api.openai.com/v1/responses", "https://api.openai.com/v1/responses"},
{"third-party bare domain", "https://api.deepseek.com", "https://api.deepseek.com/v1/responses"},
{"only domain, no scheme", "api.gptgod.online", "api.gptgod.online/v1/responses"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
got := buildOpenAIResponsesURL(tt.base)
require.Equal(t, tt.want, got)
})
}
}