mirror of
https://github.com/lukaszraczylo/traefik-llmgateway.git
synced 2026-10-17 05:37:43 +00:00
fix: force include_usage, auth event logging, credential-strip coverage
C1 (quota bypass): a client sending stream_options.include_usage=false
suppressed upstream usage reporting and escaped every token/cost budget,
since the non-streaming T12 estimate never covers a streaming response.
chatCompletion (provider_openai.go) now always forces include_usage=true
on the outgoing request, merged into a copy of the client's own
stream_options map so the caller's map is never mutated in place.
clientAskedUsage is derived from the client's own value via isTruthy, so
the usage-only chunk is still suppressed for a client that never asked
for it.
I1 (vacuous security tests): the X-Api-Key credential-strip set had zero
effective coverage — existing tests either never sent x-api-key, or
targeted anthropic, whose injectAuth overwrites x-api-key regardless of
whether the strip ran. Added one openai-passthrough and one MCP-proxy
test that authenticate via x-api-key itself and assert it arrives empty
upstream. Mutation-checked: both fail when "X-Api-Key": true is removed
from gatewayCredentialHeaders (verified, then restored).
I3 (auth-event logging): added Gateway.logAuthEvent (logger.go), called
at all six authStore.identify call sites (llmgateway.go's ServeHTTP,
routes_unified.go's handleModels). A failed attempt logs method, path,
and remote address; a success logs the resolved user's name and route.
Neither line ever includes the presented API key.
M2 (usage.estimated): runUnified now reads result.estimated and logs the
estimate explicitly when a non-streaming response's usage falls back to
the body-size heuristic, making the field genuinely read — removes the
stale //nolint:unused on limits.go's usage struct.
M8 (gosec consistency): secrets.go's file: secret path read now carries
the same "#nosec G304 -- operator-supplied file path from middleware
config" annotation as users_file.go's equivalent read.
Doc nit: mcp_a2a.go's validateTargetURLs comment now also names the
dot-only ("."/"..") rule validateConfigName enforces, not just character
set and reserved names.
Gates: go test ./... -count=1 (-race) green, gofmt -l . empty, go vet
clean, make yaegi-check OK, golangci-lint (gosec+staticcheck+unused+
errcheck+govet, global config) 0 issues.
This commit is contained in:
+19
-10
@@ -88,12 +88,17 @@ type chatStreamChunk struct {
|
||||
|
||||
// chatCompletion implements providerAdapter.
|
||||
//
|
||||
// When req["stream"] is true, chatCompletion injects
|
||||
// stream_options.include_usage=true into the upstream request only when
|
||||
// the client did not already set stream_options itself — remembering
|
||||
// whether the client asked, so the streaming response can suppress the
|
||||
// resulting usage-only chunk from a client that never asked for it while
|
||||
// still capturing its usage for accounting.
|
||||
// When req["stream"] is true, chatCompletion always forces
|
||||
// stream_options.include_usage=true on the outgoing request — a client
|
||||
// sending include_usage=false must not be able to suppress upstream usage
|
||||
// reporting and escape token/cost budgets (controller ruling C1). It merges
|
||||
// that into a copy of the client's own stream_options map when present, so
|
||||
// the caller's map is never mutated in place. clientAskedUsage records
|
||||
// whether the client itself asked for usage (via isTruthy on the client's
|
||||
// own include_usage value, never the forced upstream one), so the
|
||||
// streaming response can still suppress the resulting usage-only chunk
|
||||
// from a client that never asked for it, while always capturing the usage
|
||||
// for accounting.
|
||||
func (a *openaiAdapter) chatCompletion(ctx context.Context, w http.ResponseWriter, req map[string]any) (usage, error) {
|
||||
// gatewayAliasKey must never reach the real provider: an openai-type
|
||||
// adapter forwards req verbatim as the upstream wire body, so a
|
||||
@@ -106,11 +111,15 @@ func (a *openaiAdapter) chatCompletion(ctx context.Context, w http.ResponseWrite
|
||||
streaming, _ := req["stream"].(bool)
|
||||
clientAskedUsage := false
|
||||
if streaming {
|
||||
if _, present := req["stream_options"]; present {
|
||||
clientAskedUsage = true
|
||||
} else {
|
||||
req["stream_options"] = map[string]any{"include_usage": true}
|
||||
merged := map[string]any{}
|
||||
if existing, ok := req["stream_options"].(map[string]any); ok {
|
||||
for k, v := range existing {
|
||||
merged[k] = v
|
||||
}
|
||||
clientAskedUsage = isTruthy(existing["include_usage"])
|
||||
}
|
||||
merged["include_usage"] = true
|
||||
req["stream_options"] = merged
|
||||
}
|
||||
|
||||
hdr := a.requestHeaders(true)
|
||||
|
||||
Reference in New Issue
Block a user