M4a:神识全量网关能力——OpenAI 兼容适配器(元炁共用)、候选路由+凭证/绑定级冷却+首字超时切道+粘性会话、用量计量与 /me/usage,故障注入测试四场景
This commit is contained in:
1 parent
df5caa69e7
commit
7279904043
8 files changed
+717
-35
No files matched your search
@@ -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) }
|
||||
Reference in new issue
Block a user