fix(relay): follow-up billing integrity and conversion completions (#7170)

Deferred follow-ups from the relaykit-tools review cycle, verified by
live end-to-end billing tests:

- billing: normalize Gemini modality keys consistently between stream
  merge and settlement (case/whitespace variants no longer drop
  independent audio/image pricing) and sum duplicate modality entries
  on both paths
- billing: sync legacy flat Claude cache-creation fields from the
  CacheCreation sub-object (including zeroing) and fall back to flat
  fields only when the snapshot never carried a sub-object, closing a
  stale 1h-cache overcharge path in cascaded deployments
- relay: move Chat-to-Claude and Chat-to-Gemini stream conversion state
  from gin.Context onto RelayInfo and reset it with SendResponseCount in
  InitChannelMeta, so channel retries start clean while per-request
  state (stream error collection, conversion diagnostics, channel
  chain, billing accumulators) survives
- relay: Claude channel now serves Gemini-format clients (request via
  registry conversion, response and stream composed through the Chat
  pivot), removing the last unimplemented conversion direction
- relaykit: recognize legacy pseudo tool names (googleSearch,
  codeExecution, urlContext) in the toolconv decode stage and drop the
  string-matching bypass in the Chat-to-Gemini converter; native Gemini
  tool output is restored and non-Gemini targets follow standard loss
  diagnostics
- relaykit: attach upstream Gemini usage (with billing_usage sidecar)
  to intermediate stream chunks so converted Claude streams report
  upstream truth from message_start, and preserve the sidecar through
  Claude stream usage merges; billing settlement unchanged
- billing: clamp negative Total-Prompt completion derivation, OR the
  Estimated flag across cross-dialect snapshot replacement, and fill
  canonical OpenAI prompt details via field-wise merge
This commit is contained in:
Calcium-Ion
2026-09-03 10:40:05 +08:00
committed by GitHub
parent 0ed497f066
commit bbd97446c2
18 changed files with 958 additions and 103 deletions
@@ -56,6 +56,10 @@ func extractOpenAIChatRequest(request any) (any, Set, error) {
}
for index, tool := range source.Tools {
if tool.Type == "function" || tool.Type == "" {
if definition, ok := decodeOpenAIChatPseudoHostedTool(tool.Function.Name); ok {
set.Definitions = append(set.Definitions, definition)
continue
}
set.Definitions = append(set.Definitions, Definition{
Kind: KindFunction,
Execution: ExecutionClient,
@@ -511,6 +515,40 @@ func rawBoolPointer(raw json.RawMessage) *bool {
return &value
}
// decodeOpenAIChatPseudoHostedTool recognizes the OpenAI Chat dialect that
// declares Gemini hosted tools as function definitions named googleSearch,
// codeExecution, or urlContext. The names are the historical public contract;
// recognition lives here so every target format goes through the same hosted
// ToolDefinition pipeline.
func decodeOpenAIChatPseudoHostedTool(name string) (Definition, bool) {
switch name {
case "googleSearch":
return Definition{
Kind: KindWebSearch,
Execution: ExecutionServer,
NativeType: "googleSearch",
Name: "googleSearch",
WebSearch: &WebSearch{},
}, true
case "codeExecution":
return Definition{
Kind: KindCodeExecution,
Execution: ExecutionServer,
NativeType: "codeExecution",
Name: "codeExecution",
}, true
case "urlContext":
return Definition{
Kind: KindURLContext,
Execution: ExecutionServer,
NativeType: "urlContext",
Name: "urlContext",
}, true
default:
return Definition{}, false
}
}
func decodeOpenAIChatLocation(raw json.RawMessage) (*ApproximateLocation, error) {
if len(raw) == 0 {
return nil, nil