Skip to content

V1.8.7 - #1370

Merged
mileszhang2016 merged 39 commits into
developfrom
v1.8.7
Sep 13, 2026
Merged

V1.8.7#1370
mileszhang2016 merged 39 commits into
developfrom
v1.8.7

Conversation

@mileszhang2016

Copy link
Copy Markdown
Member

No description provided.

- Remove cacheReadTokens truncation to promptTokens (#1343)
- Skip /count_tokens endpoint in tokenRequestFinishHandler (#1344)
- Add deducted guard to prevent duplicate deduction (#1345)

Also add SC11 integration tests and update R&D workflow skill.

Signed-off-by: zhangmiao <zhangmiao@yf-networks.com>
fix(mod_ai_token_auth): resolve three billing issues (#1343 #1344 #1345)
… output_cost_per_video billing

- Add ModeResponses and /v1/responses, /v1/video/generations path detection
- Add ImageInputTokens/VideoCount to TokenUsage and QuotaUsage
- Add input_cost_per_image_token and output_cost_per_video price keys
- Extend calcCostUnits with responses/video_generation branches
- Support image input token billing in calcChatCost and calcImageGenerationCost
- Skip /count_tokens endpoint and add deducted idempotency flag
- Add ai_image_input_tokens/ai_video_count access log fields
- Add SC05 integration tests TC11-TC14 and update design docs

Signed-off-by: Kimi Code <kimi-code@example.com>
Signed-off-by: zhangmiao <zhangmiao@yf-networks.com>
…che hit (#1343)

The previous fix removed the cacheReadTokens > promptTokens truncation,
but the problem persisted: Anthropic input_tokens only counts fresh
(cache-missing) tokens and excludes cache read/write, so
normalInput = promptTokens - cacheReadTokens clamped to 0 and fresh
tokens were billed nothing. A 100% cache hit (input_tokens = 0) was
even misclassified as guess and dropped entirely.

- Normalize PromptTokens to total input (input_tokens + cache read +
  cache write) in all three Claude fallback parsers, matching OpenAI
  prompt_tokens semantics; UsedQuota derived from the normalized value
- calcChatCost: normalInput = max(promptTokens - cacheRead - cacheWrite, 0)
  so cache creation tokens are billed once at cache_creation price
- Update unit tests and SC11 TC01 integration test (812300 -> 956940),
  add parse-layer normalization tests
- Update rmb_quota.md, claude_protocol_support.md and the modification
  design doc

OpenAI/DeepSeek behavior is unchanged (cacheWrite is always 0).
…er layer

Organize protocol knowledge (auth injection, version headers, usage field
extraction, error normalization) by model_protocol instead of scattering
it across bfe_basic/reverseproxy/mod_ai_token_auth/mod_body_process.

- add bfe/bfe_model_protocol: ProtocolAdapter interface, compile-time
  registry (Get/Supports/ValidateProtocols), detect.go, openai and
  anthropic adapters, utils leaf package with UsageFields/ProtocolError
- reverseproxy: delegate auth/version headers to adapter, add
  ErrorNormalizer seam before fallback status-code whitelist (default
  no-op), clusterSupportsAuthStyle -> modelprotocol.Supports
- mod_ai_token_auth: SetApiKey/UpdateCtxByUsage delegate to adapter;
  accumulate semantics unchanged
- mod_body_process: SSEEvent/RawEvent.GetQuotaUsage converge the three
  duplicated usage field chains via extractUsageFields helper
- bfe_basic: GetApiKey/DetectAuthStyle delegate to detect.go
- validate ModelProtocols against known protocols at startup/reload

Zero behavior change; SC06 integration tests (TC-01~TC-08) all pass.

Docs: sys_design/model_protocol_adapter.md,
modifications/2026-09-03-model-protocol-adapter/design-changes.md
…mal input

Five integration cases (SC03 TC08/TC09, SC05 TC08, SC09 TC03/TC04) still
expected the pre-#1343 formula normal_input = prompt - cacheRead, which
double-bills cacheWrite tokens (charged both in normal input and as
cache_creation). Align test expectations and design docs with the current
calcChatCost formula: normal_input = prompt - cacheRead - cacheWrite,
matching rmb_quota.md. No production code changes.

Full integration suite SC01-SC11 passes.
…t abort (#1352)

Issue #1352: with EstimateToken=true, a client sending TCP RST right after
message_start was billed by full-request estimation (prompt estimate seeded
at auth time + content estimate), even though BFE had set ErrClientWrite
and never received the final usage. Root causes:

- tokenRequestFinishHandler only checked res.StatusCode == 200, which is
  meaningless for SSE (headers flushed before the stream); it never looked
  at req.ErrCode.
- calcCostUnits bills from the token fields directly, so the prompt
  estimate seeded by tokenFoundProductHandler flowed into the deduction
  regardless of the UsedQuota guard.

Changes:

- bfe_basic: add responseCompleted/finalUsageSeen state to AiBasicInfo
  (shared by mod_body_process and mod_ai_token_auth)
- mod_ai_token_auth: abort guard (ErrClientWrite/ErrClientClose/
  ErrClientReset without final usage -> no deduction); reset estimate-seeded
  token fields when estimation is not billable; EstimateToken estimation
  only applies to completed responses; tokenReadResponseHandler marks
  non-streaming responses completed/final-usage-seen
- mod_body_process: QuotaUsage gains IsFinalUsage/IsTermination flags;
  message_stop/[DONE]/final usage chunk detection; final usage from
  Anthropic message_delta is merged with prompt tokens parsed from
  message_start (previously dropped by the UsedQuota <= 0 gate)
- bfe_model_protocol: parse the real streaming shape message_start.
  message.usage (issue #1352 item 3)

Billing rule after fix: client abort without final usage -> free; abort
after final usage -> bill actual usage; normal completion unchanged.

Tests: unit tests in mod_ai_token_auth/mod_body_process/anthropic; new
integration scenario SC12 (client abort billing, 3 cases) with SSE-capable
mock backend and raw-TCP RST client. Full suite (go build/test/vet)
passes.

Docs: sys_design/rmb_quota.md 6.4/7.4/7.5, claude_protocol_support.md,
ai_access_log_fields.md, model_protocol_adapter.md, configuration
bfe.conf.md (EstimateToken semantics narrowed),
modifications/2026-09-04-client-abort-billing-fix/design-changes.md,
tests/integration/测试设计文档/scenario-SC12-客户端中断计费

Related-to: #1343
Signed-off-by: zhangmiao <zhangmiao@yf-networks.com>
fix(mod_ai_token_auth): skip full-request estimation billing on client abort (#1352)
…g Redis balance as exhausted

- token_rule_load: reject only Quota < 0 so a quota plan with Quota=0 loads
- QuotaPlan.HasBalance: treat a missing Redis balance key as zero balance and
  return QuotaExhausted instead of a 500
- redis_client: add IsKeyNotFound helper
- add unit tests and SC03 integration cases TC-13/TC-14
- update token_rule.data docs (en_us/zh_cn) and rmb_quota.md

Relates to #1348

Signed-off-by: zhangmiao <zhangmiao@yf-networks.com>
fix(mod_ai_token_auth): allow zero total_token quota and treat missin…
…with path

TLSConfReload only relocated server_cert_conf/tls_rule_conf into the
versioned dir given by ?path=, while client CA/CRL were still read from
the activated dir. A client CA that exists only in the version dir made
reload fail with ClientCALoad, so conf-agent never switched the tls_conf
symlink and hot reload deadlocked permanently:
rainway-ai-gateway/conf-agent#19

- relocate ClientCABaseDir/ClientCRLBaseDir into the version dir when
  they live under <confRoot>/tls_conf; custom absolute paths untouched
- tlsConfLoad takes the two base dirs as parameters; the startup call
  site passes the parsed config values (semantics unchanged)
- add unit tests and SC13 integration tests for the path redirection

Docs: sys_design/tls_conf_reload_path.md,
modifications/2026-09-05-tls-conf-reload-path-fix/

Signed-off-by: mileszhang2016 <zhangmiao@yf-networks.com>
fix(bfe_server): relocate client CA/CRL base dirs on tls_conf reload with path
The raw consumer API key was still persisted in pb_access3.log via three
fields that the 2026-08-19 field-701 rework did not cover:

- field 49 authorization: the raw Authorization header
- field 801 ai_route_rule_hits[].rule_owner: apikey-type rule owner is
  the raw key itself
- field 841 ai_auth_hit_quota_plans[]: plan id is composed of the raw key

Add a unified credential masking gate (credential_mask.go), invoked as
the last step of requestLogGen. It recursively scans the whole RequestLog
via protobuf reflection: authenticated requests get the raw key replaced
with the internal key_id; unauthenticated requests (including
brute-forged keys) get affected fields emptied. Field 49 is no longer
logged at all. Future fields are covered by the generic scan.

Regression: SC05 integration tests assert (for every TC) a serialized
byte-level scan against the raw key and a nil authorization; unit tests
cover authenticated/unauthenticated/no-AiInfo paths.

Fixes #1357

Signed-off-by: zhangmiao <zhangmiao@yf-networks.com>
fix(mod_access_pb3): mask raw API Key in access log (#1357)
…d SC14 integration tests

P0/P1 (data plane):
- inject llm-d.ai/inference-pool metadata (cluster name) into ext-proc
  RequestHeaders so EPP demux can route to the right cell
- ordered primary/backup EPPAddr consumption with gRPC health checking,
  failover/failback hysteresis (EPPCheck) and error-driven per-request retry
- idempotent EppClient.Close/CloseRespBody (double close panicked when the
  response body was closed in copyResponse and again by defer)
- gRPC client productionization: per-address persistent connections, TLS
  verification (EPPTLS), connect/call timeouts (EPPTimeout), cluster-level
  sliding-window circuit breaker (EPPBreaker)
- fix response body relay: bounded buffer, whole-stream abort on overflow
  instead of silently dropping chunks, EndOfStream always delivered
- observability: labeled prometheus metrics at /monitor/epp_metrics
  (epp_calls_total, failover/failback, local fallback, breaker transitions,
  active addr index) plus flat counters on the status page

Config (cluster_conf.data GslbBasic):
- new optional fields EPPCheck/EPPTimeout/EPPTLS/EPPBreaker with defaults and
  load-time validation; docs updated in zh_cn and en_us

Integration tests (tests/integration, scenario SC14):
- in-process mock EPP (ext-proc v3 + health, TLS) recording pool metadata,
  request/response headers and bodies; real llm-d-inference-sim backends
- 7 test cases: decision forwarding, SSE body relay, health-check failover,
  error-driven retry, unknown-pool/local fallback, EPP-down fallback,
  circuit breaker open
- config builder support for static (real-process) backends and EPP clusters

Docs: sys_design/epp_integration.md (system design), SC14 test design docs,
modifications/2026-09-06-epp-ai-gateway-integration design changes.
…ses (#1359)

Fixes #1359. With mod_header loaded and a global response rule hit, BFE
panicked on internally-constructed responses (e.g. mod_ai_route 404 "AI
route not found") whose request.HttpResponse was nil, tearing down the
connection (client saw EOF).

- 3.1 core fix: CreateSpecifiedContentResp now assigns resp to
  request.HttpResponse
- 3.2 centralized fallback: after both response_got labels in ServeHTTP and
  ServeHTTPForAI, backfill basicReq.HttpResponse from res when still nil,
  covering any other internal-response construction paths
- unit test: TestCreateSpecifiedContentRespAttachesHttpResponse
- SC01 regression: load mod_header with a global RSP_HEADER_SET rule, add
  TC11 asserting the internal 404 is returned normally (status, body) with
  the header rule applied; test design docs updated

Signed-off-by: zhangmiao <zhangmiao@yf-networks.com>
joinPath relocated reload target files by strings.Split(suffix, "/"),
so on Windows an absolute backslash config path (C:\...\server_cert_conf.data)
had no "/" and the whole path was appended to the version dir, producing an
invalid path and breaking TLSConfReload (and ServerDataConfReload/
GslbDataConfReload) with a "path" param on Windows. Use filepath.Base, which
accepts both separators on Windows and keeps the same last-segment semantics.

Fixes the two pre-existing Windows failures:
TestTLSConfReload_ClientCARelocatedWithPath and
TestTLSConfReload_CustomAbsoluteCABaseDirNotRelocated.

Signed-off-by: zhangmiao <zhangmiao@yf-networks.com>
… billing item

Model catalog prices may need 10-12 decimal places (e.g. 7.6234102728e-08
yuan/token). The previous load-time fixed-point conversion truncated such
prices to 1e-8 yuan integers, undercharging by ~8% per token.

- cluster_conf: drop pricesInt/tierPricesInt pre-conversion, the *_int
  key constants and the priceKeyToIntKey map; replace
  GetPriceInt(tier, key) int64 with GetPrice(tier, key) float64. Prices
  (including TierPrices) stay float64 and load-time validation now only
  checks non-negativity.
- cluster_conf: remove the custom PriceMap/TierPriceMap MarshalJSON that
  forced decimal notation; scientific notation (e.g. 1.5e-6) is now
  accepted in cluster_conf.data.
- mod_ai_token_auth: calcChatCost/calcImageGenerationCost/
  calcVideoGenerationCost convert each billing item via the new
  quota.CalcCostUnits (round(usage * (price * 1e8))) and sum integers,
  so sub-item split/fallback semantics are unchanged and Redis Lua still
  only sees integers.
- go.mod: bump github.com/bfenetworks/go-lib to v0.0.4 (adds
  quota.CalcCostUnits, rounds RmbToFixedPoint).
- tests: update GetPriceInt cases to GetPrice; add high-precision unit
  tests; add SC03 TestTC15 and SC09 TestTC07 integration tests covering
  scientific-notation prices end to end, including tier fallback.
- docs: update rmb_quota.md and add modifications design doc.

For prices with at most 8 decimal places the deduction amounts are
identical to the previous convert-at-load behavior.

Signed-off-by: zhangmiao <zhangmiao@yf-networks.com>
feat(mod_ai_token_auth): keep model prices as float64 and convert per billing item
…reaming responses

Issue #1364: when a Bearer (openai-style) request is answered with an
Anthropic body (type:"message", input_tokens/output_tokens), the final
usage was not recognized and the request-finish guard zeroed the billing
fields, leaving only cache_read to be deducted (chunked framing) or
nothing at all (Content-Length framing).

- guard in tokenRequestFinishHandler now clears all 10 billing fields
  (prompt/completion/total/quota/cache/audio/image/video) instead of
  UsedQuota alone
- UpdateCtxByUsage falls back to the composed cross-protocol parse chain
  (ParseUsageFieldsCrossProtocol) when the single auth-style adapter
  yields all-zero prompt/completion
- isFinalUsage accepts a top-level type:"message" marker; RawEvent marks
  IsFinalUsage/IsTermination for a complete non-SSE JSON body
- SC03 integration tests: TestTC16 (chunked) / TestTC17 (Content-Length),
  mock backend gains NoContentLength switch

Signed-off-by: zhangmiao <zhangmiao@yf-networks.com>
The llm-d EPP framework routes request parsers by the :path
pseudo-header; without it the request falls back to the legacy
/v1/completions default parser and chat-completions payloads fail to
parse, so EPP errors and BFE silently falls back to local WRR (200 but
no EPP scheduling). Verified with integration-test SC28: without this
change 6/7 cases fail, with it 7/7 pass.

Signed-off-by: zhangmiao <zhangmiao@yf-networks.com>
fix(mod_ai_token_auth): bill complete usage for cross-protocol non-streaming responses
Add hardcoded length-tier price keys input/output_cost_per_token_above_
{200k,256k,272k,512k}_tokens: ModelTableCheck parses them into a sorted
lengthTier table and calcChatCost picks the highest tier exceeded by total
input tokens (tier-first/fallback semantics aligned with GetPrice; models
without tier keys keep bitwise-identical billing).

Add cache_creation_input_token_cost_1h: the 1h-TTL portion of cache writes
(usage.cache_creation.ephemeral_1h_input_tokens, fallback
cache_creation_input_tokens_1h) is parsed in the protocol adapter layer,
carried through QuotaUsage/TokenUsage as CacheWriteTokens1h, billed at the
1h price with the remainder at the base 5m price.

Access log gains ai_cache_write_1h_tokens (field 788) via bfe-access-pb
v0.3.6. Includes unit tests, integration scenarios SC15 (billing) and
SC05 TC15 (log field), plus design docs.

Signed-off-by: zhangmiao <zhangmiao@yf-networks.com>
feat(billing): support length-tier pricing and 1h-TTL cache write cost
- add gemini adapter (x-goog-api-key auth, usageMetadata extraction,
  stream terminal/final-usage judgment) and registry/detect wiring
- sink hardcoded stream termination/final-usage judgment from
  mod_body_process into protocol adapters (StreamEvent abstraction);
  openai/anthropic behavior unchanged
- extend cross-protocol usage fallback chain to three stages
  (openai -> anthropic -> gemini)
- shouldTriggerFallback now resolves ErrorNormalizer by AuthStyle
- add SC16 integration scenario and design docs

Signed-off-by: zhangmiao <zhangmiao@yf-networks.com>
…docs

Signed-off-by: zhangmiao <zhangmiao@yf-networks.com>
feat(model_protocol): support gemini protocol
Signed-off-by: zhangmiao <zhangmiao@yf-networks.com>
chore(release): prepare v1.8.7 release
Signed-off-by: zhangmiao <zhangmiao@yf-networks.com>
ci(license): ignore SC16 testdata_routeonly fixture files
… in openai adapter

The gemini refactor (fac4e09) sank the hard-coded final-usage check into
per-protocol adapters, but the openai adapter (the fallback for Bearer
requests and unknown styles) kept only 'message'/'', dropping
'message_delta'. A cross-protocol stream (Bearer request answered with an
Anthropic body) then lost the final usage: the completion tokens in
message_delta were never merged, finalUsageSeen stayed false, and the
request was undercharged (complete stream) or billed for free (client
abort after final usage). SC12 TC02/TC03 integration tests caught this.

Restore the legacy combined rule in the openai adapter, mirroring
IsStreamTerminal which already keeps message_stop. Genuine OpenAI streams
are unaffected: their chunks carry type chat.completion.chunk or no type,
never message_delta.

Signed-off-by: zhangmiao <zhangmiao@yf-networks.com>
… restoration

Signed-off-by: zhangmiao <zhangmiao@yf-networks.com>
@mileszhang2016
mileszhang2016 merged commit 3447a82 into develop Sep 13, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant