-
-
Notifications
You must be signed in to change notification settings - Fork 7.6k
Expand file tree
/
Copy pathprovider.go
More file actions
399 lines (368 loc) · 11.4 KB
/
Copy pathprovider.go
File metadata and controls
399 lines (368 loc) · 11.4 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
// Package util provides utility functions used across the CLIProxyAPI application.
// These functions handle common tasks such as determining AI service providers
// from model names and managing HTTP proxies.
package util
import (
"net/url"
"sort"
"strings"
"github.com/router-for-me/CLIProxyAPI/v7/internal/config"
"github.com/router-for-me/CLIProxyAPI/v7/internal/registry"
log "github.com/sirupsen/logrus"
)
const openAICompatibleProviderPrefix = "openai-compatible-"
// OpenAICompatibleProviderKey returns the internal provider key for an OpenAI-compatible provider.
func OpenAICompatibleProviderKey(name string) string {
name = strings.ToLower(strings.TrimSpace(name))
if name == "" || name == "openai-compatibility" || strings.HasPrefix(name, openAICompatibleProviderPrefix) {
if name == "" {
return "openai-compatibility"
}
return name
}
return openAICompatibleProviderPrefix + name
}
// GetProviderName determines all AI service providers capable of serving a registered model.
// It first queries the global model registry to retrieve the providers backing the supplied model name.
// When the model has not been registered yet, it falls back to legacy string heuristics to infer
// potential providers.
//
// Supported providers include (but are not limited to):
// - "gemini" for Google's Gemini family
// - "codex" for OpenAI GPT-compatible providers
// - "claude" for Anthropic models
// - "openai-compatibility" for external OpenAI-compatible providers
//
// Parameters:
// - modelName: The name of the model to identify providers for.
// - cfg: The application configuration containing OpenAI compatibility settings.
//
// Returns:
// - []string: All provider identifiers capable of serving the model, ordered by preference.
func GetProviderName(modelName string) []string {
if modelName == "" {
return nil
}
providers := make([]string, 0, 4)
seen := make(map[string]struct{})
appendProvider := func(name string) {
if name == "" {
return
}
if _, exists := seen[name]; exists {
return
}
seen[name] = struct{}{}
providers = append(providers, name)
}
for _, provider := range registry.GetGlobalRegistry().GetModelProviders(modelName) {
appendProvider(provider)
}
if len(providers) > 0 {
return providers
}
return providers
}
// ResolveAutoModel resolves the "auto" model name to an actual available model.
// It uses an empty handler type to get any available model from the registry.
//
// Parameters:
// - modelName: The model name to check (should be "auto")
//
// Returns:
// - string: The resolved model name, or the original if not "auto" or resolution fails
func ResolveAutoModel(modelName string) string {
if modelName != "auto" {
return modelName
}
// Use empty string as handler type to get any available model
firstModel, err := registry.GetGlobalRegistry().GetFirstAvailableModel("")
if err != nil {
log.Warnf("Failed to resolve 'auto' model: %v, falling back to original model name", err)
return modelName
}
log.Infof("Resolved 'auto' model to: %s", firstModel)
return firstModel
}
// IsOpenAICompatibilityAlias checks if the given model name is an alias
// configured for OpenAI compatibility routing.
//
// Parameters:
// - modelName: The model name to check
// - cfg: The application configuration containing OpenAI compatibility settings
//
// Returns:
// - bool: True if the model name is an OpenAI compatibility alias, false otherwise
func IsOpenAICompatibilityAlias(modelName string, cfg *config.Config) bool {
if cfg == nil {
return false
}
for _, compat := range cfg.OpenAICompatibility {
if compat.Disabled {
continue
}
for _, model := range compat.Models {
if model.Alias == modelName {
return true
}
}
}
return false
}
// GetOpenAICompatibilityConfig returns the OpenAI compatibility configuration
// and model details for the given alias.
//
// Parameters:
// - alias: The model alias to find configuration for
// - cfg: The application configuration containing OpenAI compatibility settings
//
// Returns:
// - *config.OpenAICompatibility: The matching compatibility configuration, or nil if not found
// - *config.OpenAICompatibilityModel: The matching model configuration, or nil if not found
func GetOpenAICompatibilityConfig(alias string, cfg *config.Config) (*config.OpenAICompatibility, *config.OpenAICompatibilityModel) {
if cfg == nil {
return nil, nil
}
for _, compat := range cfg.OpenAICompatibility {
if compat.Disabled {
continue
}
for _, model := range compat.Models {
if model.Alias == alias {
return &compat, &model
}
}
}
return nil, nil
}
// InArray checks if a string exists in a slice of strings.
// It iterates through the slice and returns true if the target string is found,
// otherwise it returns false.
//
// Parameters:
// - hystack: The slice of strings to search in
// - needle: The string to search for
//
// Returns:
// - bool: True if the string is found, false otherwise
func InArray(hystack []string, needle string) bool {
for _, item := range hystack {
if needle == item {
return true
}
}
return false
}
// HideAPIKey obscures an API key for logging purposes, showing only the first and last few characters.
//
// Parameters:
// - apiKey: The API key to hide.
//
// Returns:
// - string: The obscured API key.
func HideAPIKey(apiKey string) string {
if len(apiKey) > 8 {
return apiKey[:4] + "..." + apiKey[len(apiKey)-4:]
} else if len(apiKey) > 4 {
return apiKey[:2] + "..." + apiKey[len(apiKey)-2:]
} else if len(apiKey) > 2 {
return apiKey[:1] + "..." + apiKey[len(apiKey)-1:]
}
return apiKey
}
// maskAuthorizationHeader masks the Authorization header value while preserving the auth type prefix.
// Common formats: "Bearer <token>", "Basic <credentials>", "ApiKey <key>", etc.
// It preserves the prefix (e.g., "Bearer ") and only masks the token/credential part.
//
// Parameters:
// - value: The Authorization header value
//
// Returns:
// - string: The masked Authorization value with prefix preserved
func MaskAuthorizationHeader(value string) string {
parts := strings.SplitN(strings.TrimSpace(value), " ", 2)
if len(parts) < 2 {
return HideAPIKey(value)
}
return parts[0] + " " + HideAPIKey(parts[1])
}
// MaskSensitiveHeaderValue masks sensitive header values while preserving expected formats.
//
// Behavior by header key (case-insensitive):
// - "Authorization": Preserve the auth type prefix (e.g., "Bearer ") and mask only the credential part.
// - Headers containing "api-key": Mask the entire value using HideAPIKey.
// - Others: Return the original value unchanged.
//
// Parameters:
// - key: The HTTP header name to inspect (case-insensitive matching).
// - value: The header value to mask when sensitive.
//
// Returns:
// - string: The masked value according to the header type; unchanged if not sensitive.
func MaskSensitiveHeaderValue(key, value string) string {
lowerKey := strings.ToLower(strings.TrimSpace(key))
switch {
case strings.Contains(lowerKey, "authorization"):
return MaskAuthorizationHeader(value)
case strings.Contains(lowerKey, "api-key"),
strings.Contains(lowerKey, "apikey"),
strings.Contains(lowerKey, "token"),
strings.Contains(lowerKey, "secret"):
return HideAPIKey(value)
default:
return value
}
}
// MaskSensitiveQuery masks sensitive query parameters, e.g. auth_token, within the raw query string.
func MaskSensitiveQuery(raw string) string {
if raw == "" {
return ""
}
parts := strings.Split(raw, "&")
changed := false
for i, part := range parts {
if part == "" {
continue
}
keyPart := part
valuePart := ""
if idx := strings.Index(part, "="); idx >= 0 {
keyPart = part[:idx]
valuePart = part[idx+1:]
}
decodedKey, err := url.QueryUnescape(keyPart)
if err != nil {
decodedKey = keyPart
}
if !shouldMaskQueryParam(decodedKey) {
continue
}
decodedValue, err := url.QueryUnescape(valuePart)
if err != nil {
decodedValue = valuePart
}
masked := HideAPIKey(strings.TrimSpace(decodedValue))
parts[i] = keyPart + "=" + url.QueryEscape(masked)
changed = true
}
if !changed {
return raw
}
return strings.Join(parts, "&")
}
func shouldMaskQueryParam(key string) bool {
key = strings.ToLower(strings.TrimSpace(key))
if key == "" {
return false
}
key = strings.TrimSuffix(key, "[]")
if key == "key" || strings.Contains(key, "api-key") || strings.Contains(key, "apikey") || strings.Contains(key, "api_key") {
return true
}
if strings.Contains(key, "token") || strings.Contains(key, "secret") {
return true
}
return false
}
// suggestMaxDistance is the maximum normalized Levenshtein distance (0..1) for a
// registered model ID to be offered as a "did you mean" suggestion. A wrong
// suggestion is worse than none, so this is kept strict.
const suggestMaxDistance = 0.34
// SuggestClosestModels returns up to limit registered model IDs that are close
// to modelName, to help users recover from an "unknown provider" error caused by
// a typo, a wrong alias, or a stale thinking suffix.
//
// modelName should already be suffix-stripped by the caller (the base model
// name). Registered IDs are matched after stripping any "provider/" prefix.
// Only matches within a normalized Levenshtein distance are returned.
//
// This runs only on the error path and GetAvailableModels is cached, so there
// is no hot-path impact.
func SuggestClosestModels(modelName string, limit int) []string {
modelName = strings.ToLower(strings.TrimSpace(modelName))
if modelName == "" || limit <= 0 {
return nil
}
type candidate struct {
id string
dist int
}
var cands []candidate
for _, m := range registry.GetGlobalRegistry().GetAvailableModels("") {
id, _ := m["id"].(string)
if id == "" {
continue
}
base := stripProviderPrefix(id)
d := levenshtein(modelName, strings.ToLower(base))
maxLen := len(modelName)
if len(base) > maxLen {
maxLen = len(base)
}
if maxLen == 0 {
continue
}
if float64(d)/float64(maxLen) <= suggestMaxDistance {
cands = append(cands, candidate{id: id, dist: d})
}
}
sort.Slice(cands, func(i, j int) bool {
if cands[i].dist != cands[j].dist {
return cands[i].dist < cands[j].dist
}
return cands[i].id < cands[j].id
})
out := make([]string, 0, limit)
for i := 0; i < limit && i < len(cands); i++ {
out = append(out, cands[i].id)
}
return out
}
// stripProviderPrefix returns the trailing segment after the last "/",
// mirroring handlers.routeModelBaseName without creating a cross-package
// dependency (internal/util must not import sdk/api/handlers).
func stripProviderPrefix(model string) string {
model = strings.TrimSpace(model)
if idx := strings.LastIndex(model, "/"); idx >= 0 && idx < len(model)-1 {
return strings.TrimSpace(model[idx+1:])
}
return model
}
// levenshtein computes the edit distance between two strings using a standard
// two-row dynamic programming algorithm. Pure stdlib, no external dependency.
func levenshtein(a, b string) int {
ra, rb := []rune(a), []rune(b)
la, lb := len(ra), len(rb)
if la == 0 {
return lb
}
if lb == 0 {
return la
}
prev := make([]int, lb+1)
curr := make([]int, lb+1)
for j := 0; j <= lb; j++ {
prev[j] = j
}
for i := 1; i <= la; i++ {
curr[0] = i
for j := 1; j <= lb; j++ {
cost := 1
if ra[i-1] == rb[j-1] {
cost = 0
}
ins := curr[j-1] + 1
del := prev[j] + 1
sub := prev[j-1] + cost
curr[j] = ins
if del < curr[j] {
curr[j] = del
}
if sub < curr[j] {
curr[j] = sub
}
}
prev, curr = curr, prev
}
return prev[lb]
}