mirror of
https://github.com/Wei-Shaw/sub2api.git
synced 2026-10-14 12:02:27 +08:00
fix(openai): separate outbound and observed tiers
This commit is contained in:
@@ -549,10 +549,10 @@ func TestForwardStreaming_ServiceTierPropagatedToResult(t *testing.T) {
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// 上游回显优先:请求 fast 但上游真实返回 default → 计费按标准价
|
||||
// 转发阶段分别保留最终出站 tier 与上游回显 tier
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
func TestForward_ResponsesUpstreamEchoesDefault_OverridesRequestFast(t *testing.T) {
|
||||
func TestForward_ResponsesKeepsOutboundAndObservedServiceTiersSeparate(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
|
||||
rec := httptest.NewRecorder()
|
||||
@@ -591,14 +591,14 @@ func TestForward_ResponsesUpstreamEchoesDefault_OverridesRequestFast(t *testing.
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, result)
|
||||
require.NotNil(t, result.ServiceTier)
|
||||
require.Equal(t, "default", *result.ServiceTier,
|
||||
"upstream-echoed default must override the client-requested fast tier for billing")
|
||||
require.Equal(t, "priority", *result.ServiceTier)
|
||||
require.Equal(t, "default", result.UpstreamResponseServiceTier)
|
||||
// 非流式响应原样透传:客户端同样看到 default。
|
||||
require.Contains(t, rec.Body.String(), `"service_tier":"default"`)
|
||||
require.NotContains(t, rec.Body.String(), `"service_tier":"priority"`)
|
||||
}
|
||||
|
||||
func TestForwardStreaming_UpstreamEchoesDefault_OverridesRequestFast(t *testing.T) {
|
||||
func TestForwardStreaming_KeepsOutboundAndObservedServiceTiersSeparate(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
|
||||
rec := httptest.NewRecorder()
|
||||
@@ -638,13 +638,13 @@ func TestForwardStreaming_UpstreamEchoesDefault_OverridesRequestFast(t *testing.
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, result)
|
||||
require.NotNil(t, result.ServiceTier)
|
||||
require.Equal(t, "default", *result.ServiceTier,
|
||||
"terminal SSE event's upstream-echoed default must win for billing")
|
||||
require.Equal(t, "priority", *result.ServiceTier)
|
||||
require.Equal(t, "default", result.UpstreamResponseServiceTier)
|
||||
// 流式原样透传:客户端在终止事件里看到 default。
|
||||
require.Contains(t, rec.Body.String(), `"service_tier":"default"`)
|
||||
}
|
||||
|
||||
func TestForwardAsChatCompletions_UpstreamEchoesDefault_BillsStandard(t *testing.T) {
|
||||
func TestForwardAsChatCompletions_KeepsOutboundAndObservedServiceTiersSeparate(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
|
||||
rec := httptest.NewRecorder()
|
||||
@@ -685,8 +685,8 @@ func TestForwardAsChatCompletions_UpstreamEchoesDefault_BillsStandard(t *testing
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, result)
|
||||
require.NotNil(t, result.ServiceTier)
|
||||
require.Equal(t, "default", *result.ServiceTier,
|
||||
"CC bridge must bill on the upstream-echoed default, not the requested fast")
|
||||
require.Equal(t, "priority", *result.ServiceTier)
|
||||
require.Equal(t, "default", result.UpstreamResponseServiceTier)
|
||||
// 缓冲转回 Chat Completions:客户端响应里如实回显 default。
|
||||
require.Contains(t, rec.Body.String(), `"service_tier":"default"`)
|
||||
require.NotContains(t, rec.Body.String(), `"service_tier":"priority"`)
|
||||
@@ -782,7 +782,7 @@ func TestResolvedOpenAIUpstreamServiceTier(t *testing.T) {
|
||||
|
||||
priority := func() *string { v := "priority"; return &v }()
|
||||
|
||||
t.Run("upstream echo wins over outbound tier", func(t *testing.T) {
|
||||
t.Run("upstream echo stays separate from outbound tier", func(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
c, _ := gin.CreateTestContext(nil)
|
||||
observer := beginUpstreamResponseModelObservation(c)
|
||||
@@ -790,7 +790,8 @@ func TestResolvedOpenAIUpstreamServiceTier(t *testing.T) {
|
||||
|
||||
got := resolvedOpenAIUpstreamServiceTier(c, priority)
|
||||
require.NotNil(t, got)
|
||||
require.Equal(t, "default", *got)
|
||||
require.Equal(t, "priority", *got)
|
||||
require.Equal(t, "default", observedUpstreamResponseServiceTier(c))
|
||||
})
|
||||
|
||||
t.Run("no upstream echo falls back to outbound tier", func(t *testing.T) {
|
||||
@@ -803,15 +804,15 @@ func TestResolvedOpenAIUpstreamServiceTier(t *testing.T) {
|
||||
require.Equal(t, "priority", *got)
|
||||
})
|
||||
|
||||
t.Run("upstream alias fast normalizes to priority", func(t *testing.T) {
|
||||
t.Run("observed tier never promotes an untiered request", func(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
c, _ := gin.CreateTestContext(nil)
|
||||
observer := beginUpstreamResponseModelObservation(c)
|
||||
observer.ObserveOpenAI([]byte(`{"type":"response.completed","response":{"model":"gpt-5.5","service_tier":"fast"}}`), "response.completed")
|
||||
|
||||
got := resolvedOpenAIUpstreamServiceTier(c, nil)
|
||||
require.NotNil(t, got)
|
||||
require.Equal(t, "priority", *got)
|
||||
require.Nil(t, got)
|
||||
require.Equal(t, "priority", observedUpstreamResponseServiceTier(c))
|
||||
})
|
||||
|
||||
t.Run("no observer keeps outbound tier", func(t *testing.T) {
|
||||
@@ -824,13 +825,13 @@ func TestResolvedOpenAIUpstreamServiceTier(t *testing.T) {
|
||||
require.Nil(t, resolvedOpenAIUpstreamServiceTier(nil, nil))
|
||||
})
|
||||
|
||||
t.Run("local observer wins without gin context", func(t *testing.T) {
|
||||
t.Run("local observer stays separate from outbound tier", func(t *testing.T) {
|
||||
observer := &upstreamResponseModelObserver{}
|
||||
observer.ObserveOpenAI([]byte(`{"type":"response.completed","response":{"model":"gpt-5.5","service_tier":"default"}}`), "response.completed")
|
||||
|
||||
got := resolvedOpenAIUpstreamServiceTierFromObserver(observer, priority)
|
||||
require.NotNil(t, got)
|
||||
require.Equal(t, "default", *got)
|
||||
require.Equal(t, "priority", *got)
|
||||
})
|
||||
|
||||
t.Run("nil local observer falls back to outbound tier", func(t *testing.T) {
|
||||
|
||||
@@ -103,8 +103,8 @@ func (s *OpenAIGatewayService) forwardAsRawChatCompletions(
|
||||
return nil, policyErr
|
||||
}
|
||||
upstreamBody = updatedBody
|
||||
// 计费兜底 tier = 最终出站 body(policy filter/force 后)里的 tier;
|
||||
// 最终值由 resolvedOpenAIUpstreamServiceTier 决定(上游回显优先)。
|
||||
// Keep the final outbound tier separate from the observed response tier so
|
||||
// usage recording can apply the selected credential's response contract.
|
||||
serviceTier := extractOpenAIServiceTierFromBody(upstreamBody)
|
||||
if account.Platform == PlatformGrok {
|
||||
strippedBody, stripErr := stripRedundantGrokChatViewImageTool(upstreamBody)
|
||||
|
||||
@@ -253,8 +253,9 @@ type OpenAIForwardResult struct {
|
||||
// UpstreamEndpoint is the actual upstream API path used for this request.
|
||||
// It avoids guessing when one downstream protocol can use multiple upstream endpoints.
|
||||
UpstreamEndpoint string
|
||||
// ServiceTier 优先取上游实际响应回显的 tier;缺失时回退到最终出站 body 的
|
||||
// tier。nil 表示两者都无识别 tier。
|
||||
// ServiceTier is the final tier sent upstream after policy rewriting.
|
||||
// The upstream response declaration remains separate above and is reconciled
|
||||
// at usage-recording time, where the credential protocol is available.
|
||||
ServiceTier *string
|
||||
// ReasoningEffort is extracted from request body (reasoning.effort) or derived from model suffix
|
||||
// after group policy rewriting and model-family remapping.
|
||||
|
||||
@@ -22,10 +22,9 @@ const (
|
||||
// (see responseModelBillingDeclaration).
|
||||
//
|
||||
// The same observer also records the service tier the upstream reports having
|
||||
// used (OpenAI service_tier, Anthropic usage.speed). The billable tier is
|
||||
// resolved by resolvedOpenAIUpstreamServiceTierFromObserver (upstream echo
|
||||
// first, outbound body tier as fallback); the upstream ResolveBillingServiceTier
|
||||
// only-lowers path additionally audits downgrades at usage-record time.
|
||||
// used (OpenAI service_tier, Anthropic usage.speed). The observed tier stays
|
||||
// separate from the final outbound request tier until usage recording resolves
|
||||
// the billable tier for the selected credential protocol.
|
||||
type upstreamResponseModelObserver struct {
|
||||
first string
|
||||
terminal string
|
||||
@@ -218,29 +217,16 @@ func observedUpstreamResponseServiceTier(c *gin.Context) string {
|
||||
return upstreamResponseModelObserverFromContext(c).ServiceTier()
|
||||
}
|
||||
|
||||
// resolvedOpenAIUpstreamServiceTierFromObserver 返回计费/用量日志实际使用的
|
||||
// service tier:
|
||||
//
|
||||
// 1. observer 记录到的上游真实回显优先——只有上游实际给了 priority/fast 才按
|
||||
// Fast 计费;上游回显 default/flex/auto 等则如实采用并据此计费;
|
||||
// 2. 上游未回显时,回退到「最终出站 body」里的 tier(经过 fast policy
|
||||
// filter/force 之后),保证 policy filter 删掉字段后不再按原请求 Fast 计费。
|
||||
//
|
||||
// HTTP→WS 等使用局部 observer 的路径必须把该 observer 传进来,不能只读
|
||||
// Gin context——局部 observer 不会自动写入 context。
|
||||
func resolvedOpenAIUpstreamServiceTierFromObserver(observer *upstreamResponseModelObserver, outboundBodyTier *string) *string {
|
||||
if observer != nil {
|
||||
if tier := strings.TrimSpace(observer.ServiceTier()); tier != "" {
|
||||
return normalizeOpenAIServiceTier(tier)
|
||||
}
|
||||
}
|
||||
// resolvedOpenAIUpstreamServiceTierFromObserver preserves the final outbound
|
||||
// request tier. The observed response tier remains separate on
|
||||
// OpenAIForwardResult.UpstreamResponseServiceTier and is reconciled once, at
|
||||
// usage time, where the account protocol is available. In particular, the
|
||||
// private ChatGPT Codex backend commonly reports default even for effective
|
||||
// Fast turns, while public API response tiers remain authoritative.
|
||||
func resolvedOpenAIUpstreamServiceTierFromObserver(_ *upstreamResponseModelObserver, outboundBodyTier *string) *string {
|
||||
return outboundBodyTier
|
||||
}
|
||||
|
||||
// resolvedOpenAIUpstreamServiceTier 读取 Gin context 上的 observer 后委托
|
||||
// resolvedOpenAIUpstreamServiceTierFromObserver。标准 HTTP 转发路径通过
|
||||
// beginUpstreamResponseModelObservation 把 observer 挂到 context;局部
|
||||
// observer 路径应直接调用 FromObserver。
|
||||
func resolvedOpenAIUpstreamServiceTier(c *gin.Context, outboundBodyTier *string) *string {
|
||||
return resolvedOpenAIUpstreamServiceTierFromObserver(upstreamResponseModelObserverFromContext(c), outboundBodyTier)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user