mirror of
https://github.com/router-for-me/CLIProxyAPI.git
synced 2026-10-18 21:45:52 +08:00
- Pre-allocate `oauthToolAliases` in Claude and Kimi executor constructors so request copies created via `ForAPIKey` reuse the same store - Replace struct-level mutex with package-level mutex for lazy store allocation to prevent copying mutexes by value Closes: #6401
145 lines
4.1 KiB
Go
145 lines
4.1 KiB
Go
package executor
|
|
|
|
import (
|
|
"net/http"
|
|
"sync"
|
|
|
|
cliproxyexecutor "github.com/router-for-me/CLIProxyAPI/v8/sdk/cliproxy/executor"
|
|
"github.com/tidwall/gjson"
|
|
)
|
|
|
|
const claudeOAuthToolAliasStateLimit = 1024
|
|
|
|
// claudeOAuthToolAliasStoreInitMu guards lazy allocation for executors built
|
|
// without a constructor. It lives outside ClaudeExecutor because ForAPIKey
|
|
// copies the executor by value, and a mutex field would be copied with it.
|
|
var claudeOAuthToolAliasStoreInitMu sync.Mutex
|
|
|
|
type claudeOAuthToolAliasStore struct {
|
|
mu sync.Mutex
|
|
entries map[string]map[string]string
|
|
order []string
|
|
}
|
|
|
|
func (s *claudeOAuthToolAliasStore) load(keys []string) (map[string]string, bool) {
|
|
if s == nil {
|
|
return nil, false
|
|
}
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
for _, key := range keys {
|
|
aliases, ok := s.entries[key]
|
|
if !ok {
|
|
continue
|
|
}
|
|
return cloneClaudeOAuthToolAliasMap(aliases), true
|
|
}
|
|
return nil, false
|
|
}
|
|
|
|
func (s *claudeOAuthToolAliasStore) save(keys []string, aliases map[string]string) {
|
|
if s == nil {
|
|
return
|
|
}
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
if s.entries == nil {
|
|
s.entries = make(map[string]map[string]string)
|
|
}
|
|
for _, key := range keys {
|
|
if key == "" {
|
|
continue
|
|
}
|
|
if _, exists := s.entries[key]; !exists {
|
|
s.order = append(s.order, key)
|
|
}
|
|
s.entries[key] = cloneClaudeOAuthToolAliasMap(aliases)
|
|
}
|
|
for len(s.order) > claudeOAuthToolAliasStateLimit {
|
|
oldest := s.order[0]
|
|
s.order = s.order[1:]
|
|
delete(s.entries, oldest)
|
|
}
|
|
}
|
|
|
|
func cloneClaudeOAuthToolAliasMap(source map[string]string) map[string]string {
|
|
if len(source) == 0 {
|
|
return map[string]string{}
|
|
}
|
|
clone := make(map[string]string, len(source))
|
|
for key, value := range source {
|
|
clone[key] = value
|
|
}
|
|
return clone
|
|
}
|
|
|
|
func (e *ClaudeExecutor) claudeOAuthToolAliasStore() *claudeOAuthToolAliasStore {
|
|
claudeOAuthToolAliasStoreInitMu.Lock()
|
|
defer claudeOAuthToolAliasStoreInitMu.Unlock()
|
|
if e.oauthToolAliases == nil {
|
|
e.oauthToolAliases = &claudeOAuthToolAliasStore{}
|
|
}
|
|
return e.oauthToolAliases
|
|
}
|
|
|
|
func claudeOAuthToolAliasKeys(payload []byte, messageID string) []string {
|
|
keys := make([]string, 0, 2)
|
|
if previousMessageID := gjson.GetBytes(payload, "thread.previous_message_id").String(); previousMessageID != "" {
|
|
keys = append(keys, "message:"+previousMessageID)
|
|
}
|
|
if messageID != "" {
|
|
keys = append(keys, "message:"+messageID)
|
|
}
|
|
return keys
|
|
}
|
|
|
|
func claudeThreadContinuationNeedsAliasState(payload []byte) bool {
|
|
if gjson.GetBytes(payload, "thread.type").String() != "continue" {
|
|
return false
|
|
}
|
|
previousMessageID := gjson.GetBytes(payload, "thread.previous_message_id")
|
|
if !previousMessageID.Exists() || previousMessageID.String() == "" {
|
|
return false
|
|
}
|
|
tools := gjson.GetBytes(payload, "tools")
|
|
return !tools.Exists() || !tools.IsArray() || len(tools.Array()) == 0
|
|
}
|
|
|
|
func (e *ClaudeExecutor) prepareClaudeOAuthToolNamesForRequest(payload []byte, options claudeMCPAliasOptions) ([]byte, map[string]string, error) {
|
|
if claudeThreadContinuationNeedsAliasState(payload) {
|
|
aliases, ok := e.claudeOAuthToolAliasStore().load(claudeOAuthToolAliasKeys(payload, ""))
|
|
if !ok {
|
|
return nil, nil, newClaudeThreadNotFoundError()
|
|
}
|
|
return payload, aliases, nil
|
|
}
|
|
remapped, aliases := prepareClaudeOAuthToolNamesForUpstream(payload, options)
|
|
return remapped, aliases, nil
|
|
}
|
|
|
|
func (e *ClaudeExecutor) rememberClaudeOAuthToolAliases(payload []byte, aliases map[string]string, messageID string) {
|
|
if gjson.GetBytes(payload, "thread.type").String() == "" {
|
|
return
|
|
}
|
|
keys := claudeOAuthToolAliasKeys(payload, messageID)
|
|
if len(keys) == 0 {
|
|
return
|
|
}
|
|
e.claudeOAuthToolAliasStore().save(keys, aliases)
|
|
}
|
|
|
|
type claudeThreadNotFoundError struct {
|
|
statusErr
|
|
}
|
|
|
|
func newClaudeThreadNotFoundError() claudeThreadNotFoundError {
|
|
return claudeThreadNotFoundError{statusErr: statusErr{
|
|
code: http.StatusNotFound,
|
|
msg: "No thread state was found for the requested previous_message_id. Replay the full conversation with thread create to start a new Thread.",
|
|
}}
|
|
}
|
|
|
|
func (claudeThreadNotFoundError) IsRequestScoped() bool { return true }
|
|
|
|
var _ cliproxyexecutor.RequestScopedError = claudeThreadNotFoundError{}
|