mirror of
https://github.com/Wei-Shaw/sub2api.git
synced 2026-10-07 13:08:03 +08:00
fix(openai): record actual upstream endpoint on errors
This commit is contained in:
@@ -312,6 +312,14 @@ func GetInboundEndpoint(c *gin.Context) string {
|
|||||||
// and the account platform. Handlers call this after scheduling an
|
// and the account platform. Handlers call this after scheduling an
|
||||||
// account, passing account.Platform.
|
// account, passing account.Platform.
|
||||||
func GetUpstreamEndpoint(c *gin.Context, platform string) string {
|
func GetUpstreamEndpoint(c *gin.Context, platform string) string {
|
||||||
|
// OpenAI 转发服务维护独立的运行时端点上下文,覆盖普通入站推导。
|
||||||
|
// 这对 force_chat_completions 的错误路径尤为重要:此时可能没有
|
||||||
|
// ForwardResult,不能把入站 /v1/responses 误报成上游端点。
|
||||||
|
if platform == service.PlatformOpenAI || platform == service.PlatformGrok || service.IsCNProvider(platform) {
|
||||||
|
if endpoint := service.GetActualOpenAIUpstreamEndpoint(c); endpoint != "" {
|
||||||
|
return endpoint
|
||||||
|
}
|
||||||
|
}
|
||||||
if c != nil {
|
if c != nil {
|
||||||
if value, ok := c.Get(ctxKeyActualUpstreamEndpoint); ok {
|
if value, ok := c.Get(ctxKeyActualUpstreamEndpoint); ok {
|
||||||
if endpoint, ok := value.(string); ok && endpoint != "" {
|
if endpoint, ok := value.(string); ok && endpoint != "" {
|
||||||
|
|||||||
@@ -184,6 +184,16 @@ func TestGetUpstreamEndpointPrefersRuntimeOverride(t *testing.T) {
|
|||||||
require.Equal(t, EndpointMessages, GetUpstreamEndpoint(c, service.PlatformAntigravity))
|
require.Equal(t, EndpointMessages, GetUpstreamEndpoint(c, service.PlatformAntigravity))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestGetUpstreamEndpointUsesOpenAIRuntimeOverride(t *testing.T) {
|
||||||
|
recorder := httptest.NewRecorder()
|
||||||
|
c, _ := gin.CreateTestContext(recorder)
|
||||||
|
c.Request = httptest.NewRequest(http.MethodPost, EndpointResponses, nil)
|
||||||
|
c.Set(ctxKeyInboundEndpoint, EndpointResponses)
|
||||||
|
|
||||||
|
service.SetActualOpenAIUpstreamEndpoint(c, EndpointChatCompletions)
|
||||||
|
require.Equal(t, EndpointChatCompletions, GetUpstreamEndpoint(c, service.PlatformOpenAI))
|
||||||
|
}
|
||||||
|
|
||||||
func TestResolveOpenAIUpstreamEndpointPrefersForwardResult(t *testing.T) {
|
func TestResolveOpenAIUpstreamEndpointPrefersForwardResult(t *testing.T) {
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
|
|||||||
@@ -186,6 +186,10 @@ func (s *OpenAIGatewayService) sendCCUpstreamRequest(
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("build upstream request: %w", err)
|
return nil, fmt.Errorf("build upstream request: %w", err)
|
||||||
}
|
}
|
||||||
|
// 记录本次实际选择的协议端点,供错误日志和用量日志在没有
|
||||||
|
// OpenAIForwardResult(例如 503/传输失败)时使用。每次发送都覆盖,
|
||||||
|
// 避免 Gin context 在账号 failover 尝试之间残留旧端点。
|
||||||
|
SetActualOpenAIUpstreamEndpoint(c, "/v1/chat/completions")
|
||||||
upstreamReq = upstreamReq.WithContext(WithHTTPUpstreamProfile(upstreamReq.Context(), HTTPUpstreamProfileOpenAI))
|
upstreamReq = upstreamReq.WithContext(WithHTTPUpstreamProfile(upstreamReq.Context(), HTTPUpstreamProfileOpenAI))
|
||||||
upstreamReq.Header.Set("Content-Type", "application/json")
|
upstreamReq.Header.Set("Content-Type", "application/json")
|
||||||
upstreamReq.Header.Set("Authorization", "Bearer "+bearerToken)
|
upstreamReq.Header.Set("Authorization", "Bearer "+bearerToken)
|
||||||
|
|||||||
@@ -60,6 +60,10 @@ func (s *OpenAIGatewayService) ForwardAsChatCompletions(
|
|||||||
defaultMappedModel string,
|
defaultMappedModel string,
|
||||||
) (*OpenAIForwardResult, error) {
|
) (*OpenAIForwardResult, error) {
|
||||||
beginUpstreamResponseModelObservation(c)
|
beginUpstreamResponseModelObservation(c)
|
||||||
|
ClearActualOpenAIUpstreamEndpoint(c)
|
||||||
|
if shouldForwardOpenAIResponsesViaRawChatCompletions(account) {
|
||||||
|
SetActualOpenAIUpstreamEndpoint(c, "/v1/chat/completions")
|
||||||
|
}
|
||||||
setCodexToolNameReverse(c, nil)
|
setCodexToolNameReverse(c, nil)
|
||||||
if _, err := s.prepareCodexAccountIdentitySource(ctx, c, account); err != nil {
|
if _, err := s.prepareCodexAccountIdentitySource(ctx, c, account); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|||||||
@@ -20,6 +20,10 @@ import (
|
|||||||
// Forward forwards request to OpenAI API
|
// Forward forwards request to OpenAI API
|
||||||
func (s *OpenAIGatewayService) Forward(ctx context.Context, c *gin.Context, account *Account, body []byte) (*OpenAIForwardResult, error) {
|
func (s *OpenAIGatewayService) Forward(ctx context.Context, c *gin.Context, account *Account, body []byte) (*OpenAIForwardResult, error) {
|
||||||
beginUpstreamResponseModelObservation(c)
|
beginUpstreamResponseModelObservation(c)
|
||||||
|
ClearActualOpenAIUpstreamEndpoint(c)
|
||||||
|
if shouldForwardOpenAIResponsesViaRawChatCompletions(account) {
|
||||||
|
SetActualOpenAIUpstreamEndpoint(c, "/v1/chat/completions")
|
||||||
|
}
|
||||||
clearGrokResponsesClientToolMapping(c)
|
clearGrokResponsesClientToolMapping(c)
|
||||||
clearOpenAIResponsesClientToolMapping(c)
|
clearOpenAIResponsesClientToolMapping(c)
|
||||||
clearOpenAIResponsesNamespaceNames(c)
|
clearOpenAIResponsesNamespaceNames(c)
|
||||||
|
|||||||
@@ -34,6 +34,10 @@ func (s *OpenAIGatewayService) ForwardAsAnthropic(
|
|||||||
defaultMappedModel string,
|
defaultMappedModel string,
|
||||||
) (*OpenAIForwardResult, error) {
|
) (*OpenAIForwardResult, error) {
|
||||||
beginUpstreamResponseModelObservation(c)
|
beginUpstreamResponseModelObservation(c)
|
||||||
|
ClearActualOpenAIUpstreamEndpoint(c)
|
||||||
|
if shouldForwardOpenAIResponsesViaRawChatCompletions(account) {
|
||||||
|
SetActualOpenAIUpstreamEndpoint(c, "/v1/chat/completions")
|
||||||
|
}
|
||||||
setCodexToolNameReverse(c, nil)
|
setCodexToolNameReverse(c, nil)
|
||||||
if _, err := s.prepareCodexAccountIdentitySource(ctx, c, account); err != nil {
|
if _, err := s.prepareCodexAccountIdentitySource(ctx, c, account); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|||||||
@@ -39,11 +39,13 @@ func TestForwardResponses_ForceChatCompletionsRoutesNonStreamingToChatCompletion
|
|||||||
cfg: rawChatCompletionsTestConfig(),
|
cfg: rawChatCompletionsTestConfig(),
|
||||||
httpUpstream: upstream,
|
httpUpstream: upstream,
|
||||||
}
|
}
|
||||||
|
SetActualOpenAIUpstreamEndpoint(c, "/v1/responses")
|
||||||
|
|
||||||
result, err := svc.Forward(context.Background(), c, forceChatResponsesFallbackAccount(), body)
|
result, err := svc.Forward(context.Background(), c, forceChatResponsesFallbackAccount(), body)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.NotNil(t, result)
|
require.NotNil(t, result)
|
||||||
require.Equal(t, "http://upstream.example/v1/chat/completions", upstream.lastReq.URL.String())
|
require.Equal(t, "http://upstream.example/v1/chat/completions", upstream.lastReq.URL.String())
|
||||||
|
require.Equal(t, "/v1/chat/completions", GetActualOpenAIUpstreamEndpoint(c))
|
||||||
require.Equal(t, HTTPUpstreamProfileOpenAI, HTTPUpstreamProfileFromContext(upstream.lastReq.Context()))
|
require.Equal(t, HTTPUpstreamProfileOpenAI, HTTPUpstreamProfileFromContext(upstream.lastReq.Context()))
|
||||||
require.Equal(t, "hello", gjson.GetBytes(upstream.lastBody, "messages.0.content").String())
|
require.Equal(t, "hello", gjson.GetBytes(upstream.lastBody, "messages.0.content").String())
|
||||||
require.False(t, gjson.GetBytes(upstream.lastBody, "input").Exists())
|
require.False(t, gjson.GetBytes(upstream.lastBody, "input").Exists())
|
||||||
|
|||||||
@@ -319,6 +319,16 @@ func SetActualOpenAIUpstreamEndpoint(c *gin.Context, endpoint string) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ClearActualOpenAIUpstreamEndpoint 清理当前转发尝试记录的端点。
|
||||||
|
// Handler 会在账号 failover 尝试间复用同一个 Gin context,因此每次尝试
|
||||||
|
// 都必须从无残留状态开始。
|
||||||
|
func ClearActualOpenAIUpstreamEndpoint(c *gin.Context) {
|
||||||
|
if c == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.Set(openAIUpstreamEndpointContextKey, "")
|
||||||
|
}
|
||||||
|
|
||||||
// GetActualOpenAIUpstreamEndpoint returns the endpoint recorded by the latest
|
// GetActualOpenAIUpstreamEndpoint returns the endpoint recorded by the latest
|
||||||
// forwarding attempt in this request.
|
// forwarding attempt in this request.
|
||||||
func GetActualOpenAIUpstreamEndpoint(c *gin.Context) string {
|
func GetActualOpenAIUpstreamEndpoint(c *gin.Context) string {
|
||||||
|
|||||||
Reference in New Issue
Block a user