Files
openteam/server/internal/cli/serve.go
T
Sakurasan 0e10f0bbde feat: 余额预扣 reserve/capture、冷却兜底重试、Prometheus /metrics
- 预扣:每请求按 body 估算成本上界计入冻结额(1e-8 USD 原子计数),402 判定
  改为「余额 - 在途冻结 ≤ 0」,并发的长流式请求不再互相透支;OT_PROXY_RESERVE_MAX_TOKENS
  控制估算基数(默认 4096,0=关闭),handler defer 释放,失败/中断/panic 均覆盖
- 调度:全部候选都在运行期冷却时,剩余冷却 ≤ OT_PROXY_COOLDOWN_GRACE 的最早恢复渠道
  兜底重试一次而不是 503(429 Retry-After 语义);DB 层不可用的渠道不参与兜底
- 指标:openteam_proxy_requests_total / request_duration / channel_failures /
  usage_queue_depth / frozen_balance,/metrics 默认关闭(OT_METRICS_ENABLED 开启)
2026-11-09 01:04:40 +08:00

82 lines
2.2 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Package cli 命令行入口。
package cli
import (
"context"
"errors"
"log"
"net/http"
"os"
"os/signal"
"strconv"
"syscall"
"time"
"github.com/openteam/server/internal/api"
"github.com/openteam/server/internal/app"
"github.com/openteam/server/internal/config"
"github.com/openteam/server/internal/proxy"
"github.com/shopspring/decimal"
)
// serve 启动 HTTP 服务(默认子命令)。
func serve() error {
// 金额序列化为 JSON number 而非字符串:前端 types(balance/cost 等均为 number)零改动
decimal.MarshalJSONWithoutQuotes = true
cfg, err := config.Load()
if err != nil {
return err
}
a, err := app.New(cfg)
if err != nil {
return err
}
defer a.Shutdown(context.Background())
gw := proxy.NewGateway(a.DB, a.Enc, a.Usage, a.Limit, proxy.Options{
UserRPS: cfg.RateLimit.UserRPS,
LogRaw: cfg.Proxy.LogRaw,
GWTimeout: cfg.Proxy.Timeout,
MaxBodyMB: cfg.Proxy.MaxBodyMB,
FailoverBudget: cfg.Proxy.FailoverBudget,
FailoverSwitchCaps: cfg.Proxy.FailoverSwitchCaps,
CooldownGrace: cfg.Proxy.CooldownGrace,
ReserveMaxTokens: cfg.Proxy.ReserveMaxTokens,
StreamHeartbeat: cfg.Proxy.StreamHeartbeat,
FirstTokenWait: cfg.Proxy.FirstTokenWait,
AffinityTTL: cfg.Proxy.AffinityTTL,
ChannelAcquireWait: cfg.Proxy.ChannelAcquireWait,
AuthCacheTTL: cfg.Proxy.AuthCacheTTL,
})
// 渠道健康状态变化后让目录缓存立刻失效,而不是等 TTL
a.Health.OnChange = gw.InvalidateCatalog
router := api.NewRouter(a, gw)
srv := &http.Server{
Addr: ":" + strconv.Itoa(cfg.Port),
Handler: router,
ReadHeaderTimeout: 10 * time.Second,
}
go func() {
log.Printf("openteam listening on %s (env=%s)", srv.Addr, cfg.Env)
if err := srv.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
log.Fatalf("server: %v", err)
}
}()
quit := make(chan os.Signal, 1)
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
<-quit
log.Println("shutting down...")
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
if err := srv.Shutdown(ctx); err != nil {
log.Printf("server shutdown: %v", err)
}
a.Usage.Close()
return nil
}