Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions apps/docs/en/gateway/providers.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,3 +39,15 @@ Model requests must go to CPA, not CPAMP. For same-domain routing, see [Reverse
Model prices affect CPAMP local cost estimates only. They do not change CPA routing or provider billing.

:::

### xAI weekly periods with zero usage

Some unified-billing accounts omit `creditUsagePercent` when usage is zero or rounds to zero.
The panel and server inspection accept an implicit 0% used only when settings confirm SuperGrok / SuperGrok Heavy
and a complete, successful gRPC billing response from the same credential matches the active REST weekly period.
The existing remaining-quota display then shows 100%. This does not prove that no tokens were consumed
and cannot be converted into a remaining token count.

REST period metadata alone, Free / unconfirmed plans, and expired or incomplete responses remain unknown (`-`).
The optional reads use the existing CPA management proxy, add at most six seconds, and never turn an enrichment failure
into an account health failure or an automatic account action.
10 changes: 10 additions & 0 deletions apps/docs/gateway/providers.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,3 +39,13 @@ Muse / Meta 是一个需要特别区分凭证类型的场景:OAuth Device Flow
模型价格只影响 CPAMP 的本地成本估算,不会改变 CPA 路由或 Provider 账单。

:::

### xAI 零使用量的周额度

部分统一计费账号在已用百分比为零(或舍入为零)时会省略 `creditUsagePercent`。
只有设置接口明确确认 SuperGrok / SuperGrok Heavy,且同一凭证的补充 gRPC 账单响应完整、成功、
处于当前周账期并与 REST 账期一致时,面板和服务端巡检才将其识别为隐含的 0% 已用,页面显示 100% 剩余。
这不代表没有消耗过 token,也不能换算成剩余 token 数。

仅有 REST 周期信息、Free / 未确认套餐、过期或不完整响应仍保持未知(`-`)。
补充查询只读、通过现有 CPA 管理接口转发,最多额外等待 6 秒;失败不改变原有账单健康判定或触发账号操作。
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
package codexinspection

import (
"context"
"encoding/base64"
"encoding/binary"
"strconv"
"strings"
"time"

"github.com/seakee/cpa-manager-plus/apps/manager-server/internal/model"
"github.com/seakee/cpa-manager-plus/apps/manager-server/internal/store"
)

const xaiWebBillingURL = "https://grok.com/grok_api_v2.GrokBuildBilling/GetGrokCreditsConfig"

// An omitted REST percentage alone remains unknown (#710). Only a complete,
// successful gRPC response for the same active period establishes implicit zero.
func isXAIValidatedZero(text, start, end string, now time.Time) bool {
from, err := time.Parse(time.RFC3339Nano, start)
if err != nil {
return false
}
until, err := time.Parse(time.RFC3339Nano, end)
if err != nil || now.Before(from) || !now.Before(until) || len(text) > 64*1024 {
return false
}
encoded := strings.Join(strings.Fields(text), "")
if len(encoded) == 0 || len(encoded)%4 != 0 {
return false
}
// grpc-web-text can flush independently padded chunks, including trailers.
var data []byte
for len(encoded) > 0 {
n := len(encoded)
if i := strings.IndexByte(encoded, '='); i >= 0 {
n = ((i / 4) + 1) * 4
}
chunk, err := base64.StdEncoding.Strict().DecodeString(encoded[:n])
if err != nil {
return false
}
data = append(data, chunk...)
encoded = encoded[n:]
}
var payload []byte
done := false
for len(data) > 0 {
if done || len(data) < 5 {
return false
}
flag := data[0]
size := uint64(binary.BigEndian.Uint32(data[1:5]))
data = data[5:]
if size > uint64(len(data)) {
return false
}
frame := data[:int(size)]
data = data[int(size):]
if flag == 0 && payload == nil {
payload = frame
} else if flag == 128 && payload != nil {
statuses := 0
for _, line := range strings.Split(string(frame), "\r\n") {
if strings.HasPrefix(strings.ToLower(line), "grpc-status:") {
statuses++
if strings.TrimSpace(line[len("grpc-status:"):]) != "0" {
return false
}
}
}
if statuses != 1 {
return false
}
done = true
} else {
return false
}
}
if payload == nil || !done {
return false
}
messages := map[string]bool{"1": true, "1.2": true, "1.3": true, "1.4": true, "1.5": true, "1.6": true, "1.7": true, "1.8": true, "1.12": true, "1.6.1": true, "1.6.2": true, "1.6.3": true, "1.8.2": true, "1.8.3": true, "1.6.3.2": true, "1.6.3.3": true}
structural := map[string]bool{"1": true, "1.8": true, "1.8.1": true, "1.8.2": true, "1.8.3": true, "1.8.2.1": true, "1.8.3.1": true}
seen := map[string]bool{}
values := map[string]uint64{}
var scan func([]byte, string) bool
scan = func(b []byte, path string) bool {
for len(b) > 0 {
tag, n := binary.Uvarint(b)
if n <= 0 {
return false
}
b = b[n:]
field, wire := tag>>3, tag&7
if field == 0 || field > 536870911 {
return false
}
key := strconv.FormatUint(field, 10)
if path != "" {
key = path + "." + key
}
if key == "1.1" || (messages[key] && wire != 2) {
return false
}
if structural[key] {
if seen[key] {
return false
}
seen[key] = true
}
switch wire {
case 0:
value, n := binary.Uvarint(b)
if n <= 0 {
return false
}
b = b[n:]
values[key] = value
case 2:
size, n := binary.Uvarint(b)
if n <= 0 {
return false
}
b = b[n:]
if size > uint64(len(b)) {
return false
}
if messages[key] && !scan(b[:int(size)], key) {
return false
}
b = b[int(size):]
case 1:
if len(b) < 8 {
return false
}
b = b[8:]
default:
// Any float could contradict implicit zero; never infer from it.
return false
}
}
return true
}
return scan(payload, "") && values["1.8.1"] == 2 && values["1.8.2.1"] == uint64(from.Unix()) && values["1.8.3.1"] == uint64(until.Unix())
}

func (s *Service) enrichXAIBillingZero(ctx context.Context, setup store.Setup, settings model.ManagerCodexInspectionConfig, item account, summary *xaiBillingSummary) {
if summary == nil || summary.UsagePercent != nil || !summary.HasWeeklyData || len(summary.ProductUsage) > 0 {
return
}
start, err := time.Parse(time.RFC3339Nano, summary.PeriodStart)
if err != nil {
return
}
end, err := time.Parse(time.RFC3339Nano, summary.PeriodEnd)
if err != nil {
return
}
now := time.Now()
if now.Before(start) || !now.Before(end) {
return
}
bounded, cancel := context.WithTimeout(ctx, 6*time.Second)
defer cancel()
settingsResponse, _, err := s.requestProviderBillingAt(bounded, setup, settings, item, xaiCLIChatProxyBaseURL+"/settings", map[string]string{"Authorization": "Bearer $TOKEN$", "x-xai-token-auth": "xai-grok-cli", "x-grok-client-version": xaiGrokVersion})
if err != nil || settingsResponse.StatusCode != 200 {
return
}
tier := readString(parseRecord(settingsResponse.Body), "subscription_tier_display")
if tier != "SuperGrok" && tier != "SuperGrok Heavy" {
return
}
response, _, err := s.requestProviderAPICallAt(bounded, setup, settings, item, "POST", xaiWebBillingURL, map[string]string{
"Authorization": "Bearer $TOKEN$", "Content-Type": "application/grpc-web-text+proto", "Accept": "application/grpc-web-text+proto", "x-grpc-web": "1", "Origin": "https://grok.com",
}, "AAAAAAIIAA==")
if err == nil && response.StatusCode == 200 && isXAIValidatedZero(response.BodyText, summary.PeriodStart, summary.PeriodEnd, time.Now()) {
zero := float64(0)
summary.UsagePercent = &zero
summary.UsagePercentSource = "grpc-implicit-zero"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
package codexinspection

import (
"context"
"encoding/base64"
"encoding/binary"
"encoding/json"
"github.com/seakee/cpa-manager-plus/apps/manager-server/internal/model"
"github.com/seakee/cpa-manager-plus/apps/manager-server/internal/store"
"net/http"
"net/http/httptest"
"os"
"strings"
"testing"
"time"
)

func TestXAIValidatedZero(t *testing.T) {
data, err := os.ReadFile("../../../../../tests/fixtures/xai-billing-zero.json")
if err != nil {
t.Fatal(err)
}
var cases []struct {
Name, Text, Start, End, Now string
Want bool
}
if err := json.Unmarshal(data, &cases); err != nil {
t.Fatal(err)
}
for _, tt := range cases {
t.Run(tt.Name, func(t *testing.T) {
now, err := time.Parse(time.RFC3339, tt.Now)
if err != nil {
t.Fatal(err)
}
if got := isXAIValidatedZero(tt.Text, tt.Start, tt.End, now); got != tt.Want {
t.Fatalf("got %v want %v", got, tt.Want)
}
})
}
}

func TestRequestXAIBillingZeroEnrichment(t *testing.T) {
for _, tier := range []string{"SuperGrok Heavy", "Free", "unknown"} {
t.Run(tier, func(t *testing.T) {
start := time.Now().UTC().Truncate(time.Second).Add(-time.Hour)
end := start.Add(7 * 24 * time.Hour)
field := func(n byte, b []byte) []byte { return append([]byte{n*8 + 2, byte(len(b))}, b...) }
stamp := func(v int64) []byte { return binary.AppendUvarint([]byte{8}, uint64(v)) }
period := append([]byte{8, 2}, field(2, stamp(start.Unix()))...)
period = append(period, field(3, stamp(end.Unix()))...)
payload := field(1, field(8, period))
frame := func(flag byte, b []byte) string {
return base64.StdEncoding.EncodeToString(append(binary.BigEndian.AppendUint32([]byte{flag}, uint32(len(b))), b...))
}
grpc := frame(0, payload) + frame(128, []byte("grpc-status:0\r\n"))
grpcCalls := 0
upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
var req struct {
URL, AuthIndex, Method, Data string
Header map[string]string
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
t.Error(err)
return
}
if req.AuthIndex != "test-account" || req.Header["Authorization"] != "Bearer $TOKEN$" {
t.Error("credential scope not preserved")
}
var body any = map[string]any{"config": map[string]any{"isUnifiedBillingUser": true, "currentPeriod": map[string]any{"type": "weekly", "start": start.Format(time.RFC3339), "end": end.Format(time.RFC3339)}}}
if strings.HasSuffix(req.URL, "/settings") {
body = map[string]any{"subscription_tier_display": tier}
}
if req.URL == xaiWebBillingURL {
grpcCalls++
body = grpc
if req.Method != "POST" || req.Data != "AAAAAAIIAA==" {
t.Error("invalid grpc request")
}
}
_ = json.NewEncoder(w).Encode(map[string]any{"status_code": 200, "body": body})
}))
defer upstream.Close()
settings := model.DefaultCodexInspectionConfig()
probe, err := New(nil, nil, upstream.Client()).requestXAIBilling(context.Background(), store.Setup{CPAUpstreamURL: upstream.URL, ManagementKey: "test"}, settings, account{AuthIndex: "test-account", File: authFile{}})
if err != nil || !probe.Healthy || probe.Partial {
t.Fatalf("unexpected health: %+v %v", probe, err)
}
if tier == "SuperGrok Heavy" {
if grpcCalls != 1 || probe.Summary.UsagePercent == nil || *probe.Summary.UsagePercent != 0 || probe.Summary.UsagePercentSource != "grpc-implicit-zero" {
t.Fatalf("zero not adopted: %+v", probe.Summary)
}
} else if grpcCalls != 0 || probe.Summary.UsagePercent != nil {
t.Fatal("unconfirmed/free usage must stay unknown")
}
})
}
}
17 changes: 12 additions & 5 deletions apps/manager-server/internal/service/codexinspection/xai_probe.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ type xaiProbeDecision struct {
}

type xaiBillingSummary struct {
PeriodStart string
UsagePercentSource string
UsagePercent *float64
UsedPercent *float64
OnDemandUsedPercent *float64
Expand Down Expand Up @@ -527,6 +529,9 @@ func (s *Service) requestXAIBilling(

probe := xaiBillingProbe{}
if weekly.Summary != nil {
if weekly.ZeroFallbackEligible {
s.enrichXAIBillingZero(ctx, setup, settings, item, weekly.Summary)
}
probe.Summary = mergeXAIBillingSummary(weekly.Summary, monthly.Summary)
probe.StatusCode = weekly.StatusCode
probe.Healthy = true
Expand Down Expand Up @@ -780,10 +785,11 @@ func positiveXAIFloat(value *float64) bool {
}

type xaiBillingResult struct {
Summary *xaiBillingSummary
Failure *xaiProbeDecision
Partial bool
StatusCode int
ZeroFallbackEligible bool
Summary *xaiBillingSummary
Failure *xaiProbeDecision
Partial bool
StatusCode int
}

func (s *Service) requestProviderBilling(
Expand Down Expand Up @@ -811,7 +817,7 @@ func (s *Service) requestProviderBilling(
if summary == nil {
return xaiBillingResult{Failure: xaiDecision(response.StatusCode, "protocol_changed", "xAI billing response schema changed")}, nil
}
return xaiBillingResult{Summary: summary, StatusCode: response.StatusCode}, nil
return xaiBillingResult{Summary: summary, StatusCode: response.StatusCode, ZeroFallbackEligible: config["isUnifiedBillingUser"] == true && config["creditUsagePercent"] == nil && config["credit_usage_percent"] == nil}, nil
}
return xaiBillingResult{Failure: xaiDecision(
response.StatusCode,
Expand Down Expand Up @@ -1022,6 +1028,7 @@ func parseXAIBillingSummary(config map[string]any) *xaiBillingSummary {
return nil
}
return &xaiBillingSummary{
PeriodStart: readString(period, "start"),
UsagePercent: usage,
UsedPercent: monthlyUsed,
OnDemandUsedPercent: onDemandUsedPercent,
Expand Down
Loading