mirror of
https://github.com/lukaszraczylo/traefik-llmgateway.git
synced 2026-10-11 04:47:00 +00:00
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.
43 lines
1.3 KiB
Go
43 lines
1.3 KiB
Go
package traefikllmgateway
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"strings"
|
|
)
|
|
|
|
// resolveSecret resolves a config string value to its underlying secret. An
|
|
// empty value returns empty with no error. A "env:NAME" value returns
|
|
// os.Getenv(NAME), erroring if the variable is unset or empty. A
|
|
// "file:/path" value returns the file's contents with surrounding
|
|
// whitespace trimmed, erroring if the file cannot be read or its trimmed
|
|
// contents are empty — symmetric with the env: case. Any other value is
|
|
// returned unchanged as a literal.
|
|
//
|
|
// Error messages never include a resolved secret value — only the env var
|
|
// name or file path, which are config, not secrets.
|
|
func resolveSecret(v string) (string, error) {
|
|
if v == "" {
|
|
return "", nil
|
|
}
|
|
if name, ok := strings.CutPrefix(v, "env:"); ok {
|
|
val := os.Getenv(name)
|
|
if val == "" {
|
|
return "", fmt.Errorf("llmgateway: env var %q is unset or empty", name)
|
|
}
|
|
return val, nil
|
|
}
|
|
if path, ok := strings.CutPrefix(v, "file:"); ok {
|
|
b, err := os.ReadFile(path) // #nosec G304 -- operator-supplied file path from middleware config
|
|
if err != nil {
|
|
return "", fmt.Errorf("llmgateway: cannot read secret file %q: %w", path, err)
|
|
}
|
|
val := strings.TrimSpace(string(b))
|
|
if val == "" {
|
|
return "", fmt.Errorf("llmgateway: secret file %q is empty or whitespace-only", path)
|
|
}
|
|
return val, nil
|
|
}
|
|
return v, nil
|
|
}
|