M4a:神识全量网关能力——OpenAI 兼容适配器(元炁共用)、候选路由+凭证/绑定级冷却+首字超时切道+粘性会话、用量计量与 /me/usage,故障注入测试四场景

This commit is contained in:
chenxin committed 2026-08-27 09:32:37 +08:00
1 parent df5caa69e7
commit 7279904043
8 files changed
+717 -35

No files matched your search

+75
View File
@@ -0,0 +1,75 @@
// 候选路由与冷却:凭证级(401/403)与绑定级(429/5xx/超时)冷却,粘性会话与故障迭代。
package route
import (
"sync"
"time"
)
// 冷却时长:凭证错误长冷却,限流/超时短冷却(避免整渠道误杀,shenshi-gateway.md §4)。
const (
credentialCooldown = 10 * time.Minute
bindingCooldown = 30 * time.Second
)
// Cooldowns 进程内冷却状态:key 形如 "cred:<endpoint>:<keyhash>" / "bind:<endpoint>:<model>"。
type Cooldowns struct {
mu sync.Mutex
until map[string]time.Time
}
// NewCooldowns 构造冷却表。
func NewCooldowns() *Cooldowns {
return &Cooldowns{until: make(map[string]time.Time)}
}
// Mark 凭证级(credential=true)或绑定级冷却。
func (c *Cooldowns) Mark(key string, credential bool) {
c.mu.Lock()
defer c.mu.Unlock()
duration := bindingCooldown
if credential {
duration = credentialCooldown
}
c.until[key] = time.Now().Add(duration)
}
// Active 该冷却键当前是否生效。
func (c *Cooldowns) Active(key string) bool {
c.mu.Lock()
defer c.mu.Unlock()
t, ok := c.until[key]
return ok && time.Now().Before(t)
}
// Sticky 粘性会话:同一 run 连续请求优先同端点(进程内,run 生命周期短)。
type Sticky struct {
mu sync.Mutex
binds map[string]string
}
// NewSticky 构造粘性表。
func NewSticky() *Sticky {
return &Sticky{binds: make(map[string]string)}
}
// Remember 记录 run → 端点绑定。
func (s *Sticky) Remember(runID, endpointName string) {
s.mu.Lock()
defer s.mu.Unlock()
s.binds[runID] = endpointName
}
// Recall 取 run 的粘性端点(无则空串)。
func (s *Sticky) Recall(runID string) string {
s.mu.Lock()
defer s.mu.Unlock()
return s.binds[runID]
}
// Forget run 结束后清理。
func (s *Sticky) Forget(runID string) {
s.mu.Lock()
defer s.mu.Unlock()
delete(s.binds, runID)
}
+115 -11
View File
@@ -1,46 +1,150 @@
// Package route 维护逻辑模型/能力槽位到候选端点的路由注册表。
// M0 为单一 stub 端点的静态表;候选迭代、健康冷却与故障切换按
// docs/architecture/shenshi-gateway.md §4 于 M4 落地。
// 端点来源:环境 SHENSHI_ENDPOINTS(JSON 数组);未配置时回落 stub(骨架形态)。
// 冷却粒度(shenshi-gateway.md §4):401/403 只冷却凭证;429/5xx/超时只冷却该端点绑定。
package route
import (
"encoding/json"
"fmt"
"os"
"sort"
"time"
"hunjing/shenshi/internal/transform"
)
// Endpoint 一个可路由的出站端点:类型 + 适配器实例。
// Endpoint 一个可路由的出站端点。
type Endpoint struct {
Type string
Name string
Type string // openai_compatible / local / stub(契约词典 §14.2)
Adaptor transform.Adaptor
Priority int
// 凭证指纹(凭证级冷却键);stub/openai 兼容端点各自填充
CredentialKey string
// 绑定级冷却键的模型名部分
UpstreamModel string
}
// EndpointSpec 环境配置形态。
type EndpointSpec struct {
Type string `json:"type"`
Name string `json:"name"`
BaseURL string `json:"base_url"`
APIKey string `json:"api_key"`
Model string `json:"model"`
Priority int `json:"priority"`
}
// Registry 槽位 → 候选端点列表(按 Priority 升序尝试)。
type Registry struct {
bySlot map[string][]Endpoint
// Cooldowns/FirstByteTimeoutMS 由 server 层共享使用
Cooldowns *Cooldowns
Sticky *Sticky
FirstByteTimeout time.Duration
}
// New 构建注册表并登记默认端点。
// New 构建注册表:读 SHENSHI_ENDPOINTS(JSON),无配置时登记 stub。
func New() *Registry {
r := &Registry{bySlot: make(map[string][]Endpoint)}
r.Register("chat", Endpoint{Type: "stub", Adaptor: transform.StubAdaptor{}, Priority: 100})
r := &Registry{
bySlot: make(map[string][]Endpoint),
Cooldowns: NewCooldowns(),
Sticky: NewSticky(),
FirstByteTimeout: 10 * time.Second,
}
specs := loadSpecs()
if len(specs) == 0 {
r.Register("chat", Endpoint{Type: "stub", Adaptor: transform.StubAdaptor{}, Priority: 100, UpstreamModel: "stub"})
return r
}
for _, spec := range specs {
if spec.Type != "openai_compatible" && spec.Type != "local" {
continue
}
adaptor := &transform.OpenAICompatAdaptor{
EndpointName: spec.Name,
BaseURL: spec.BaseURL,
APIKey: spec.APIKey,
UpstreamModel: spec.Model,
}
cred := spec.Name
if spec.APIKey != "" {
cred = spec.Name + ":" + fmt.Sprintf("%x", len(spec.APIKey)) // 不落密钥原文,仅指纹
}
r.Register("chat", Endpoint{
Name: spec.Name, Type: spec.Type, Adaptor: adaptor,
Priority: spec.Priority, CredentialKey: cred, UpstreamModel: spec.Model,
})
}
return r
}
// Register 登记端点到指定能力槽位(契约词典 §14.1 model_slot 取值)。
func loadSpecs() []EndpointSpec {
raw := os.Getenv("SHENSHI_ENDPOINTS")
if raw == "" {
return nil
}
var specs []EndpointSpec
if err := json.Unmarshal([]byte(raw), &specs); err != nil {
return nil
}
return specs
}
// Reset 清空槽位(测试装配用)。
func (r *Registry) Reset(slot string) { delete(r.bySlot, slot) }
// Register 登记端点到指定能力槽位(契约词典 §14.1)。
func (r *Registry) Register(slot string, ep Endpoint) {
r.bySlot[slot] = append(r.bySlot[slot], ep)
}
// Resolve 返回槽位的候选端点;无候选即错误(HJ-2001 语义)。
func (r *Registry) Resolve(slot string) ([]Endpoint, error) {
// Candidates 返回槽位候选(Priority 升序;粘性端点置首;冷却中端点排后不剔除——
// 全部冷却时仍可尝试最优先者,避免死锁)。
func (r *Registry) Candidates(slot, runID string) ([]Endpoint, error) {
if slot == "" {
slot = "chat"
}
eps := r.bySlot[slot]
eps := append([]Endpoint(nil), r.bySlot[slot]...)
if len(eps) == 0 {
return nil, fmt.Errorf("槽位 %s 无可用端点", slot)
}
sort.SliceStable(eps, func(i, j int) bool { return eps[i].Priority < eps[j].Priority })
if runID != "" {
if stickyName := r.Sticky.Recall(runID); stickyName != "" {
for i, ep := range eps {
if ep.Name == stickyName {
eps = append([]Endpoint{ep}, append(eps[:i:i], eps[i+1:]...)...)
break
}
}
}
}
// 冷却中的端点移到队尾(保留兜底能力)
cooling := 0
for i := 0; i < len(eps)-cooling; {
if r.Cooldowns.Active(bindingKey(eps[i])) || r.Cooldowns.Active(credKey(eps[i])) {
ep := eps[i]
eps = append(append(eps[:i:i], eps[i+1:]...), ep)
cooling++
continue
}
i++
}
return eps, nil
}
func bindingKey(ep Endpoint) string {
return "bind:" + ep.Name + ":" + ep.UpstreamModel
}
func credKey(ep Endpoint) string {
if ep.CredentialKey == "" {
return "cred:" + ep.Name
}
return "cred:" + ep.CredentialKey
}
// BindingCooldownKey / CredentialCooldownKey 供 server 层标记冷却。
func BindingCooldownKey(ep Endpoint) string { return bindingKey(ep) }
func CredentialCooldownKey(ep Endpoint) string { return credKey(ep) }