Files
nyoungo 86492460b0 feat: 工具调用解析、Responses 状态缓存、幂等重放与模型别名
- 重写工具调用解析:XML、DSML 未加引号键、fence 内跳过、参数外溢等形状,并按请求携带的 tools 过滤工具名
- 新增 GET /v1/responses/{id} 与 previous_response_id 有界状态缓存(进程内、TTL、store=false 不写入)
- /v1/chat/completions 支持 Idempotency-Key 幂等回放(1 MiB 上限、24h TTL、并发去重)
- 模型别名与 default_model 降级;错误体统一输出 param/code
- 补 e2e 修复场景与 docs/compat-audit.md 兼容性审计
2026-10-09 00:50:01 +08:00

96 lines
2.1 KiB
Go

package api
import (
"sync"
"time"
"github.com/nyoungo/dsfree2api/internal/openai"
)
// responseStore is an in-process, bounded, TTL cache backing the Responses
// API's previous_response_id and GET /v1/responses/{id}. It is intentionally
// small: the gateway is stateless and only needs to reconstruct context for
// the next turn, so it keeps the minimum needed, never the raw request body.
type responseStore struct {
mu sync.Mutex
entries map[string]responseEntry
order []string
capacity int
ttl time.Duration
}
type responseEntry struct {
turn openai.StoredTurn
inserted time.Time
}
func newResponseStore(capacity int, ttl time.Duration) *responseStore {
if capacity < 1 {
capacity = 1
}
if ttl <= 0 {
ttl = time.Hour
}
return &responseStore{
entries: map[string]responseEntry{},
capacity: capacity,
ttl: ttl,
}
}
// insert saves a turn, evicting the oldest entry when over capacity.
func (s *responseStore) insert(id string, turn openai.StoredTurn) {
s.mu.Lock()
defer s.mu.Unlock()
if _, exists := s.entries[id]; exists {
s.order = removeString(s.order, id)
}
s.entries[id] = responseEntry{turn: turn, inserted: time.Now()}
s.order = append(s.order, id)
for len(s.order) > s.capacity {
oldest := s.order[0]
s.order = s.order[1:]
delete(s.entries, oldest)
}
}
// get returns a live turn; an expired entry is treated as missing.
func (s *responseStore) get(id string) (openai.StoredTurn, bool) {
s.mu.Lock()
defer s.mu.Unlock()
entry, ok := s.entries[id]
if !ok {
return openai.StoredTurn{}, false
}
if time.Since(entry.inserted) > s.ttl {
delete(s.entries, id)
s.order = removeString(s.order, id)
return openai.StoredTurn{}, false
}
return entry.turn, true
}
func (s *responseStore) getResponse(id string) (map[string]any, bool) {
turn, ok := s.get(id)
if !ok {
return nil, false
}
return turn.Response, true
}
func (s *responseStore) len() int {
s.mu.Lock()
defer s.mu.Unlock()
return len(s.entries)
}
func removeString(list []string, value string) []string {
out := list[:0]
for _, v := range list {
if v != value {
out = append(out, v)
}
}
return out
}