Skip to content

按请求 fast/priority 意图计费可能与 OpenAI Responses actual service_tier 语义不一致 #183

@yanhao98

Description

@yanhao98

说明:这是一次 Agent 分析报告。报告基于公开源码、OpenAI 官方接口契约,以及一次受控请求复现实验整理;不包含任何密钥、账号凭据或私有请求正文。

摘要

当前 codex2api 对 fast / priority 请求的计费逻辑是:只要客户端请求了 fastpriority,即使上游最终响应里的 response.service_tierdefault,usage log 仍记录为 fast,并按 priority / fast 价格计费。

这与 OpenAI Responses API 对 service_tier 的说明可能不一致。OpenAI 官方契约说明:响应体里的 service_tier 表示实际用于服务本次请求的 processing mode,且可能不同于请求参数。

When the service_tier parameter is set, the response body will include the service_tier value based on the processing mode actually used to serve the request. This response value may be different from the value set in the parameter.

因此,如果计费口径应为“按实际获得的服务等级计费”,则应优先使用响应中的 actual service_tier,而不是请求中的 intended service_tier

复现现象

受控请求:

{
  "model": "gpt-5.5",
  "input": "Reply with exactly: ok",
  "stream": true,
  "service_tier": "priority",
  "max_output_tokens": 16,
  "reasoning": { "effort": "minimal" }
}

raw SSE 终态返回:

{
  "type": "response.completed",
  "response": {
    "service_tier": "default",
    "usage": {
      "input_tokens": 108,
      "output_tokens": 17,
      "total_tokens": 125
    }
  }
}

但 codex2api usage log 中记录为:

service_tier = fast
input_tokens = 108
cached_tokens = 0
output_tokens = 17
reasoning_tokens = 10
input_price_per_mtoken = 12.5
output_price_per_mtoken = 75
cache_read_price_per_mtoken = 1.25
account_billed = 0.002625
user_billed = 0.002625

如果按 actual default 计费,金额应为:

108 * 5 / 1_000_000 + 17 * 30 / 1_000_000 = 0.00105

实际按 priority / fast 价格计为:

108 * 12.5 / 1_000_000 + 17 * 75 / 1_000_000 = 0.002625

源码路径

1. 请求 tier 会被提取为 requested tier

proxy/handler.go 会从请求体提取 service_tier

serviceTier := extractServiceTier(rawBody)

2. 上游响应的 actual tier 会被读取

streaming /v1/responses 成功路径会读取终态 SSE:

if eventType == "response.completed" {
    usage = extractUsageFromResult(parsed.Get("response.usage"))
    if tier := parsed.Get("response.service_tier").String(); tier != "" {
        actualServiceTier = tier
    }
    gotTerminal = true
}

并且 SSE payload 是原样向下游转发的,因此 downstream 看到的 response.service_tier = default 不是 codex2api 伪造的,而是上游实际返回值。

3. usage log tier 被 request intent 覆盖

proxy/translator.go 中:

func resolveServiceTier(actualTier, requestedTier string) string {
    requestedTier = strings.TrimSpace(requestedTier)
    if requestedTier == "fast" || requestedTier == "priority" {
        return "fast"
    }
    ...
}

因此:

resolveServiceTier("default", "priority") == "fast"

4. billing tier 也被 request intent 覆盖

当前 main 中:

func resolveBillingServiceTier(actualTier, requestedTier string) string {
    requestedTier = strings.ToLower(strings.TrimSpace(requestedTier))
    if requestedTier == "priority" || requestedTier == "fast" {
        return "priority"
    }

    actualTier = strings.ToLower(strings.TrimSpace(actualTier))
    if actualTier == "priority" || actualTier == "fast" {
        return "priority"
    }
    if actualTier != "" {
        return actualTier
    }
    return requestedTier
}

因此:

resolveBillingServiceTier("default", "priority") == "priority"

5. InsertUsageLog 使用 BillingServiceTier 计算金额

database/postgres.go 中:

billingServiceTier := log.BillingServiceTier
if billingServiceTier == "" {
    billingServiceTier = log.ServiceTier
}

accountBilled := calculateCost(
    log.InputTokens,
    log.OutputTokens,
    log.CachedTokens,
    billingModel,
    billingServiceTier,
)

这会导致持久化的 service_tier = fastaccount_billed = priority-priced amount,但无法保留 upstream actual tier。

历史背景

PR #153 Fix/fast pricing 曾经引入过“actual default downgrade wins”的测试逻辑:

actual default downgrade wins: actual=default, requested=fast, want=default

但后续 commit eb4e1cd451f317485d7a2b54bac53e53fed13bab 明确改成:

fix(billing): bill fast/priority by client intent, not upstream-reported tier

提交说明中写道:当 Codex backend 把 fast request 降级为 service_tier="default" 时,仍应按 priority 计费,以避免 fast 用户“漏费”。

这个行为也许是当前项目的产品策略,但它和 OpenAI Responses API 的 response.service_tier 语义不同:OpenAI 响应字段代表 actual processing mode,而不是 request intent。

期望行为

如果项目希望“按实际服务等级计费”,建议:

  1. billingServiceTier 优先使用 actual response tier:
resolveBillingServiceTier("default", "priority") == "default"
  1. 日志中拆分三个概念,避免 UI / 对账歧义:
requested_service_tier = priority
actual_service_tier = default
billing_service_tier = default 或 priority(取决于项目策略)
  1. 如果项目坚持“按请求 fast/priority 意图计费”,建议在 UI / API 字段上明确标注:
service_tier = requested/billing intent, not actual response tier

否则用户会误以为 usage log 中的 fast 是上游实际返回的 tier。

影响

在上游实际返回 default 时,当前逻辑仍可能按 priority / fast 价格计费。对于需要按 OpenAI actual response tier 对账的场景,会出现:

响应 actual tier: default
codex2api usage tier: fast
codex2api billing tier: priority

这会造成用户侧与网关 / 上游 actual tier 的成本核算不一致。

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions