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

+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) }