- 预扣:每请求按 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 开启)
82 lines
2.2 KiB
Go
82 lines
2.2 KiB
Go
// 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
|
||
}
|