Compare commits
6 Commits
96e0a686d7
...
fac9578975
| Author | SHA1 | Date | |
|---|---|---|---|
| fac9578975 | |||
| 76aeeb6be1 | |||
| 9cc052c162 | |||
| 20049881ad | |||
| 3435f5f4c5 | |||
| 807e8ba133 |
@@ -4,3 +4,5 @@
|
|||||||
anthropic-proxy
|
anthropic-proxy
|
||||||
result
|
result
|
||||||
config.yaml
|
config.yaml
|
||||||
|
|
||||||
|
vendor/**
|
||||||
|
|||||||
+16
-8
@@ -1,4 +1,20 @@
|
|||||||
port: 8082
|
port: 8082
|
||||||
|
|
||||||
|
# telemetry:
|
||||||
|
# endpoint: "localhost:4317" # OTLP gRPC endpoint (omit to disable export)
|
||||||
|
# insecure: true # disable TLS for local dev
|
||||||
|
# service_name: "anthropic-proxy"
|
||||||
|
# headers: # optional auth headers (e.g. Grafana Cloud)
|
||||||
|
# Authorization: "Basic ..."
|
||||||
|
|
||||||
|
logging:
|
||||||
|
level: debug
|
||||||
|
file: /home/fujin/.local/log/anthropic-proxy.log
|
||||||
|
max_size_mb: 100
|
||||||
|
max_backups: 5
|
||||||
|
max_age_days: 30
|
||||||
|
compress: true
|
||||||
|
|
||||||
api_keys:
|
api_keys:
|
||||||
- "your-proxy-api-key"
|
- "your-proxy-api-key"
|
||||||
claude_binary: "claude"
|
claude_binary: "claude"
|
||||||
@@ -45,11 +61,3 @@ sanitize:
|
|||||||
replace: "claude.ai"
|
replace: "claude.ai"
|
||||||
- match: "opencode"
|
- match: "opencode"
|
||||||
replace: "agent"
|
replace: "agent"
|
||||||
|
|
||||||
logging:
|
|
||||||
level: info
|
|
||||||
# file: /var/log/anthropic-proxy.log # omit to log to stderr
|
|
||||||
# max_size_mb: 100
|
|
||||||
# max_backups: 5
|
|
||||||
# max_age_days: 30
|
|
||||||
# compress: true
|
|
||||||
|
|||||||
@@ -42,6 +42,9 @@
|
|||||||
shellHook = ''
|
shellHook = ''
|
||||||
export GOPATH="$PWD/.go"
|
export GOPATH="$PWD/.go"
|
||||||
export PATH="$GOPATH/bin:$PATH"
|
export PATH="$GOPATH/bin:$PATH"
|
||||||
|
|
||||||
|
export ANTHROPIC_BASE_URL=http://localhost:8082
|
||||||
|
export ANTHROPIC_API_KEY=sk-cliproxy-fujin
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,23 +5,45 @@ go 1.26
|
|||||||
require (
|
require (
|
||||||
github.com/gin-gonic/gin v1.12.0
|
github.com/gin-gonic/gin v1.12.0
|
||||||
github.com/google/uuid v1.6.0
|
github.com/google/uuid v1.6.0
|
||||||
|
github.com/refraction-networking/utls v1.8.2
|
||||||
|
github.com/rs/zerolog v1.35.0
|
||||||
github.com/tidwall/gjson v1.18.0
|
github.com/tidwall/gjson v1.18.0
|
||||||
|
github.com/tidwall/sjson v1.2.5
|
||||||
|
go.opentelemetry.io/contrib/instrumentation/github.com/gin-gonic/gin/otelgin v0.68.0
|
||||||
|
go.opentelemetry.io/otel v1.43.0
|
||||||
|
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.19.0
|
||||||
|
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.43.0
|
||||||
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.43.0
|
||||||
|
go.opentelemetry.io/otel/log v0.19.0
|
||||||
|
go.opentelemetry.io/otel/metric v1.43.0
|
||||||
|
go.opentelemetry.io/otel/sdk v1.43.0
|
||||||
|
go.opentelemetry.io/otel/sdk/log v0.19.0
|
||||||
|
go.opentelemetry.io/otel/sdk/metric v1.43.0
|
||||||
|
golang.org/x/net v0.52.0
|
||||||
|
google.golang.org/grpc v1.80.0
|
||||||
|
gopkg.in/lumberjack.v2 v2.0.0
|
||||||
gopkg.in/yaml.v3 v3.0.1
|
gopkg.in/yaml.v3 v3.0.1
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
|
github.com/BurntSushi/toml v1.6.0 // indirect
|
||||||
github.com/andybalholm/brotli v1.0.6 // indirect
|
github.com/andybalholm/brotli v1.0.6 // indirect
|
||||||
github.com/bytedance/gopkg v0.1.3 // indirect
|
github.com/bytedance/gopkg v0.1.4 // indirect
|
||||||
github.com/bytedance/sonic v1.15.0 // indirect
|
github.com/bytedance/sonic v1.15.0 // indirect
|
||||||
github.com/bytedance/sonic/loader v0.5.0 // indirect
|
github.com/bytedance/sonic/loader v0.5.1 // indirect
|
||||||
|
github.com/cenkalti/backoff/v5 v5.0.3 // indirect
|
||||||
|
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||||
github.com/cloudwego/base64x v0.1.6 // indirect
|
github.com/cloudwego/base64x v0.1.6 // indirect
|
||||||
github.com/gabriel-vasile/mimetype v1.4.12 // indirect
|
github.com/gabriel-vasile/mimetype v1.4.13 // indirect
|
||||||
github.com/gin-contrib/sse v1.1.0 // indirect
|
github.com/gin-contrib/sse v1.1.1 // indirect
|
||||||
|
github.com/go-logr/logr v1.4.3 // indirect
|
||||||
|
github.com/go-logr/stdr v1.2.2 // indirect
|
||||||
github.com/go-playground/locales v0.14.1 // indirect
|
github.com/go-playground/locales v0.14.1 // indirect
|
||||||
github.com/go-playground/universal-translator v0.18.1 // indirect
|
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||||
github.com/go-playground/validator/v10 v10.30.1 // indirect
|
github.com/go-playground/validator/v10 v10.30.2 // indirect
|
||||||
github.com/goccy/go-json v0.10.5 // indirect
|
github.com/goccy/go-json v0.10.6 // indirect
|
||||||
github.com/goccy/go-yaml v1.19.2 // indirect
|
github.com/goccy/go-yaml v1.19.2 // indirect
|
||||||
|
github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 // indirect
|
||||||
github.com/json-iterator/go v1.1.12 // indirect
|
github.com/json-iterator/go v1.1.12 // indirect
|
||||||
github.com/klauspost/compress v1.17.6 // indirect
|
github.com/klauspost/compress v1.17.6 // indirect
|
||||||
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
|
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
|
||||||
@@ -30,22 +52,25 @@ require (
|
|||||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||||
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
|
github.com/pelletier/go-toml/v2 v2.3.0 // indirect
|
||||||
github.com/quic-go/qpack v0.6.0 // indirect
|
github.com/quic-go/qpack v0.6.0 // indirect
|
||||||
github.com/quic-go/quic-go v0.59.0 // indirect
|
github.com/quic-go/quic-go v0.59.0 // indirect
|
||||||
github.com/refraction-networking/utls v1.8.2 // indirect
|
|
||||||
github.com/rs/zerolog v1.35.0 // indirect
|
|
||||||
github.com/tidwall/match v1.1.1 // indirect
|
github.com/tidwall/match v1.1.1 // indirect
|
||||||
github.com/tidwall/pretty v1.2.0 // indirect
|
github.com/tidwall/pretty v1.2.0 // indirect
|
||||||
github.com/tidwall/sjson v1.2.5 // indirect
|
|
||||||
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||||
github.com/ugorji/go/codec v1.3.1 // indirect
|
github.com/ugorji/go/codec v1.3.1 // indirect
|
||||||
go.mongodb.org/mongo-driver/v2 v2.5.0 // indirect
|
go.mongodb.org/mongo-driver/v2 v2.5.0 // indirect
|
||||||
golang.org/x/arch v0.22.0 // indirect
|
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
|
||||||
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.43.0 // indirect
|
||||||
|
go.opentelemetry.io/otel/trace v1.43.0 // indirect
|
||||||
|
go.opentelemetry.io/proto/otlp v1.10.0 // indirect
|
||||||
|
golang.org/x/arch v0.25.0 // indirect
|
||||||
golang.org/x/crypto v0.49.0 // indirect
|
golang.org/x/crypto v0.49.0 // indirect
|
||||||
golang.org/x/net v0.52.0 // indirect
|
|
||||||
golang.org/x/sys v0.42.0 // indirect
|
golang.org/x/sys v0.42.0 // indirect
|
||||||
golang.org/x/text v0.35.0 // indirect
|
golang.org/x/text v0.35.0 // indirect
|
||||||
google.golang.org/protobuf v1.36.10 // indirect
|
google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9 // indirect
|
||||||
gopkg.in/lumberjack.v2 v2.0.0 // indirect
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20260401024825-9d38bb4040a9 // indirect
|
||||||
|
google.golang.org/protobuf v1.36.11 // indirect
|
||||||
|
gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect
|
||||||
|
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,39 +1,54 @@
|
|||||||
|
github.com/BurntSushi/toml v1.6.0 h1:dRaEfpa2VI55EwlIW72hMRHdWouJeRF7TPYhI+AUQjk=
|
||||||
|
github.com/BurntSushi/toml v1.6.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
|
||||||
github.com/andybalholm/brotli v1.0.6 h1:Yf9fFpf49Zrxb9NlQaluyE92/+X7UVHlhMNJN2sxfOI=
|
github.com/andybalholm/brotli v1.0.6 h1:Yf9fFpf49Zrxb9NlQaluyE92/+X7UVHlhMNJN2sxfOI=
|
||||||
github.com/andybalholm/brotli v1.0.6/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
|
github.com/andybalholm/brotli v1.0.6/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
|
||||||
github.com/bytedance/gopkg v0.1.3 h1:TPBSwH8RsouGCBcMBktLt1AymVo2TVsBVCY4b6TnZ/M=
|
github.com/bytedance/gopkg v0.1.4 h1:oZnQwnX82KAIWb7033bEwtxvTqXcYMxDBaQxo5JJHWM=
|
||||||
github.com/bytedance/gopkg v0.1.3/go.mod h1:576VvJ+eJgyCzdjS+c4+77QF3p7ubbtiKARP3TxducM=
|
github.com/bytedance/gopkg v0.1.4/go.mod h1:v1zWfPm21Fb+OsyXN2VAHdL6TBb2L88anLQgdyje6R4=
|
||||||
github.com/bytedance/sonic v1.15.0 h1:/PXeWFaR5ElNcVE84U0dOHjiMHQOwNIx3K4ymzh/uSE=
|
github.com/bytedance/sonic v1.15.0 h1:/PXeWFaR5ElNcVE84U0dOHjiMHQOwNIx3K4ymzh/uSE=
|
||||||
github.com/bytedance/sonic v1.15.0/go.mod h1:tFkWrPz0/CUCLEF4ri4UkHekCIcdnkqXw9VduqpJh0k=
|
github.com/bytedance/sonic v1.15.0/go.mod h1:tFkWrPz0/CUCLEF4ri4UkHekCIcdnkqXw9VduqpJh0k=
|
||||||
github.com/bytedance/sonic/loader v0.5.0 h1:gXH3KVnatgY7loH5/TkeVyXPfESoqSBSBEiDd5VjlgE=
|
github.com/bytedance/sonic/loader v0.5.1 h1:Ygpfa9zwRCCKSlrp5bBP/b/Xzc3VxsAW+5NIYXrOOpI=
|
||||||
github.com/bytedance/sonic/loader v0.5.0/go.mod h1:AR4NYCk5DdzZizZ5djGqQ92eEhCCcdf5x77udYiSJRo=
|
github.com/bytedance/sonic/loader v0.5.1/go.mod h1:AR4NYCk5DdzZizZ5djGqQ92eEhCCcdf5x77udYiSJRo=
|
||||||
|
github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM=
|
||||||
|
github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw=
|
||||||
|
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||||
|
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||||
github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M=
|
github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M=
|
||||||
github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU=
|
github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU=
|
||||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/gabriel-vasile/mimetype v1.4.12 h1:e9hWvmLYvtp846tLHam2o++qitpguFiYCKbn0w9jyqw=
|
github.com/gabriel-vasile/mimetype v1.4.13 h1:46nXokslUBsAJE/wMsp5gtO500a4F3Nkz9Ufpk2AcUM=
|
||||||
github.com/gabriel-vasile/mimetype v1.4.12/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
|
github.com/gabriel-vasile/mimetype v1.4.13/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
|
||||||
github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w=
|
github.com/gin-contrib/sse v1.1.1 h1:uGYpNwTacv5R68bSGMapo62iLTRa9l5zxGCps4hK6ko=
|
||||||
github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM=
|
github.com/gin-contrib/sse v1.1.1/go.mod h1:QXzuVkA0YO7o/gun03UI1Q+FTI8ZV/n5t03kIQAI89s=
|
||||||
github.com/gin-gonic/gin v1.12.0 h1:b3YAbrZtnf8N//yjKeU2+MQsh2mY5htkZidOM7O0wG8=
|
github.com/gin-gonic/gin v1.12.0 h1:b3YAbrZtnf8N//yjKeU2+MQsh2mY5htkZidOM7O0wG8=
|
||||||
github.com/gin-gonic/gin v1.12.0/go.mod h1:VxccKfsSllpKshkBWgVgRniFFAzFb9csfngsqANjnLc=
|
github.com/gin-gonic/gin v1.12.0/go.mod h1:VxccKfsSllpKshkBWgVgRniFFAzFb9csfngsqANjnLc=
|
||||||
|
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||||
|
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
|
||||||
|
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||||
|
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
||||||
|
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
||||||
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
|
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
|
||||||
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
||||||
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
|
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
|
||||||
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
|
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
|
||||||
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
|
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
|
||||||
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
|
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
|
||||||
github.com/go-playground/validator/v10 v10.30.1 h1:f3zDSN/zOma+w6+1Wswgd9fLkdwy06ntQJp0BBvFG0w=
|
github.com/go-playground/validator/v10 v10.30.2 h1:JiFIMtSSHb2/XBUbWM4i/MpeQm9ZK2xqPNk8vgvu5JQ=
|
||||||
github.com/go-playground/validator/v10 v10.30.1/go.mod h1:oSuBIQzuJxL//3MelwSLD5hc2Tu889bF0Idm9Dg26cM=
|
github.com/go-playground/validator/v10 v10.30.2/go.mod h1:mAf2pIOVXjTEBrwUMGKkCWKKPs9NheYGabeB04txQSc=
|
||||||
github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
|
github.com/goccy/go-json v0.10.6 h1:p8HrPJzOakx/mn/bQtjgNjdTcN+/S6FcG2CTtQOrHVU=
|
||||||
github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
|
github.com/goccy/go-json v0.10.6/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
|
||||||
github.com/goccy/go-yaml v1.19.2 h1:PmFC1S6h8ljIz6gMRBopkjP1TVT7xuwrButHID66PoM=
|
github.com/goccy/go-yaml v1.19.2 h1:PmFC1S6h8ljIz6gMRBopkjP1TVT7xuwrButHID66PoM=
|
||||||
github.com/goccy/go-yaml v1.19.2/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
|
github.com/goccy/go-yaml v1.19.2/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
|
||||||
|
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
|
||||||
|
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
|
||||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
|
github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 h1:HWRh5R2+9EifMyIHV7ZV+MIZqgz+PMpZ14Jynv3O2Zs=
|
||||||
|
github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0/go.mod h1:JfhWUomR1baixubs02l85lZYYOm7LV6om4ceouMv45c=
|
||||||
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
||||||
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
||||||
github.com/klauspost/compress v1.17.6 h1:60eq2E/jlfwQXtvZEeBUYADs+BwKBWURIY+Gj2eRGjI=
|
github.com/klauspost/compress v1.17.6 h1:60eq2E/jlfwQXtvZEeBUYADs+BwKBWURIY+Gj2eRGjI=
|
||||||
@@ -55,8 +70,8 @@ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w
|
|||||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||||
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
|
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
|
||||||
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||||
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
|
github.com/pelletier/go-toml/v2 v2.3.0 h1:k59bC/lIZREW0/iVaQR8nDHxVq8OVlIzYCOJf421CaM=
|
||||||
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
|
github.com/pelletier/go-toml/v2 v2.3.0/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
|
||||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8=
|
github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8=
|
||||||
@@ -65,8 +80,8 @@ github.com/quic-go/quic-go v0.59.0 h1:OLJkp1Mlm/aS7dpKgTc6cnpynnD2Xg7C1pwL6vy/SA
|
|||||||
github.com/quic-go/quic-go v0.59.0/go.mod h1:upnsH4Ju1YkqpLXC305eW3yDZ4NfnNbmQRCMWS58IKU=
|
github.com/quic-go/quic-go v0.59.0/go.mod h1:upnsH4Ju1YkqpLXC305eW3yDZ4NfnNbmQRCMWS58IKU=
|
||||||
github.com/refraction-networking/utls v1.8.2 h1:j4Q1gJj0xngdeH+Ox/qND11aEfhpgoEvV+S9iJ2IdQo=
|
github.com/refraction-networking/utls v1.8.2 h1:j4Q1gJj0xngdeH+Ox/qND11aEfhpgoEvV+S9iJ2IdQo=
|
||||||
github.com/refraction-networking/utls v1.8.2/go.mod h1:jkSOEkLqn+S/jtpEHPOsVv/4V4EVnelwbMQl4vCWXAM=
|
github.com/refraction-networking/utls v1.8.2/go.mod h1:jkSOEkLqn+S/jtpEHPOsVv/4V4EVnelwbMQl4vCWXAM=
|
||||||
github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
|
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
|
||||||
github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
|
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
|
||||||
github.com/rs/zerolog v1.35.0 h1:VD0ykx7HMiMJytqINBsKcbLS+BJ4WYjz+05us+LRTdI=
|
github.com/rs/zerolog v1.35.0 h1:VD0ykx7HMiMJytqINBsKcbLS+BJ4WYjz+05us+LRTdI=
|
||||||
github.com/rs/zerolog v1.35.0/go.mod h1:EjML9kdfa/RMA7h/6z6pYmq1ykOuA8/mjWaEvGI+jcw=
|
github.com/rs/zerolog v1.35.0/go.mod h1:EjML9kdfa/RMA7h/6z6pYmq1ykOuA8/mjWaEvGI+jcw=
|
||||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
@@ -95,34 +110,74 @@ github.com/ugorji/go/codec v1.3.1 h1:waO7eEiFDwidsBN6agj1vJQ4AG7lh2yqXyOXqhgQuyY
|
|||||||
github.com/ugorji/go/codec v1.3.1/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4=
|
github.com/ugorji/go/codec v1.3.1/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4=
|
||||||
go.mongodb.org/mongo-driver/v2 v2.5.0 h1:yXUhImUjjAInNcpTcAlPHiT7bIXhshCTL3jVBkF3xaE=
|
go.mongodb.org/mongo-driver/v2 v2.5.0 h1:yXUhImUjjAInNcpTcAlPHiT7bIXhshCTL3jVBkF3xaE=
|
||||||
go.mongodb.org/mongo-driver/v2 v2.5.0/go.mod h1:yOI9kBsufol30iFsl1slpdq1I0eHPzybRWdyYUs8K/0=
|
go.mongodb.org/mongo-driver/v2 v2.5.0/go.mod h1:yOI9kBsufol30iFsl1slpdq1I0eHPzybRWdyYUs8K/0=
|
||||||
|
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
|
||||||
|
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
|
||||||
|
go.opentelemetry.io/contrib/instrumentation/github.com/gin-gonic/gin/otelgin v0.68.0 h1:5FXSL2s6afUC1bzNzl1iedZZ8yqR7GOhbCoEXtyeK6Q=
|
||||||
|
go.opentelemetry.io/contrib/instrumentation/github.com/gin-gonic/gin/otelgin v0.68.0/go.mod h1:MdHW7tLtkeGJnR4TyOrnd5D0zUGZQB1l84uHCe8hRpE=
|
||||||
|
go.opentelemetry.io/contrib/propagators/b3 v1.43.0 h1:CETqV3QLLPTy5yNrqyMr41VnAOOD4lsRved7n4QG00A=
|
||||||
|
go.opentelemetry.io/contrib/propagators/b3 v1.43.0/go.mod h1:Q4mCiCdziYzpNR0g+6UqVotAlCDZdzz6L8jwY4knOrw=
|
||||||
|
go.opentelemetry.io/otel v1.43.0 h1:mYIM03dnh5zfN7HautFE4ieIig9amkNANT+xcVxAj9I=
|
||||||
|
go.opentelemetry.io/otel v1.43.0/go.mod h1:JuG+u74mvjvcm8vj8pI5XiHy1zDeoCS2LB1spIq7Ay0=
|
||||||
|
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.19.0 h1:Dn8rkudDzY6KV9dr/D/bTUuWgqDf9xe0rr4G2elrn0Y=
|
||||||
|
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.19.0/go.mod h1:gMk9F0xDgyN9M/3Ed5Y1wKcx/9mlU91NXY2SNq7RQuU=
|
||||||
|
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.43.0 h1:8UQVDcZxOJLtX6gxtDt3vY2WTgvZqMQRzjsqiIHQdkc=
|
||||||
|
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.43.0/go.mod h1:2lmweYCiHYpEjQ/lSJBYhj9jP1zvCvQW4BqL9dnT7FQ=
|
||||||
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.43.0 h1:88Y4s2C8oTui1LGM6bTWkw0ICGcOLCAI5l6zsD1j20k=
|
||||||
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.43.0/go.mod h1:Vl1/iaggsuRlrHf/hfPJPvVag77kKyvrLeD10kpMl+A=
|
||||||
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.43.0 h1:RAE+JPfvEmvy+0LzyUA25/SGawPwIUbZ6u0Wug54sLc=
|
||||||
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.43.0/go.mod h1:AGmbycVGEsRx9mXMZ75CsOyhSP6MFIcj/6dnG+vhVjk=
|
||||||
|
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.43.0 h1:mS47AX77OtFfKG4vtp+84kuGSFZHTyxtXIN269vChY0=
|
||||||
|
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.43.0/go.mod h1:PJnsC41lAGncJlPUniSwM81gc80GkgWJWr3cu2nKEtU=
|
||||||
|
go.opentelemetry.io/otel/log v0.19.0 h1:KUZs/GOsw79TBBMfDWsXS+KZ4g2Ckzksd1ymzsIEbo4=
|
||||||
|
go.opentelemetry.io/otel/log v0.19.0/go.mod h1:5DQYeGmxVIr4n0/BcJvF4upsraHjg6vudJJpnkL6Ipk=
|
||||||
|
go.opentelemetry.io/otel/metric v1.43.0 h1:d7638QeInOnuwOONPp4JAOGfbCEpYb+K6DVWvdxGzgM=
|
||||||
|
go.opentelemetry.io/otel/metric v1.43.0/go.mod h1:RDnPtIxvqlgO8GRW18W6Z/4P462ldprJtfxHxyKd2PY=
|
||||||
|
go.opentelemetry.io/otel/sdk v1.43.0 h1:pi5mE86i5rTeLXqoF/hhiBtUNcrAGHLKQdhg4h4V9Dg=
|
||||||
|
go.opentelemetry.io/otel/sdk v1.43.0/go.mod h1:P+IkVU3iWukmiit/Yf9AWvpyRDlUeBaRg6Y+C58QHzg=
|
||||||
|
go.opentelemetry.io/otel/sdk/log v0.19.0 h1:scYVLqT22D2gqXItnWiocLUKGH9yvkkeql5dBDiXyko=
|
||||||
|
go.opentelemetry.io/otel/sdk/log v0.19.0/go.mod h1:vFBowwXGLlW9AvpuF7bMgnNI95LiW10szrOdvzBHlAg=
|
||||||
|
go.opentelemetry.io/otel/sdk/log/logtest v0.19.0 h1:BEbF7ZBB6qQloV/Ub1+3NQoOUnVtcGkU3XX4Ws3GQfk=
|
||||||
|
go.opentelemetry.io/otel/sdk/log/logtest v0.19.0/go.mod h1:Lua81/3yM0wOmoHTokLj9y9ADeA02v1naRrVrkAZuKk=
|
||||||
|
go.opentelemetry.io/otel/sdk/metric v1.43.0 h1:S88dyqXjJkuBNLeMcVPRFXpRw2fuwdvfCGLEo89fDkw=
|
||||||
|
go.opentelemetry.io/otel/sdk/metric v1.43.0/go.mod h1:C/RJtwSEJ5hzTiUz5pXF1kILHStzb9zFlIEe85bhj6A=
|
||||||
|
go.opentelemetry.io/otel/trace v1.43.0 h1:BkNrHpup+4k4w+ZZ86CZoHHEkohws8AY+WTX09nk+3A=
|
||||||
|
go.opentelemetry.io/otel/trace v1.43.0/go.mod h1:/QJhyVBUUswCphDVxq+8mld+AvhXZLhe+8WVFxiFff0=
|
||||||
|
go.opentelemetry.io/proto/otlp v1.10.0 h1:IQRWgT5srOCYfiWnpqUYz9CVmbO8bFmKcwYxpuCSL2g=
|
||||||
|
go.opentelemetry.io/proto/otlp v1.10.0/go.mod h1:/CV4QoCR/S9yaPj8utp3lvQPoqMtxXdzn7ozvvozVqk=
|
||||||
|
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||||
|
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
||||||
go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y=
|
go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y=
|
||||||
go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU=
|
go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU=
|
||||||
golang.org/x/arch v0.22.0 h1:c/Zle32i5ttqRXjdLyyHZESLD/bB90DCU1g9l/0YBDI=
|
golang.org/x/arch v0.25.0 h1:qnk6Ksugpi5Bz32947rkUgDt9/s5qvqDPl/gBKdMJLE=
|
||||||
golang.org/x/arch v0.22.0/go.mod h1:dNHoOeKiyja7GTvF9NJS1l3Z2yntpQNzgrjh1cU103A=
|
golang.org/x/arch v0.25.0/go.mod h1:0X+GdSIP+kL5wPmpK7sdkEVTt2XoYP0cSjQSbZBwOi8=
|
||||||
golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts=
|
|
||||||
golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos=
|
|
||||||
golang.org/x/crypto v0.49.0 h1:+Ng2ULVvLHnJ/ZFEq4KdcDd/cfjrrjjNSXNzxg0Y4U4=
|
golang.org/x/crypto v0.49.0 h1:+Ng2ULVvLHnJ/ZFEq4KdcDd/cfjrrjjNSXNzxg0Y4U4=
|
||||||
golang.org/x/crypto v0.49.0/go.mod h1:ErX4dUh2UM+CFYiXZRTcMpEcN8b/1gxEuv3nODoYtCA=
|
golang.org/x/crypto v0.49.0/go.mod h1:ErX4dUh2UM+CFYiXZRTcMpEcN8b/1gxEuv3nODoYtCA=
|
||||||
golang.org/x/net v0.51.0 h1:94R/GTO7mt3/4wIKpcR5gkGmRLOuE/2hNGeWq/GBIFo=
|
|
||||||
golang.org/x/net v0.51.0/go.mod h1:aamm+2QF5ogm02fjy5Bb7CQ0WMt1/WVM7FtyaTLlA9Y=
|
|
||||||
golang.org/x/net v0.52.0 h1:He/TN1l0e4mmR3QqHMT2Xab3Aj3L9qjbhRm78/6jrW0=
|
golang.org/x/net v0.52.0 h1:He/TN1l0e4mmR3QqHMT2Xab3Aj3L9qjbhRm78/6jrW0=
|
||||||
golang.org/x/net v0.52.0/go.mod h1:R1MAz7uMZxVMualyPXb+VaqGSa3LIaUqk0eEt3w36Sw=
|
golang.org/x/net v0.52.0/go.mod h1:R1MAz7uMZxVMualyPXb+VaqGSa3LIaUqk0eEt3w36Sw=
|
||||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
|
|
||||||
golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
|
||||||
golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=
|
golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=
|
||||||
golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||||
golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk=
|
|
||||||
golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA=
|
|
||||||
golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8=
|
golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8=
|
||||||
golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA=
|
golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA=
|
||||||
google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE=
|
gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4=
|
||||||
google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E=
|
||||||
|
google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9 h1:VPWxll4HlMw1Vs/qXtN7BvhZqsS9cdAittCNvVENElA=
|
||||||
|
google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9/go.mod h1:7QBABkRtR8z+TEnmXTqIqwJLlzrZKVfAUm7tY3yGv0M=
|
||||||
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20260401024825-9d38bb4040a9 h1:m8qni9SQFH0tJc1X0vmnpw/0t+AImlSvp30sEupozUg=
|
||||||
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20260401024825-9d38bb4040a9/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
|
||||||
|
google.golang.org/grpc v1.80.0 h1:Xr6m2WmWZLETvUNvIUmeD5OAagMw3FiKmMlTdViWsHM=
|
||||||
|
google.golang.org/grpc v1.80.0/go.mod h1:ho/dLnxwi3EDJA4Zghp7k2Ec1+c2jqup0bFkw07bwF4=
|
||||||
|
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
|
||||||
|
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||||
gopkg.in/lumberjack.v2 v2.0.0 h1:IDj6hi8KbNiPQ5VaYNFZ7dBJLF5LFeKvsFrWHjA5aq4=
|
gopkg.in/lumberjack.v2 v2.0.0 h1:IDj6hi8KbNiPQ5VaYNFZ7dBJLF5LFeKvsFrWHjA5aq4=
|
||||||
gopkg.in/lumberjack.v2 v2.0.0/go.mod h1:bp5nQ2kK/lLQSmTk29azj9+JB6bWci56xFn/lvd5GLI=
|
gopkg.in/lumberjack.v2 v2.0.0/go.mod h1:bp5nQ2kK/lLQSmTk29azj9+JB6bWci56xFn/lvd5GLI=
|
||||||
|
gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc=
|
||||||
|
gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc=
|
||||||
|
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||||
|
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
|||||||
@@ -64,6 +64,13 @@ func RefreshToken(ctx context.Context, cred *Credential) error {
|
|||||||
}
|
}
|
||||||
req.Header.Set("Content-Type", "application/json")
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
|
||||||
|
log.Debug().
|
||||||
|
Str("url", tokenEndpoint).
|
||||||
|
Str("grant_type", "refresh_token").
|
||||||
|
Str("client_id", clientID).
|
||||||
|
Str("scope", oauthScopes).
|
||||||
|
Msg("token refresh request")
|
||||||
|
|
||||||
resp, err := utlsClient.Do(req)
|
resp, err := utlsClient.Do(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("execute request: %w", err)
|
return fmt.Errorf("execute request: %w", err)
|
||||||
@@ -71,6 +78,12 @@ func RefreshToken(ctx context.Context, cred *Credential) error {
|
|||||||
defer resp.Body.Close()
|
defer resp.Body.Close()
|
||||||
|
|
||||||
body, _ := io.ReadAll(resp.Body)
|
body, _ := io.ReadAll(resp.Body)
|
||||||
|
|
||||||
|
log.Debug().
|
||||||
|
Int("status", resp.StatusCode).
|
||||||
|
Int("response_size", len(body)).
|
||||||
|
Msg("token refresh response")
|
||||||
|
|
||||||
if resp.StatusCode != http.StatusOK {
|
if resp.StatusCode != http.StatusOK {
|
||||||
return fmt.Errorf("refresh returned %d: %s", resp.StatusCode, string(body))
|
return fmt.Errorf("refresh returned %d: %s", resp.StatusCode, string(body))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,11 +11,12 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type Config struct {
|
type Config struct {
|
||||||
Port int `yaml:"port"`
|
Port int `yaml:"port"`
|
||||||
APIKeys []string `yaml:"api_keys"`
|
APIKeys []string `yaml:"api_keys"`
|
||||||
ClaudeBinary string `yaml:"claude_binary"`
|
ClaudeBinary string `yaml:"claude_binary"`
|
||||||
Sanitize SanitizeConfig `yaml:"sanitize"`
|
Sanitize SanitizeConfig `yaml:"sanitize"`
|
||||||
Logging LoggingConfig `yaml:"logging"`
|
Logging LoggingConfig `yaml:"logging"`
|
||||||
|
Telemetry TelemetryConfig `yaml:"telemetry"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type SanitizeConfig struct {
|
type SanitizeConfig struct {
|
||||||
@@ -34,6 +35,15 @@ type ReplaceRule struct {
|
|||||||
Replace string `yaml:"replace"`
|
Replace string `yaml:"replace"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type TelemetryConfig struct {
|
||||||
|
Endpoint string `yaml:"endpoint"`
|
||||||
|
Insecure bool `yaml:"insecure"`
|
||||||
|
ServiceName string `yaml:"service_name"`
|
||||||
|
Headers map[string]string `yaml:"headers"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t TelemetryConfig) ExportEnabled() bool { return t.Endpoint != "" }
|
||||||
|
|
||||||
type LoggingConfig struct {
|
type LoggingConfig struct {
|
||||||
Level string `yaml:"level"`
|
Level string `yaml:"level"`
|
||||||
File string `yaml:"file"`
|
File string `yaml:"file"`
|
||||||
@@ -76,6 +86,10 @@ func Load(path string) (*Config, error) {
|
|||||||
cfg.Logging.MaxAgeDays = 30
|
cfg.Logging.MaxAgeDays = 30
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if cfg.Telemetry.ServiceName == "" {
|
||||||
|
cfg.Telemetry.ServiceName = "anthropic-proxy"
|
||||||
|
}
|
||||||
|
|
||||||
// Check for deprecated claude_credentials field
|
// Check for deprecated claude_credentials field
|
||||||
var rawCfg map[string]interface{}
|
var rawCfg map[string]interface{}
|
||||||
if err := yaml.Unmarshal(data, &rawCfg); err == nil {
|
if err := yaml.Unmarshal(data, &rawCfg); err == nil {
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package logging
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
@@ -28,7 +29,9 @@ type Config struct {
|
|||||||
// - File set: JSON → lumberjack rotating file
|
// - File set: JSON → lumberjack rotating file
|
||||||
// - File empty + TTY: colored ConsoleWriter → stderr
|
// - File empty + TTY: colored ConsoleWriter → stderr
|
||||||
// - File empty + not TTY: JSON → stderr (for systemd journal)
|
// - File empty + not TTY: JSON → stderr (for systemd journal)
|
||||||
func Setup(cfg Config) zerolog.Logger {
|
// Extra writers (e.g., OTLP log bridge) are added via io.MultiWriter so logs
|
||||||
|
// are written to both the primary destination and any extra writers.
|
||||||
|
func Setup(cfg Config, extraWriters ...io.Writer) zerolog.Logger {
|
||||||
// Parse log level
|
// Parse log level
|
||||||
level, err := zerolog.ParseLevel(cfg.Level)
|
level, err := zerolog.ParseLevel(cfg.Level)
|
||||||
if err != nil || cfg.Level == "" {
|
if err != nil || cfg.Level == "" {
|
||||||
@@ -48,20 +51,32 @@ func Setup(cfg Config) zerolog.Logger {
|
|||||||
MaxAge: cfg.MaxAgeDays,
|
MaxAge: cfg.MaxAgeDays,
|
||||||
Compress: cfg.Compress,
|
Compress: cfg.Compress,
|
||||||
}
|
}
|
||||||
logger = zerolog.New(jack).With().Timestamp().Caller().Logger()
|
var w io.Writer = jack
|
||||||
|
if len(extraWriters) > 0 {
|
||||||
|
w = io.MultiWriter(append([]io.Writer{jack}, extraWriters...)...)
|
||||||
|
}
|
||||||
|
logger = zerolog.New(w).With().Timestamp().Caller().Logger()
|
||||||
} else {
|
} else {
|
||||||
fi, err := os.Stderr.Stat()
|
fi, err := os.Stderr.Stat()
|
||||||
isTTY := err == nil && (fi.Mode()&os.ModeCharDevice) != 0
|
isTTY := err == nil && (fi.Mode()&os.ModeCharDevice) != 0
|
||||||
if isTTY {
|
if isTTY {
|
||||||
// Dev mode: colored console
|
// Dev mode: colored console (extra writers get JSON, console gets pretty)
|
||||||
cw := zerolog.ConsoleWriter{
|
cw := zerolog.ConsoleWriter{
|
||||||
Out: os.Stderr,
|
Out: os.Stderr,
|
||||||
TimeFormat: time.RFC3339,
|
TimeFormat: time.RFC3339,
|
||||||
}
|
}
|
||||||
logger = zerolog.New(cw).With().Timestamp().Caller().Logger()
|
var w io.Writer = cw
|
||||||
|
if len(extraWriters) > 0 {
|
||||||
|
w = io.MultiWriter(append([]io.Writer{cw}, extraWriters...)...)
|
||||||
|
}
|
||||||
|
logger = zerolog.New(w).With().Timestamp().Caller().Logger()
|
||||||
} else {
|
} else {
|
||||||
// Systemd journal: JSON to stderr
|
// Systemd journal: JSON to stderr
|
||||||
logger = zerolog.New(os.Stderr).With().Timestamp().Caller().Logger()
|
var w io.Writer = os.Stderr
|
||||||
|
if len(extraWriters) > 0 {
|
||||||
|
w = io.MultiWriter(append([]io.Writer{os.Stderr}, extraWriters...)...)
|
||||||
|
}
|
||||||
|
logger = zerolog.New(w).With().Timestamp().Caller().Logger()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+154
-13
@@ -9,12 +9,16 @@ import (
|
|||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/rs/zerolog/log"
|
"github.com/rs/zerolog/log"
|
||||||
"github.com/tidwall/gjson"
|
"github.com/tidwall/gjson"
|
||||||
|
"go.opentelemetry.io/otel/attribute"
|
||||||
|
"go.opentelemetry.io/otel/metric"
|
||||||
|
|
||||||
"github.com/fujin/anthropic-proxy/internal/auth"
|
"github.com/fujin/anthropic-proxy/internal/auth"
|
||||||
"github.com/fujin/anthropic-proxy/internal/logging"
|
"github.com/fujin/anthropic-proxy/internal/logging"
|
||||||
|
"github.com/fujin/anthropic-proxy/internal/ratelimit"
|
||||||
|
"github.com/fujin/anthropic-proxy/internal/telemetry"
|
||||||
)
|
)
|
||||||
|
|
||||||
func HandleMessages(pool *auth.Pool, profile *SniffedProfile, getSanitizer func() *Sanitizer) gin.HandlerFunc {
|
func HandleMessages(pool *auth.Pool, profile *SniffedProfile, getSanitizer func() *Sanitizer, tracker *ratelimit.Tracker) gin.HandlerFunc {
|
||||||
upstream := NewUpstreamClient(profile)
|
upstream := NewUpstreamClient(profile)
|
||||||
|
|
||||||
return func(c *gin.Context) {
|
return func(c *gin.Context) {
|
||||||
@@ -46,19 +50,25 @@ func HandleMessages(pool *auth.Pool, profile *SniffedProfile, getSanitizer func(
|
|||||||
isStream := gjson.GetBytes(body, "stream").Bool()
|
isStream := gjson.GetBytes(body, "stream").Bool()
|
||||||
|
|
||||||
if isStream {
|
if isStream {
|
||||||
handleStream(c, upstream, san, pool, cred, body, originalBody)
|
handleStream(c, upstream, san, pool, cred, body, originalBody, tracker)
|
||||||
} else {
|
} else {
|
||||||
handleNonStream(c, upstream, san, pool, cred, body, originalBody)
|
handleNonStream(c, upstream, san, pool, cred, body, originalBody, tracker)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func handleNonStream(c *gin.Context, upstream *UpstreamClient, san *Sanitizer, pool *auth.Pool, cred *auth.Credential, body []byte, originalBody []byte) {
|
func handleNonStream(c *gin.Context, upstream *UpstreamClient, san *Sanitizer, pool *auth.Pool, cred *auth.Credential, body []byte, originalBody []byte, tracker *ratelimit.Tracker) {
|
||||||
startTime := time.Now()
|
startTime := time.Now()
|
||||||
respBody, headers, statusCode, err := upstream.Execute(c.Request.Context(), cred, body)
|
model := gjson.GetBytes(body, "model").String()
|
||||||
|
ctx := c.Request.Context()
|
||||||
|
|
||||||
|
telemetry.RequestBodySize.Record(ctx, int64(len(body)),
|
||||||
|
metric.WithAttributes(attribute.String("model", model), attribute.Bool("stream", false)))
|
||||||
|
|
||||||
|
respBody, headers, statusCode, err := upstream.Execute(ctx, cred, body)
|
||||||
|
latencyMs := float64(time.Since(startTime).Milliseconds())
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
latencyMs := float64(time.Since(startTime).Milliseconds())
|
|
||||||
model := gjson.GetBytes(body, "model").String()
|
|
||||||
log.Error().
|
log.Error().
|
||||||
Err(err).
|
Err(err).
|
||||||
Str("credential", cred.Email).
|
Str("credential", cred.Email).
|
||||||
@@ -69,14 +79,39 @@ func handleNonStream(c *gin.Context, upstream *UpstreamClient, san *Sanitizer, p
|
|||||||
Int("request_body_size", len(body)).
|
Int("request_body_size", len(body)).
|
||||||
Float64("latency_ms", latencyMs).
|
Float64("latency_ms", latencyMs).
|
||||||
Msg("upstream connection error")
|
Msg("upstream connection error")
|
||||||
|
|
||||||
|
telemetry.UpstreamErrors.Add(ctx, 1,
|
||||||
|
metric.WithAttributes(
|
||||||
|
attribute.String("error_type", "connection"),
|
||||||
|
attribute.String("credential", cred.Email),
|
||||||
|
attribute.Int("status_code", http.StatusBadGateway),
|
||||||
|
))
|
||||||
|
telemetry.RequestCounter.Add(ctx, 1,
|
||||||
|
metric.WithAttributes(
|
||||||
|
attribute.String("model", model),
|
||||||
|
attribute.Bool("stream", false),
|
||||||
|
attribute.Int("status_code", http.StatusBadGateway),
|
||||||
|
))
|
||||||
|
telemetry.RequestDuration.Record(ctx, latencyMs,
|
||||||
|
metric.WithAttributes(attribute.String("model", model), attribute.Bool("stream", false), attribute.Int("status_code", http.StatusBadGateway)))
|
||||||
|
|
||||||
c.JSON(http.StatusBadGateway, gin.H{"error": "upstream request failed"})
|
c.JSON(http.StatusBadGateway, gin.H{"error": "upstream request failed"})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
attrs := []attribute.KeyValue{
|
||||||
|
attribute.String("model", model),
|
||||||
|
attribute.Bool("stream", false),
|
||||||
|
attribute.Int("status_code", statusCode),
|
||||||
|
}
|
||||||
|
|
||||||
|
telemetry.RequestCounter.Add(ctx, 1, metric.WithAttributes(attrs...))
|
||||||
|
telemetry.RequestDuration.Record(ctx, latencyMs, metric.WithAttributes(attrs...))
|
||||||
|
|
||||||
if statusCode >= 400 {
|
if statusCode >= 400 {
|
||||||
pool.MarkFailure(cred, statusCode)
|
pool.MarkFailure(cred, statusCode)
|
||||||
latencyMs := float64(time.Since(startTime).Milliseconds())
|
telemetry.CredentialCooldowns.Add(ctx, 1,
|
||||||
model := gjson.GetBytes(body, "model").String()
|
metric.WithAttributes(attribute.Int("status_code", statusCode)))
|
||||||
errorType := gjson.GetBytes(respBody, "error.type").String()
|
errorType := gjson.GetBytes(respBody, "error.type").String()
|
||||||
errorMessage := gjson.GetBytes(respBody, "error.message").String()
|
errorMessage := gjson.GetBytes(respBody, "error.message").String()
|
||||||
log.Error().
|
log.Error().
|
||||||
@@ -94,9 +129,37 @@ func handleNonStream(c *gin.Context, upstream *UpstreamClient, san *Sanitizer, p
|
|||||||
Int("request_body_size", len(body)).
|
Int("request_body_size", len(body)).
|
||||||
Str("request_headers", logging.RedactHeaders(c.Request.Header)).
|
Str("request_headers", logging.RedactHeaders(c.Request.Header)).
|
||||||
Msg("upstream error")
|
Msg("upstream error")
|
||||||
|
|
||||||
|
telemetry.UpstreamErrors.Add(ctx, 1,
|
||||||
|
metric.WithAttributes(
|
||||||
|
attribute.Int("status_code", statusCode),
|
||||||
|
attribute.String("error_type", errorType),
|
||||||
|
attribute.String("credential", cred.Email),
|
||||||
|
))
|
||||||
} else {
|
} else {
|
||||||
pool.MarkSuccess(cred)
|
pool.MarkSuccess(cred)
|
||||||
respBody = san.DesanitizeResponse(respBody)
|
respBody = san.DesanitizeResponse(respBody)
|
||||||
|
|
||||||
|
inputTokens := gjson.GetBytes(respBody, "usage.input_tokens").Int()
|
||||||
|
outputTokens := gjson.GetBytes(respBody, "usage.output_tokens").Int()
|
||||||
|
tokenAttrs := metric.WithAttributes(
|
||||||
|
attribute.String("model", model),
|
||||||
|
attribute.String("credential", cred.Email),
|
||||||
|
)
|
||||||
|
telemetry.TokensInput.Add(ctx, inputTokens, tokenAttrs)
|
||||||
|
telemetry.TokensOutput.Add(ctx, outputTokens, tokenAttrs)
|
||||||
|
if tracker != nil {
|
||||||
|
tracker.RecordTokens(inputTokens, outputTokens)
|
||||||
|
tracker.UpdateFromHeaders(headers)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Info().
|
||||||
|
Int("status", statusCode).
|
||||||
|
Float64("latency_ms", latencyMs).
|
||||||
|
Str("model", model).
|
||||||
|
Int64("input_tokens", inputTokens).
|
||||||
|
Int64("output_tokens", outputTokens).
|
||||||
|
Msg("request completed")
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, h := range []string{"Content-Type", "X-Request-Id"} {
|
for _, h := range []string{"Content-Type", "X-Request-Id"} {
|
||||||
@@ -108,12 +171,18 @@ func handleNonStream(c *gin.Context, upstream *UpstreamClient, san *Sanitizer, p
|
|||||||
c.Data(statusCode, headers.Get("Content-Type"), respBody)
|
c.Data(statusCode, headers.Get("Content-Type"), respBody)
|
||||||
}
|
}
|
||||||
|
|
||||||
func handleStream(c *gin.Context, upstream *UpstreamClient, san *Sanitizer, pool *auth.Pool, cred *auth.Credential, body []byte, originalBody []byte) {
|
func handleStream(c *gin.Context, upstream *UpstreamClient, san *Sanitizer, pool *auth.Pool, cred *auth.Credential, body []byte, originalBody []byte, tracker *ratelimit.Tracker) {
|
||||||
startTime := time.Now()
|
startTime := time.Now()
|
||||||
resp, err := upstream.ExecuteStream(c.Request.Context(), cred, body)
|
model := gjson.GetBytes(body, "model").String()
|
||||||
|
ctx := c.Request.Context()
|
||||||
|
|
||||||
|
telemetry.StreamRequests.Add(ctx, 1, metric.WithAttributes(attribute.String("model", model)))
|
||||||
|
telemetry.RequestBodySize.Record(ctx, int64(len(body)),
|
||||||
|
metric.WithAttributes(attribute.String("model", model), attribute.Bool("stream", true)))
|
||||||
|
|
||||||
|
resp, err := upstream.ExecuteStream(ctx, cred, body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
latencyMs := float64(time.Since(startTime).Milliseconds())
|
latencyMs := float64(time.Since(startTime).Milliseconds())
|
||||||
model := gjson.GetBytes(body, "model").String()
|
|
||||||
log.Error().
|
log.Error().
|
||||||
Err(err).
|
Err(err).
|
||||||
Str("credential", cred.Email).
|
Str("credential", cred.Email).
|
||||||
@@ -124,6 +193,22 @@ func handleStream(c *gin.Context, upstream *UpstreamClient, san *Sanitizer, pool
|
|||||||
Int("request_body_size", len(body)).
|
Int("request_body_size", len(body)).
|
||||||
Float64("latency_ms", latencyMs).
|
Float64("latency_ms", latencyMs).
|
||||||
Msg("upstream connection error")
|
Msg("upstream connection error")
|
||||||
|
|
||||||
|
telemetry.UpstreamErrors.Add(ctx, 1,
|
||||||
|
metric.WithAttributes(
|
||||||
|
attribute.String("error_type", "connection"),
|
||||||
|
attribute.String("credential", cred.Email),
|
||||||
|
attribute.Int("status_code", http.StatusBadGateway),
|
||||||
|
))
|
||||||
|
telemetry.RequestCounter.Add(ctx, 1,
|
||||||
|
metric.WithAttributes(
|
||||||
|
attribute.String("model", model),
|
||||||
|
attribute.Bool("stream", true),
|
||||||
|
attribute.Int("status_code", http.StatusBadGateway),
|
||||||
|
))
|
||||||
|
telemetry.RequestDuration.Record(ctx, latencyMs,
|
||||||
|
metric.WithAttributes(attribute.String("model", model), attribute.Bool("stream", true), attribute.Int("status_code", http.StatusBadGateway)))
|
||||||
|
|
||||||
c.JSON(http.StatusBadGateway, gin.H{"error": "upstream stream request failed"})
|
c.JSON(http.StatusBadGateway, gin.H{"error": "upstream stream request failed"})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -131,9 +216,10 @@ func handleStream(c *gin.Context, upstream *UpstreamClient, san *Sanitizer, pool
|
|||||||
|
|
||||||
if resp.StatusCode >= 400 {
|
if resp.StatusCode >= 400 {
|
||||||
pool.MarkFailure(cred, resp.StatusCode)
|
pool.MarkFailure(cred, resp.StatusCode)
|
||||||
|
telemetry.CredentialCooldowns.Add(ctx, 1,
|
||||||
|
metric.WithAttributes(attribute.Int("status_code", resp.StatusCode)))
|
||||||
respBody, _ := io.ReadAll(resp.Body)
|
respBody, _ := io.ReadAll(resp.Body)
|
||||||
latencyMs := float64(time.Since(startTime).Milliseconds())
|
latencyMs := float64(time.Since(startTime).Milliseconds())
|
||||||
model := gjson.GetBytes(body, "model").String()
|
|
||||||
errorType := gjson.GetBytes(respBody, "error.type").String()
|
errorType := gjson.GetBytes(respBody, "error.type").String()
|
||||||
errorMessage := gjson.GetBytes(respBody, "error.message").String()
|
errorMessage := gjson.GetBytes(respBody, "error.message").String()
|
||||||
log.Error().
|
log.Error().
|
||||||
@@ -151,6 +237,21 @@ func handleStream(c *gin.Context, upstream *UpstreamClient, san *Sanitizer, pool
|
|||||||
Int("request_body_size", len(body)).
|
Int("request_body_size", len(body)).
|
||||||
Str("request_headers", logging.RedactHeaders(c.Request.Header)).
|
Str("request_headers", logging.RedactHeaders(c.Request.Header)).
|
||||||
Msg("upstream error")
|
Msg("upstream error")
|
||||||
|
|
||||||
|
attrs := []attribute.KeyValue{
|
||||||
|
attribute.String("model", model),
|
||||||
|
attribute.Bool("stream", true),
|
||||||
|
attribute.Int("status_code", resp.StatusCode),
|
||||||
|
}
|
||||||
|
telemetry.RequestCounter.Add(ctx, 1, metric.WithAttributes(attrs...))
|
||||||
|
telemetry.RequestDuration.Record(ctx, latencyMs, metric.WithAttributes(attrs...))
|
||||||
|
telemetry.UpstreamErrors.Add(ctx, 1,
|
||||||
|
metric.WithAttributes(
|
||||||
|
attribute.Int("status_code", resp.StatusCode),
|
||||||
|
attribute.String("error_type", errorType),
|
||||||
|
attribute.String("credential", cred.Email),
|
||||||
|
))
|
||||||
|
|
||||||
c.Data(resp.StatusCode, resp.Header.Get("Content-Type"), respBody)
|
c.Data(resp.StatusCode, resp.Header.Get("Content-Type"), respBody)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -169,14 +270,54 @@ func handleStream(c *gin.Context, upstream *UpstreamClient, san *Sanitizer, pool
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var inputTokens, outputTokens int64
|
||||||
scanner := bufio.NewScanner(resp.Body)
|
scanner := bufio.NewScanner(resp.Body)
|
||||||
scanner.Buffer(make([]byte, 0, 64*1024), 1024*1024)
|
scanner.Buffer(make([]byte, 0, 64*1024), 1024*1024)
|
||||||
for scanner.Scan() {
|
for scanner.Scan() {
|
||||||
line := san.DesanitizeStreamEvent(scanner.Text())
|
line := san.DesanitizeStreamEvent(scanner.Text())
|
||||||
c.Writer.WriteString(line + "\n")
|
c.Writer.WriteString(line + "\n")
|
||||||
flusher.Flush()
|
flusher.Flush()
|
||||||
|
|
||||||
|
// Extract token usage from message_delta event
|
||||||
|
if len(line) > 5 && line[:5] == "data:" {
|
||||||
|
data := line[5:]
|
||||||
|
if gjson.Get(data, "type").String() == "message_delta" {
|
||||||
|
inputTokens = gjson.Get(data, "usage.input_tokens").Int()
|
||||||
|
outputTokens = gjson.Get(data, "usage.output_tokens").Int()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
latencyMs := float64(time.Since(startTime).Milliseconds())
|
||||||
|
attrs := []attribute.KeyValue{
|
||||||
|
attribute.String("model", model),
|
||||||
|
attribute.Bool("stream", true),
|
||||||
|
attribute.Int("status_code", http.StatusOK),
|
||||||
|
}
|
||||||
|
telemetry.RequestCounter.Add(ctx, 1, metric.WithAttributes(attrs...))
|
||||||
|
telemetry.RequestDuration.Record(ctx, latencyMs, metric.WithAttributes(attrs...))
|
||||||
|
|
||||||
|
if inputTokens > 0 || outputTokens > 0 {
|
||||||
|
tokenAttrs := metric.WithAttributes(
|
||||||
|
attribute.String("model", model),
|
||||||
|
attribute.String("credential", cred.Email),
|
||||||
|
)
|
||||||
|
telemetry.TokensInput.Add(ctx, inputTokens, tokenAttrs)
|
||||||
|
telemetry.TokensOutput.Add(ctx, outputTokens, tokenAttrs)
|
||||||
|
if tracker != nil {
|
||||||
|
tracker.RecordTokens(inputTokens, outputTokens)
|
||||||
|
tracker.UpdateFromHeaders(resp.Header)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Info().
|
||||||
|
Float64("latency_ms", latencyMs).
|
||||||
|
Str("model", model).
|
||||||
|
Bool("stream", true).
|
||||||
|
Int64("input_tokens", inputTokens).
|
||||||
|
Int64("output_tokens", outputTokens).
|
||||||
|
Msg("stream completed")
|
||||||
|
|
||||||
if err := scanner.Err(); err != nil {
|
if err := scanner.Err(); err != nil {
|
||||||
log.Error().Err(err).Msg("stream scan error")
|
log.Error().Err(err).Msg("stream scan error")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -122,6 +122,11 @@ func SniffClaudeCode(claudeBinary string) (*SniffedProfile, error) {
|
|||||||
Int("headers", len(profile.Headers)).
|
Int("headers", len(profile.Headers)).
|
||||||
Int("body_size", len(profile.Body)).
|
Int("body_size", len(profile.Body)).
|
||||||
Msg("sniffed claude-code profile")
|
Msg("sniffed claude-code profile")
|
||||||
|
|
||||||
|
for _, h := range profile.Headers {
|
||||||
|
log.Debug().Str("header", h[0]).Str("value", h[1]).Msg("sniffed header")
|
||||||
|
}
|
||||||
|
|
||||||
return profile, nil
|
return profile, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -9,8 +9,10 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
|
"github.com/rs/zerolog/log"
|
||||||
|
|
||||||
"github.com/fujin/anthropic-proxy/internal/auth"
|
"github.com/fujin/anthropic-proxy/internal/auth"
|
||||||
|
"github.com/fujin/anthropic-proxy/internal/logging"
|
||||||
)
|
)
|
||||||
|
|
||||||
const messagesURL = "https://api.anthropic.com/v1/messages?beta=true"
|
const messagesURL = "https://api.anthropic.com/v1/messages?beta=true"
|
||||||
@@ -51,6 +53,15 @@ func (u *UpstreamClient) applyHeaders(req *http.Request, token string, streaming
|
|||||||
req.Header.Del("x-api-key")
|
req.Header.Del("x-api-key")
|
||||||
if strings.HasPrefix(token, "sk-ant-oat") {
|
if strings.HasPrefix(token, "sk-ant-oat") {
|
||||||
req.Header.Set("Authorization", "Bearer "+token)
|
req.Header.Set("Authorization", "Bearer "+token)
|
||||||
|
// OAuth tokens require this beta flag — without it the API rejects with 401
|
||||||
|
existing := req.Header.Get("anthropic-beta")
|
||||||
|
if !strings.Contains(existing, "oauth-2025-04-20") {
|
||||||
|
if existing == "" {
|
||||||
|
req.Header.Set("anthropic-beta", "oauth-2025-04-20")
|
||||||
|
} else {
|
||||||
|
req.Header.Set("anthropic-beta", existing+",oauth-2025-04-20")
|
||||||
|
}
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
req.Header.Set("x-api-key", token)
|
req.Header.Set("x-api-key", token)
|
||||||
}
|
}
|
||||||
@@ -75,6 +86,12 @@ func (u *UpstreamClient) Execute(ctx context.Context, cred *auth.Credential, bod
|
|||||||
}
|
}
|
||||||
u.applyHeaders(req, cred.Token(), false)
|
u.applyHeaders(req, cred.Token(), false)
|
||||||
|
|
||||||
|
log.Debug().
|
||||||
|
Str("url", messagesURL).
|
||||||
|
Str("upstream_headers", logging.RedactHeaders(req.Header)).
|
||||||
|
Int("body_size", len(body)).
|
||||||
|
Msg("upstream request")
|
||||||
|
|
||||||
resp, err := u.client.Do(req)
|
resp, err := u.client.Do(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, 0, fmt.Errorf("upstream request: %w", err)
|
return nil, nil, 0, fmt.Errorf("upstream request: %w", err)
|
||||||
@@ -85,6 +102,12 @@ func (u *UpstreamClient) Execute(ctx context.Context, cred *auth.Credential, bod
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, resp.StatusCode, fmt.Errorf("read upstream response: %w", err)
|
return nil, nil, resp.StatusCode, fmt.Errorf("read upstream response: %w", err)
|
||||||
}
|
}
|
||||||
|
log.Debug().
|
||||||
|
Int("status", resp.StatusCode).
|
||||||
|
Str("response_headers", logging.RedactHeaders(resp.Header)).
|
||||||
|
Int("response_size", len(respBody)).
|
||||||
|
Msg("upstream response")
|
||||||
|
|
||||||
return respBody, resp.Header, resp.StatusCode, nil
|
return respBody, resp.Header, resp.StatusCode, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -97,9 +120,21 @@ func (u *UpstreamClient) ExecuteStream(ctx context.Context, cred *auth.Credentia
|
|||||||
}
|
}
|
||||||
u.applyHeaders(req, cred.Token(), true)
|
u.applyHeaders(req, cred.Token(), true)
|
||||||
|
|
||||||
|
log.Debug().
|
||||||
|
Str("url", messagesURL).
|
||||||
|
Str("upstream_headers", logging.RedactHeaders(req.Header)).
|
||||||
|
Int("body_size", len(body)).
|
||||||
|
Msg("upstream stream request")
|
||||||
|
|
||||||
resp, err := u.client.Do(req)
|
resp, err := u.client.Do(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("upstream stream request: %w", err)
|
return nil, fmt.Errorf("upstream stream request: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
log.Debug().
|
||||||
|
Int("status", resp.StatusCode).
|
||||||
|
Str("response_headers", logging.RedactHeaders(resp.Header)).
|
||||||
|
Msg("upstream stream response")
|
||||||
|
|
||||||
return resp, nil
|
return resp, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,213 @@
|
|||||||
|
package ratelimit
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
"sync"
|
||||||
|
"sync/atomic"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/rs/zerolog/log"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Window holds per-window usage state.
|
||||||
|
type Window struct {
|
||||||
|
Utilization float64 // 0-100 from API
|
||||||
|
ResetsAt time.Time // when window resets
|
||||||
|
TokensIn atomic.Int64
|
||||||
|
TokensOut atomic.Int64
|
||||||
|
}
|
||||||
|
|
||||||
|
// Snapshot is a read-only copy of a Window's state.
|
||||||
|
type Snapshot struct {
|
||||||
|
Utilization float64
|
||||||
|
ResetsAt time.Time
|
||||||
|
TokensIn int64
|
||||||
|
TokensOut int64
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tracker polls /api/oauth/usage and tracks per-window token usage.
|
||||||
|
type Tracker struct {
|
||||||
|
tokenFn func() string // returns current OAuth access token
|
||||||
|
mu sync.RWMutex
|
||||||
|
fiveHour Window
|
||||||
|
sevenDay Window
|
||||||
|
sonnet Window
|
||||||
|
extra ExtraUsage
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewTracker creates a tracker. tokenFn should return the current access token.
|
||||||
|
func NewTracker(tokenFn func() string) *Tracker {
|
||||||
|
return &Tracker{tokenFn: tokenFn}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start begins the background poll loop.
|
||||||
|
func (t *Tracker) Start(ctx context.Context) {
|
||||||
|
go func() {
|
||||||
|
// Poll immediately on start
|
||||||
|
t.poll(ctx)
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
return
|
||||||
|
case <-time.After(5 * time.Minute):
|
||||||
|
t.poll(ctx)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateFromHeaders extracts rate limit data from /v1/messages response headers.
|
||||||
|
// This provides real-time utilization updates without polling the usage API.
|
||||||
|
func (t *Tracker) UpdateFromHeaders(h http.Header) {
|
||||||
|
t.mu.Lock()
|
||||||
|
defer t.mu.Unlock()
|
||||||
|
|
||||||
|
if v := h.Get("Anthropic-Ratelimit-Unified-5h-Utilization"); v != "" {
|
||||||
|
if f, err := strconv.ParseFloat(v, 64); err == nil {
|
||||||
|
t.fiveHour.Utilization = f * 100 // header is 0-1, we store 0-100
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if v := h.Get("Anthropic-Ratelimit-Unified-5h-Reset"); v != "" {
|
||||||
|
if ts, err := strconv.ParseInt(v, 10, 64); err == nil {
|
||||||
|
t.fiveHour.ResetsAt = time.Unix(ts, 0).UTC().Truncate(time.Minute)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if v := h.Get("Anthropic-Ratelimit-Unified-7d-Utilization"); v != "" {
|
||||||
|
if f, err := strconv.ParseFloat(v, 64); err == nil {
|
||||||
|
t.sevenDay.Utilization = f * 100
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if v := h.Get("Anthropic-Ratelimit-Unified-7d-Reset"); v != "" {
|
||||||
|
if ts, err := strconv.ParseInt(v, 10, 64); err == nil {
|
||||||
|
t.sevenDay.ResetsAt = time.Unix(ts, 0).UTC().Truncate(time.Minute)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// RecordTokens adds tokens to all active windows.
|
||||||
|
func (t *Tracker) RecordTokens(inputTokens, outputTokens int64) {
|
||||||
|
t.fiveHour.TokensIn.Add(inputTokens)
|
||||||
|
t.fiveHour.TokensOut.Add(outputTokens)
|
||||||
|
t.sevenDay.TokensIn.Add(inputTokens)
|
||||||
|
t.sevenDay.TokensOut.Add(outputTokens)
|
||||||
|
}
|
||||||
|
|
||||||
|
// FiveHour returns a snapshot of the 5-hour window.
|
||||||
|
func (t *Tracker) FiveHour() Snapshot {
|
||||||
|
t.mu.RLock()
|
||||||
|
defer t.mu.RUnlock()
|
||||||
|
return Snapshot{
|
||||||
|
Utilization: t.fiveHour.Utilization,
|
||||||
|
ResetsAt: t.fiveHour.ResetsAt,
|
||||||
|
TokensIn: t.fiveHour.TokensIn.Load(),
|
||||||
|
TokensOut: t.fiveHour.TokensOut.Load(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// SevenDay returns a snapshot of the 7-day window.
|
||||||
|
func (t *Tracker) SevenDay() Snapshot {
|
||||||
|
t.mu.RLock()
|
||||||
|
defer t.mu.RUnlock()
|
||||||
|
return Snapshot{
|
||||||
|
Utilization: t.sevenDay.Utilization,
|
||||||
|
ResetsAt: t.sevenDay.ResetsAt,
|
||||||
|
TokensIn: t.sevenDay.TokensIn.Load(),
|
||||||
|
TokensOut: t.sevenDay.TokensOut.Load(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sonnet returns a snapshot of the 7-day sonnet window.
|
||||||
|
func (t *Tracker) Sonnet() Snapshot {
|
||||||
|
t.mu.RLock()
|
||||||
|
defer t.mu.RUnlock()
|
||||||
|
return Snapshot{
|
||||||
|
Utilization: t.sonnet.Utilization,
|
||||||
|
ResetsAt: t.sonnet.ResetsAt,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extra returns the current extra usage state.
|
||||||
|
func (t *Tracker) Extra() ExtraUsage {
|
||||||
|
t.mu.RLock()
|
||||||
|
defer t.mu.RUnlock()
|
||||||
|
return t.extra
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Tracker) poll(ctx context.Context) {
|
||||||
|
token := t.tokenFn()
|
||||||
|
if token == "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
usage, err := fetchUsage(ctx, token)
|
||||||
|
if err != nil {
|
||||||
|
log.Warn().Err(err).Msg("usage poll failed")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
t.mu.Lock()
|
||||||
|
defer t.mu.Unlock()
|
||||||
|
|
||||||
|
if usage.FiveHour != nil {
|
||||||
|
t.updateWindow(&t.fiveHour, usage.FiveHour, "5h")
|
||||||
|
}
|
||||||
|
if usage.SevenDay != nil {
|
||||||
|
t.updateWindow(&t.sevenDay, usage.SevenDay, "7d")
|
||||||
|
}
|
||||||
|
if usage.SevenDaySonnet != nil {
|
||||||
|
t.updateWindow(&t.sonnet, usage.SevenDaySonnet, "7d_sonnet")
|
||||||
|
}
|
||||||
|
if usage.ExtraUsage != nil {
|
||||||
|
t.extra = *usage.ExtraUsage
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Debug().
|
||||||
|
Float64("5h_util", t.fiveHour.Utilization).
|
||||||
|
Time("5h_resets", t.fiveHour.ResetsAt).
|
||||||
|
Int64("5h_tokens_in", t.fiveHour.TokensIn.Load()).
|
||||||
|
Int64("5h_tokens_out", t.fiveHour.TokensOut.Load()).
|
||||||
|
Float64("7d_util", t.sevenDay.Utilization).
|
||||||
|
Time("7d_resets", t.sevenDay.ResetsAt).
|
||||||
|
Int64("7d_tokens_in", t.sevenDay.TokensIn.Load()).
|
||||||
|
Int64("7d_tokens_out", t.sevenDay.TokensOut.Load()).
|
||||||
|
Msg("usage poll")
|
||||||
|
|
||||||
|
// Warn on high utilization
|
||||||
|
if t.fiveHour.Utilization > 80 {
|
||||||
|
log.Warn().Float64("utilization", t.fiveHour.Utilization).Time("resets_at", t.fiveHour.ResetsAt).Msg("5h window utilization high")
|
||||||
|
}
|
||||||
|
if t.sevenDay.Utilization > 80 {
|
||||||
|
log.Warn().Float64("utilization", t.sevenDay.Utilization).Time("resets_at", t.sevenDay.ResetsAt).Msg("7d window utilization high")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Tracker) updateWindow(w *Window, rl *RateLimit, name string) {
|
||||||
|
if rl.Utilization != nil {
|
||||||
|
w.Utilization = *rl.Utilization
|
||||||
|
}
|
||||||
|
if rl.ResetsAt != nil {
|
||||||
|
parsed, err := time.Parse(time.RFC3339Nano, *rl.ResetsAt)
|
||||||
|
if err != nil {
|
||||||
|
// Fallback to RFC3339 without fractional seconds
|
||||||
|
parsed, err = time.Parse(time.RFC3339, *rl.ResetsAt)
|
||||||
|
}
|
||||||
|
parsed = parsed.UTC().Truncate(time.Minute)
|
||||||
|
if err == nil && parsed != w.ResetsAt && !w.ResetsAt.IsZero() {
|
||||||
|
// Window reset detected — zero token counters
|
||||||
|
log.Info().
|
||||||
|
Str("window", name).
|
||||||
|
Int64("prev_tokens_in", w.TokensIn.Load()).
|
||||||
|
Int64("prev_tokens_out", w.TokensOut.Load()).
|
||||||
|
Time("old_reset", w.ResetsAt).
|
||||||
|
Time("new_reset", parsed).
|
||||||
|
Msg("window reset detected")
|
||||||
|
w.TokensIn.Store(0)
|
||||||
|
w.TokensOut.Store(0)
|
||||||
|
}
|
||||||
|
if err == nil {
|
||||||
|
w.ResetsAt = parsed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
package ratelimit
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
const usageURL = "https://api.anthropic.com/api/oauth/usage"
|
||||||
|
|
||||||
|
type RateLimit struct {
|
||||||
|
Utilization *float64 `json:"utilization"` // 0-100
|
||||||
|
ResetsAt *string `json:"resets_at"` // ISO 8601
|
||||||
|
}
|
||||||
|
|
||||||
|
type ExtraUsage struct {
|
||||||
|
IsEnabled bool `json:"is_enabled"`
|
||||||
|
MonthlyLimit *float64 `json:"monthly_limit"`
|
||||||
|
UsedCredits *float64 `json:"used_credits"`
|
||||||
|
Utilization *float64 `json:"utilization"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type UsageResponse struct {
|
||||||
|
FiveHour *RateLimit `json:"five_hour"`
|
||||||
|
SevenDay *RateLimit `json:"seven_day"`
|
||||||
|
SevenDaySonnet *RateLimit `json:"seven_day_sonnet"`
|
||||||
|
ExtraUsage *ExtraUsage `json:"extra_usage"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func fetchUsage(ctx context.Context, token string) (*UsageResponse, error) {
|
||||||
|
ctx, cancel := context.WithTimeout(ctx, 5*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
req, err := http.NewRequestWithContext(ctx, http.MethodGet, usageURL, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("build request: %w", err)
|
||||||
|
}
|
||||||
|
req.Header.Set("Authorization", "Bearer "+token)
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
req.Header.Set("anthropic-beta", "oauth-2025-04-20")
|
||||||
|
req.Header.Set("User-Agent", "claude-cli/2.1.92")
|
||||||
|
|
||||||
|
resp, err := http.DefaultClient.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("request: %w", err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
body, _ := io.ReadAll(resp.Body)
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
return nil, fmt.Errorf("usage returned %d: %s", resp.StatusCode, string(body))
|
||||||
|
}
|
||||||
|
|
||||||
|
var usage UsageResponse
|
||||||
|
if err := json.Unmarshal(body, &usage); err != nil {
|
||||||
|
return nil, fmt.Errorf("decode: %w", err)
|
||||||
|
}
|
||||||
|
return &usage, nil
|
||||||
|
}
|
||||||
@@ -14,6 +14,8 @@ import (
|
|||||||
"github.com/fujin/anthropic-proxy/internal/config"
|
"github.com/fujin/anthropic-proxy/internal/config"
|
||||||
"github.com/fujin/anthropic-proxy/internal/logging"
|
"github.com/fujin/anthropic-proxy/internal/logging"
|
||||||
"github.com/fujin/anthropic-proxy/internal/proxy"
|
"github.com/fujin/anthropic-proxy/internal/proxy"
|
||||||
|
"github.com/fujin/anthropic-proxy/internal/ratelimit"
|
||||||
|
"go.opentelemetry.io/contrib/instrumentation/github.com/gin-gonic/gin/otelgin"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Server struct {
|
type Server struct {
|
||||||
@@ -24,7 +26,7 @@ type Server struct {
|
|||||||
apiKeys atomic.Pointer[map[string]struct{}]
|
apiKeys atomic.Pointer[map[string]struct{}]
|
||||||
}
|
}
|
||||||
|
|
||||||
func New(cfg *config.Config, pool *auth.Pool, profile *proxy.SniffedProfile) *Server {
|
func New(cfg *config.Config, pool *auth.Pool, profile *proxy.SniffedProfile, tracker *ratelimit.Tracker) *Server {
|
||||||
s := &Server{configPath: "config.yaml"}
|
s := &Server{configPath: "config.yaml"}
|
||||||
|
|
||||||
san := proxy.NewSanitizer(cfg.Sanitize)
|
san := proxy.NewSanitizer(cfg.Sanitize)
|
||||||
@@ -37,12 +39,15 @@ func New(cfg *config.Config, pool *auth.Pool, profile *proxy.SniffedProfile) *Se
|
|||||||
engine := gin.New()
|
engine := gin.New()
|
||||||
engine.Use(gin.Recovery())
|
engine.Use(gin.Recovery())
|
||||||
engine.Use(corsMiddleware())
|
engine.Use(corsMiddleware())
|
||||||
|
if cfg.Telemetry.ExportEnabled() {
|
||||||
|
engine.Use(otelgin.Middleware(cfg.Telemetry.ServiceName))
|
||||||
|
}
|
||||||
engine.Use(s.authMiddleware())
|
engine.Use(s.authMiddleware())
|
||||||
engine.Use(logging.GinRequestLogger())
|
engine.Use(logging.GinRequestLogger())
|
||||||
|
|
||||||
handler := proxy.HandleMessages(pool, profile, func() *proxy.Sanitizer {
|
handler := proxy.HandleMessages(pool, profile, func() *proxy.Sanitizer {
|
||||||
return s.sanitizer.Load()
|
return s.sanitizer.Load()
|
||||||
})
|
}, tracker)
|
||||||
engine.POST("/v1/messages", handler)
|
engine.POST("/v1/messages", handler)
|
||||||
engine.POST("/messages", handler)
|
engine.POST("/messages", handler)
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,81 @@
|
|||||||
|
package telemetry
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
otellog "go.opentelemetry.io/otel/log"
|
||||||
|
sdklog "go.opentelemetry.io/otel/sdk/log"
|
||||||
|
)
|
||||||
|
|
||||||
|
// LogBridge implements io.Writer and forwards zerolog JSON lines to the
|
||||||
|
// OTel LoggerProvider. It is used as an extra writer in zerolog's MultiWriter
|
||||||
|
// so that logs go to both file and OTLP.
|
||||||
|
type LogBridge struct {
|
||||||
|
provider *sdklog.LoggerProvider
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *LogBridge) Write(p []byte) (n int, err error) {
|
||||||
|
var entry map[string]interface{}
|
||||||
|
if err := json.Unmarshal(p, &entry); err != nil {
|
||||||
|
return len(p), nil // skip malformed lines
|
||||||
|
}
|
||||||
|
|
||||||
|
logger := b.provider.Logger("zerolog")
|
||||||
|
|
||||||
|
var rec otellog.Record
|
||||||
|
rec.SetTimestamp(time.Now())
|
||||||
|
|
||||||
|
if msg, ok := entry["message"].(string); ok {
|
||||||
|
rec.SetBody(otellog.StringValue(msg))
|
||||||
|
}
|
||||||
|
|
||||||
|
if lvl, ok := entry["level"].(string); ok {
|
||||||
|
rec.SetSeverity(mapSeverity(lvl))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Forward all fields as attributes
|
||||||
|
attrs := make([]otellog.KeyValue, 0, len(entry))
|
||||||
|
for k, v := range entry {
|
||||||
|
if k == "message" || k == "level" || k == "time" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
switch val := v.(type) {
|
||||||
|
case string:
|
||||||
|
attrs = append(attrs, otellog.String(k, val))
|
||||||
|
case float64:
|
||||||
|
attrs = append(attrs, otellog.Float64(k, val))
|
||||||
|
case bool:
|
||||||
|
attrs = append(attrs, otellog.Bool(k, val))
|
||||||
|
default:
|
||||||
|
b, _ := json.Marshal(val)
|
||||||
|
attrs = append(attrs, otellog.String(k, string(b)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
rec.AddAttributes(attrs...)
|
||||||
|
|
||||||
|
logger.Emit(context.Background(), rec)
|
||||||
|
return len(p), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func mapSeverity(level string) otellog.Severity {
|
||||||
|
switch level {
|
||||||
|
case "trace":
|
||||||
|
return otellog.SeverityTrace
|
||||||
|
case "debug":
|
||||||
|
return otellog.SeverityDebug
|
||||||
|
case "info":
|
||||||
|
return otellog.SeverityInfo
|
||||||
|
case "warn", "warning":
|
||||||
|
return otellog.SeverityWarn
|
||||||
|
case "error":
|
||||||
|
return otellog.SeverityError
|
||||||
|
case "fatal":
|
||||||
|
return otellog.SeverityFatal
|
||||||
|
case "panic":
|
||||||
|
return otellog.SeverityFatal2
|
||||||
|
default:
|
||||||
|
return otellog.SeverityInfo
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,102 @@
|
|||||||
|
package telemetry
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"go.opentelemetry.io/otel/attribute"
|
||||||
|
"go.opentelemetry.io/otel/metric"
|
||||||
|
|
||||||
|
"github.com/fujin/anthropic-proxy/internal/ratelimit"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
RequestCounter metric.Int64Counter
|
||||||
|
RequestDuration metric.Float64Histogram
|
||||||
|
RequestBodySize metric.Int64Histogram
|
||||||
|
UpstreamErrors metric.Int64Counter
|
||||||
|
TokensInput metric.Int64Counter
|
||||||
|
TokensOutput metric.Int64Counter
|
||||||
|
CredentialCooldowns metric.Int64Counter
|
||||||
|
ActiveCredentials metric.Int64UpDownCounter
|
||||||
|
StreamRequests metric.Int64Counter
|
||||||
|
)
|
||||||
|
|
||||||
|
// InitMetrics creates all metric instruments from the given meter.
|
||||||
|
// If tracker is non-nil, registers observable gauges for per-window usage.
|
||||||
|
func InitMetrics(meter metric.Meter, tracker *ratelimit.Tracker) {
|
||||||
|
RequestCounter, _ = meter.Int64Counter("proxy.request.count",
|
||||||
|
metric.WithDescription("Total proxy requests"),
|
||||||
|
)
|
||||||
|
RequestDuration, _ = meter.Float64Histogram("proxy.request.duration_ms",
|
||||||
|
metric.WithDescription("Request latency in milliseconds"),
|
||||||
|
metric.WithUnit("ms"),
|
||||||
|
)
|
||||||
|
RequestBodySize, _ = meter.Int64Histogram("proxy.request.body_size_bytes",
|
||||||
|
metric.WithDescription("Request body size in bytes"),
|
||||||
|
metric.WithUnit("By"),
|
||||||
|
)
|
||||||
|
UpstreamErrors, _ = meter.Int64Counter("proxy.upstream.errors",
|
||||||
|
metric.WithDescription("Upstream error count"),
|
||||||
|
)
|
||||||
|
TokensInput, _ = meter.Int64Counter("proxy.tokens.input",
|
||||||
|
metric.WithDescription("Input tokens consumed"),
|
||||||
|
)
|
||||||
|
TokensOutput, _ = meter.Int64Counter("proxy.tokens.output",
|
||||||
|
metric.WithDescription("Output tokens consumed"),
|
||||||
|
)
|
||||||
|
CredentialCooldowns, _ = meter.Int64Counter("proxy.credential.cooldowns",
|
||||||
|
metric.WithDescription("Credential cooldown activations"),
|
||||||
|
)
|
||||||
|
ActiveCredentials, _ = meter.Int64UpDownCounter("proxy.credential.active",
|
||||||
|
metric.WithDescription("Currently active (non-cooldown) credentials"),
|
||||||
|
)
|
||||||
|
StreamRequests, _ = meter.Int64Counter("proxy.stream.requests",
|
||||||
|
metric.WithDescription("Streaming request count"),
|
||||||
|
)
|
||||||
|
|
||||||
|
if tracker == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
attr5h := attribute.String("window", "5h")
|
||||||
|
attr7d := attribute.String("window", "7d")
|
||||||
|
attrSonnet := attribute.String("window", "7d_sonnet")
|
||||||
|
|
||||||
|
meter.Float64ObservableGauge("proxy.usage.utilization",
|
||||||
|
metric.WithDescription("Current utilization % from API"),
|
||||||
|
metric.WithFloat64Callback(func(_ context.Context, o metric.Float64Observer) error {
|
||||||
|
o.Observe(tracker.FiveHour().Utilization, metric.WithAttributes(attr5h))
|
||||||
|
o.Observe(tracker.SevenDay().Utilization, metric.WithAttributes(attr7d))
|
||||||
|
o.Observe(tracker.Sonnet().Utilization, metric.WithAttributes(attrSonnet))
|
||||||
|
return nil
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
meter.Int64ObservableGauge("proxy.usage.resets_at",
|
||||||
|
metric.WithDescription("Unix seconds when window resets"),
|
||||||
|
metric.WithInt64Callback(func(_ context.Context, o metric.Int64Observer) error {
|
||||||
|
o.Observe(tracker.FiveHour().ResetsAt.Unix(), metric.WithAttributes(attr5h))
|
||||||
|
o.Observe(tracker.SevenDay().ResetsAt.Unix(), metric.WithAttributes(attr7d))
|
||||||
|
o.Observe(tracker.Sonnet().ResetsAt.Unix(), metric.WithAttributes(attrSonnet))
|
||||||
|
return nil
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
meter.Int64ObservableGauge("proxy.usage.window_tokens.input",
|
||||||
|
metric.WithDescription("Proxy input tokens in current window"),
|
||||||
|
metric.WithInt64Callback(func(_ context.Context, o metric.Int64Observer) error {
|
||||||
|
o.Observe(tracker.FiveHour().TokensIn, metric.WithAttributes(attr5h))
|
||||||
|
o.Observe(tracker.SevenDay().TokensIn, metric.WithAttributes(attr7d))
|
||||||
|
return nil
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
meter.Int64ObservableGauge("proxy.usage.window_tokens.output",
|
||||||
|
metric.WithDescription("Proxy output tokens in current window"),
|
||||||
|
metric.WithInt64Callback(func(_ context.Context, o metric.Int64Observer) error {
|
||||||
|
o.Observe(tracker.FiveHour().TokensOut, metric.WithAttributes(attr5h))
|
||||||
|
o.Observe(tracker.SevenDay().TokensOut, metric.WithAttributes(attr7d))
|
||||||
|
return nil
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,108 @@
|
|||||||
|
package telemetry
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"io"
|
||||||
|
|
||||||
|
"github.com/fujin/anthropic-proxy/internal/config"
|
||||||
|
"github.com/fujin/anthropic-proxy/internal/ratelimit"
|
||||||
|
"go.opentelemetry.io/otel"
|
||||||
|
"go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc"
|
||||||
|
"go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc"
|
||||||
|
"go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc"
|
||||||
|
otellog "go.opentelemetry.io/otel/log/global"
|
||||||
|
"go.opentelemetry.io/otel/sdk/log"
|
||||||
|
sdkmetric "go.opentelemetry.io/otel/sdk/metric"
|
||||||
|
"go.opentelemetry.io/otel/sdk/resource"
|
||||||
|
"go.opentelemetry.io/otel/sdk/trace"
|
||||||
|
semconv "go.opentelemetry.io/otel/semconv/v1.26.0"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Setup initializes OpenTelemetry providers. It always creates a MeterProvider
|
||||||
|
// so metrics can be recorded in-process. When cfg.ExportEnabled(), OTLP gRPC
|
||||||
|
// exporters are additionally configured to push to the LGTM stack.
|
||||||
|
// Returns a shutdown function and an optional io.Writer for the log bridge.
|
||||||
|
func Setup(ctx context.Context, cfg config.TelemetryConfig, tracker *ratelimit.Tracker) (shutdown func(context.Context) error, logWriter io.Writer, err error) {
|
||||||
|
res, err := resource.New(ctx,
|
||||||
|
resource.WithAttributes(
|
||||||
|
semconv.ServiceName(cfg.ServiceName),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if !cfg.ExportEnabled() {
|
||||||
|
// No export — set up in-memory meter provider only so metric
|
||||||
|
// instruments are valid (they just don't export anywhere).
|
||||||
|
mp := sdkmetric.NewMeterProvider(sdkmetric.WithResource(res))
|
||||||
|
otel.SetMeterProvider(mp)
|
||||||
|
InitMetrics(mp.Meter(cfg.ServiceName), tracker)
|
||||||
|
return func(ctx context.Context) error { return mp.Shutdown(ctx) }, nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build exporter options
|
||||||
|
traceOpts := []otlptracegrpc.Option{otlptracegrpc.WithEndpoint(cfg.Endpoint)}
|
||||||
|
metricOpts := []otlpmetricgrpc.Option{
|
||||||
|
otlpmetricgrpc.WithEndpoint(cfg.Endpoint),
|
||||||
|
otlpmetricgrpc.WithTemporalitySelector(sdkmetric.CumulativeTemporalitySelector),
|
||||||
|
}
|
||||||
|
logOpts := []otlploggrpc.Option{otlploggrpc.WithEndpoint(cfg.Endpoint)}
|
||||||
|
if cfg.Insecure {
|
||||||
|
traceOpts = append(traceOpts, otlptracegrpc.WithInsecure())
|
||||||
|
metricOpts = append(metricOpts, otlpmetricgrpc.WithInsecure())
|
||||||
|
logOpts = append(logOpts, otlploggrpc.WithInsecure())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Trace exporter
|
||||||
|
traceExp, err := otlptracegrpc.New(ctx, traceOpts...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
tp := trace.NewTracerProvider(
|
||||||
|
trace.WithBatcher(traceExp),
|
||||||
|
trace.WithResource(res),
|
||||||
|
)
|
||||||
|
otel.SetTracerProvider(tp)
|
||||||
|
|
||||||
|
// Metric exporter
|
||||||
|
metricExp, err := otlpmetricgrpc.New(ctx, metricOpts...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
mp := sdkmetric.NewMeterProvider(
|
||||||
|
sdkmetric.WithReader(sdkmetric.NewPeriodicReader(metricExp)),
|
||||||
|
sdkmetric.WithResource(res),
|
||||||
|
)
|
||||||
|
otel.SetMeterProvider(mp)
|
||||||
|
InitMetrics(mp.Meter(cfg.ServiceName), tracker)
|
||||||
|
|
||||||
|
// Log exporter
|
||||||
|
logExp, err := otlploggrpc.New(ctx, logOpts...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
lp := log.NewLoggerProvider(
|
||||||
|
log.WithProcessor(log.NewBatchProcessor(logExp)),
|
||||||
|
log.WithResource(res),
|
||||||
|
)
|
||||||
|
otellog.SetLoggerProvider(lp)
|
||||||
|
|
||||||
|
bridge := &LogBridge{provider: lp}
|
||||||
|
|
||||||
|
shutdownFn := func(ctx context.Context) error {
|
||||||
|
var firstErr error
|
||||||
|
if e := tp.Shutdown(ctx); e != nil && firstErr == nil {
|
||||||
|
firstErr = e
|
||||||
|
}
|
||||||
|
if e := mp.Shutdown(ctx); e != nil && firstErr == nil {
|
||||||
|
firstErr = e
|
||||||
|
}
|
||||||
|
if e := lp.Shutdown(ctx); e != nil && firstErr == nil {
|
||||||
|
firstErr = e
|
||||||
|
}
|
||||||
|
return firstErr
|
||||||
|
}
|
||||||
|
|
||||||
|
return shutdownFn, bridge, nil
|
||||||
|
}
|
||||||
@@ -3,6 +3,7 @@ package main
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"os/signal"
|
"os/signal"
|
||||||
@@ -13,7 +14,9 @@ import (
|
|||||||
"github.com/fujin/anthropic-proxy/internal/config"
|
"github.com/fujin/anthropic-proxy/internal/config"
|
||||||
"github.com/fujin/anthropic-proxy/internal/logging"
|
"github.com/fujin/anthropic-proxy/internal/logging"
|
||||||
"github.com/fujin/anthropic-proxy/internal/proxy"
|
"github.com/fujin/anthropic-proxy/internal/proxy"
|
||||||
|
"github.com/fujin/anthropic-proxy/internal/ratelimit"
|
||||||
"github.com/fujin/anthropic-proxy/internal/server"
|
"github.com/fujin/anthropic-proxy/internal/server"
|
||||||
|
"github.com/fujin/anthropic-proxy/internal/telemetry"
|
||||||
"github.com/rs/zerolog/log"
|
"github.com/rs/zerolog/log"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -23,6 +26,27 @@ func run() error {
|
|||||||
return fmt.Errorf("load config: %w", err)
|
return fmt.Errorf("load config: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Create usage tracker (started later once credential is loaded)
|
||||||
|
var credForTracker *auth.Credential
|
||||||
|
tracker := ratelimit.NewTracker(func() string {
|
||||||
|
if credForTracker == nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return credForTracker.Token()
|
||||||
|
})
|
||||||
|
|
||||||
|
// Initialize telemetry (metrics always active; OTLP export when endpoint set)
|
||||||
|
telemetryShutdown, logBridge, err := telemetry.Setup(context.Background(), cfg.Telemetry, tracker)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("telemetry setup: %w", err)
|
||||||
|
}
|
||||||
|
defer telemetryShutdown(context.Background())
|
||||||
|
|
||||||
|
var extraWriters []io.Writer
|
||||||
|
if logBridge != nil {
|
||||||
|
extraWriters = append(extraWriters, logBridge)
|
||||||
|
}
|
||||||
|
|
||||||
logging.Setup(logging.Config{
|
logging.Setup(logging.Config{
|
||||||
Level: cfg.Logging.Level,
|
Level: cfg.Logging.Level,
|
||||||
File: cfg.Logging.File,
|
File: cfg.Logging.File,
|
||||||
@@ -30,7 +54,7 @@ func run() error {
|
|||||||
MaxBackups: cfg.Logging.MaxBackups,
|
MaxBackups: cfg.Logging.MaxBackups,
|
||||||
MaxAgeDays: cfg.Logging.MaxAgeDays,
|
MaxAgeDays: cfg.Logging.MaxAgeDays,
|
||||||
Compress: cfg.Logging.Compress,
|
Compress: cfg.Logging.Compress,
|
||||||
})
|
}, extraWriters...)
|
||||||
|
|
||||||
// Load credentials from ~/.claude/.credentials.json
|
// Load credentials from ~/.claude/.credentials.json
|
||||||
creds, err := config.LoadDefaultCredentials()
|
creds, err := config.LoadDefaultCredentials()
|
||||||
@@ -71,6 +95,8 @@ func run() error {
|
|||||||
|
|
||||||
log.Info().Str("credential", cred.Email).Msg("credential loaded")
|
log.Info().Str("credential", cred.Email).Msg("credential loaded")
|
||||||
|
|
||||||
|
credForTracker = cred
|
||||||
|
|
||||||
pool := auth.NewPool([]*auth.Credential{cred})
|
pool := auth.NewPool([]*auth.Credential{cred})
|
||||||
|
|
||||||
ctx, cancel := context.WithCancel(context.Background())
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
@@ -78,6 +104,7 @@ func run() error {
|
|||||||
|
|
||||||
pool.RefreshExpiring(context.Background())
|
pool.RefreshExpiring(context.Background())
|
||||||
auth.StartBackgroundRefresh(ctx, pool)
|
auth.StartBackgroundRefresh(ctx, pool)
|
||||||
|
tracker.Start(ctx)
|
||||||
|
|
||||||
var profile *proxy.SniffedProfile
|
var profile *proxy.SniffedProfile
|
||||||
if cfg.ClaudeBinary != "" {
|
if cfg.ClaudeBinary != "" {
|
||||||
@@ -89,7 +116,7 @@ func run() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
log.Info().Int("port", cfg.Port).Msg("starting server")
|
log.Info().Int("port", cfg.Port).Msg("starting server")
|
||||||
srv := server.New(cfg, pool, profile)
|
srv := server.New(cfg, pool, profile, tracker)
|
||||||
|
|
||||||
quit := make(chan os.Signal, 1)
|
quit := make(chan os.Signal, 1)
|
||||||
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
|
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
|
||||||
|
|||||||
+7
-2
@@ -1,4 +1,9 @@
|
|||||||
{ buildGoModule, lib, pkgs, ... }:
|
{
|
||||||
|
buildGoModule,
|
||||||
|
lib,
|
||||||
|
pkgs,
|
||||||
|
...
|
||||||
|
}:
|
||||||
|
|
||||||
buildGoModule rec {
|
buildGoModule rec {
|
||||||
pname = "anthropic-proxy";
|
pname = "anthropic-proxy";
|
||||||
@@ -6,7 +11,7 @@ buildGoModule rec {
|
|||||||
|
|
||||||
src = ./.;
|
src = ./.;
|
||||||
|
|
||||||
vendorHash = "sha256-QUb/DIX3x/MBZd3srF0hV2x/o0wf7zNzj2SEhTIpq58=";
|
vendorHash = "sha256-8pq4GYFjOfYcYLcZSuXMWn77RUxVGP18AcyzIJGbKf4=";
|
||||||
|
|
||||||
meta = with lib; {
|
meta = with lib; {
|
||||||
description = "Reverse proxy that lets OpenCode (and similar tools) use a Claude subscription instead of an API key.";
|
description = "Reverse proxy that lets OpenCode (and similar tools) use a Claude subscription instead of an API key.";
|
||||||
|
|||||||
Reference in New Issue
Block a user