fix(openai): separate outbound and observed tiers

This commit is contained in:
Ritel-T
2026-08-30 13:06:11 +09:00
parent b5827cfd54
commit 624e4eef64
4 changed files with 33 additions and 45 deletions
@@ -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)
}