Add projects
This commit is contained in:
parent
2d3a9ad623
commit
8b607dd700
1802 changed files with 503346 additions and 2 deletions
927
backend/sdk/cliproxy/auth/conductor_models.go
Normal file
927
backend/sdk/cliproxy/auth/conductor_models.go
Normal file
|
|
@ -0,0 +1,927 @@
|
|||
package auth
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
internalconfig "github.com/router-for-me/CLIProxyAPI/v7/internal/config"
|
||||
"github.com/router-for-me/CLIProxyAPI/v7/internal/thinking"
|
||||
"github.com/router-for-me/CLIProxyAPI/v7/internal/util"
|
||||
cliproxyexecutor "github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/executor"
|
||||
)
|
||||
|
||||
func (m *Manager) lookupAPIKeyUpstreamModel(authID, requestedModel string) string {
|
||||
return lookupAPIKeyUpstreamModel(m.loadAPIKeyModelRouting(), authID, requestedModel)
|
||||
}
|
||||
|
||||
func lookupAPIKeyUpstreamModel(routing *apiKeyModelRoutingSnapshot, authID, requestedModel string) string {
|
||||
if routing == nil {
|
||||
return ""
|
||||
}
|
||||
authID = strings.TrimSpace(authID)
|
||||
if authID == "" {
|
||||
return ""
|
||||
}
|
||||
requestedModel = strings.TrimSpace(requestedModel)
|
||||
if requestedModel == "" {
|
||||
return ""
|
||||
}
|
||||
byAlias := routing.aliases[authID]
|
||||
if len(byAlias) == 0 {
|
||||
return ""
|
||||
}
|
||||
keys := []string{strings.ToLower(requestedModel)}
|
||||
baseKey := strings.ToLower(strings.TrimSpace(thinking.ParseSuffix(requestedModel).ModelName))
|
||||
if baseKey != "" && baseKey != keys[0] {
|
||||
keys = append(keys, baseKey)
|
||||
}
|
||||
for _, key := range keys {
|
||||
if resolved := strings.TrimSpace(byAlias[key]); resolved != "" {
|
||||
return preserveRequestedModelSuffix(requestedModel, resolved)
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func isAPIKeyAuth(auth *Auth) bool {
|
||||
if auth == nil {
|
||||
return false
|
||||
}
|
||||
return auth.AuthKind() == AuthKindAPIKey
|
||||
}
|
||||
|
||||
func isConfiguredOpenAICompatAuth(auth *Auth) bool {
|
||||
if !isConfiguredModelRoutingAuth(auth) {
|
||||
return false
|
||||
}
|
||||
if strings.EqualFold(strings.TrimSpace(auth.Provider), "openai-compatibility") {
|
||||
return true
|
||||
}
|
||||
if auth.Attributes == nil {
|
||||
return false
|
||||
}
|
||||
return strings.TrimSpace(auth.Attributes["compat_name"]) != ""
|
||||
}
|
||||
|
||||
func openAICompatProviderKey(auth *Auth) string {
|
||||
if auth == nil {
|
||||
return ""
|
||||
}
|
||||
if auth.Attributes != nil {
|
||||
if providerKey := strings.TrimSpace(auth.Attributes["provider_key"]); providerKey != "" {
|
||||
return util.OpenAICompatibleProviderKey(providerKey)
|
||||
}
|
||||
if compatName := strings.TrimSpace(auth.Attributes["compat_name"]); compatName != "" {
|
||||
return util.OpenAICompatibleProviderKey(compatName)
|
||||
}
|
||||
}
|
||||
return util.OpenAICompatibleProviderKey(auth.Provider)
|
||||
}
|
||||
|
||||
func openAICompatModelPoolKey(auth *Auth, requestedModel string) string {
|
||||
base := strings.TrimSpace(thinking.ParseSuffix(requestedModel).ModelName)
|
||||
if base == "" {
|
||||
base = strings.TrimSpace(requestedModel)
|
||||
}
|
||||
return strings.ToLower(strings.TrimSpace(auth.ID)) + "|" + openAICompatProviderKey(auth) + "|" + strings.ToLower(base)
|
||||
}
|
||||
|
||||
func (m *Manager) nextModelPoolOffset(key string, size int) int {
|
||||
if m == nil || size <= 1 {
|
||||
return 0
|
||||
}
|
||||
key = strings.TrimSpace(key)
|
||||
if key == "" {
|
||||
return 0
|
||||
}
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
if m.modelPoolOffsets == nil {
|
||||
m.modelPoolOffsets = make(map[string]int)
|
||||
}
|
||||
offset := m.modelPoolOffsets[key]
|
||||
if offset >= 2_147_483_640 {
|
||||
offset = 0
|
||||
}
|
||||
m.modelPoolOffsets[key] = offset + 1
|
||||
if size <= 0 {
|
||||
return 0
|
||||
}
|
||||
return offset % size
|
||||
}
|
||||
|
||||
func rotateStrings(values []string, offset int) []string {
|
||||
if len(values) <= 1 {
|
||||
return values
|
||||
}
|
||||
if offset <= 0 {
|
||||
out := make([]string, len(values))
|
||||
copy(out, values)
|
||||
return out
|
||||
}
|
||||
offset = offset % len(values)
|
||||
out := make([]string, 0, len(values))
|
||||
out = append(out, values[offset:]...)
|
||||
out = append(out, values[:offset]...)
|
||||
return out
|
||||
}
|
||||
|
||||
func (m *Manager) resolveOpenAICompatUpstreamModelPool(auth *Auth, requestedModel string) []string {
|
||||
return resolveOpenAICompatUpstreamModelPool(m.loadAPIKeyModelRouting().config, auth, requestedModel)
|
||||
}
|
||||
|
||||
func resolveOpenAICompatUpstreamModelPool(cfg *internalconfig.Config, auth *Auth, requestedModel string) []string {
|
||||
if !isConfiguredOpenAICompatAuth(auth) {
|
||||
return nil
|
||||
}
|
||||
requestedModel = strings.TrimSpace(requestedModel)
|
||||
if requestedModel == "" {
|
||||
return nil
|
||||
}
|
||||
if cfg == nil {
|
||||
cfg = &internalconfig.Config{}
|
||||
}
|
||||
providerKey := ""
|
||||
compatName := ""
|
||||
if auth.Attributes != nil {
|
||||
providerKey = strings.TrimSpace(auth.Attributes["provider_key"])
|
||||
compatName = strings.TrimSpace(auth.Attributes["compat_name"])
|
||||
}
|
||||
entry := resolveOpenAICompatConfigForAuth(cfg, auth, providerKey, compatName)
|
||||
if entry == nil {
|
||||
return nil
|
||||
}
|
||||
return resolveModelAliasPoolFromConfigModels(requestedModel, asModelAliasEntries(entry.Models))
|
||||
}
|
||||
|
||||
func preserveRequestedModelSuffix(requestedModel, resolved string) string {
|
||||
return preserveResolvedModelSuffix(resolved, thinking.ParseSuffix(requestedModel))
|
||||
}
|
||||
|
||||
func (m *Manager) executionModelCandidates(auth *Auth, routeModel string) []string {
|
||||
if auth != nil && auth.Attributes != nil {
|
||||
if homeModel := strings.TrimSpace(auth.Attributes[homeUpstreamModelAttributeKey]); homeModel != "" {
|
||||
return []string{homeModel}
|
||||
}
|
||||
}
|
||||
requestedModel := rewriteModelForAuth(routeModel, auth)
|
||||
requestedModel = m.applyOAuthModelAlias(auth, requestedModel)
|
||||
if pool := m.resolveOpenAICompatUpstreamModelPool(auth, requestedModel); len(pool) > 0 {
|
||||
if len(pool) == 1 {
|
||||
return pool
|
||||
}
|
||||
offset := m.nextModelPoolOffset(openAICompatModelPoolKey(auth, requestedModel), len(pool))
|
||||
return rotateStrings(pool, offset)
|
||||
}
|
||||
resolved := m.applyAPIKeyModelAlias(auth, requestedModel)
|
||||
if strings.TrimSpace(resolved) == "" {
|
||||
resolved = requestedModel
|
||||
}
|
||||
return []string{resolved}
|
||||
}
|
||||
|
||||
// ResolveExecutionModel returns the credential-aware upstream model used by
|
||||
// normal execution. It strips auth prefixes, applies configured aliases, and
|
||||
// prefers Home-dispatched upstream models when present.
|
||||
func (m *Manager) ResolveExecutionModel(auth *Auth, routeModel string) string {
|
||||
routeModel = strings.TrimSpace(routeModel)
|
||||
if m == nil {
|
||||
return routeModel
|
||||
}
|
||||
candidates := m.executionModelCandidates(auth, routeModel)
|
||||
if len(candidates) == 0 {
|
||||
return routeModel
|
||||
}
|
||||
if resolved := strings.TrimSpace(candidates[0]); resolved != "" {
|
||||
return resolved
|
||||
}
|
||||
return routeModel
|
||||
}
|
||||
|
||||
func (m *Manager) selectionModelForAuth(auth *Auth, routeModel string) string {
|
||||
requestedModel := rewriteModelForAuth(routeModel, auth)
|
||||
if strings.TrimSpace(requestedModel) == "" {
|
||||
requestedModel = strings.TrimSpace(routeModel)
|
||||
}
|
||||
resolvedModel := m.applyOAuthModelAlias(auth, requestedModel)
|
||||
if strings.TrimSpace(resolvedModel) == "" {
|
||||
resolvedModel = requestedModel
|
||||
}
|
||||
return resolvedModel
|
||||
}
|
||||
|
||||
func (m *Manager) selectionModelKeyForAuth(auth *Auth, routeModel string) string {
|
||||
return canonicalModelKey(m.selectionModelForAuth(auth, routeModel))
|
||||
}
|
||||
|
||||
func (m *Manager) stateModelForExecution(auth *Auth, routeModel, upstreamModel string, pooled bool) string {
|
||||
if auth != nil && auth.Attributes != nil {
|
||||
if homeModel := strings.TrimSpace(auth.Attributes[homeUpstreamModelAttributeKey]); homeModel != "" {
|
||||
if resolved := strings.TrimSpace(upstreamModel); resolved != "" {
|
||||
return resolved
|
||||
}
|
||||
return homeModel
|
||||
}
|
||||
}
|
||||
stateModel := executionResultModel(routeModel, upstreamModel, pooled)
|
||||
selectionModel := m.selectionModelForAuth(auth, routeModel)
|
||||
if canonicalModelKey(selectionModel) == canonicalModelKey(upstreamModel) && strings.TrimSpace(selectionModel) != "" {
|
||||
return strings.TrimSpace(upstreamModel)
|
||||
}
|
||||
return stateModel
|
||||
}
|
||||
|
||||
func executionResultModel(routeModel, upstreamModel string, pooled bool) string {
|
||||
if pooled {
|
||||
if resolved := strings.TrimSpace(upstreamModel); resolved != "" {
|
||||
return resolved
|
||||
}
|
||||
}
|
||||
if requested := strings.TrimSpace(routeModel); requested != "" {
|
||||
return requested
|
||||
}
|
||||
return strings.TrimSpace(upstreamModel)
|
||||
}
|
||||
|
||||
func (m *Manager) filterExecutionModels(auth *Auth, routeModel string, candidates []string, pooled bool) []string {
|
||||
if len(candidates) == 0 {
|
||||
return nil
|
||||
}
|
||||
now := time.Now()
|
||||
out := make([]string, 0, len(candidates))
|
||||
for _, upstreamModel := range candidates {
|
||||
stateModel := m.stateModelForExecution(auth, routeModel, upstreamModel, pooled)
|
||||
blocked, _, _ := isAuthBlockedForModel(auth, stateModel, now)
|
||||
if blocked {
|
||||
continue
|
||||
}
|
||||
out = append(out, upstreamModel)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func (m *Manager) preparedExecutionModels(auth *Auth, routeModel string) ([]string, bool) {
|
||||
candidates := m.executionModelCandidates(auth, routeModel)
|
||||
pooled := len(candidates) > 1
|
||||
return m.filterExecutionModels(auth, routeModel, candidates, pooled), pooled
|
||||
}
|
||||
|
||||
func (m *Manager) preparedExecutionModelsWithAlias(auth *Auth, routeModel string) ([]string, bool, OAuthModelAliasResult, *apiKeyModelRoutingSnapshot) {
|
||||
candidates, pooled, aliasResult, routing := m.executionModelCandidatesWithAlias(auth, routeModel)
|
||||
return m.filterExecutionModels(auth, routeModel, candidates, pooled), pooled, aliasResult, routing
|
||||
}
|
||||
|
||||
func (m *Manager) executionModelCandidatesWithAlias(auth *Auth, routeModel string) ([]string, bool, OAuthModelAliasResult, *apiKeyModelRoutingSnapshot) {
|
||||
routing := m.loadAPIKeyModelRouting()
|
||||
requestedModel := rewriteModelForAuth(routeModel, auth)
|
||||
aliasResult := m.resolveExecutionAliasResultForRequestedWithRouting(routing, auth, requestedModel)
|
||||
if aliasResult.ForceMapping && auth != nil && auth.Attributes != nil && strings.EqualFold(strings.TrimSpace(auth.Attributes[homeForceMappingAttributeKey]), "true") {
|
||||
aliasResult.OriginalAlias = strings.TrimSpace(routeModel)
|
||||
}
|
||||
upstreamModel := executionAliasPoolModel(auth, requestedModel, aliasResult)
|
||||
|
||||
var candidates []string
|
||||
if auth != nil && auth.Attributes != nil {
|
||||
if homeModel := strings.TrimSpace(auth.Attributes[homeUpstreamModelAttributeKey]); homeModel != "" {
|
||||
candidates = []string{homeModel}
|
||||
}
|
||||
}
|
||||
if len(candidates) == 0 {
|
||||
if pool := resolveOpenAICompatUpstreamModelPool(routing.config, auth, upstreamModel); len(pool) > 0 {
|
||||
if len(pool) == 1 {
|
||||
candidates = pool
|
||||
} else {
|
||||
offset := m.nextModelPoolOffset(openAICompatModelPoolKey(auth, upstreamModel), len(pool))
|
||||
candidates = rotateStrings(pool, offset)
|
||||
}
|
||||
} else {
|
||||
resolved := m.applyAPIKeyModelAliasWithRouting(routing, auth, upstreamModel)
|
||||
if strings.TrimSpace(resolved) == "" {
|
||||
resolved = upstreamModel
|
||||
}
|
||||
candidates = []string{resolved}
|
||||
}
|
||||
}
|
||||
pooled := len(candidates) > 1
|
||||
return candidates, pooled, aliasResult, routing
|
||||
}
|
||||
|
||||
func (m *Manager) resolveExecutionAliasResult(auth *Auth, routeModel string) OAuthModelAliasResult {
|
||||
requestedModel := rewriteModelForAuth(routeModel, auth)
|
||||
return m.resolveExecutionAliasResultForRequested(auth, requestedModel)
|
||||
}
|
||||
|
||||
func (m *Manager) resolveExecutionAliasResultForRequested(auth *Auth, requestedModel string) OAuthModelAliasResult {
|
||||
return m.resolveExecutionAliasResultForRequestedWithRouting(m.loadAPIKeyModelRouting(), auth, requestedModel)
|
||||
}
|
||||
|
||||
func (m *Manager) resolveExecutionAliasResultForRequestedWithRouting(routing *apiKeyModelRoutingSnapshot, auth *Auth, requestedModel string) OAuthModelAliasResult {
|
||||
if result := homeForceMappingAliasResult(auth, requestedModel); result.ForceMapping {
|
||||
return result
|
||||
}
|
||||
if isConfiguredModelRoutingAuth(auth) {
|
||||
return resolveAPIKeyModelAliasWithResult(routing.config, auth, requestedModel)
|
||||
}
|
||||
return m.applyOAuthModelAliasWithResult(auth, requestedModel)
|
||||
}
|
||||
|
||||
func homeForceMappingAliasResult(auth *Auth, requestedModel string) OAuthModelAliasResult {
|
||||
if auth == nil || auth.Attributes == nil || !strings.EqualFold(strings.TrimSpace(auth.Attributes[homeForceMappingAttributeKey]), "true") {
|
||||
return OAuthModelAliasResult{}
|
||||
}
|
||||
originalAlias := strings.TrimSpace(auth.Attributes[homeOriginalAliasAttributeKey])
|
||||
canonicalOriginalAlias := canonicalHomeConcurrencyModelKey(auth.Attributes[homeOriginalAliasAttributeKey])
|
||||
canonicalRequestedModel := canonicalHomeConcurrencyModelKey(requestedModel)
|
||||
if canonicalOriginalAlias == "" || canonicalOriginalAlias != canonicalRequestedModel {
|
||||
return OAuthModelAliasResult{}
|
||||
}
|
||||
upstreamModel := strings.TrimSpace(auth.Attributes[homeUpstreamModelAttributeKey])
|
||||
if upstreamModel == "" {
|
||||
upstreamModel = strings.TrimSpace(requestedModel)
|
||||
}
|
||||
return OAuthModelAliasResult{
|
||||
UpstreamModel: upstreamModel,
|
||||
ForceMapping: true,
|
||||
OriginalAlias: originalAlias,
|
||||
}
|
||||
}
|
||||
|
||||
func executionAliasPoolModel(auth *Auth, requestedModel string, aliasResult OAuthModelAliasResult) string {
|
||||
if isConfiguredModelRoutingAuth(auth) {
|
||||
if strings.TrimSpace(requestedModel) != "" {
|
||||
return requestedModel
|
||||
}
|
||||
}
|
||||
if strings.TrimSpace(aliasResult.UpstreamModel) != "" {
|
||||
return aliasResult.UpstreamModel
|
||||
}
|
||||
return requestedModel
|
||||
}
|
||||
|
||||
func (m *Manager) resolveAPIKeyModelAliasWithResult(auth *Auth, requestedModel string) OAuthModelAliasResult {
|
||||
return resolveAPIKeyModelAliasWithResult(m.loadAPIKeyModelRouting().config, auth, requestedModel)
|
||||
}
|
||||
|
||||
func resolveAPIKeyModelAliasWithResult(cfg *internalconfig.Config, auth *Auth, requestedModel string) OAuthModelAliasResult {
|
||||
if auth == nil {
|
||||
return OAuthModelAliasResult{}
|
||||
}
|
||||
requestedModel = strings.TrimSpace(requestedModel)
|
||||
if requestedModel == "" {
|
||||
return OAuthModelAliasResult{}
|
||||
}
|
||||
if cfg == nil {
|
||||
cfg = &internalconfig.Config{}
|
||||
}
|
||||
models := configuredModelAliasEntries(cfg, auth)
|
||||
if len(models) == 0 {
|
||||
return OAuthModelAliasResult{UpstreamModel: requestedModel}
|
||||
}
|
||||
result := resolveModelAliasResultFromConfigModels(requestedModel, models)
|
||||
if strings.TrimSpace(result.UpstreamModel) == "" {
|
||||
return OAuthModelAliasResult{UpstreamModel: requestedModel}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func configuredModelAliasEntries(cfg *internalconfig.Config, auth *Auth) []modelAliasEntry {
|
||||
if cfg == nil || auth == nil {
|
||||
return nil
|
||||
}
|
||||
provider := strings.ToLower(strings.TrimSpace(auth.Provider))
|
||||
var models []modelAliasEntry
|
||||
switch provider {
|
||||
case "gemini":
|
||||
if entry := resolveGeminiAPIKeyConfig(cfg, auth); entry != nil {
|
||||
models = asModelAliasEntries(entry.Models)
|
||||
}
|
||||
case "gemini-interactions":
|
||||
if entry := resolveInteractionsAPIKeyConfig(cfg, auth); entry != nil {
|
||||
models = asModelAliasEntries(entry.Models)
|
||||
}
|
||||
case "claude":
|
||||
if entry := resolveClaudeAPIKeyConfig(cfg, auth); entry != nil {
|
||||
models = asModelAliasEntries(entry.Models)
|
||||
}
|
||||
case "codex":
|
||||
if entry := resolveCodexAPIKeyConfig(cfg, auth); entry != nil {
|
||||
models = asModelAliasEntries(entry.Models)
|
||||
}
|
||||
case "xai":
|
||||
if entry := resolveXAIAPIKeyConfig(cfg, auth); entry != nil {
|
||||
models = asModelAliasEntries(entry.Models)
|
||||
}
|
||||
case "vertex":
|
||||
if entry := resolveVertexAPIKeyConfig(cfg, auth); entry != nil {
|
||||
models = asModelAliasEntries(entry.Models)
|
||||
}
|
||||
default:
|
||||
providerKey := ""
|
||||
compatName := ""
|
||||
if auth.Attributes != nil {
|
||||
providerKey = strings.TrimSpace(auth.Attributes["provider_key"])
|
||||
compatName = strings.TrimSpace(auth.Attributes["compat_name"])
|
||||
}
|
||||
if compatName != "" || strings.EqualFold(strings.TrimSpace(auth.Provider), "openai-compatibility") {
|
||||
if entry := resolveOpenAICompatConfigForAuth(cfg, auth, providerKey, compatName); entry != nil {
|
||||
models = asModelAliasEntries(entry.Models)
|
||||
}
|
||||
}
|
||||
}
|
||||
return models
|
||||
}
|
||||
|
||||
func resolveModelAliasResultForUpstream(cfg *internalconfig.Config, auth *Auth, requestedModel, upstreamModel string) OAuthModelAliasResult {
|
||||
requestedModel = strings.TrimSpace(requestedModel)
|
||||
upstreamModel = strings.TrimSpace(upstreamModel)
|
||||
if requestedModel == "" || upstreamModel == "" {
|
||||
return OAuthModelAliasResult{}
|
||||
}
|
||||
requestResult := thinking.ParseSuffix(requestedModel)
|
||||
models := configuredModelAliasEntries(cfg, auth)
|
||||
filtered := make([]modelAliasEntry, 0, 1)
|
||||
for _, model := range models {
|
||||
name := strings.TrimSpace(model.GetName())
|
||||
if name != "" && strings.EqualFold(preserveResolvedModelSuffix(name, requestResult), upstreamModel) {
|
||||
filtered = append(filtered, model)
|
||||
}
|
||||
}
|
||||
if len(filtered) == 0 {
|
||||
return OAuthModelAliasResult{}
|
||||
}
|
||||
return resolveModelAliasResultFromConfigModels(requestedModel, filtered)
|
||||
}
|
||||
|
||||
func resolveAttemptAliasResult(routing *apiKeyModelRoutingSnapshot, auth *Auth, routeModel, upstreamModel string, fallback OAuthModelAliasResult) OAuthModelAliasResult {
|
||||
if routing == nil || !isConfiguredModelRoutingAuth(auth) {
|
||||
return fallback
|
||||
}
|
||||
requestedModel := rewriteModelForAuth(routeModel, auth)
|
||||
result := resolveModelAliasResultForUpstream(routing.config, auth, requestedModel, upstreamModel)
|
||||
if strings.TrimSpace(result.UpstreamModel) == "" {
|
||||
return fallback
|
||||
}
|
||||
if result.ForceMapping && fallback.ForceMapping && strings.TrimSpace(fallback.OriginalAlias) != "" {
|
||||
result.OriginalAlias = fallback.OriginalAlias
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func (m *Manager) prepareExecutionModels(auth *Auth, routeModel string) []string {
|
||||
models, _ := m.preparedExecutionModels(auth, routeModel)
|
||||
return models
|
||||
}
|
||||
|
||||
func rewriteForceMappedResponse(resp *cliproxyexecutor.Response, aliasResult OAuthModelAliasResult) {
|
||||
if resp == nil || !aliasResult.ForceMapping || strings.TrimSpace(aliasResult.OriginalAlias) == "" {
|
||||
return
|
||||
}
|
||||
resp.Payload = rewriteModelInResponse(resp.Payload, aliasResult.OriginalAlias)
|
||||
}
|
||||
|
||||
func rewriteForceMappedStreamChunk(rewriter *StreamRewriter, payload []byte) []byte {
|
||||
if rewriter == nil || len(payload) == 0 {
|
||||
return payload
|
||||
}
|
||||
rewritten := rewriter.RewriteChunk(payload)
|
||||
if len(rewritten) > 0 {
|
||||
return rewritten
|
||||
}
|
||||
if bytes.Contains(payload, []byte("data:")) {
|
||||
if lineWise := rewriteSSEPayloadLines(payload, rewriter.options.RewriteModel); len(lineWise) > 0 {
|
||||
return lineWise
|
||||
}
|
||||
}
|
||||
if len(rewriter.pendingBuf) > 0 {
|
||||
return nil
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func finishForceMappedStreamChunks(rewriter *StreamRewriter) []byte {
|
||||
if rewriter == nil {
|
||||
return nil
|
||||
}
|
||||
return rewriter.Finish()
|
||||
}
|
||||
|
||||
func (m *Manager) rebuildAPIKeyModelAliasFromRuntimeConfig() {
|
||||
if m == nil {
|
||||
return
|
||||
}
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
cfg, _ := m.runtimeConfig.Load().(*internalconfig.Config)
|
||||
if cfg == nil {
|
||||
cfg = &internalconfig.Config{}
|
||||
}
|
||||
m.rebuildAPIKeyModelAliasLocked(cfg)
|
||||
}
|
||||
|
||||
// RefreshAPIKeyModelAlias rebuilds the API-key model alias table from the current runtime config.
|
||||
func (m *Manager) RefreshAPIKeyModelAlias() {
|
||||
m.rebuildAPIKeyModelAliasFromRuntimeConfig()
|
||||
}
|
||||
|
||||
func (m *Manager) rebuildAPIKeyModelAliasLocked(cfg *internalconfig.Config) {
|
||||
if m == nil {
|
||||
return
|
||||
}
|
||||
if cfg == nil {
|
||||
cfg = &internalconfig.Config{}
|
||||
}
|
||||
|
||||
out := make(apiKeyModelAliasTable)
|
||||
capabilities := make(apiKeyModelCapabilityTable)
|
||||
for _, auth := range m.auths {
|
||||
if auth == nil {
|
||||
continue
|
||||
}
|
||||
if strings.TrimSpace(auth.ID) == "" {
|
||||
continue
|
||||
}
|
||||
if !isConfiguredModelRoutingAuth(auth) {
|
||||
continue
|
||||
}
|
||||
|
||||
byAlias := make(map[string]string)
|
||||
provider := strings.ToLower(strings.TrimSpace(auth.Provider))
|
||||
switch provider {
|
||||
case "gemini":
|
||||
if entry := resolveGeminiAPIKeyConfig(cfg, auth); entry != nil {
|
||||
compileAPIKeyModelAliasForModels(byAlias, entry.Models)
|
||||
}
|
||||
case "gemini-interactions":
|
||||
if entry := resolveInteractionsAPIKeyConfig(cfg, auth); entry != nil {
|
||||
compileAPIKeyModelAliasForModels(byAlias, entry.Models)
|
||||
}
|
||||
case "claude":
|
||||
if entry := resolveClaudeAPIKeyConfig(cfg, auth); entry != nil {
|
||||
compileAPIKeyModelAliasForModels(byAlias, entry.Models)
|
||||
}
|
||||
case "codex":
|
||||
if entry := resolveCodexAPIKeyConfig(cfg, auth); entry != nil {
|
||||
compileAPIKeyModelAliasForModels(byAlias, entry.Models)
|
||||
}
|
||||
case "xai":
|
||||
if entry := resolveXAIAPIKeyConfig(cfg, auth); entry != nil {
|
||||
compileAPIKeyModelAliasForModels(byAlias, entry.Models)
|
||||
}
|
||||
case "vertex":
|
||||
if entry := resolveVertexAPIKeyConfig(cfg, auth); entry != nil {
|
||||
compileAPIKeyModelAliasForModels(byAlias, entry.Models)
|
||||
}
|
||||
default:
|
||||
// OpenAI-compat uses config selection from auth.Attributes.
|
||||
providerKey := ""
|
||||
compatName := ""
|
||||
if auth.Attributes != nil {
|
||||
providerKey = strings.TrimSpace(auth.Attributes["provider_key"])
|
||||
compatName = strings.TrimSpace(auth.Attributes["compat_name"])
|
||||
}
|
||||
if compatName != "" || strings.EqualFold(strings.TrimSpace(auth.Provider), "openai-compatibility") {
|
||||
if entry := resolveOpenAICompatConfigForAuth(cfg, auth, providerKey, compatName); entry != nil {
|
||||
compileAPIKeyModelAliasForModels(byAlias, entry.Models)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(byAlias) > 0 {
|
||||
out[auth.ID] = byAlias
|
||||
}
|
||||
if byCapability := compileAPIKeyModelCapabilitiesForAuth(cfg, auth); len(byCapability) > 0 {
|
||||
capabilities[auth.ID] = byCapability
|
||||
}
|
||||
}
|
||||
|
||||
m.apiKeyModelRouting.Store(&apiKeyModelRoutingSnapshot{
|
||||
config: cfg,
|
||||
aliases: out,
|
||||
capabilities: capabilities,
|
||||
})
|
||||
}
|
||||
|
||||
func compileAPIKeyModelAliasForModels[T interface {
|
||||
GetName() string
|
||||
GetAlias() string
|
||||
}](out map[string]string, models []T) {
|
||||
if out == nil {
|
||||
return
|
||||
}
|
||||
add := func(key, name string) {
|
||||
key = strings.ToLower(strings.TrimSpace(key))
|
||||
if key == "" {
|
||||
return
|
||||
}
|
||||
if _, exists := out[key]; !exists {
|
||||
out[key] = name
|
||||
}
|
||||
}
|
||||
for i := range models {
|
||||
alias := strings.TrimSpace(models[i].GetAlias())
|
||||
name := strings.TrimSpace(models[i].GetName())
|
||||
if alias == "" || name == "" {
|
||||
continue
|
||||
}
|
||||
// Exact suffix routes are retained alongside first-entry base fallbacks.
|
||||
add(alias, name)
|
||||
add(thinking.ParseSuffix(alias).ModelName, name)
|
||||
// Direct upstream requests use the same exact-first lookup behavior.
|
||||
add(name, name)
|
||||
add(thinking.ParseSuffix(name).ModelName, name)
|
||||
}
|
||||
}
|
||||
|
||||
func rewriteModelForAuth(model string, auth *Auth) string {
|
||||
if auth == nil || model == "" {
|
||||
return model
|
||||
}
|
||||
prefix := strings.TrimSpace(auth.Prefix)
|
||||
if prefix == "" {
|
||||
return model
|
||||
}
|
||||
needle := prefix + "/"
|
||||
if !strings.HasPrefix(model, needle) {
|
||||
return model
|
||||
}
|
||||
return strings.TrimPrefix(model, needle)
|
||||
}
|
||||
|
||||
func (m *Manager) applyAPIKeyModelAlias(auth *Auth, requestedModel string) string {
|
||||
return m.applyAPIKeyModelAliasWithRouting(m.loadAPIKeyModelRouting(), auth, requestedModel)
|
||||
}
|
||||
|
||||
func (m *Manager) applyAPIKeyModelAliasWithRouting(routing *apiKeyModelRoutingSnapshot, auth *Auth, requestedModel string) string {
|
||||
if auth == nil {
|
||||
return requestedModel
|
||||
}
|
||||
|
||||
if auth.AuthKind() != AuthKindAPIKey {
|
||||
return requestedModel
|
||||
}
|
||||
|
||||
requestedModel = strings.TrimSpace(requestedModel)
|
||||
if requestedModel == "" {
|
||||
return requestedModel
|
||||
}
|
||||
|
||||
// Fast path: lookup per-auth mapping table (keyed by auth.ID).
|
||||
if resolved := lookupAPIKeyUpstreamModel(routing, auth.ID, requestedModel); resolved != "" {
|
||||
return resolved
|
||||
}
|
||||
|
||||
// Slow path: scan the same config snapshot used to compile the alias table.
|
||||
cfg := routing.config
|
||||
if cfg == nil {
|
||||
cfg = &internalconfig.Config{}
|
||||
}
|
||||
|
||||
provider := strings.ToLower(strings.TrimSpace(auth.Provider))
|
||||
upstreamModel := ""
|
||||
switch provider {
|
||||
case "gemini":
|
||||
upstreamModel = resolveUpstreamModelForGeminiAPIKey(cfg, auth, requestedModel)
|
||||
case "gemini-interactions":
|
||||
upstreamModel = resolveUpstreamModelForInteractionsAPIKey(cfg, auth, requestedModel)
|
||||
case "claude":
|
||||
upstreamModel = resolveUpstreamModelForClaudeAPIKey(cfg, auth, requestedModel)
|
||||
case "codex":
|
||||
upstreamModel = resolveUpstreamModelForCodexAPIKey(cfg, auth, requestedModel)
|
||||
case "xai":
|
||||
upstreamModel = resolveUpstreamModelForXAIAPIKey(cfg, auth, requestedModel)
|
||||
case "vertex":
|
||||
upstreamModel = resolveUpstreamModelForVertexAPIKey(cfg, auth, requestedModel)
|
||||
default:
|
||||
upstreamModel = resolveUpstreamModelForOpenAICompatAPIKey(cfg, auth, requestedModel)
|
||||
}
|
||||
|
||||
// Return upstream model if found, otherwise return requested model.
|
||||
if upstreamModel != "" {
|
||||
return upstreamModel
|
||||
}
|
||||
return requestedModel
|
||||
}
|
||||
|
||||
// APIKeyConfigEntry is a generic interface for API key configurations.
|
||||
type APIKeyConfigEntry interface {
|
||||
GetAPIKey() string
|
||||
GetBaseURL() string
|
||||
GetPrefix() string
|
||||
GetProxyURL() string
|
||||
}
|
||||
|
||||
func resolveAPIKeyConfig[T APIKeyConfigEntry](entries []T, auth *Auth) *T {
|
||||
if auth == nil || len(entries) == 0 {
|
||||
return nil
|
||||
}
|
||||
attrKey, attrBase := "", ""
|
||||
if auth.Attributes != nil {
|
||||
attrKey = strings.TrimSpace(auth.Attributes[AttributeAPIKey])
|
||||
attrBase = strings.TrimSpace(auth.Attributes["base_url"])
|
||||
}
|
||||
matchesCredentials := func(entry T) bool {
|
||||
cfgKey := strings.TrimSpace(entry.GetAPIKey())
|
||||
cfgBase := strings.TrimSpace(entry.GetBaseURL())
|
||||
if attrKey != "" && attrBase != "" {
|
||||
return strings.EqualFold(cfgKey, attrKey) && strings.EqualFold(cfgBase, attrBase)
|
||||
}
|
||||
if attrKey != "" {
|
||||
return strings.EqualFold(cfgKey, attrKey) && (cfgBase == "" || strings.EqualFold(cfgBase, attrBase))
|
||||
}
|
||||
return attrBase != "" && strings.EqualFold(cfgBase, attrBase)
|
||||
}
|
||||
if auth.AuthSourceKind() == AuthSourceConfig && auth.Attributes != nil {
|
||||
if index, errIndex := strconv.Atoi(strings.TrimSpace(auth.Attributes[AttributeConfigIndex])); errIndex == nil && index >= 0 && index < len(entries) && matchesCredentials(entries[index]) {
|
||||
return &entries[index]
|
||||
}
|
||||
}
|
||||
for i := range entries {
|
||||
entry := entries[i]
|
||||
if matchesCredentials(entry) && strings.EqualFold(strings.TrimSpace(entry.GetPrefix()), strings.TrimSpace(auth.Prefix)) && strings.EqualFold(strings.TrimSpace(entry.GetProxyURL()), strings.TrimSpace(auth.ProxyURL)) {
|
||||
return &entries[i]
|
||||
}
|
||||
}
|
||||
for i := range entries {
|
||||
if matchesCredentials(entries[i]) {
|
||||
return &entries[i]
|
||||
}
|
||||
}
|
||||
if attrKey != "" {
|
||||
for i := range entries {
|
||||
if strings.EqualFold(strings.TrimSpace(entries[i].GetAPIKey()), attrKey) {
|
||||
return &entries[i]
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func resolveGeminiAPIKeyConfig(cfg *internalconfig.Config, auth *Auth) *internalconfig.GeminiKey {
|
||||
if cfg == nil {
|
||||
return nil
|
||||
}
|
||||
return resolveAPIKeyConfig(cfg.GeminiKey, auth)
|
||||
}
|
||||
|
||||
func resolveInteractionsAPIKeyConfig(cfg *internalconfig.Config, auth *Auth) *internalconfig.GeminiKey {
|
||||
if cfg == nil {
|
||||
return nil
|
||||
}
|
||||
return resolveAPIKeyConfig(cfg.InteractionsKey, auth)
|
||||
}
|
||||
|
||||
func resolveClaudeAPIKeyConfig(cfg *internalconfig.Config, auth *Auth) *internalconfig.ClaudeKey {
|
||||
if cfg == nil {
|
||||
return nil
|
||||
}
|
||||
return resolveAPIKeyConfig(cfg.ClaudeKey, auth)
|
||||
}
|
||||
|
||||
func resolveCodexAPIKeyConfig(cfg *internalconfig.Config, auth *Auth) *internalconfig.CodexKey {
|
||||
if cfg == nil {
|
||||
return nil
|
||||
}
|
||||
return resolveAPIKeyConfig(cfg.CodexKey, auth)
|
||||
}
|
||||
|
||||
func resolveXAIAPIKeyConfig(cfg *internalconfig.Config, auth *Auth) *internalconfig.XAIKey {
|
||||
if cfg == nil {
|
||||
return nil
|
||||
}
|
||||
return resolveAPIKeyConfig(cfg.XAIKey, auth)
|
||||
}
|
||||
|
||||
func resolveVertexAPIKeyConfig(cfg *internalconfig.Config, auth *Auth) *internalconfig.VertexCompatKey {
|
||||
if cfg == nil {
|
||||
return nil
|
||||
}
|
||||
return resolveAPIKeyConfig(cfg.VertexCompatAPIKey, auth)
|
||||
}
|
||||
|
||||
func resolveUpstreamModelForGeminiAPIKey(cfg *internalconfig.Config, auth *Auth, requestedModel string) string {
|
||||
entry := resolveGeminiAPIKeyConfig(cfg, auth)
|
||||
if entry == nil {
|
||||
return ""
|
||||
}
|
||||
return resolveModelAliasFromConfigModels(requestedModel, asModelAliasEntries(entry.Models))
|
||||
}
|
||||
|
||||
func resolveUpstreamModelForInteractionsAPIKey(cfg *internalconfig.Config, auth *Auth, requestedModel string) string {
|
||||
entry := resolveInteractionsAPIKeyConfig(cfg, auth)
|
||||
if entry == nil {
|
||||
return ""
|
||||
}
|
||||
return resolveModelAliasFromConfigModels(requestedModel, asModelAliasEntries(entry.Models))
|
||||
}
|
||||
|
||||
func resolveUpstreamModelForClaudeAPIKey(cfg *internalconfig.Config, auth *Auth, requestedModel string) string {
|
||||
entry := resolveClaudeAPIKeyConfig(cfg, auth)
|
||||
if entry == nil {
|
||||
return ""
|
||||
}
|
||||
return resolveModelAliasFromConfigModels(requestedModel, asModelAliasEntries(entry.Models))
|
||||
}
|
||||
|
||||
func resolveUpstreamModelForCodexAPIKey(cfg *internalconfig.Config, auth *Auth, requestedModel string) string {
|
||||
entry := resolveCodexAPIKeyConfig(cfg, auth)
|
||||
if entry == nil {
|
||||
return ""
|
||||
}
|
||||
return resolveModelAliasFromConfigModels(requestedModel, asModelAliasEntries(entry.Models))
|
||||
}
|
||||
|
||||
func resolveUpstreamModelForXAIAPIKey(cfg *internalconfig.Config, auth *Auth, requestedModel string) string {
|
||||
entry := resolveXAIAPIKeyConfig(cfg, auth)
|
||||
if entry == nil {
|
||||
return ""
|
||||
}
|
||||
return resolveModelAliasFromConfigModels(requestedModel, asModelAliasEntries(entry.Models))
|
||||
}
|
||||
|
||||
func resolveUpstreamModelForVertexAPIKey(cfg *internalconfig.Config, auth *Auth, requestedModel string) string {
|
||||
entry := resolveVertexAPIKeyConfig(cfg, auth)
|
||||
if entry == nil {
|
||||
return ""
|
||||
}
|
||||
return resolveModelAliasFromConfigModels(requestedModel, asModelAliasEntries(entry.Models))
|
||||
}
|
||||
|
||||
func resolveUpstreamModelForOpenAICompatAPIKey(cfg *internalconfig.Config, auth *Auth, requestedModel string) string {
|
||||
providerKey := ""
|
||||
compatName := ""
|
||||
if auth != nil && len(auth.Attributes) > 0 {
|
||||
providerKey = strings.TrimSpace(auth.Attributes["provider_key"])
|
||||
compatName = strings.TrimSpace(auth.Attributes["compat_name"])
|
||||
}
|
||||
if compatName == "" && !strings.EqualFold(strings.TrimSpace(auth.Provider), "openai-compatibility") {
|
||||
return ""
|
||||
}
|
||||
entry := resolveOpenAICompatConfigForAuth(cfg, auth, providerKey, compatName)
|
||||
if entry == nil {
|
||||
return ""
|
||||
}
|
||||
return resolveModelAliasFromConfigModels(requestedModel, asModelAliasEntries(entry.Models))
|
||||
}
|
||||
|
||||
type apiKeyModelAliasTable map[string]map[string]string
|
||||
|
||||
func resolveOpenAICompatConfigForAuth(cfg *internalconfig.Config, auth *Auth, providerKey, compatName string) *internalconfig.OpenAICompatibility {
|
||||
if cfg == nil {
|
||||
return nil
|
||||
}
|
||||
if auth != nil && auth.AuthSourceKind() == AuthSourceConfig && auth.Attributes != nil {
|
||||
if index, errIndex := strconv.Atoi(strings.TrimSpace(auth.Attributes[AttributeConfigIndex])); errIndex == nil && index >= 0 && index < len(cfg.OpenAICompatibility) && !cfg.OpenAICompatibility[index].Disabled {
|
||||
return &cfg.OpenAICompatibility[index]
|
||||
}
|
||||
}
|
||||
authProvider := ""
|
||||
if auth != nil {
|
||||
authProvider = auth.Provider
|
||||
}
|
||||
return resolveOpenAICompatConfig(cfg, providerKey, compatName, authProvider)
|
||||
}
|
||||
|
||||
func resolveOpenAICompatConfig(cfg *internalconfig.Config, providerKey, compatName, authProvider string) *internalconfig.OpenAICompatibility {
|
||||
if cfg == nil {
|
||||
return nil
|
||||
}
|
||||
candidates := make([]string, 0, 3)
|
||||
if v := strings.TrimSpace(compatName); v != "" {
|
||||
candidates = append(candidates, v)
|
||||
}
|
||||
if v := strings.TrimSpace(providerKey); v != "" {
|
||||
candidates = append(candidates, v)
|
||||
}
|
||||
if v := strings.TrimSpace(authProvider); v != "" {
|
||||
candidates = append(candidates, v)
|
||||
}
|
||||
for i := range cfg.OpenAICompatibility {
|
||||
compat := &cfg.OpenAICompatibility[i]
|
||||
if compat.Disabled {
|
||||
continue
|
||||
}
|
||||
for _, candidate := range candidates {
|
||||
if candidate != "" && strings.EqualFold(strings.TrimSpace(candidate), compat.Name) {
|
||||
return compat
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func asModelAliasEntries[T interface {
|
||||
GetName() string
|
||||
GetAlias() string
|
||||
GetForceMapping() bool
|
||||
}](models []T) []modelAliasEntry {
|
||||
if len(models) == 0 {
|
||||
return nil
|
||||
}
|
||||
out := make([]modelAliasEntry, 0, len(models))
|
||||
for i := range models {
|
||||
out = append(out, models[i])
|
||||
}
|
||||
return out
|
||||
}
|
||||
Loading…
Reference in a new issue