mirror of
https://github.com/router-for-me/CLIProxyAPI.git
synced 2026-02-03 04:50:52 +08:00
feat(gemini): add Gemini API key endpoints
This commit is contained in:
@@ -124,10 +124,137 @@ func (h *Handler) GetGlKeys(c *gin.Context) {
|
||||
c.JSON(200, gin.H{"generative-language-api-key": h.cfg.GlAPIKey})
|
||||
}
|
||||
func (h *Handler) PutGlKeys(c *gin.Context) {
|
||||
h.putStringList(c, func(v []string) { h.cfg.GlAPIKey = v }, nil)
|
||||
h.putStringList(c, func(v []string) {
|
||||
h.cfg.GlAPIKey = append([]string(nil), v...)
|
||||
}, func() {
|
||||
h.cfg.SyncGeminiKeys()
|
||||
})
|
||||
}
|
||||
func (h *Handler) PatchGlKeys(c *gin.Context) {
|
||||
h.patchStringList(c, &h.cfg.GlAPIKey, func() { h.cfg.SyncGeminiKeys() })
|
||||
}
|
||||
func (h *Handler) DeleteGlKeys(c *gin.Context) {
|
||||
h.deleteFromStringList(c, &h.cfg.GlAPIKey, func() { h.cfg.SyncGeminiKeys() })
|
||||
}
|
||||
|
||||
// gemini-api-key: []GeminiKey
|
||||
func (h *Handler) GetGeminiKeys(c *gin.Context) {
|
||||
c.JSON(200, gin.H{"gemini-api-key": h.cfg.GeminiKey})
|
||||
}
|
||||
func (h *Handler) PutGeminiKeys(c *gin.Context) {
|
||||
data, err := c.GetRawData()
|
||||
if err != nil {
|
||||
c.JSON(400, gin.H{"error": "failed to read body"})
|
||||
return
|
||||
}
|
||||
var arr []config.GeminiKey
|
||||
if err = json.Unmarshal(data, &arr); err != nil {
|
||||
var obj struct {
|
||||
Items []config.GeminiKey `json:"items"`
|
||||
}
|
||||
if err2 := json.Unmarshal(data, &obj); err2 != nil || len(obj.Items) == 0 {
|
||||
c.JSON(400, gin.H{"error": "invalid body"})
|
||||
return
|
||||
}
|
||||
arr = obj.Items
|
||||
}
|
||||
h.cfg.GeminiKey = append([]config.GeminiKey(nil), arr...)
|
||||
h.cfg.SyncGeminiKeys()
|
||||
h.persist(c)
|
||||
}
|
||||
func (h *Handler) PatchGeminiKey(c *gin.Context) {
|
||||
var body struct {
|
||||
Index *int `json:"index"`
|
||||
Match *string `json:"match"`
|
||||
Value *config.GeminiKey `json:"value"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&body); err != nil || body.Value == nil {
|
||||
c.JSON(400, gin.H{"error": "invalid body"})
|
||||
return
|
||||
}
|
||||
value := *body.Value
|
||||
value.APIKey = strings.TrimSpace(value.APIKey)
|
||||
value.BaseURL = strings.TrimSpace(value.BaseURL)
|
||||
value.ProxyURL = strings.TrimSpace(value.ProxyURL)
|
||||
if value.APIKey == "" {
|
||||
// Treat empty API key as delete.
|
||||
if body.Index != nil && *body.Index >= 0 && *body.Index < len(h.cfg.GeminiKey) {
|
||||
h.cfg.GeminiKey = append(h.cfg.GeminiKey[:*body.Index], h.cfg.GeminiKey[*body.Index+1:]...)
|
||||
h.cfg.SyncGeminiKeys()
|
||||
h.persist(c)
|
||||
return
|
||||
}
|
||||
if body.Match != nil {
|
||||
match := strings.TrimSpace(*body.Match)
|
||||
if match != "" {
|
||||
out := make([]config.GeminiKey, 0, len(h.cfg.GeminiKey))
|
||||
removed := false
|
||||
for i := range h.cfg.GeminiKey {
|
||||
if !removed && h.cfg.GeminiKey[i].APIKey == match {
|
||||
removed = true
|
||||
continue
|
||||
}
|
||||
out = append(out, h.cfg.GeminiKey[i])
|
||||
}
|
||||
if removed {
|
||||
h.cfg.GeminiKey = out
|
||||
h.cfg.SyncGeminiKeys()
|
||||
h.persist(c)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
c.JSON(404, gin.H{"error": "item not found"})
|
||||
return
|
||||
}
|
||||
|
||||
if body.Index != nil && *body.Index >= 0 && *body.Index < len(h.cfg.GeminiKey) {
|
||||
h.cfg.GeminiKey[*body.Index] = value
|
||||
h.cfg.SyncGeminiKeys()
|
||||
h.persist(c)
|
||||
return
|
||||
}
|
||||
if body.Match != nil {
|
||||
match := strings.TrimSpace(*body.Match)
|
||||
for i := range h.cfg.GeminiKey {
|
||||
if h.cfg.GeminiKey[i].APIKey == match {
|
||||
h.cfg.GeminiKey[i] = value
|
||||
h.cfg.SyncGeminiKeys()
|
||||
h.persist(c)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
c.JSON(404, gin.H{"error": "item not found"})
|
||||
}
|
||||
func (h *Handler) DeleteGeminiKey(c *gin.Context) {
|
||||
if val := strings.TrimSpace(c.Query("api-key")); val != "" {
|
||||
out := make([]config.GeminiKey, 0, len(h.cfg.GeminiKey))
|
||||
for _, v := range h.cfg.GeminiKey {
|
||||
if v.APIKey != val {
|
||||
out = append(out, v)
|
||||
}
|
||||
}
|
||||
if len(out) != len(h.cfg.GeminiKey) {
|
||||
h.cfg.GeminiKey = out
|
||||
h.cfg.SyncGeminiKeys()
|
||||
h.persist(c)
|
||||
} else {
|
||||
c.JSON(404, gin.H{"error": "item not found"})
|
||||
}
|
||||
return
|
||||
}
|
||||
if idxStr := c.Query("index"); idxStr != "" {
|
||||
var idx int
|
||||
if _, err := fmt.Sscanf(idxStr, "%d", &idx); err == nil && idx >= 0 && idx < len(h.cfg.GeminiKey) {
|
||||
h.cfg.GeminiKey = append(h.cfg.GeminiKey[:idx], h.cfg.GeminiKey[idx+1:]...)
|
||||
h.cfg.SyncGeminiKeys()
|
||||
h.persist(c)
|
||||
return
|
||||
}
|
||||
}
|
||||
c.JSON(400, gin.H{"error": "missing api-key or index"})
|
||||
}
|
||||
func (h *Handler) PatchGlKeys(c *gin.Context) { h.patchStringList(c, &h.cfg.GlAPIKey, nil) }
|
||||
func (h *Handler) DeleteGlKeys(c *gin.Context) { h.deleteFromStringList(c, &h.cfg.GlAPIKey, nil) }
|
||||
|
||||
// claude-api-key: []ClaudeKey
|
||||
func (h *Handler) GetClaudeKeys(c *gin.Context) {
|
||||
|
||||
@@ -474,6 +474,11 @@ func (s *Server) registerManagementRoutes() {
|
||||
mgmt.PATCH("/generative-language-api-key", s.mgmt.PatchGlKeys)
|
||||
mgmt.DELETE("/generative-language-api-key", s.mgmt.DeleteGlKeys)
|
||||
|
||||
mgmt.GET("/gemini-api-key", s.mgmt.GetGeminiKeys)
|
||||
mgmt.PUT("/gemini-api-key", s.mgmt.PutGeminiKeys)
|
||||
mgmt.PATCH("/gemini-api-key", s.mgmt.PatchGeminiKey)
|
||||
mgmt.DELETE("/gemini-api-key", s.mgmt.DeleteGeminiKey)
|
||||
|
||||
mgmt.GET("/logs", s.mgmt.GetLogs)
|
||||
mgmt.DELETE("/logs", s.mgmt.DeleteLogs)
|
||||
mgmt.GET("/request-log", s.mgmt.GetRequestLog)
|
||||
@@ -847,7 +852,7 @@ func (s *Server) UpdateClients(cfg *config.Config) {
|
||||
|
||||
// Count client sources from configuration and auth directory
|
||||
authFiles := util.CountAuthFiles(cfg.AuthDir)
|
||||
glAPIKeyCount := len(cfg.GlAPIKey)
|
||||
geminiAPIKeyCount := len(cfg.GeminiKey)
|
||||
claudeAPIKeyCount := len(cfg.ClaudeKey)
|
||||
codexAPIKeyCount := len(cfg.CodexKey)
|
||||
openAICompatCount := 0
|
||||
@@ -860,11 +865,11 @@ func (s *Server) UpdateClients(cfg *config.Config) {
|
||||
openAICompatCount += len(entry.APIKeys)
|
||||
}
|
||||
|
||||
total := authFiles + glAPIKeyCount + claudeAPIKeyCount + codexAPIKeyCount + openAICompatCount
|
||||
fmt.Printf("server clients and configuration updated: %d clients (%d auth files + %d GL API keys + %d Claude API keys + %d Codex keys + %d OpenAI-compat)\n",
|
||||
total := authFiles + geminiAPIKeyCount + claudeAPIKeyCount + codexAPIKeyCount + openAICompatCount
|
||||
fmt.Printf("server clients and configuration updated: %d clients (%d auth files + %d Gemini API keys + %d Claude API keys + %d Codex keys + %d OpenAI-compat)\n",
|
||||
total,
|
||||
authFiles,
|
||||
glAPIKeyCount,
|
||||
geminiAPIKeyCount,
|
||||
claudeAPIKeyCount,
|
||||
codexAPIKeyCount,
|
||||
openAICompatCount,
|
||||
|
||||
@@ -43,9 +43,12 @@ type Config struct {
|
||||
// WebsocketAuth enables or disables authentication for the WebSocket API.
|
||||
WebsocketAuth bool `yaml:"ws-auth" json:"ws-auth"`
|
||||
|
||||
// GlAPIKey is the API key for the generative language API.
|
||||
// GlAPIKey exposes the legacy generative language API key list for backward compatibility.
|
||||
GlAPIKey []string `yaml:"generative-language-api-key" json:"generative-language-api-key"`
|
||||
|
||||
// GeminiKey defines Gemini API key configurations with optional routing overrides.
|
||||
GeminiKey []GeminiKey `yaml:"gemini-api-key" json:"gemini-api-key"`
|
||||
|
||||
// RequestRetry defines the retry times when the request failed.
|
||||
RequestRetry int `yaml:"request-retry" json:"request-retry"`
|
||||
|
||||
@@ -122,6 +125,22 @@ type CodexKey struct {
|
||||
ProxyURL string `yaml:"proxy-url" json:"proxy-url"`
|
||||
}
|
||||
|
||||
// GeminiKey represents the configuration for a Gemini API key,
|
||||
// including optional overrides for upstream base URL, proxy routing, and headers.
|
||||
type GeminiKey struct {
|
||||
// APIKey is the authentication key for accessing Gemini API services.
|
||||
APIKey string `yaml:"api-key" json:"api-key"`
|
||||
|
||||
// BaseURL optionally overrides the Gemini API endpoint.
|
||||
BaseURL string `yaml:"base-url,omitempty" json:"base-url,omitempty"`
|
||||
|
||||
// ProxyURL optionally overrides the global proxy for this API key.
|
||||
ProxyURL string `yaml:"proxy-url,omitempty" json:"proxy-url,omitempty"`
|
||||
|
||||
// Headers optionally adds extra HTTP headers for requests sent with this key.
|
||||
Headers map[string]string `yaml:"headers,omitempty" json:"headers,omitempty"`
|
||||
}
|
||||
|
||||
// OpenAICompatibility represents the configuration for OpenAI API compatibility
|
||||
// with external providers, allowing model aliases to be routed through OpenAI API format.
|
||||
type OpenAICompatibility struct {
|
||||
@@ -227,6 +246,9 @@ func LoadConfigOptional(configFile string, optional bool) (*Config, error) {
|
||||
// Sync request authentication providers with inline API keys for backwards compatibility.
|
||||
syncInlineAccessProvider(&cfg)
|
||||
|
||||
// Normalize Gemini API key configuration and migrate legacy entries.
|
||||
cfg.SyncGeminiKeys()
|
||||
|
||||
// Sanitize OpenAI compatibility providers: drop entries without base-url
|
||||
sanitizeOpenAICompatibility(&cfg)
|
||||
|
||||
@@ -276,6 +298,63 @@ func sanitizeCodexKeys(cfg *Config) {
|
||||
cfg.CodexKey = out
|
||||
}
|
||||
|
||||
func (cfg *Config) SyncGeminiKeys() {
|
||||
if cfg == nil {
|
||||
return
|
||||
}
|
||||
|
||||
if len(cfg.GeminiKey) > 0 {
|
||||
out := make([]GeminiKey, 0, len(cfg.GeminiKey))
|
||||
for i := range cfg.GeminiKey {
|
||||
entry := cfg.GeminiKey[i]
|
||||
entry.APIKey = strings.TrimSpace(entry.APIKey)
|
||||
entry.BaseURL = strings.TrimSpace(entry.BaseURL)
|
||||
entry.ProxyURL = strings.TrimSpace(entry.ProxyURL)
|
||||
if entry.APIKey == "" {
|
||||
continue
|
||||
}
|
||||
if len(entry.Headers) > 0 {
|
||||
clean := make(map[string]string, len(entry.Headers))
|
||||
for hk, hv := range entry.Headers {
|
||||
key := strings.TrimSpace(hk)
|
||||
val := strings.TrimSpace(hv)
|
||||
if key == "" || val == "" {
|
||||
continue
|
||||
}
|
||||
clean[key] = val
|
||||
}
|
||||
if len(clean) == 0 {
|
||||
entry.Headers = nil
|
||||
} else {
|
||||
entry.Headers = clean
|
||||
}
|
||||
}
|
||||
out = append(out, entry)
|
||||
}
|
||||
cfg.GeminiKey = out
|
||||
}
|
||||
|
||||
if len(cfg.GeminiKey) == 0 && len(cfg.GlAPIKey) > 0 {
|
||||
out := make([]GeminiKey, 0, len(cfg.GlAPIKey))
|
||||
for i := range cfg.GlAPIKey {
|
||||
key := strings.TrimSpace(cfg.GlAPIKey[i])
|
||||
if key == "" {
|
||||
continue
|
||||
}
|
||||
out = append(out, GeminiKey{APIKey: key})
|
||||
}
|
||||
cfg.GeminiKey = out
|
||||
}
|
||||
|
||||
cfg.GlAPIKey = cfg.GlAPIKey[:0]
|
||||
if len(cfg.GeminiKey) > 0 {
|
||||
cfg.GlAPIKey = make([]string, 0, len(cfg.GeminiKey))
|
||||
for i := range cfg.GeminiKey {
|
||||
cfg.GlAPIKey = append(cfg.GlAPIKey, cfg.GeminiKey[i].APIKey)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func syncInlineAccessProvider(cfg *Config) {
|
||||
if cfg == nil {
|
||||
return
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/router-for-me/CLIProxyAPI/v6/internal/config"
|
||||
@@ -94,7 +95,8 @@ func (e *GeminiExecutor) Execute(ctx context.Context, auth *cliproxyauth.Auth, r
|
||||
action = "countTokens"
|
||||
}
|
||||
}
|
||||
url := fmt.Sprintf("%s/%s/models/%s:%s", glEndpoint, glAPIVersion, req.Model, action)
|
||||
baseURL := resolveGeminiBaseURL(auth)
|
||||
url := fmt.Sprintf("%s/%s/models/%s:%s", baseURL, glAPIVersion, req.Model, action)
|
||||
if opts.Alt != "" && action != "countTokens" {
|
||||
url = url + fmt.Sprintf("?$alt=%s", opts.Alt)
|
||||
}
|
||||
@@ -111,6 +113,7 @@ func (e *GeminiExecutor) Execute(ctx context.Context, auth *cliproxyauth.Auth, r
|
||||
} else if bearer != "" {
|
||||
httpReq.Header.Set("Authorization", "Bearer "+bearer)
|
||||
}
|
||||
applyGeminiHeaders(httpReq, auth)
|
||||
var authID, authLabel, authType, authValue string
|
||||
if auth != nil {
|
||||
authID = auth.ID
|
||||
@@ -180,7 +183,8 @@ func (e *GeminiExecutor) ExecuteStream(ctx context.Context, auth *cliproxyauth.A
|
||||
body = util.StripThinkingConfigIfUnsupported(req.Model, body)
|
||||
body = fixGeminiImageAspectRatio(req.Model, body)
|
||||
|
||||
url := fmt.Sprintf("%s/%s/models/%s:%s", glEndpoint, glAPIVersion, req.Model, "streamGenerateContent")
|
||||
baseURL := resolveGeminiBaseURL(auth)
|
||||
url := fmt.Sprintf("%s/%s/models/%s:%s", baseURL, glAPIVersion, req.Model, "streamGenerateContent")
|
||||
if opts.Alt == "" {
|
||||
url = url + "?alt=sse"
|
||||
} else {
|
||||
@@ -199,6 +203,7 @@ func (e *GeminiExecutor) ExecuteStream(ctx context.Context, auth *cliproxyauth.A
|
||||
} else {
|
||||
httpReq.Header.Set("Authorization", "Bearer "+bearer)
|
||||
}
|
||||
applyGeminiHeaders(httpReq, auth)
|
||||
var authID, authLabel, authType, authValue string
|
||||
if auth != nil {
|
||||
authID = auth.ID
|
||||
@@ -290,7 +295,8 @@ func (e *GeminiExecutor) CountTokens(ctx context.Context, auth *cliproxyauth.Aut
|
||||
translatedReq, _ = sjson.DeleteBytes(translatedReq, "tools")
|
||||
translatedReq, _ = sjson.DeleteBytes(translatedReq, "generationConfig")
|
||||
|
||||
url := fmt.Sprintf("%s/%s/models/%s:%s", glEndpoint, glAPIVersion, req.Model, "countTokens")
|
||||
baseURL := resolveGeminiBaseURL(auth)
|
||||
url := fmt.Sprintf("%s/%s/models/%s:%s", baseURL, glAPIVersion, req.Model, "countTokens")
|
||||
|
||||
requestBody := bytes.NewReader(translatedReq)
|
||||
|
||||
@@ -304,6 +310,7 @@ func (e *GeminiExecutor) CountTokens(ctx context.Context, auth *cliproxyauth.Aut
|
||||
} else {
|
||||
httpReq.Header.Set("Authorization", "Bearer "+bearer)
|
||||
}
|
||||
applyGeminiHeaders(httpReq, auth)
|
||||
var authID, authLabel, authType, authValue string
|
||||
if auth != nil {
|
||||
authID = auth.ID
|
||||
@@ -473,6 +480,60 @@ func geminiCreds(a *cliproxyauth.Auth) (apiKey, bearer string) {
|
||||
return
|
||||
}
|
||||
|
||||
func resolveGeminiBaseURL(auth *cliproxyauth.Auth) string {
|
||||
base := glEndpoint
|
||||
if auth != nil && auth.Attributes != nil {
|
||||
if custom := strings.TrimSpace(auth.Attributes["base_url"]); custom != "" {
|
||||
base = strings.TrimRight(custom, "/")
|
||||
}
|
||||
}
|
||||
if base == "" {
|
||||
return glEndpoint
|
||||
}
|
||||
return base
|
||||
}
|
||||
|
||||
func applyGeminiHeaders(req *http.Request, auth *cliproxyauth.Auth) {
|
||||
if req == nil {
|
||||
return
|
||||
}
|
||||
headers := geminiCustomHeaders(auth)
|
||||
if len(headers) == 0 {
|
||||
return
|
||||
}
|
||||
for k, v := range headers {
|
||||
if k == "" || v == "" {
|
||||
continue
|
||||
}
|
||||
req.Header.Set(k, v)
|
||||
}
|
||||
}
|
||||
|
||||
func geminiCustomHeaders(auth *cliproxyauth.Auth) map[string]string {
|
||||
if auth == nil || auth.Attributes == nil {
|
||||
return nil
|
||||
}
|
||||
headers := make(map[string]string, len(auth.Attributes))
|
||||
for k, v := range auth.Attributes {
|
||||
if !strings.HasPrefix(k, "header:") {
|
||||
continue
|
||||
}
|
||||
name := strings.TrimSpace(strings.TrimPrefix(k, "header:"))
|
||||
if name == "" {
|
||||
continue
|
||||
}
|
||||
val := strings.TrimSpace(v)
|
||||
if val == "" {
|
||||
continue
|
||||
}
|
||||
headers[name] = val
|
||||
}
|
||||
if len(headers) == 0 {
|
||||
return nil
|
||||
}
|
||||
return headers
|
||||
}
|
||||
|
||||
func fixGeminiImageAspectRatio(modelName string, rawJSON []byte) []byte {
|
||||
if modelName == "gemini-2.5-flash-image-preview" {
|
||||
aspectRatioResult := gjson.GetBytes(rawJSON, "generationConfig.imageConfig.aspectRatio")
|
||||
|
||||
@@ -178,7 +178,7 @@ func MaskAuthorizationHeader(value string) string {
|
||||
func MaskSensitiveHeaderValue(key, value string) string {
|
||||
lowerKey := strings.ToLower(strings.TrimSpace(key))
|
||||
switch {
|
||||
case lowerKey == "authorization":
|
||||
case strings.Contains(lowerKey, "authorization"):
|
||||
return MaskAuthorizationHeader(value)
|
||||
case strings.Contains(lowerKey, "api-key"),
|
||||
strings.Contains(lowerKey, "apikey"),
|
||||
|
||||
@@ -604,8 +604,8 @@ func (w *Watcher) reloadClients(rescanAuth bool) {
|
||||
// no legacy clients to unregister
|
||||
|
||||
// Create new API key clients based on the new config
|
||||
glAPIKeyCount, claudeAPIKeyCount, codexAPIKeyCount, openAICompatCount := BuildAPIKeyClients(cfg)
|
||||
totalAPIKeyClients := glAPIKeyCount + claudeAPIKeyCount + codexAPIKeyCount + openAICompatCount
|
||||
geminiAPIKeyCount, claudeAPIKeyCount, codexAPIKeyCount, openAICompatCount := BuildAPIKeyClients(cfg)
|
||||
totalAPIKeyClients := geminiAPIKeyCount + claudeAPIKeyCount + codexAPIKeyCount + openAICompatCount
|
||||
log.Debugf("loaded %d API key clients", totalAPIKeyClients)
|
||||
|
||||
var authFileCount int
|
||||
@@ -648,7 +648,7 @@ func (w *Watcher) reloadClients(rescanAuth bool) {
|
||||
w.clientsMutex.Unlock()
|
||||
}
|
||||
|
||||
totalNewClients := authFileCount + glAPIKeyCount + claudeAPIKeyCount + codexAPIKeyCount + openAICompatCount
|
||||
totalNewClients := authFileCount + geminiAPIKeyCount + claudeAPIKeyCount + codexAPIKeyCount + openAICompatCount
|
||||
|
||||
// Ensure consumers observe the new configuration before auth updates dispatch.
|
||||
if w.reloadCallback != nil {
|
||||
@@ -658,10 +658,10 @@ func (w *Watcher) reloadClients(rescanAuth bool) {
|
||||
|
||||
w.refreshAuthState()
|
||||
|
||||
log.Infof("full client load complete - %d clients (%d auth files + %d GL API keys + %d Claude API keys + %d Codex keys + %d OpenAI-compat)",
|
||||
log.Infof("full client load complete - %d clients (%d auth files + %d Gemini API keys + %d Claude API keys + %d Codex keys + %d OpenAI-compat)",
|
||||
totalNewClients,
|
||||
authFileCount,
|
||||
glAPIKeyCount,
|
||||
geminiAPIKeyCount,
|
||||
claudeAPIKeyCount,
|
||||
codexAPIKeyCount,
|
||||
openAICompatCount,
|
||||
@@ -746,23 +746,41 @@ func (w *Watcher) SnapshotCoreAuths() []*coreauth.Auth {
|
||||
w.clientsMutex.RUnlock()
|
||||
if cfg != nil {
|
||||
// Gemini official API keys -> synthesize auths
|
||||
for i := range cfg.GlAPIKey {
|
||||
k := strings.TrimSpace(cfg.GlAPIKey[i])
|
||||
if k == "" {
|
||||
for i := range cfg.GeminiKey {
|
||||
entry := cfg.GeminiKey[i]
|
||||
key := strings.TrimSpace(entry.APIKey)
|
||||
if key == "" {
|
||||
continue
|
||||
}
|
||||
id, token := idGen.next("gemini:apikey", k)
|
||||
base := strings.TrimSpace(entry.BaseURL)
|
||||
proxyURL := strings.TrimSpace(entry.ProxyURL)
|
||||
id, token := idGen.next("gemini:apikey", key, base)
|
||||
attrs := map[string]string{
|
||||
"source": fmt.Sprintf("config:gemini[%s]", token),
|
||||
"api_key": key,
|
||||
}
|
||||
if base != "" {
|
||||
attrs["base_url"] = base
|
||||
}
|
||||
if len(entry.Headers) > 0 {
|
||||
for hk, hv := range entry.Headers {
|
||||
key := strings.TrimSpace(hk)
|
||||
val := strings.TrimSpace(hv)
|
||||
if key == "" || val == "" {
|
||||
continue
|
||||
}
|
||||
attrs["header:"+key] = val
|
||||
}
|
||||
}
|
||||
a := &coreauth.Auth{
|
||||
ID: id,
|
||||
Provider: "gemini",
|
||||
Label: "gemini-apikey",
|
||||
Status: coreauth.StatusActive,
|
||||
Attributes: map[string]string{
|
||||
"source": fmt.Sprintf("config:gemini[%s]", token),
|
||||
"api_key": k,
|
||||
},
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
ID: id,
|
||||
Provider: "gemini",
|
||||
Label: "gemini-apikey",
|
||||
Status: coreauth.StatusActive,
|
||||
ProxyURL: proxyURL,
|
||||
Attributes: attrs,
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
}
|
||||
out = append(out, a)
|
||||
}
|
||||
@@ -1030,14 +1048,14 @@ func (w *Watcher) loadFileClients(cfg *config.Config) int {
|
||||
}
|
||||
|
||||
func BuildAPIKeyClients(cfg *config.Config) (int, int, int, int) {
|
||||
glAPIKeyCount := 0
|
||||
geminiAPIKeyCount := 0
|
||||
claudeAPIKeyCount := 0
|
||||
codexAPIKeyCount := 0
|
||||
openAICompatCount := 0
|
||||
|
||||
if len(cfg.GlAPIKey) > 0 {
|
||||
if len(cfg.GeminiKey) > 0 {
|
||||
// Stateless executor handles Gemini API keys; avoid constructing legacy clients.
|
||||
glAPIKeyCount += len(cfg.GlAPIKey)
|
||||
geminiAPIKeyCount += len(cfg.GeminiKey)
|
||||
}
|
||||
if len(cfg.ClaudeKey) > 0 {
|
||||
claudeAPIKeyCount += len(cfg.ClaudeKey)
|
||||
@@ -1056,7 +1074,7 @@ func BuildAPIKeyClients(cfg *config.Config) (int, int, int, int) {
|
||||
}
|
||||
}
|
||||
}
|
||||
return glAPIKeyCount, claudeAPIKeyCount, codexAPIKeyCount, openAICompatCount
|
||||
return geminiAPIKeyCount, claudeAPIKeyCount, codexAPIKeyCount, openAICompatCount
|
||||
}
|
||||
|
||||
func diffOpenAICompatibility(oldList, newList []config.OpenAICompatibility) []string {
|
||||
@@ -1239,10 +1257,31 @@ func buildConfigChangeDetails(oldCfg, newCfg *config.Config) []string {
|
||||
} else if !reflect.DeepEqual(trimStrings(oldCfg.APIKeys), trimStrings(newCfg.APIKeys)) {
|
||||
changes = append(changes, "api-keys: values updated (count unchanged, redacted)")
|
||||
}
|
||||
if len(oldCfg.GlAPIKey) != len(newCfg.GlAPIKey) {
|
||||
changes = append(changes, fmt.Sprintf("generative-language-api-key count: %d -> %d", len(oldCfg.GlAPIKey), len(newCfg.GlAPIKey)))
|
||||
} else if !reflect.DeepEqual(trimStrings(oldCfg.GlAPIKey), trimStrings(newCfg.GlAPIKey)) {
|
||||
changes = append(changes, "generative-language-api-key: values updated (count unchanged, redacted)")
|
||||
if len(oldCfg.GeminiKey) != len(newCfg.GeminiKey) {
|
||||
changes = append(changes, fmt.Sprintf("gemini-api-key count: %d -> %d", len(oldCfg.GeminiKey), len(newCfg.GeminiKey)))
|
||||
} else {
|
||||
for i := range oldCfg.GeminiKey {
|
||||
if i >= len(newCfg.GeminiKey) {
|
||||
break
|
||||
}
|
||||
o := oldCfg.GeminiKey[i]
|
||||
n := newCfg.GeminiKey[i]
|
||||
if strings.TrimSpace(o.BaseURL) != strings.TrimSpace(n.BaseURL) {
|
||||
changes = append(changes, fmt.Sprintf("gemini[%d].base-url: %s -> %s", i, strings.TrimSpace(o.BaseURL), strings.TrimSpace(n.BaseURL)))
|
||||
}
|
||||
if strings.TrimSpace(o.ProxyURL) != strings.TrimSpace(n.ProxyURL) {
|
||||
changes = append(changes, fmt.Sprintf("gemini[%d].proxy-url: %s -> %s", i, strings.TrimSpace(o.ProxyURL), strings.TrimSpace(n.ProxyURL)))
|
||||
}
|
||||
if strings.TrimSpace(o.APIKey) != strings.TrimSpace(n.APIKey) {
|
||||
changes = append(changes, fmt.Sprintf("gemini[%d].api-key: updated", i))
|
||||
}
|
||||
if !equalStringMap(o.Headers, n.Headers) {
|
||||
changes = append(changes, fmt.Sprintf("gemini[%d].headers: updated", i))
|
||||
}
|
||||
}
|
||||
if !reflect.DeepEqual(trimStrings(oldCfg.GlAPIKey), trimStrings(newCfg.GlAPIKey)) {
|
||||
changes = append(changes, "generative-language-api-key: values updated (legacy view, redacted)")
|
||||
}
|
||||
}
|
||||
|
||||
// Claude keys (do not print key material)
|
||||
@@ -1325,3 +1364,15 @@ func trimStrings(in []string) []string {
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func equalStringMap(a, b map[string]string) bool {
|
||||
if len(a) != len(b) {
|
||||
return false
|
||||
}
|
||||
for k, v := range a {
|
||||
if b[k] != v {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user