Merge remote-tracking branch 'origin/dev-next' into dev-next
# Conflicts: # go.sum
This commit is contained in:
Submodule clients/android updated: 4bdde0ae4d...e71b426090
Submodule clients/apple updated: 015dcd266b...b8979ea9ee
@@ -2,9 +2,9 @@
|
||||
icon: material/alert-decagram
|
||||
---
|
||||
|
||||
#### 1.13.0-rc.6
|
||||
#### 1.13.0-rc.7
|
||||
|
||||
* Fixes and improvements
|
||||
* Add advertise tags support for Tailscale endpoint
|
||||
|
||||
Important changes since 1.12:
|
||||
|
||||
@@ -136,6 +136,7 @@ See [Dial Fields](/configuration/shared/dial/#bind_address_no_port).
|
||||
|
||||
Tailscale endpoint can now create a system TUN interface to handle traffic directly.
|
||||
New `relay_server_port` and `relay_server_static_endpoints` options for incoming relay connections.
|
||||
New `advertise_tags` option for ACL tag advertisement.
|
||||
|
||||
See [Tailscale endpoint](/configuration/endpoint/tailscale/).
|
||||
|
||||
|
||||
@@ -8,7 +8,8 @@ icon: material/new-box
|
||||
:material-plus: [relay_server_static_endpoints](#relay_server_static_endpoints)
|
||||
:material-plus: [system_interface](#system_interface)
|
||||
:material-plus: [system_interface_name](#system_interface_name)
|
||||
:material-plus: [system_interface_mtu](#system_interface_mtu)
|
||||
:material-plus: [system_interface_mtu](#system_interface_mtu)
|
||||
:material-plus: [advertise_tags](#advertise_tags)
|
||||
|
||||
!!! question "Since sing-box 1.12.0"
|
||||
|
||||
@@ -28,6 +29,7 @@ icon: material/new-box
|
||||
"exit_node_allow_lan_access": false,
|
||||
"advertise_routes": [],
|
||||
"advertise_exit_node": false,
|
||||
"advertise_tags": [],
|
||||
"relay_server_port": 0,
|
||||
"relay_server_static_endpoints": [],
|
||||
"system_interface": false,
|
||||
@@ -102,6 +104,14 @@ Example: `["192.168.1.1/24"]`
|
||||
|
||||
Indicates whether the node should advertise itself as an exit node.
|
||||
|
||||
#### advertise_tags
|
||||
|
||||
!!! question "Since sing-box 1.13.0"
|
||||
|
||||
Tags to advertise for this node, for ACL enforcement purposes.
|
||||
|
||||
Example: `["tag:server"]`
|
||||
|
||||
#### relay_server_port
|
||||
|
||||
!!! question "Since sing-box 1.13.0"
|
||||
|
||||
@@ -8,7 +8,8 @@ icon: material/new-box
|
||||
:material-plus: [relay_server_static_endpoints](#relay_server_static_endpoints)
|
||||
:material-plus: [system_interface](#system_interface)
|
||||
:material-plus: [system_interface_name](#system_interface_name)
|
||||
:material-plus: [system_interface_mtu](#system_interface_mtu)
|
||||
:material-plus: [system_interface_mtu](#system_interface_mtu)
|
||||
:material-plus: [advertise_tags](#advertise_tags)
|
||||
|
||||
!!! question "自 sing-box 1.12.0 起"
|
||||
|
||||
@@ -28,6 +29,7 @@ icon: material/new-box
|
||||
"exit_node_allow_lan_access": false,
|
||||
"advertise_routes": [],
|
||||
"advertise_exit_node": false,
|
||||
"advertise_tags": [],
|
||||
"relay_server_port": 0,
|
||||
"relay_server_static_endpoints": [],
|
||||
"system_interface": false,
|
||||
@@ -101,6 +103,14 @@ icon: material/new-box
|
||||
|
||||
指示节点是否应将自己通告为出口节点。
|
||||
|
||||
#### advertise_tags
|
||||
|
||||
!!! question "自 sing-box 1.13.0 起"
|
||||
|
||||
为此节点通告的标签,用于 ACL 执行。
|
||||
|
||||
示例:`["tag:server"]`
|
||||
|
||||
#### relay_server_port
|
||||
|
||||
!!! question "自 sing-box 1.13.0 起"
|
||||
|
||||
6
go.mod
6
go.mod
@@ -3,7 +3,7 @@ module github.com/sagernet/sing-box
|
||||
go 1.24.7
|
||||
|
||||
require (
|
||||
github.com/anthropics/anthropic-sdk-go v1.19.0
|
||||
github.com/anthropics/anthropic-sdk-go v1.26.0
|
||||
github.com/anytls/sing-anytls v0.0.11
|
||||
github.com/caddyserver/certmagic v0.25.0
|
||||
github.com/coder/websocket v1.8.14
|
||||
@@ -23,7 +23,7 @@ require (
|
||||
github.com/metacubex/utls v1.8.4
|
||||
github.com/mholt/acmez/v3 v3.1.4
|
||||
github.com/miekg/dns v1.1.69
|
||||
github.com/openai/openai-go/v3 v3.15.0
|
||||
github.com/openai/openai-go/v3 v3.23.0
|
||||
github.com/oschwald/maxminddb-golang v1.13.1
|
||||
github.com/sagernet/asc-go v0.0.0-20241217030726-d563060fe4e1
|
||||
github.com/sagernet/bbolt v0.0.0-20231014093535-ea5cb2fe9f0a
|
||||
@@ -44,7 +44,7 @@ require (
|
||||
github.com/sagernet/sing-vmess v0.2.8-0.20250909125414-3aed155119a1
|
||||
github.com/sagernet/smux v1.5.50-sing-box-mod.1
|
||||
github.com/sagernet/tailscale v1.92.4-sing-box-1.13-mod.6
|
||||
github.com/sagernet/wireguard-go v0.0.2-beta.1.0.20250917110311-16510ac47288
|
||||
github.com/sagernet/wireguard-go v0.0.2-beta.1.0.20260224074747-506b7631853c
|
||||
github.com/sagernet/ws v0.0.0-20231204124109-acfe8907c854
|
||||
github.com/spf13/cobra v1.10.2
|
||||
github.com/stretchr/testify v1.11.1
|
||||
|
||||
18
go.sum
18
go.sum
@@ -8,8 +8,8 @@ github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa h1:LHTHcTQiSGT7V
|
||||
github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa/go.mod h1:cEWa1LVoE5KvSD9ONXsZrj0z6KqySlCCNKHlLzbqAt4=
|
||||
github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M=
|
||||
github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY=
|
||||
github.com/anthropics/anthropic-sdk-go v1.19.0 h1:mO6E+ffSzLRvR/YUH9KJC0uGw0uV8GjISIuzem//3KE=
|
||||
github.com/anthropics/anthropic-sdk-go v1.19.0/go.mod h1:WTz31rIUHUHqai2UslPpw5CwXrQP3geYBioRV4WOLvE=
|
||||
github.com/anthropics/anthropic-sdk-go v1.26.0 h1:oUTzFaUpAevfuELAP1sjL6CQJ9HHAfT7CoSYSac11PY=
|
||||
github.com/anthropics/anthropic-sdk-go v1.26.0/go.mod h1:qUKmaW+uuPB64iy1l+4kOSvaLqPXnHTTBKH6RVZ7q5Q=
|
||||
github.com/anytls/sing-anytls v0.0.11 h1:w8e9Uj1oP3m4zxkyZDewPk0EcQbvVxb7Nn+rapEx4fc=
|
||||
github.com/anytls/sing-anytls v0.0.11/go.mod h1:7rjN6IukwysmdusYsrV51Fgu1uW6vsrdd6ctjnEAln8=
|
||||
github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
|
||||
@@ -40,6 +40,8 @@ github.com/dblohm7/wingoes v0.0.0-20240119213807-a09d6be7affa h1:h8TfIT1xc8FWbww
|
||||
github.com/dblohm7/wingoes v0.0.0-20240119213807-a09d6be7affa/go.mod h1:Nx87SkVqTKd8UtT+xu7sM/l+LgXs6c0aHrlKusR+2EQ=
|
||||
github.com/dgrijalva/jwt-go/v4 v4.0.0-preview1 h1:CaO/zOnF8VvUfEbhRatPcwKVWamvbYd8tQGRWacE9kU=
|
||||
github.com/dgrijalva/jwt-go/v4 v4.0.0-preview1/go.mod h1:+hnT3ywWDTAFrW5aE+u2Sa/wT555ZqwoCS+pk3p6ry4=
|
||||
github.com/dnaeon/go-vcr v1.2.0 h1:zHCHvJYTMh1N7xnV7zf1m1GPBF9Ad0Jk/whtQ1663qI=
|
||||
github.com/dnaeon/go-vcr v1.2.0/go.mod h1:R4UdLID7HZT3taECzJs4YgbbH6PIGXB6W/sc5OLb6RQ=
|
||||
github.com/ebitengine/purego v0.9.1 h1:a/k2f2HQU3Pi399RPW1MOaZyhKJL9w/xFpKAg4q1s0A=
|
||||
github.com/ebitengine/purego v0.9.1/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
|
||||
github.com/florianl/go-nfqueue/v2 v2.0.2 h1:FL5lQTeetgpCvac1TRwSfgaXUn0YSO7WzGvWNIp3JPE=
|
||||
@@ -141,8 +143,8 @@ github.com/mitchellh/go-ps v1.0.0 h1:i6ampVEEF4wQFF+bkYfwYgY+F/uYJDktmvLPf7qIgjc
|
||||
github.com/mitchellh/go-ps v1.0.0/go.mod h1:J4lOc8z8yJs6vUwklHw2XEIiT4z4C40KtWVN3nvg8Pg=
|
||||
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 h1:zYyBkD/k9seD2A7fsi6Oo2LfFZAehjjQMERAvZLEDnQ=
|
||||
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8=
|
||||
github.com/openai/openai-go/v3 v3.15.0 h1:hk99rM7YPz+M99/5B/zOQcVwFRLLMdprVGx1vaZ8XMo=
|
||||
github.com/openai/openai-go/v3 v3.15.0/go.mod h1:cdufnVK14cWcT9qA1rRtrXx4FTRsgbDPW7Ia7SS5cZo=
|
||||
github.com/openai/openai-go/v3 v3.23.0 h1:FRFwTcB4FoWFtIunTY/8fgHvzSHgqbfWjiCwOMVrsvw=
|
||||
github.com/openai/openai-go/v3 v3.23.0/go.mod h1:cdufnVK14cWcT9qA1rRtrXx4FTRsgbDPW7Ia7SS5cZo=
|
||||
github.com/oschwald/maxminddb-golang v1.13.1 h1:G3wwjdN9JmIK2o/ermkHM+98oX5fS+k5MbwsmL4MRQE=
|
||||
github.com/oschwald/maxminddb-golang v1.13.1/go.mod h1:K4pgV9N/GcK694KSTmVSDTODk4IsCNThNdTmnaBZ/F8=
|
||||
github.com/pierrec/lz4/v4 v4.1.21 h1:yOVMLb6qSIDP67pl/5F7RepeKYu/VmTyEXvuMI5d9mQ=
|
||||
@@ -261,8 +263,6 @@ github.com/sagernet/sing-shadowsocks2 v0.2.1 h1:dWV9OXCeFPuYGHb6IRqlSptVnSzOelnq
|
||||
github.com/sagernet/sing-shadowsocks2 v0.2.1/go.mod h1:RnXS0lExcDAovvDeniJ4IKa2IuChrdipolPYWBv9hWQ=
|
||||
github.com/sagernet/sing-shadowtls v0.2.1-0.20250503051639-fcd445d33c11 h1:tK+75l64tm9WvEFrYRE1t0YxoFdWQqw/h7Uhzj0vJ+w=
|
||||
github.com/sagernet/sing-shadowtls v0.2.1-0.20250503051639-fcd445d33c11/go.mod h1:sWqKnGlMipCHaGsw1sTTlimyUpgzP4WP3pjhCsYt9oA=
|
||||
github.com/sagernet/sing-tun v0.8.0-beta.17.0.20260223095246-5715a3919a7f h1:3B8auqVameLvPhWxzw5S8QWdLTv19o0M8LYxPOXNm0g=
|
||||
github.com/sagernet/sing-tun v0.8.0-beta.17.0.20260223095246-5715a3919a7f/go.mod h1:+HAK/y9GZljdT0KYKMYDR8MjjqnqDDQZYp5ZZQoRzS8=
|
||||
github.com/sagernet/sing-tun v0.8.0-beta.18 h1:C6oHxP9BNBVEVdC9ABMTXmKej9mUVtcuw2v+IiBS8yw=
|
||||
github.com/sagernet/sing-tun v0.8.0-beta.18/go.mod h1:+HAK/y9GZljdT0KYKMYDR8MjjqnqDDQZYp5ZZQoRzS8=
|
||||
github.com/sagernet/sing-vmess v0.2.8-0.20250909125414-3aed155119a1 h1:aSwUNYUkVyVvdmBSufR8/nRFonwJeKSIROxHcm5br9o=
|
||||
@@ -271,8 +271,8 @@ github.com/sagernet/smux v1.5.50-sing-box-mod.1 h1:XkJcivBC9V4wBjiGXIXZ229aZCU1h
|
||||
github.com/sagernet/smux v1.5.50-sing-box-mod.1/go.mod h1:NjhsCEWedJm7eFLyhuBgIEzwfhRmytrUoiLluxs5Sk8=
|
||||
github.com/sagernet/tailscale v1.92.4-sing-box-1.13-mod.6 h1:eYz/OpMqWCvO2++iw3dEuzrlfC2xv78GdlGvprIM6O8=
|
||||
github.com/sagernet/tailscale v1.92.4-sing-box-1.13-mod.6/go.mod h1:m87GAn4UcesHQF3leaPFEINZETO5za1LGn1GJdNDgNc=
|
||||
github.com/sagernet/wireguard-go v0.0.2-beta.1.0.20250917110311-16510ac47288 h1:E2tZFeg9mGYGQ7E7BbxMv1cU35HxwgRm6tPKI2Pp7DA=
|
||||
github.com/sagernet/wireguard-go v0.0.2-beta.1.0.20250917110311-16510ac47288/go.mod h1:WUxgxUDZoCF2sxVmW+STSxatP02Qn3FcafTiI2BLtE0=
|
||||
github.com/sagernet/wireguard-go v0.0.2-beta.1.0.20260224074747-506b7631853c h1:f9cXNB+IOOPnR8DOLMTpr42jf7naxh5Un5Y09BBf5Cg=
|
||||
github.com/sagernet/wireguard-go v0.0.2-beta.1.0.20260224074747-506b7631853c/go.mod h1:WUxgxUDZoCF2sxVmW+STSxatP02Qn3FcafTiI2BLtE0=
|
||||
github.com/sagernet/ws v0.0.0-20231204124109-acfe8907c854 h1:6uUiZcDRnZSAegryaUGwPC/Fj13JSHwiTftrXhMmYOc=
|
||||
github.com/sagernet/ws v0.0.0-20231204124109-acfe8907c854/go.mod h1:LtfoSK3+NG57tvnVEHgcuBW9ujgE8enPSgzgwStwCAA=
|
||||
github.com/shopspring/decimal v1.2.0 h1:abSATXmQEYyShuxI4/vyW3tV1MrKAJzCZ/0zLUXYbsQ=
|
||||
@@ -434,6 +434,8 @@ gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8
|
||||
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.v1 v1.0.0-20140924161607-9f9df34309c0/go.mod h1:WDnlLJ4WF5VGsH/HVa3CI79GS0ol3YnhVnKP89i0kNg=
|
||||
gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10=
|
||||
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
|
||||
@@ -12,22 +12,23 @@ import (
|
||||
|
||||
type TailscaleEndpointOptions struct {
|
||||
DialerOptions
|
||||
StateDirectory string `json:"state_directory,omitempty"`
|
||||
AuthKey string `json:"auth_key,omitempty"`
|
||||
ControlURL string `json:"control_url,omitempty"`
|
||||
Ephemeral bool `json:"ephemeral,omitempty"`
|
||||
Hostname string `json:"hostname,omitempty"`
|
||||
AcceptRoutes bool `json:"accept_routes,omitempty"`
|
||||
ExitNode string `json:"exit_node,omitempty"`
|
||||
ExitNodeAllowLANAccess bool `json:"exit_node_allow_lan_access,omitempty"`
|
||||
AdvertiseRoutes []netip.Prefix `json:"advertise_routes,omitempty"`
|
||||
AdvertiseExitNode bool `json:"advertise_exit_node,omitempty"`
|
||||
RelayServerPort *uint16 `json:"relay_server_port,omitempty"`
|
||||
RelayServerStaticEndpoints []netip.AddrPort `json:"relay_server_static_endpoints,omitempty"`
|
||||
SystemInterface bool `json:"system_interface,omitempty"`
|
||||
SystemInterfaceName string `json:"system_interface_name,omitempty"`
|
||||
SystemInterfaceMTU uint32 `json:"system_interface_mtu,omitempty"`
|
||||
UDPTimeout UDPTimeoutCompat `json:"udp_timeout,omitempty"`
|
||||
StateDirectory string `json:"state_directory,omitempty"`
|
||||
AuthKey string `json:"auth_key,omitempty"`
|
||||
ControlURL string `json:"control_url,omitempty"`
|
||||
Ephemeral bool `json:"ephemeral,omitempty"`
|
||||
Hostname string `json:"hostname,omitempty"`
|
||||
AcceptRoutes bool `json:"accept_routes,omitempty"`
|
||||
ExitNode string `json:"exit_node,omitempty"`
|
||||
ExitNodeAllowLANAccess bool `json:"exit_node_allow_lan_access,omitempty"`
|
||||
AdvertiseRoutes []netip.Prefix `json:"advertise_routes,omitempty"`
|
||||
AdvertiseExitNode bool `json:"advertise_exit_node,omitempty"`
|
||||
AdvertiseTags badoption.Listable[string] `json:"advertise_tags,omitempty"`
|
||||
RelayServerPort *uint16 `json:"relay_server_port,omitempty"`
|
||||
RelayServerStaticEndpoints []netip.AddrPort `json:"relay_server_static_endpoints,omitempty"`
|
||||
SystemInterface bool `json:"system_interface,omitempty"`
|
||||
SystemInterfaceName string `json:"system_interface_name,omitempty"`
|
||||
SystemInterfaceMTU uint32 `json:"system_interface_mtu,omitempty"`
|
||||
UDPTimeout UDPTimeoutCompat `json:"udp_timeout,omitempty"`
|
||||
}
|
||||
|
||||
type TailscaleDNSServerOptions struct {
|
||||
|
||||
@@ -97,6 +97,7 @@ type Endpoint struct {
|
||||
exitNodeAllowLANAccess bool
|
||||
advertiseRoutes []netip.Prefix
|
||||
advertiseExitNode bool
|
||||
advertiseTags []string
|
||||
relayServerPort *uint16
|
||||
relayServerStaticEndpoints []netip.AddrPort
|
||||
|
||||
@@ -244,6 +245,7 @@ func NewEndpoint(ctx context.Context, router adapter.Router, logger log.ContextL
|
||||
exitNodeAllowLANAccess: options.ExitNodeAllowLANAccess,
|
||||
advertiseRoutes: options.AdvertiseRoutes,
|
||||
advertiseExitNode: options.AdvertiseExitNode,
|
||||
advertiseTags: options.AdvertiseTags,
|
||||
relayServerPort: options.RelayServerPort,
|
||||
relayServerStaticEndpoints: options.RelayServerStaticEndpoints,
|
||||
udpTimeout: udpTimeout,
|
||||
@@ -359,25 +361,25 @@ func (t *Endpoint) Start(stage adapter.StartStage) error {
|
||||
localBackend := t.server.ExportLocalBackend()
|
||||
perfs := &ipn.MaskedPrefs{
|
||||
Prefs: ipn.Prefs{
|
||||
RouteAll: t.acceptRoutes,
|
||||
RouteAll: t.acceptRoutes,
|
||||
AdvertiseRoutes: t.advertiseRoutes,
|
||||
AdvertiseTags: t.advertiseTags,
|
||||
},
|
||||
RouteAllSet: true,
|
||||
ExitNodeIPSet: true,
|
||||
AdvertiseRoutesSet: true,
|
||||
}
|
||||
if len(t.advertiseRoutes) > 0 {
|
||||
perfs.AdvertiseRoutes = t.advertiseRoutes
|
||||
RouteAllSet: true,
|
||||
ExitNodeIPSet: true,
|
||||
AdvertiseRoutesSet: true,
|
||||
AdvertiseTagsSet: true,
|
||||
RelayServerPortSet: true,
|
||||
RelayServerStaticEndpointsSet: true,
|
||||
}
|
||||
if t.advertiseExitNode {
|
||||
perfs.AdvertiseRoutes = append(perfs.AdvertiseRoutes, tsaddr.ExitRoutes()...)
|
||||
}
|
||||
if t.relayServerPort != nil {
|
||||
perfs.RelayServerPort = t.relayServerPort
|
||||
perfs.RelayServerPortSet = true
|
||||
}
|
||||
if len(t.relayServerStaticEndpoints) > 0 {
|
||||
perfs.RelayServerStaticEndpoints = t.relayServerStaticEndpoints
|
||||
perfs.RelayServerStaticEndpointsSet = true
|
||||
}
|
||||
_, err = localBackend.EditPrefs(perfs)
|
||||
if err != nil {
|
||||
|
||||
@@ -425,6 +425,8 @@ func (s *Service) handleResponseWithTracking(writer http.ResponseWriter, respons
|
||||
usage.OutputTokens,
|
||||
usage.CacheReadInputTokens,
|
||||
usage.CacheCreationInputTokens,
|
||||
usage.CacheCreation.Ephemeral5mInputTokens,
|
||||
usage.CacheCreation.Ephemeral1hInputTokens,
|
||||
username,
|
||||
)
|
||||
}
|
||||
@@ -485,6 +487,8 @@ func (s *Service) handleResponseWithTracking(writer http.ResponseWriter, respons
|
||||
accumulatedUsage.InputTokens = messageStart.Message.Usage.InputTokens
|
||||
accumulatedUsage.CacheReadInputTokens = messageStart.Message.Usage.CacheReadInputTokens
|
||||
accumulatedUsage.CacheCreationInputTokens = messageStart.Message.Usage.CacheCreationInputTokens
|
||||
accumulatedUsage.CacheCreation.Ephemeral5mInputTokens = messageStart.Message.Usage.CacheCreation.Ephemeral5mInputTokens
|
||||
accumulatedUsage.CacheCreation.Ephemeral1hInputTokens = messageStart.Message.Usage.CacheCreation.Ephemeral1hInputTokens
|
||||
}
|
||||
case "message_delta":
|
||||
messageDelta := event.AsMessageDelta()
|
||||
@@ -519,6 +523,8 @@ func (s *Service) handleResponseWithTracking(writer http.ResponseWriter, respons
|
||||
accumulatedUsage.OutputTokens,
|
||||
accumulatedUsage.CacheReadInputTokens,
|
||||
accumulatedUsage.CacheCreationInputTokens,
|
||||
accumulatedUsage.CacheCreation.Ephemeral5mInputTokens,
|
||||
accumulatedUsage.CacheCreation.Ephemeral1hInputTokens,
|
||||
username,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -13,12 +13,14 @@ import (
|
||||
)
|
||||
|
||||
type UsageStats struct {
|
||||
RequestCount int `json:"request_count"`
|
||||
MessagesCount int `json:"messages_count"`
|
||||
InputTokens int64 `json:"input_tokens"`
|
||||
OutputTokens int64 `json:"output_tokens"`
|
||||
CacheReadInputTokens int64 `json:"cache_read_input_tokens"`
|
||||
CacheCreationInputTokens int64 `json:"cache_creation_input_tokens"`
|
||||
RequestCount int `json:"request_count"`
|
||||
MessagesCount int `json:"messages_count"`
|
||||
InputTokens int64 `json:"input_tokens"`
|
||||
OutputTokens int64 `json:"output_tokens"`
|
||||
CacheReadInputTokens int64 `json:"cache_read_input_tokens"`
|
||||
CacheCreationInputTokens int64 `json:"cache_creation_input_tokens"`
|
||||
CacheCreation5MinuteInputTokens int64 `json:"cache_creation_5m_input_tokens,omitempty"`
|
||||
CacheCreation1HourInputTokens int64 `json:"cache_creation_1h_input_tokens,omitempty"`
|
||||
}
|
||||
|
||||
type CostCombination struct {
|
||||
@@ -41,13 +43,15 @@ type AggregatedUsage struct {
|
||||
}
|
||||
|
||||
type UsageStatsJSON struct {
|
||||
RequestCount int `json:"request_count"`
|
||||
MessagesCount int `json:"messages_count"`
|
||||
InputTokens int64 `json:"input_tokens"`
|
||||
OutputTokens int64 `json:"output_tokens"`
|
||||
CacheReadInputTokens int64 `json:"cache_read_input_tokens"`
|
||||
CacheCreationInputTokens int64 `json:"cache_creation_input_tokens"`
|
||||
CostUSD float64 `json:"cost_usd"`
|
||||
RequestCount int `json:"request_count"`
|
||||
MessagesCount int `json:"messages_count"`
|
||||
InputTokens int64 `json:"input_tokens"`
|
||||
OutputTokens int64 `json:"output_tokens"`
|
||||
CacheReadInputTokens int64 `json:"cache_read_input_tokens"`
|
||||
CacheCreationInputTokens int64 `json:"cache_creation_input_tokens"`
|
||||
CacheCreation5MinuteInputTokens int64 `json:"cache_creation_5m_input_tokens,omitempty"`
|
||||
CacheCreation1HourInputTokens int64 `json:"cache_creation_1h_input_tokens,omitempty"`
|
||||
CostUSD float64 `json:"cost_usd"`
|
||||
}
|
||||
|
||||
type CostCombinationJSON struct {
|
||||
@@ -69,10 +73,11 @@ type AggregatedUsageJSON struct {
|
||||
}
|
||||
|
||||
type ModelPricing struct {
|
||||
InputPrice float64
|
||||
OutputPrice float64
|
||||
CacheReadPrice float64
|
||||
CacheWritePrice float64
|
||||
InputPrice float64
|
||||
OutputPrice float64
|
||||
CacheReadPrice float64
|
||||
CacheWritePrice5Minute float64
|
||||
CacheWritePrice1Hour float64
|
||||
}
|
||||
|
||||
type modelFamily struct {
|
||||
@@ -82,143 +87,205 @@ type modelFamily struct {
|
||||
}
|
||||
|
||||
var (
|
||||
opus4Pricing = ModelPricing{
|
||||
InputPrice: 15.0,
|
||||
OutputPrice: 75.0,
|
||||
CacheReadPrice: 1.5,
|
||||
CacheWritePrice: 18.75,
|
||||
opus46StandardPricing = ModelPricing{
|
||||
InputPrice: 5.0,
|
||||
OutputPrice: 25.0,
|
||||
CacheReadPrice: 0.5,
|
||||
CacheWritePrice5Minute: 6.25,
|
||||
CacheWritePrice1Hour: 10.0,
|
||||
}
|
||||
|
||||
sonnet4StandardPricing = ModelPricing{
|
||||
InputPrice: 3.0,
|
||||
OutputPrice: 15.0,
|
||||
CacheReadPrice: 0.3,
|
||||
CacheWritePrice: 3.75,
|
||||
}
|
||||
|
||||
sonnet4PremiumPricing = ModelPricing{
|
||||
InputPrice: 6.0,
|
||||
OutputPrice: 22.5,
|
||||
CacheReadPrice: 0.6,
|
||||
CacheWritePrice: 7.5,
|
||||
}
|
||||
|
||||
haiku4Pricing = ModelPricing{
|
||||
InputPrice: 1.0,
|
||||
OutputPrice: 5.0,
|
||||
CacheReadPrice: 0.1,
|
||||
CacheWritePrice: 1.25,
|
||||
}
|
||||
|
||||
haiku35Pricing = ModelPricing{
|
||||
InputPrice: 0.8,
|
||||
OutputPrice: 4.0,
|
||||
CacheReadPrice: 0.08,
|
||||
CacheWritePrice: 1.0,
|
||||
}
|
||||
|
||||
sonnet35Pricing = ModelPricing{
|
||||
InputPrice: 3.0,
|
||||
OutputPrice: 15.0,
|
||||
CacheReadPrice: 0.3,
|
||||
CacheWritePrice: 3.75,
|
||||
opus46PremiumPricing = ModelPricing{
|
||||
InputPrice: 10.0,
|
||||
OutputPrice: 37.5,
|
||||
CacheReadPrice: 1.0,
|
||||
CacheWritePrice5Minute: 12.5,
|
||||
CacheWritePrice1Hour: 20.0,
|
||||
}
|
||||
|
||||
opus45Pricing = ModelPricing{
|
||||
InputPrice: 5.0,
|
||||
OutputPrice: 25.0,
|
||||
CacheReadPrice: 0.5,
|
||||
CacheWritePrice: 6.25,
|
||||
InputPrice: 5.0,
|
||||
OutputPrice: 25.0,
|
||||
CacheReadPrice: 0.5,
|
||||
CacheWritePrice5Minute: 6.25,
|
||||
CacheWritePrice1Hour: 10.0,
|
||||
}
|
||||
|
||||
opus4Pricing = ModelPricing{
|
||||
InputPrice: 15.0,
|
||||
OutputPrice: 75.0,
|
||||
CacheReadPrice: 1.5,
|
||||
CacheWritePrice5Minute: 18.75,
|
||||
CacheWritePrice1Hour: 30.0,
|
||||
}
|
||||
|
||||
sonnet46StandardPricing = ModelPricing{
|
||||
InputPrice: 3.0,
|
||||
OutputPrice: 15.0,
|
||||
CacheReadPrice: 0.3,
|
||||
CacheWritePrice5Minute: 3.75,
|
||||
CacheWritePrice1Hour: 6.0,
|
||||
}
|
||||
|
||||
sonnet46PremiumPricing = ModelPricing{
|
||||
InputPrice: 6.0,
|
||||
OutputPrice: 22.5,
|
||||
CacheReadPrice: 0.6,
|
||||
CacheWritePrice5Minute: 7.5,
|
||||
CacheWritePrice1Hour: 12.0,
|
||||
}
|
||||
|
||||
sonnet45StandardPricing = ModelPricing{
|
||||
InputPrice: 3.0,
|
||||
OutputPrice: 15.0,
|
||||
CacheReadPrice: 0.3,
|
||||
CacheWritePrice: 3.75,
|
||||
InputPrice: 3.0,
|
||||
OutputPrice: 15.0,
|
||||
CacheReadPrice: 0.3,
|
||||
CacheWritePrice5Minute: 3.75,
|
||||
CacheWritePrice1Hour: 6.0,
|
||||
}
|
||||
|
||||
sonnet45PremiumPricing = ModelPricing{
|
||||
InputPrice: 6.0,
|
||||
OutputPrice: 22.5,
|
||||
CacheReadPrice: 0.6,
|
||||
CacheWritePrice: 7.5,
|
||||
InputPrice: 6.0,
|
||||
OutputPrice: 22.5,
|
||||
CacheReadPrice: 0.6,
|
||||
CacheWritePrice5Minute: 7.5,
|
||||
CacheWritePrice1Hour: 12.0,
|
||||
}
|
||||
|
||||
sonnet4StandardPricing = ModelPricing{
|
||||
InputPrice: 3.0,
|
||||
OutputPrice: 15.0,
|
||||
CacheReadPrice: 0.3,
|
||||
CacheWritePrice5Minute: 3.75,
|
||||
CacheWritePrice1Hour: 6.0,
|
||||
}
|
||||
|
||||
sonnet4PremiumPricing = ModelPricing{
|
||||
InputPrice: 6.0,
|
||||
OutputPrice: 22.5,
|
||||
CacheReadPrice: 0.6,
|
||||
CacheWritePrice5Minute: 7.5,
|
||||
CacheWritePrice1Hour: 12.0,
|
||||
}
|
||||
|
||||
sonnet37Pricing = ModelPricing{
|
||||
InputPrice: 3.0,
|
||||
OutputPrice: 15.0,
|
||||
CacheReadPrice: 0.3,
|
||||
CacheWritePrice5Minute: 3.75,
|
||||
CacheWritePrice1Hour: 6.0,
|
||||
}
|
||||
|
||||
sonnet35Pricing = ModelPricing{
|
||||
InputPrice: 3.0,
|
||||
OutputPrice: 15.0,
|
||||
CacheReadPrice: 0.3,
|
||||
CacheWritePrice5Minute: 3.75,
|
||||
CacheWritePrice1Hour: 6.0,
|
||||
}
|
||||
|
||||
haiku45Pricing = ModelPricing{
|
||||
InputPrice: 1.0,
|
||||
OutputPrice: 5.0,
|
||||
CacheReadPrice: 0.1,
|
||||
CacheWritePrice: 1.25,
|
||||
InputPrice: 1.0,
|
||||
OutputPrice: 5.0,
|
||||
CacheReadPrice: 0.1,
|
||||
CacheWritePrice5Minute: 1.25,
|
||||
CacheWritePrice1Hour: 2.0,
|
||||
}
|
||||
|
||||
haiku4Pricing = ModelPricing{
|
||||
InputPrice: 1.0,
|
||||
OutputPrice: 5.0,
|
||||
CacheReadPrice: 0.1,
|
||||
CacheWritePrice5Minute: 1.25,
|
||||
CacheWritePrice1Hour: 2.0,
|
||||
}
|
||||
|
||||
haiku35Pricing = ModelPricing{
|
||||
InputPrice: 0.8,
|
||||
OutputPrice: 4.0,
|
||||
CacheReadPrice: 0.08,
|
||||
CacheWritePrice5Minute: 1.0,
|
||||
CacheWritePrice1Hour: 1.6,
|
||||
}
|
||||
|
||||
haiku3Pricing = ModelPricing{
|
||||
InputPrice: 0.25,
|
||||
OutputPrice: 1.25,
|
||||
CacheReadPrice: 0.03,
|
||||
CacheWritePrice: 0.3,
|
||||
InputPrice: 0.25,
|
||||
OutputPrice: 1.25,
|
||||
CacheReadPrice: 0.03,
|
||||
CacheWritePrice5Minute: 0.3,
|
||||
CacheWritePrice1Hour: 0.5,
|
||||
}
|
||||
|
||||
opus3Pricing = ModelPricing{
|
||||
InputPrice: 15.0,
|
||||
OutputPrice: 75.0,
|
||||
CacheReadPrice: 1.5,
|
||||
CacheWritePrice: 18.75,
|
||||
InputPrice: 15.0,
|
||||
OutputPrice: 75.0,
|
||||
CacheReadPrice: 1.5,
|
||||
CacheWritePrice5Minute: 18.75,
|
||||
CacheWritePrice1Hour: 30.0,
|
||||
}
|
||||
|
||||
modelFamilies = []modelFamily{
|
||||
{
|
||||
pattern: regexp.MustCompile(`^claude-opus-4-5-`),
|
||||
pattern: regexp.MustCompile(`^claude-opus-4-6(?:-|$)`),
|
||||
standardPricing: opus46StandardPricing,
|
||||
premiumPricing: &opus46PremiumPricing,
|
||||
},
|
||||
{
|
||||
pattern: regexp.MustCompile(`^claude-opus-4-5(?:-|$)`),
|
||||
standardPricing: opus45Pricing,
|
||||
premiumPricing: nil,
|
||||
},
|
||||
{
|
||||
pattern: regexp.MustCompile(`^claude-(?:opus-4-|4-opus-|opus-4-1-)`),
|
||||
pattern: regexp.MustCompile(`^claude-(?:opus-4(?:-|$)|4-opus-)`),
|
||||
standardPricing: opus4Pricing,
|
||||
premiumPricing: nil,
|
||||
},
|
||||
{
|
||||
pattern: regexp.MustCompile(`^claude-(?:opus-3-|3-opus-)`),
|
||||
pattern: regexp.MustCompile(`^claude-(?:opus-3(?:-|$)|3-opus-)`),
|
||||
standardPricing: opus3Pricing,
|
||||
premiumPricing: nil,
|
||||
},
|
||||
{
|
||||
pattern: regexp.MustCompile(`^claude-(?:sonnet-4-5-|4-5-sonnet-)`),
|
||||
pattern: regexp.MustCompile(`^claude-(?:sonnet-4-6(?:-|$)|4-6-sonnet-)`),
|
||||
standardPricing: sonnet46StandardPricing,
|
||||
premiumPricing: &sonnet46PremiumPricing,
|
||||
},
|
||||
{
|
||||
pattern: regexp.MustCompile(`^claude-(?:sonnet-4-5(?:-|$)|4-5-sonnet-)`),
|
||||
standardPricing: sonnet45StandardPricing,
|
||||
premiumPricing: &sonnet45PremiumPricing,
|
||||
},
|
||||
{
|
||||
pattern: regexp.MustCompile(`^claude-3-7-sonnet-`),
|
||||
pattern: regexp.MustCompile(`^claude-(?:sonnet-4(?:-|$)|4-sonnet-)`),
|
||||
standardPricing: sonnet4StandardPricing,
|
||||
premiumPricing: &sonnet4PremiumPricing,
|
||||
},
|
||||
{
|
||||
pattern: regexp.MustCompile(`^claude-(?:sonnet-4-|4-sonnet-)`),
|
||||
standardPricing: sonnet4StandardPricing,
|
||||
premiumPricing: &sonnet4PremiumPricing,
|
||||
pattern: regexp.MustCompile(`^claude-3-7-sonnet(?:-|$)`),
|
||||
standardPricing: sonnet37Pricing,
|
||||
premiumPricing: nil,
|
||||
},
|
||||
{
|
||||
pattern: regexp.MustCompile(`^claude-3-5-sonnet-`),
|
||||
pattern: regexp.MustCompile(`^claude-3-5-sonnet(?:-|$)`),
|
||||
standardPricing: sonnet35Pricing,
|
||||
premiumPricing: nil,
|
||||
},
|
||||
{
|
||||
pattern: regexp.MustCompile(`^claude-(?:haiku-4-5-|4-5-haiku-)`),
|
||||
pattern: regexp.MustCompile(`^claude-(?:haiku-4-5(?:-|$)|4-5-haiku-)`),
|
||||
standardPricing: haiku45Pricing,
|
||||
premiumPricing: nil,
|
||||
},
|
||||
{
|
||||
pattern: regexp.MustCompile(`^claude-haiku-4-`),
|
||||
pattern: regexp.MustCompile(`^claude-haiku-4(?:-|$)`),
|
||||
standardPricing: haiku4Pricing,
|
||||
premiumPricing: nil,
|
||||
},
|
||||
{
|
||||
pattern: regexp.MustCompile(`^claude-3-5-haiku-`),
|
||||
pattern: regexp.MustCompile(`^claude-3-5-haiku(?:-|$)`),
|
||||
standardPricing: haiku35Pricing,
|
||||
premiumPricing: nil,
|
||||
},
|
||||
{
|
||||
pattern: regexp.MustCompile(`^claude-3-haiku-`),
|
||||
pattern: regexp.MustCompile(`^claude-3-haiku(?:-|$)`),
|
||||
standardPricing: haiku3Pricing,
|
||||
premiumPricing: nil,
|
||||
},
|
||||
@@ -243,10 +310,20 @@ func getPricing(model string, contextWindow int) ModelPricing {
|
||||
func calculateCost(stats UsageStats, model string, contextWindow int) float64 {
|
||||
pricing := getPricing(model, contextWindow)
|
||||
|
||||
cacheCreationCost := 0.0
|
||||
if stats.CacheCreation5MinuteInputTokens > 0 || stats.CacheCreation1HourInputTokens > 0 {
|
||||
cacheCreationCost =
|
||||
float64(stats.CacheCreation5MinuteInputTokens)*pricing.CacheWritePrice5Minute +
|
||||
float64(stats.CacheCreation1HourInputTokens)*pricing.CacheWritePrice1Hour
|
||||
} else {
|
||||
// Backward compatibility for usage files generated before TTL split tracking.
|
||||
cacheCreationCost = float64(stats.CacheCreationInputTokens) * pricing.CacheWritePrice5Minute
|
||||
}
|
||||
|
||||
cost := (float64(stats.InputTokens)*pricing.InputPrice +
|
||||
float64(stats.OutputTokens)*pricing.OutputPrice +
|
||||
float64(stats.CacheReadInputTokens)*pricing.CacheReadPrice +
|
||||
float64(stats.CacheCreationInputTokens)*pricing.CacheWritePrice) / 1_000_000
|
||||
cacheCreationCost) / 1_000_000
|
||||
|
||||
return math.Round(cost*100) / 100
|
||||
}
|
||||
@@ -273,13 +350,15 @@ func (u *AggregatedUsage) ToJSON() *AggregatedUsageJSON {
|
||||
Model: combo.Model,
|
||||
ContextWindow: combo.ContextWindow,
|
||||
Total: UsageStatsJSON{
|
||||
RequestCount: combo.Total.RequestCount,
|
||||
MessagesCount: combo.Total.MessagesCount,
|
||||
InputTokens: combo.Total.InputTokens,
|
||||
OutputTokens: combo.Total.OutputTokens,
|
||||
CacheReadInputTokens: combo.Total.CacheReadInputTokens,
|
||||
CacheCreationInputTokens: combo.Total.CacheCreationInputTokens,
|
||||
CostUSD: totalCost,
|
||||
RequestCount: combo.Total.RequestCount,
|
||||
MessagesCount: combo.Total.MessagesCount,
|
||||
InputTokens: combo.Total.InputTokens,
|
||||
OutputTokens: combo.Total.OutputTokens,
|
||||
CacheReadInputTokens: combo.Total.CacheReadInputTokens,
|
||||
CacheCreationInputTokens: combo.Total.CacheCreationInputTokens,
|
||||
CacheCreation5MinuteInputTokens: combo.Total.CacheCreation5MinuteInputTokens,
|
||||
CacheCreation1HourInputTokens: combo.Total.CacheCreation1HourInputTokens,
|
||||
CostUSD: totalCost,
|
||||
},
|
||||
ByUser: make(map[string]UsageStatsJSON),
|
||||
}
|
||||
@@ -289,13 +368,15 @@ func (u *AggregatedUsage) ToJSON() *AggregatedUsageJSON {
|
||||
result.Costs.ByUser[user] += userCost
|
||||
|
||||
comboJSON.ByUser[user] = UsageStatsJSON{
|
||||
RequestCount: userStats.RequestCount,
|
||||
MessagesCount: userStats.MessagesCount,
|
||||
InputTokens: userStats.InputTokens,
|
||||
OutputTokens: userStats.OutputTokens,
|
||||
CacheReadInputTokens: userStats.CacheReadInputTokens,
|
||||
CacheCreationInputTokens: userStats.CacheCreationInputTokens,
|
||||
CostUSD: userCost,
|
||||
RequestCount: userStats.RequestCount,
|
||||
MessagesCount: userStats.MessagesCount,
|
||||
InputTokens: userStats.InputTokens,
|
||||
OutputTokens: userStats.OutputTokens,
|
||||
CacheReadInputTokens: userStats.CacheReadInputTokens,
|
||||
CacheCreationInputTokens: userStats.CacheCreationInputTokens,
|
||||
CacheCreation5MinuteInputTokens: userStats.CacheCreation5MinuteInputTokens,
|
||||
CacheCreation1HourInputTokens: userStats.CacheCreation1HourInputTokens,
|
||||
CostUSD: userCost,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -367,7 +448,13 @@ func (u *AggregatedUsage) Save() error {
|
||||
return err
|
||||
}
|
||||
|
||||
func (u *AggregatedUsage) AddUsage(model string, contextWindow int, messagesCount int, inputTokens, outputTokens, cacheReadTokens, cacheCreationTokens int64, user string) error {
|
||||
func (u *AggregatedUsage) AddUsage(
|
||||
model string,
|
||||
contextWindow int,
|
||||
messagesCount int,
|
||||
inputTokens, outputTokens, cacheReadTokens, cacheCreationTokens, cacheCreation5MinuteTokens, cacheCreation1HourTokens int64,
|
||||
user string,
|
||||
) error {
|
||||
if model == "" {
|
||||
return E.New("model cannot be empty")
|
||||
}
|
||||
@@ -400,6 +487,10 @@ func (u *AggregatedUsage) AddUsage(model string, contextWindow int, messagesCoun
|
||||
combo = &u.Combinations[len(u.Combinations)-1]
|
||||
}
|
||||
|
||||
if cacheCreationTokens == 0 {
|
||||
cacheCreationTokens = cacheCreation5MinuteTokens + cacheCreation1HourTokens
|
||||
}
|
||||
|
||||
// Update total stats
|
||||
combo.Total.RequestCount++
|
||||
combo.Total.MessagesCount += messagesCount
|
||||
@@ -407,6 +498,8 @@ func (u *AggregatedUsage) AddUsage(model string, contextWindow int, messagesCoun
|
||||
combo.Total.OutputTokens += outputTokens
|
||||
combo.Total.CacheReadInputTokens += cacheReadTokens
|
||||
combo.Total.CacheCreationInputTokens += cacheCreationTokens
|
||||
combo.Total.CacheCreation5MinuteInputTokens += cacheCreation5MinuteTokens
|
||||
combo.Total.CacheCreation1HourInputTokens += cacheCreation1HourTokens
|
||||
|
||||
// Update per-user stats if user is specified
|
||||
if user != "" {
|
||||
@@ -417,6 +510,8 @@ func (u *AggregatedUsage) AddUsage(model string, contextWindow int, messagesCoun
|
||||
userStats.OutputTokens += outputTokens
|
||||
userStats.CacheReadInputTokens += cacheReadTokens
|
||||
userStats.CacheCreationInputTokens += cacheCreationTokens
|
||||
userStats.CacheCreation5MinuteInputTokens += cacheCreation5MinuteTokens
|
||||
userStats.CacheCreation1HourInputTokens += cacheCreation1HourTokens
|
||||
combo.ByUser[user] = userStats
|
||||
}
|
||||
|
||||
|
||||
@@ -406,7 +406,9 @@ func (s *Service) handleResponseWithTracking(writer http.ResponseWriter, respons
|
||||
isChatCompletions := path == "/v1/chat/completions"
|
||||
mediaType, _, err := mime.ParseMediaType(response.Header.Get("Content-Type"))
|
||||
isStreaming := err == nil && mediaType == "text/event-stream"
|
||||
|
||||
if !isStreaming && !isChatCompletions && response.Header.Get("Content-Type") == "" {
|
||||
isStreaming = true
|
||||
}
|
||||
if !isStreaming {
|
||||
bodyBytes, err := io.ReadAll(response.Body)
|
||||
if err != nil {
|
||||
@@ -414,13 +416,14 @@ func (s *Service) handleResponseWithTracking(writer http.ResponseWriter, respons
|
||||
return
|
||||
}
|
||||
|
||||
var responseModel string
|
||||
var responseModel, serviceTier string
|
||||
var inputTokens, outputTokens, cachedTokens int64
|
||||
|
||||
if isChatCompletions {
|
||||
var chatCompletion openai.ChatCompletion
|
||||
if json.Unmarshal(bodyBytes, &chatCompletion) == nil {
|
||||
responseModel = chatCompletion.Model
|
||||
serviceTier = string(chatCompletion.ServiceTier)
|
||||
inputTokens = chatCompletion.Usage.PromptTokens
|
||||
outputTokens = chatCompletion.Usage.CompletionTokens
|
||||
cachedTokens = chatCompletion.Usage.PromptTokensDetails.CachedTokens
|
||||
@@ -429,6 +432,7 @@ func (s *Service) handleResponseWithTracking(writer http.ResponseWriter, respons
|
||||
var responsesResponse responses.Response
|
||||
if json.Unmarshal(bodyBytes, &responsesResponse) == nil {
|
||||
responseModel = string(responsesResponse.Model)
|
||||
serviceTier = string(responsesResponse.ServiceTier)
|
||||
inputTokens = responsesResponse.Usage.InputTokens
|
||||
outputTokens = responsesResponse.Usage.OutputTokens
|
||||
cachedTokens = responsesResponse.Usage.InputTokensDetails.CachedTokens
|
||||
@@ -440,7 +444,7 @@ func (s *Service) handleResponseWithTracking(writer http.ResponseWriter, respons
|
||||
responseModel = requestModel
|
||||
}
|
||||
if responseModel != "" {
|
||||
s.usageTracker.AddUsage(responseModel, inputTokens, outputTokens, cachedTokens, username)
|
||||
s.usageTracker.AddUsage(responseModel, inputTokens, outputTokens, cachedTokens, serviceTier, username)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -455,7 +459,7 @@ func (s *Service) handleResponseWithTracking(writer http.ResponseWriter, respons
|
||||
}
|
||||
|
||||
var inputTokens, outputTokens, cachedTokens int64
|
||||
var responseModel string
|
||||
var responseModel, serviceTier string
|
||||
buffer := make([]byte, buf.BufferSize)
|
||||
var leftover []byte
|
||||
|
||||
@@ -490,6 +494,9 @@ func (s *Service) handleResponseWithTracking(writer http.ResponseWriter, respons
|
||||
if chatChunk.Model != "" {
|
||||
responseModel = chatChunk.Model
|
||||
}
|
||||
if chatChunk.ServiceTier != "" {
|
||||
serviceTier = string(chatChunk.ServiceTier)
|
||||
}
|
||||
if chatChunk.Usage.PromptTokens > 0 {
|
||||
inputTokens = chatChunk.Usage.PromptTokens
|
||||
cachedTokens = chatChunk.Usage.PromptTokensDetails.CachedTokens
|
||||
@@ -506,6 +513,9 @@ func (s *Service) handleResponseWithTracking(writer http.ResponseWriter, respons
|
||||
if string(completedEvent.Response.Model) != "" {
|
||||
responseModel = string(completedEvent.Response.Model)
|
||||
}
|
||||
if completedEvent.Response.ServiceTier != "" {
|
||||
serviceTier = string(completedEvent.Response.ServiceTier)
|
||||
}
|
||||
if completedEvent.Response.Usage.InputTokens > 0 {
|
||||
inputTokens = completedEvent.Response.Usage.InputTokens
|
||||
cachedTokens = completedEvent.Response.Usage.InputTokensDetails.CachedTokens
|
||||
@@ -534,7 +544,7 @@ func (s *Service) handleResponseWithTracking(writer http.ResponseWriter, respons
|
||||
|
||||
if inputTokens > 0 || outputTokens > 0 {
|
||||
if responseModel != "" {
|
||||
s.usageTracker.AddUsage(responseModel, inputTokens, outputTokens, cachedTokens, username)
|
||||
s.usageTracker.AddUsage(responseModel, inputTokens, outputTokens, cachedTokens, serviceTier, username)
|
||||
}
|
||||
}
|
||||
return
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"math"
|
||||
"os"
|
||||
"regexp"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
@@ -42,9 +43,10 @@ func (u *UsageStats) UnmarshalJSON(data []byte) error {
|
||||
}
|
||||
|
||||
type CostCombination struct {
|
||||
Model string `json:"model"`
|
||||
Total UsageStats `json:"total"`
|
||||
ByUser map[string]UsageStats `json:"by_user"`
|
||||
Model string `json:"model"`
|
||||
ServiceTier string `json:"service_tier,omitempty"`
|
||||
Total UsageStats `json:"total"`
|
||||
ByUser map[string]UsageStats `json:"by_user"`
|
||||
}
|
||||
|
||||
type AggregatedUsage struct {
|
||||
@@ -68,9 +70,10 @@ type UsageStatsJSON struct {
|
||||
}
|
||||
|
||||
type CostCombinationJSON struct {
|
||||
Model string `json:"model"`
|
||||
Total UsageStatsJSON `json:"total"`
|
||||
ByUser map[string]UsageStatsJSON `json:"by_user"`
|
||||
Model string `json:"model"`
|
||||
ServiceTier string `json:"service_tier,omitempty"`
|
||||
Total UsageStatsJSON `json:"total"`
|
||||
ByUser map[string]UsageStatsJSON `json:"by_user"`
|
||||
}
|
||||
|
||||
type CostsSummaryJSON struct {
|
||||
@@ -95,7 +98,123 @@ type modelFamily struct {
|
||||
pricing ModelPricing
|
||||
}
|
||||
|
||||
const (
|
||||
serviceTierAuto = "auto"
|
||||
serviceTierDefault = "default"
|
||||
serviceTierFlex = "flex"
|
||||
serviceTierPriority = "priority"
|
||||
serviceTierScale = "scale"
|
||||
)
|
||||
|
||||
var (
|
||||
gpt52Pricing = ModelPricing{
|
||||
InputPrice: 1.75,
|
||||
OutputPrice: 14.0,
|
||||
CachedInputPrice: 0.175,
|
||||
}
|
||||
|
||||
gpt5Pricing = ModelPricing{
|
||||
InputPrice: 1.25,
|
||||
OutputPrice: 10.0,
|
||||
CachedInputPrice: 0.125,
|
||||
}
|
||||
|
||||
gpt5MiniPricing = ModelPricing{
|
||||
InputPrice: 0.25,
|
||||
OutputPrice: 2.0,
|
||||
CachedInputPrice: 0.025,
|
||||
}
|
||||
|
||||
gpt5NanoPricing = ModelPricing{
|
||||
InputPrice: 0.05,
|
||||
OutputPrice: 0.4,
|
||||
CachedInputPrice: 0.005,
|
||||
}
|
||||
|
||||
gpt52CodexPricing = ModelPricing{
|
||||
InputPrice: 1.75,
|
||||
OutputPrice: 14.0,
|
||||
CachedInputPrice: 0.175,
|
||||
}
|
||||
|
||||
gpt51CodexPricing = ModelPricing{
|
||||
InputPrice: 1.25,
|
||||
OutputPrice: 10.0,
|
||||
CachedInputPrice: 0.125,
|
||||
}
|
||||
|
||||
gpt51CodexMiniPricing = ModelPricing{
|
||||
InputPrice: 0.25,
|
||||
OutputPrice: 2.0,
|
||||
CachedInputPrice: 0.025,
|
||||
}
|
||||
|
||||
gpt52ProPricing = ModelPricing{
|
||||
InputPrice: 21.0,
|
||||
OutputPrice: 168.0,
|
||||
CachedInputPrice: 21.0,
|
||||
}
|
||||
|
||||
gpt5ProPricing = ModelPricing{
|
||||
InputPrice: 15.0,
|
||||
OutputPrice: 120.0,
|
||||
CachedInputPrice: 15.0,
|
||||
}
|
||||
|
||||
gpt52FlexPricing = ModelPricing{
|
||||
InputPrice: 0.875,
|
||||
OutputPrice: 7.0,
|
||||
CachedInputPrice: 0.0875,
|
||||
}
|
||||
|
||||
gpt5FlexPricing = ModelPricing{
|
||||
InputPrice: 0.625,
|
||||
OutputPrice: 5.0,
|
||||
CachedInputPrice: 0.0625,
|
||||
}
|
||||
|
||||
gpt5MiniFlexPricing = ModelPricing{
|
||||
InputPrice: 0.125,
|
||||
OutputPrice: 1.0,
|
||||
CachedInputPrice: 0.0125,
|
||||
}
|
||||
|
||||
gpt5NanoFlexPricing = ModelPricing{
|
||||
InputPrice: 0.025,
|
||||
OutputPrice: 0.2,
|
||||
CachedInputPrice: 0.0025,
|
||||
}
|
||||
|
||||
gpt52PriorityPricing = ModelPricing{
|
||||
InputPrice: 3.5,
|
||||
OutputPrice: 28.0,
|
||||
CachedInputPrice: 0.35,
|
||||
}
|
||||
|
||||
gpt5PriorityPricing = ModelPricing{
|
||||
InputPrice: 2.5,
|
||||
OutputPrice: 20.0,
|
||||
CachedInputPrice: 0.25,
|
||||
}
|
||||
|
||||
gpt5MiniPriorityPricing = ModelPricing{
|
||||
InputPrice: 0.45,
|
||||
OutputPrice: 3.6,
|
||||
CachedInputPrice: 0.045,
|
||||
}
|
||||
|
||||
gpt52CodexPriorityPricing = ModelPricing{
|
||||
InputPrice: 3.5,
|
||||
OutputPrice: 28.0,
|
||||
CachedInputPrice: 0.35,
|
||||
}
|
||||
|
||||
gpt51CodexPriorityPricing = ModelPricing{
|
||||
InputPrice: 2.5,
|
||||
OutputPrice: 20.0,
|
||||
CachedInputPrice: 0.25,
|
||||
}
|
||||
|
||||
gpt4oPricing = ModelPricing{
|
||||
InputPrice: 2.5,
|
||||
OutputPrice: 10.0,
|
||||
@@ -111,7 +230,19 @@ var (
|
||||
gpt4oAudioPricing = ModelPricing{
|
||||
InputPrice: 2.5,
|
||||
OutputPrice: 10.0,
|
||||
CachedInputPrice: 1.25,
|
||||
CachedInputPrice: 2.5,
|
||||
}
|
||||
|
||||
gpt4oMiniAudioPricing = ModelPricing{
|
||||
InputPrice: 0.15,
|
||||
OutputPrice: 0.6,
|
||||
CachedInputPrice: 0.15,
|
||||
}
|
||||
|
||||
gptAudioMiniPricing = ModelPricing{
|
||||
InputPrice: 0.6,
|
||||
OutputPrice: 2.4,
|
||||
CachedInputPrice: 0.6,
|
||||
}
|
||||
|
||||
o1Pricing = ModelPricing{
|
||||
@@ -120,6 +251,12 @@ var (
|
||||
CachedInputPrice: 7.5,
|
||||
}
|
||||
|
||||
o1ProPricing = ModelPricing{
|
||||
InputPrice: 150.0,
|
||||
OutputPrice: 600.0,
|
||||
CachedInputPrice: 150.0,
|
||||
}
|
||||
|
||||
o1MiniPricing = ModelPricing{
|
||||
InputPrice: 1.1,
|
||||
OutputPrice: 4.4,
|
||||
@@ -135,13 +272,55 @@ var (
|
||||
o3Pricing = ModelPricing{
|
||||
InputPrice: 2.0,
|
||||
OutputPrice: 8.0,
|
||||
CachedInputPrice: 1.0,
|
||||
CachedInputPrice: 0.5,
|
||||
}
|
||||
|
||||
o3ProPricing = ModelPricing{
|
||||
InputPrice: 20.0,
|
||||
OutputPrice: 80.0,
|
||||
CachedInputPrice: 20.0,
|
||||
}
|
||||
|
||||
o3DeepResearchPricing = ModelPricing{
|
||||
InputPrice: 10.0,
|
||||
OutputPrice: 40.0,
|
||||
CachedInputPrice: 2.5,
|
||||
}
|
||||
|
||||
o4MiniPricing = ModelPricing{
|
||||
InputPrice: 1.1,
|
||||
OutputPrice: 4.4,
|
||||
CachedInputPrice: 0.55,
|
||||
CachedInputPrice: 0.275,
|
||||
}
|
||||
|
||||
o4MiniDeepResearchPricing = ModelPricing{
|
||||
InputPrice: 2.0,
|
||||
OutputPrice: 8.0,
|
||||
CachedInputPrice: 0.5,
|
||||
}
|
||||
|
||||
o3FlexPricing = ModelPricing{
|
||||
InputPrice: 1.0,
|
||||
OutputPrice: 4.0,
|
||||
CachedInputPrice: 0.25,
|
||||
}
|
||||
|
||||
o4MiniFlexPricing = ModelPricing{
|
||||
InputPrice: 0.55,
|
||||
OutputPrice: 2.2,
|
||||
CachedInputPrice: 0.138,
|
||||
}
|
||||
|
||||
o3PriorityPricing = ModelPricing{
|
||||
InputPrice: 3.5,
|
||||
OutputPrice: 14.0,
|
||||
CachedInputPrice: 0.875,
|
||||
}
|
||||
|
||||
o4MiniPriorityPricing = ModelPricing{
|
||||
InputPrice: 2.0,
|
||||
OutputPrice: 8.0,
|
||||
CachedInputPrice: 0.5,
|
||||
}
|
||||
|
||||
gpt41Pricing = ModelPricing{
|
||||
@@ -162,69 +341,358 @@ var (
|
||||
CachedInputPrice: 0.025,
|
||||
}
|
||||
|
||||
modelFamilies = []modelFamily{
|
||||
gpt41PriorityPricing = ModelPricing{
|
||||
InputPrice: 3.5,
|
||||
OutputPrice: 14.0,
|
||||
CachedInputPrice: 0.875,
|
||||
}
|
||||
|
||||
gpt41MiniPriorityPricing = ModelPricing{
|
||||
InputPrice: 0.7,
|
||||
OutputPrice: 2.8,
|
||||
CachedInputPrice: 0.175,
|
||||
}
|
||||
|
||||
gpt41NanoPriorityPricing = ModelPricing{
|
||||
InputPrice: 0.2,
|
||||
OutputPrice: 0.8,
|
||||
CachedInputPrice: 0.05,
|
||||
}
|
||||
|
||||
gpt4oPriorityPricing = ModelPricing{
|
||||
InputPrice: 4.25,
|
||||
OutputPrice: 17.0,
|
||||
CachedInputPrice: 2.125,
|
||||
}
|
||||
|
||||
gpt4oMiniPriorityPricing = ModelPricing{
|
||||
InputPrice: 0.25,
|
||||
OutputPrice: 1.0,
|
||||
CachedInputPrice: 0.125,
|
||||
}
|
||||
|
||||
standardModelFamilies = []modelFamily{
|
||||
{
|
||||
pattern: regexp.MustCompile(`^gpt-4\.1-nano`),
|
||||
pricing: gpt41NanoPricing,
|
||||
pattern: regexp.MustCompile(`^gpt-5\.2-codex(?:$|-)`),
|
||||
pricing: gpt52CodexPricing,
|
||||
},
|
||||
{
|
||||
pattern: regexp.MustCompile(`^gpt-4\.1-mini`),
|
||||
pricing: gpt41MiniPricing,
|
||||
pattern: regexp.MustCompile(`^gpt-5\.1-codex-max(?:$|-)`),
|
||||
pricing: gpt51CodexPricing,
|
||||
},
|
||||
{
|
||||
pattern: regexp.MustCompile(`^gpt-4\.1`),
|
||||
pricing: gpt41Pricing,
|
||||
pattern: regexp.MustCompile(`^gpt-5\.1-codex-mini(?:$|-)`),
|
||||
pricing: gpt51CodexMiniPricing,
|
||||
},
|
||||
{
|
||||
pattern: regexp.MustCompile(`^o4-mini`),
|
||||
pattern: regexp.MustCompile(`^gpt-5\.1-codex(?:$|-)`),
|
||||
pricing: gpt51CodexPricing,
|
||||
},
|
||||
{
|
||||
pattern: regexp.MustCompile(`^gpt-5-codex(?:$|-)`),
|
||||
pricing: gpt51CodexPricing,
|
||||
},
|
||||
{
|
||||
pattern: regexp.MustCompile(`^gpt-5\.2-chat-latest$`),
|
||||
pricing: gpt52Pricing,
|
||||
},
|
||||
{
|
||||
pattern: regexp.MustCompile(`^gpt-5\.1-chat-latest$`),
|
||||
pricing: gpt5Pricing,
|
||||
},
|
||||
{
|
||||
pattern: regexp.MustCompile(`^gpt-5-chat-latest$`),
|
||||
pricing: gpt5Pricing,
|
||||
},
|
||||
{
|
||||
pattern: regexp.MustCompile(`^gpt-5\.2-pro(?:$|-)`),
|
||||
pricing: gpt52ProPricing,
|
||||
},
|
||||
{
|
||||
pattern: regexp.MustCompile(`^gpt-5-pro(?:$|-)`),
|
||||
pricing: gpt5ProPricing,
|
||||
},
|
||||
{
|
||||
pattern: regexp.MustCompile(`^gpt-5-mini(?:$|-)`),
|
||||
pricing: gpt5MiniPricing,
|
||||
},
|
||||
{
|
||||
pattern: regexp.MustCompile(`^gpt-5-nano(?:$|-)`),
|
||||
pricing: gpt5NanoPricing,
|
||||
},
|
||||
{
|
||||
pattern: regexp.MustCompile(`^gpt-5\.2(?:$|-)`),
|
||||
pricing: gpt52Pricing,
|
||||
},
|
||||
{
|
||||
pattern: regexp.MustCompile(`^gpt-5\.1(?:$|-)`),
|
||||
pricing: gpt5Pricing,
|
||||
},
|
||||
{
|
||||
pattern: regexp.MustCompile(`^gpt-5(?:$|-)`),
|
||||
pricing: gpt5Pricing,
|
||||
},
|
||||
{
|
||||
pattern: regexp.MustCompile(`^o4-mini-deep-research(?:$|-)`),
|
||||
pricing: o4MiniDeepResearchPricing,
|
||||
},
|
||||
{
|
||||
pattern: regexp.MustCompile(`^o4-mini(?:$|-)`),
|
||||
pricing: o4MiniPricing,
|
||||
},
|
||||
{
|
||||
pattern: regexp.MustCompile(`^o3-mini`),
|
||||
pattern: regexp.MustCompile(`^o3-pro(?:$|-)`),
|
||||
pricing: o3ProPricing,
|
||||
},
|
||||
{
|
||||
pattern: regexp.MustCompile(`^o3-deep-research(?:$|-)`),
|
||||
pricing: o3DeepResearchPricing,
|
||||
},
|
||||
{
|
||||
pattern: regexp.MustCompile(`^o3-mini(?:$|-)`),
|
||||
pricing: o3MiniPricing,
|
||||
},
|
||||
{
|
||||
pattern: regexp.MustCompile(`^o3`),
|
||||
pattern: regexp.MustCompile(`^o3(?:$|-)`),
|
||||
pricing: o3Pricing,
|
||||
},
|
||||
{
|
||||
pattern: regexp.MustCompile(`^o1-mini`),
|
||||
pattern: regexp.MustCompile(`^o1-pro(?:$|-)`),
|
||||
pricing: o1ProPricing,
|
||||
},
|
||||
{
|
||||
pattern: regexp.MustCompile(`^o1-mini(?:$|-)`),
|
||||
pricing: o1MiniPricing,
|
||||
},
|
||||
{
|
||||
pattern: regexp.MustCompile(`^o1`),
|
||||
pattern: regexp.MustCompile(`^o1(?:$|-)`),
|
||||
pricing: o1Pricing,
|
||||
},
|
||||
{
|
||||
pattern: regexp.MustCompile(`^gpt-4o-audio`),
|
||||
pattern: regexp.MustCompile(`^gpt-4o-mini-audio(?:$|-)`),
|
||||
pricing: gpt4oMiniAudioPricing,
|
||||
},
|
||||
{
|
||||
pattern: regexp.MustCompile(`^gpt-audio-mini(?:$|-)`),
|
||||
pricing: gptAudioMiniPricing,
|
||||
},
|
||||
{
|
||||
pattern: regexp.MustCompile(`^(?:gpt-4o-audio|gpt-audio)(?:$|-)`),
|
||||
pricing: gpt4oAudioPricing,
|
||||
},
|
||||
{
|
||||
pattern: regexp.MustCompile(`^gpt-4o-mini`),
|
||||
pattern: regexp.MustCompile(`^gpt-4\.1-nano(?:$|-)`),
|
||||
pricing: gpt41NanoPricing,
|
||||
},
|
||||
{
|
||||
pattern: regexp.MustCompile(`^gpt-4\.1-mini(?:$|-)`),
|
||||
pricing: gpt41MiniPricing,
|
||||
},
|
||||
{
|
||||
pattern: regexp.MustCompile(`^gpt-4\.1(?:$|-)`),
|
||||
pricing: gpt41Pricing,
|
||||
},
|
||||
{
|
||||
pattern: regexp.MustCompile(`^gpt-4o-mini(?:$|-)`),
|
||||
pricing: gpt4oMiniPricing,
|
||||
},
|
||||
{
|
||||
pattern: regexp.MustCompile(`^gpt-4o`),
|
||||
pattern: regexp.MustCompile(`^gpt-4o(?:$|-)`),
|
||||
pricing: gpt4oPricing,
|
||||
},
|
||||
{
|
||||
pattern: regexp.MustCompile(`^chatgpt-4o`),
|
||||
pattern: regexp.MustCompile(`^chatgpt-4o(?:$|-)`),
|
||||
pricing: gpt4oPricing,
|
||||
},
|
||||
}
|
||||
|
||||
flexModelFamilies = []modelFamily{
|
||||
{
|
||||
pattern: regexp.MustCompile(`^gpt-5-mini(?:$|-)`),
|
||||
pricing: gpt5MiniFlexPricing,
|
||||
},
|
||||
{
|
||||
pattern: regexp.MustCompile(`^gpt-5-nano(?:$|-)`),
|
||||
pricing: gpt5NanoFlexPricing,
|
||||
},
|
||||
{
|
||||
pattern: regexp.MustCompile(`^gpt-5\.2(?:$|-)`),
|
||||
pricing: gpt52FlexPricing,
|
||||
},
|
||||
{
|
||||
pattern: regexp.MustCompile(`^gpt-5\.1(?:$|-)`),
|
||||
pricing: gpt5FlexPricing,
|
||||
},
|
||||
{
|
||||
pattern: regexp.MustCompile(`^gpt-5(?:$|-)`),
|
||||
pricing: gpt5FlexPricing,
|
||||
},
|
||||
{
|
||||
pattern: regexp.MustCompile(`^o4-mini(?:$|-)`),
|
||||
pricing: o4MiniFlexPricing,
|
||||
},
|
||||
{
|
||||
pattern: regexp.MustCompile(`^o3(?:$|-)`),
|
||||
pricing: o3FlexPricing,
|
||||
},
|
||||
}
|
||||
|
||||
priorityModelFamilies = []modelFamily{
|
||||
{
|
||||
pattern: regexp.MustCompile(`^gpt-5\.2-codex(?:$|-)`),
|
||||
pricing: gpt52CodexPriorityPricing,
|
||||
},
|
||||
{
|
||||
pattern: regexp.MustCompile(`^gpt-5\.1-codex-max(?:$|-)`),
|
||||
pricing: gpt51CodexPriorityPricing,
|
||||
},
|
||||
{
|
||||
pattern: regexp.MustCompile(`^gpt-5\.1-codex(?:$|-)`),
|
||||
pricing: gpt51CodexPriorityPricing,
|
||||
},
|
||||
{
|
||||
pattern: regexp.MustCompile(`^gpt-5-codex(?:$|-)`),
|
||||
pricing: gpt51CodexPriorityPricing,
|
||||
},
|
||||
{
|
||||
pattern: regexp.MustCompile(`^gpt-5-mini(?:$|-)`),
|
||||
pricing: gpt5MiniPriorityPricing,
|
||||
},
|
||||
{
|
||||
pattern: regexp.MustCompile(`^gpt-5\.2(?:$|-)`),
|
||||
pricing: gpt52PriorityPricing,
|
||||
},
|
||||
{
|
||||
pattern: regexp.MustCompile(`^gpt-5\.1(?:$|-)`),
|
||||
pricing: gpt5PriorityPricing,
|
||||
},
|
||||
{
|
||||
pattern: regexp.MustCompile(`^gpt-5(?:$|-)`),
|
||||
pricing: gpt5PriorityPricing,
|
||||
},
|
||||
{
|
||||
pattern: regexp.MustCompile(`^o4-mini(?:$|-)`),
|
||||
pricing: o4MiniPriorityPricing,
|
||||
},
|
||||
{
|
||||
pattern: regexp.MustCompile(`^o3(?:$|-)`),
|
||||
pricing: o3PriorityPricing,
|
||||
},
|
||||
{
|
||||
pattern: regexp.MustCompile(`^gpt-4\.1-nano(?:$|-)`),
|
||||
pricing: gpt41NanoPriorityPricing,
|
||||
},
|
||||
{
|
||||
pattern: regexp.MustCompile(`^gpt-4\.1-mini(?:$|-)`),
|
||||
pricing: gpt41MiniPriorityPricing,
|
||||
},
|
||||
{
|
||||
pattern: regexp.MustCompile(`^gpt-4\.1(?:$|-)`),
|
||||
pricing: gpt41PriorityPricing,
|
||||
},
|
||||
{
|
||||
pattern: regexp.MustCompile(`^gpt-4o-mini(?:$|-)`),
|
||||
pricing: gpt4oMiniPriorityPricing,
|
||||
},
|
||||
{
|
||||
pattern: regexp.MustCompile(`^gpt-4o(?:$|-)`),
|
||||
pricing: gpt4oPriorityPricing,
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
func getPricing(model string) ModelPricing {
|
||||
func modelFamiliesForTier(serviceTier string) []modelFamily {
|
||||
switch serviceTier {
|
||||
case serviceTierFlex:
|
||||
return flexModelFamilies
|
||||
case serviceTierPriority:
|
||||
return priorityModelFamilies
|
||||
default:
|
||||
return standardModelFamilies
|
||||
}
|
||||
}
|
||||
|
||||
func findPricingInFamilies(model string, modelFamilies []modelFamily) (ModelPricing, bool) {
|
||||
for _, family := range modelFamilies {
|
||||
if family.pattern.MatchString(model) {
|
||||
return family.pricing
|
||||
return family.pricing, true
|
||||
}
|
||||
}
|
||||
return ModelPricing{}, false
|
||||
}
|
||||
|
||||
func normalizeServiceTier(serviceTier string) string {
|
||||
switch strings.ToLower(strings.TrimSpace(serviceTier)) {
|
||||
case "", serviceTierAuto, serviceTierDefault:
|
||||
return serviceTierDefault
|
||||
case serviceTierFlex:
|
||||
return serviceTierFlex
|
||||
case serviceTierPriority:
|
||||
return serviceTierPriority
|
||||
case serviceTierScale:
|
||||
// Scale-tier requests are prepaid differently and not listed in this usage file.
|
||||
return serviceTierDefault
|
||||
default:
|
||||
return serviceTierDefault
|
||||
}
|
||||
}
|
||||
|
||||
func getPricing(model string, serviceTier string) ModelPricing {
|
||||
normalizedServiceTier := normalizeServiceTier(serviceTier)
|
||||
modelFamilies := modelFamiliesForTier(normalizedServiceTier)
|
||||
|
||||
if pricing, found := findPricingInFamilies(model, modelFamilies); found {
|
||||
return pricing
|
||||
}
|
||||
|
||||
normalizedModel := normalizeGPT5Model(model)
|
||||
if normalizedModel != model {
|
||||
if pricing, found := findPricingInFamilies(normalizedModel, modelFamilies); found {
|
||||
return pricing
|
||||
}
|
||||
}
|
||||
|
||||
if normalizedServiceTier != serviceTierDefault {
|
||||
if pricing, found := findPricingInFamilies(model, standardModelFamilies); found {
|
||||
return pricing
|
||||
}
|
||||
if normalizedModel != model {
|
||||
if pricing, found := findPricingInFamilies(normalizedModel, standardModelFamilies); found {
|
||||
return pricing
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return gpt4oPricing
|
||||
}
|
||||
|
||||
func calculateCost(stats UsageStats, model string) float64 {
|
||||
pricing := getPricing(model)
|
||||
func normalizeGPT5Model(model string) string {
|
||||
if !strings.HasPrefix(model, "gpt-5.") {
|
||||
return model
|
||||
}
|
||||
|
||||
switch {
|
||||
case strings.Contains(model, "-codex-mini"):
|
||||
return "gpt-5.1-codex-mini"
|
||||
case strings.Contains(model, "-codex-max"):
|
||||
return "gpt-5.1-codex-max"
|
||||
case strings.Contains(model, "-codex"):
|
||||
return "gpt-5.2-codex"
|
||||
case strings.Contains(model, "-chat-latest"):
|
||||
return "gpt-5.2-chat-latest"
|
||||
case strings.Contains(model, "-pro"):
|
||||
return "gpt-5.2-pro"
|
||||
case strings.Contains(model, "-mini"):
|
||||
return "gpt-5-mini"
|
||||
case strings.Contains(model, "-nano"):
|
||||
return "gpt-5-nano"
|
||||
default:
|
||||
return "gpt-5.2"
|
||||
}
|
||||
}
|
||||
|
||||
func calculateCost(stats UsageStats, model string, serviceTier string) float64 {
|
||||
pricing := getPricing(model, serviceTier)
|
||||
|
||||
regularInputTokens := stats.InputTokens - stats.CachedTokens
|
||||
if regularInputTokens < 0 {
|
||||
@@ -252,12 +720,13 @@ func (u *AggregatedUsage) ToJSON() *AggregatedUsageJSON {
|
||||
}
|
||||
|
||||
for i, combo := range u.Combinations {
|
||||
totalCost := calculateCost(combo.Total, combo.Model)
|
||||
totalCost := calculateCost(combo.Total, combo.Model, combo.ServiceTier)
|
||||
|
||||
result.Costs.TotalUSD += totalCost
|
||||
|
||||
comboJSON := CostCombinationJSON{
|
||||
Model: combo.Model,
|
||||
Model: combo.Model,
|
||||
ServiceTier: combo.ServiceTier,
|
||||
Total: UsageStatsJSON{
|
||||
RequestCount: combo.Total.RequestCount,
|
||||
InputTokens: combo.Total.InputTokens,
|
||||
@@ -269,7 +738,7 @@ func (u *AggregatedUsage) ToJSON() *AggregatedUsageJSON {
|
||||
}
|
||||
|
||||
for user, userStats := range combo.ByUser {
|
||||
userCost := calculateCost(userStats, combo.Model)
|
||||
userCost := calculateCost(userStats, combo.Model, combo.ServiceTier)
|
||||
result.Costs.ByUser[user] += userCost
|
||||
|
||||
comboJSON.ByUser[user] = UsageStatsJSON{
|
||||
@@ -318,6 +787,7 @@ func (u *AggregatedUsage) Load() error {
|
||||
u.Combinations = temp.Combinations
|
||||
|
||||
for i := range u.Combinations {
|
||||
u.Combinations[i].ServiceTier = normalizeServiceTier(u.Combinations[i].ServiceTier)
|
||||
if u.Combinations[i].ByUser == nil {
|
||||
u.Combinations[i].ByUser = make(map[string]UsageStats)
|
||||
}
|
||||
@@ -349,11 +819,13 @@ func (u *AggregatedUsage) Save() error {
|
||||
return err
|
||||
}
|
||||
|
||||
func (u *AggregatedUsage) AddUsage(model string, inputTokens, outputTokens, cachedTokens int64, user string) error {
|
||||
func (u *AggregatedUsage) AddUsage(model string, inputTokens, outputTokens, cachedTokens int64, serviceTier string, user string) error {
|
||||
if model == "" {
|
||||
return E.New("model cannot be empty")
|
||||
}
|
||||
|
||||
normalizedServiceTier := normalizeServiceTier(serviceTier)
|
||||
|
||||
u.mutex.Lock()
|
||||
defer u.mutex.Unlock()
|
||||
|
||||
@@ -361,7 +833,11 @@ func (u *AggregatedUsage) AddUsage(model string, inputTokens, outputTokens, cach
|
||||
|
||||
var combo *CostCombination
|
||||
for i := range u.Combinations {
|
||||
if u.Combinations[i].Model == model {
|
||||
comboServiceTier := normalizeServiceTier(u.Combinations[i].ServiceTier)
|
||||
if u.Combinations[i].ServiceTier != comboServiceTier {
|
||||
u.Combinations[i].ServiceTier = comboServiceTier
|
||||
}
|
||||
if u.Combinations[i].Model == model && comboServiceTier == normalizedServiceTier {
|
||||
combo = &u.Combinations[i]
|
||||
break
|
||||
}
|
||||
@@ -369,9 +845,10 @@ func (u *AggregatedUsage) AddUsage(model string, inputTokens, outputTokens, cach
|
||||
|
||||
if combo == nil {
|
||||
newCombo := CostCombination{
|
||||
Model: model,
|
||||
Total: UsageStats{},
|
||||
ByUser: make(map[string]UsageStats),
|
||||
Model: model,
|
||||
ServiceTier: normalizedServiceTier,
|
||||
Total: UsageStats{},
|
||||
ByUser: make(map[string]UsageStats),
|
||||
}
|
||||
u.Combinations = append(u.Combinations, newCombo)
|
||||
combo = &u.Combinations[len(u.Combinations)-1]
|
||||
|
||||
Reference in New Issue
Block a user