mirror of
https://github.com/router-for-me/CLIProxyAPI.git
synced 2026-02-10 16:30:51 +08:00
Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fb760718e2 | ||
|
|
d6721e4e75 | ||
|
|
514f5a8ad4 | ||
|
|
a68e0dd8aa | ||
|
|
75d7763c5c | ||
|
|
9bb7df7af7 | ||
|
|
43665cb649 | ||
|
|
39337627b9 |
16
.github/workflows/homebrew.yml
vendored
Normal file
16
.github/workflows/homebrew.yml
vendored
Normal file
@@ -0,0 +1,16 @@
|
||||
name: Bump Homebrew formula
|
||||
|
||||
on:
|
||||
release:
|
||||
types: [ released ]
|
||||
|
||||
jobs:
|
||||
homebrew:
|
||||
name: Bump Homebrew formula
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Update Homebrew Formula
|
||||
uses: dawidd6/action-homebrew-bump-formula@v5
|
||||
with:
|
||||
token: ${{ secrets.HOMEBREW_TOKEN }}
|
||||
formula: cliproxyapi
|
||||
@@ -328,7 +328,7 @@ The server uses a YAML configuration file (`config.yaml`) located in the project
|
||||
| `openai-compatibility.*.models.*.alias` | string | "" | The alias used in the API. |
|
||||
| `gemini-web` | object | {} | Configuration specific to the Gemini Web client. |
|
||||
| `gemini-web.context` | boolean | true | Enables conversation context reuse for continuous dialogue. |
|
||||
| `gemini-web.code-mode` | boolean | false | Enables code mode for optimized responses in coding-related tasks. |
|
||||
| `gemini-web.gem-mode` | string | "" | Selects a predefined Gem to attach for Gemini Web requests; allowed values: `coding-partner`, `writing-editor`. When empty, no Gem is attached. |
|
||||
| `gemini-web.max-chars-per-request` | integer | 1,000,000 | The maximum number of characters to send to Gemini Web in a single request. |
|
||||
| `gemini-web.disable-continuation-hint` | boolean | false | Disables the continuation hint for split prompts. |
|
||||
|
||||
@@ -378,7 +378,7 @@ quota-exceeded:
|
||||
# Gemini Web client configuration
|
||||
gemini-web:
|
||||
context: true # Enable conversation context reuse
|
||||
code-mode: false # Enable code mode
|
||||
gem-mode: "" # Select Gem: "coding-partner" or "writing-editor"; empty means no Gem
|
||||
max-chars-per-request: 1000000 # Max characters per request
|
||||
|
||||
# API keys for official Generative Language API
|
||||
|
||||
@@ -340,7 +340,7 @@ console.log(await claudeResponse.json());
|
||||
| `openai-compatibility.*.models.*.alias` | string | "" | 在API中使用的别名。 |
|
||||
| `gemini-web` | object | {} | Gemini Web 客户端的特定配置。 |
|
||||
| `gemini-web.context` | boolean | true | 是否启用会话上下文重用,以实现连续对话。 |
|
||||
| `gemini-web.code-mode` | boolean | false | 是否启用代码模式,优化代码相关任务的响应。 |
|
||||
| `gemini-web.gem-mode` | string | "" | 选择要附加的预设 Gem(`coding-partner` 或 `writing-editor`);为空表示不附加。 |
|
||||
| `gemini-web.max-chars-per-request` | integer | 1,000,000 | 单次请求发送给 Gemini Web 的最大字符数。 |
|
||||
| `gemini-web.disable-continuation-hint` | boolean | false | 当提示被拆分时,是否禁用连续提示的暗示。 |
|
||||
|
||||
@@ -390,7 +390,7 @@ quota-exceeded:
|
||||
# Gemini Web 客户端配置
|
||||
gemini-web:
|
||||
context: true # 启用会话上下文重用
|
||||
code-mode: false # 启用代码模式
|
||||
gem-mode: "" # 选择 Gem:"coding-partner" 或 "writing-editor";为空表示不附加
|
||||
max-chars-per-request: 1000000 # 单次请求最大字符数
|
||||
|
||||
# AIStduio Gemini API 的 API 密钥
|
||||
|
||||
@@ -90,10 +90,8 @@ quota-exceeded:
|
||||
# # Disable the short continuation hint appended to intermediate chunks
|
||||
# # when splitting long prompts. Default is false (hint enabled by default).
|
||||
# disable-continuation-hint: false
|
||||
# # Code mode:
|
||||
# # - true: enable XML wrapping hint and attach the coding-partner Gem.
|
||||
# # Thought merging (<think> into visible content) applies to STREAMING only;
|
||||
# # non-stream responses keep reasoning/thought parts separate for clients
|
||||
# # that expect explicit reasoning fields.
|
||||
# # - false: disable XML hint and keep <think> separate
|
||||
# code-mode: false
|
||||
# # Gem selection (Gem Mode):
|
||||
# # - "coding-partner": attach the predefined Coding partner Gem
|
||||
# # - "writing-editor": attach the predefined Writing editor Gem
|
||||
# # - empty: do not attach any Gem
|
||||
# gem-mode: ""
|
||||
|
||||
@@ -255,7 +255,9 @@ func (h *Handler) ListAuthFiles(c *gin.Context) {
|
||||
full := filepath.Join(h.cfg.AuthDir, name)
|
||||
if data, errRead := os.ReadFile(full); errRead == nil {
|
||||
typeValue := gjson.GetBytes(data, "type").String()
|
||||
emailValue := gjson.GetBytes(data, "email").String()
|
||||
fileData["type"] = typeValue
|
||||
fileData["email"] = emailValue
|
||||
}
|
||||
|
||||
files = append(files, fileData)
|
||||
|
||||
@@ -62,10 +62,19 @@ type GeminiWebConfig struct {
|
||||
// Defaults to true if not set in YAML (see LoadConfig).
|
||||
Context bool `yaml:"context" json:"context"`
|
||||
|
||||
// CodeMode, when true, enables coding mode behaviors for Gemini Web:
|
||||
// - Attach the predefined "Coding partner" Gem
|
||||
// - Enable XML wrapping hint for tool markup
|
||||
// - Merge <think> content into visible content for tool-friendly output
|
||||
// GemMode selects a predefined Gem to attach for Gemini Web requests.
|
||||
// Allowed values:
|
||||
// - "coding-partner"
|
||||
// - "writing-editor"
|
||||
// When empty, no Gem is attached by configuration.
|
||||
// This is independent from CodeMode below, which is kept for backwards compatibility.
|
||||
GemMode string `yaml:"gem-mode" json:"gem-mode"`
|
||||
|
||||
// CodeMode enables legacy coding-mode behaviors for Gemini Web.
|
||||
// Backwards compatibility: when true, the service behaves as before by
|
||||
// attaching the predefined "Coding partner" Gem and enabling extra
|
||||
// conveniences (e.g., XML wrapping hints). Prefer GemMode for selecting
|
||||
// a Gem going forward.
|
||||
CodeMode bool `yaml:"code-mode" json:"code-mode"`
|
||||
|
||||
// MaxCharsPerRequest caps the number of characters (runes) sent to
|
||||
|
||||
@@ -225,7 +225,7 @@ func MaskToken28(s string) string {
|
||||
}
|
||||
|
||||
var NanoBananaModel = map[string]struct{}{
|
||||
"gemini-2.5-flash-image-preview": {},
|
||||
"gemini-2.5-flash-image-web": {},
|
||||
}
|
||||
|
||||
// NewGeminiClient creates a client. Pass empty strings to auto-detect via browser cookies (not implemented in Go port).
|
||||
@@ -380,6 +380,15 @@ func (c *GeminiClient) generateOnce(prompt string, files []string, model Model,
|
||||
}
|
||||
|
||||
inner := []any{item0, nil, item2}
|
||||
// Attach Gem first to keep index alignment with reference implementation
|
||||
// so the Gemini Web UI can recognize the selected Gem.
|
||||
if gem != nil {
|
||||
// pad with 16 nils then gem ID
|
||||
for i := 0; i < 16; i++ {
|
||||
inner = append(inner, nil)
|
||||
}
|
||||
inner = append(inner, gem.ID)
|
||||
}
|
||||
requestedModel := strings.ToLower(model.Name)
|
||||
if chat != nil && chat.RequestedModel() != "" {
|
||||
requestedModel = chat.RequestedModel()
|
||||
@@ -388,13 +397,6 @@ func (c *GeminiClient) generateOnce(prompt string, files []string, model Model,
|
||||
inner = ensureAnyLen(inner, 49)
|
||||
inner[49] = 14
|
||||
}
|
||||
if gem != nil {
|
||||
// pad with 16 nils then gem ID
|
||||
for i := 0; i < 16; i++ {
|
||||
inner = append(inner, nil)
|
||||
}
|
||||
inner = append(inner, gem.ID)
|
||||
}
|
||||
innerJSON, _ := json.Marshal(inner)
|
||||
outer := []any{nil, string(innerJSON)}
|
||||
outerJSON, _ := json.Marshal(outer)
|
||||
|
||||
@@ -21,7 +21,7 @@ func EnsureGeminiWebAliasMap() {
|
||||
continue
|
||||
}
|
||||
if m.ID == "gemini-2.5-flash" {
|
||||
aliasMap["gemini-2.5-flash-image-preview"] = "gemini-2.5-flash"
|
||||
aliasMap["gemini-2.5-flash-image-web"] = "gemini-2.5-flash"
|
||||
}
|
||||
alias := AliasFromModelID(m.ID)
|
||||
aliasMap[strings.ToLower(alias)] = strings.ToLower(m.ID)
|
||||
@@ -65,8 +65,8 @@ func GetGeminiWebAliasedModels() []*registry.ModelInfo {
|
||||
continue
|
||||
} else if m.ID == "gemini-2.5-flash" {
|
||||
cpy := *m
|
||||
cpy.ID = "gemini-2.5-flash-image-preview"
|
||||
cpy.Name = "gemini-2.5-flash-image-preview"
|
||||
cpy.ID = "gemini-2.5-flash-image-web"
|
||||
cpy.Name = "gemini-2.5-flash-image-web"
|
||||
cpy.DisplayName = "Nano Banana"
|
||||
cpy.Description = "Gemini 2.5 Flash Preview Image"
|
||||
aliased = append(aliased, &cpy)
|
||||
|
||||
@@ -460,10 +460,10 @@ func (s *GeminiWebState) Send(ctx context.Context, modelName string, reqPayload
|
||||
return nil, s.wrapSendError(err), nil
|
||||
}
|
||||
|
||||
// Hook: For gemini-2.5-flash-image-preview, if the API returns only images without any text,
|
||||
// Hook: For gemini-2.5-flash-image-web, if the API returns only images without any text,
|
||||
// inject a small textual summary so that conversation persistence has non-empty assistant text.
|
||||
// This helps conversation recovery (conv store) to match sessions reliably.
|
||||
if strings.EqualFold(modelName, "gemini-2.5-flash-image-preview") {
|
||||
if strings.EqualFold(modelName, "gemini-2.5-flash-image-web") {
|
||||
if len(output.Candidates) > 0 {
|
||||
c := output.Candidates[output.Chosen]
|
||||
hasNoText := strings.TrimSpace(c.Text) == ""
|
||||
@@ -696,7 +696,22 @@ func (s *GeminiWebState) findReusableSession(modelName string, msgs []RoleText)
|
||||
}
|
||||
|
||||
func (s *GeminiWebState) getConfiguredGem() *Gem {
|
||||
if s.cfg != nil && s.cfg.GeminiWeb.CodeMode {
|
||||
if s.cfg == nil {
|
||||
return nil
|
||||
}
|
||||
// New behavior: attach Gem based on explicit GemMode selection.
|
||||
// Only attaches the Gem; does not toggle any other behavior.
|
||||
if gm := strings.ToLower(strings.TrimSpace(s.cfg.GeminiWeb.GemMode)); gm != "" {
|
||||
switch gm {
|
||||
case "coding-partner":
|
||||
return &Gem{ID: "coding-partner", Name: "Coding partner", Predefined: true}
|
||||
case "writing-editor":
|
||||
return &Gem{ID: "writing-editor", Name: "Writing editor", Predefined: true}
|
||||
}
|
||||
}
|
||||
// Backwards compatibility: legacy CodeMode still attaches Coding partner
|
||||
// and may enable extra behaviors elsewhere.
|
||||
if s.cfg.GeminiWeb.CodeMode {
|
||||
return &Gem{ID: "coding-partner", Name: "Coding partner", Predefined: true}
|
||||
}
|
||||
return nil
|
||||
@@ -1015,3 +1030,10 @@ func FindReusableSessionIn(items map[string]ConversationRecord, index map[string
|
||||
}
|
||||
return ConversationRecord{}, nil, 0, false
|
||||
}
|
||||
|
||||
// SetConfig updates the configuration reference used by the state.
|
||||
// This allows hot-reload of configuration to take effect for existing
|
||||
// runtime states that were cached on auth during previous requests.
|
||||
func (s *GeminiWebState) SetConfig(cfg *config.Config) {
|
||||
s.cfg = cfg
|
||||
}
|
||||
|
||||
@@ -168,6 +168,8 @@ func (e *GeminiWebExecutor) stateFor(auth *cliproxyauth.Auth) (*geminiwebapi.Gem
|
||||
return nil, fmt.Errorf("gemini-web executor: auth is nil")
|
||||
}
|
||||
if runtime, ok := auth.Runtime.(*geminiWebRuntime); ok && runtime != nil && runtime.state != nil {
|
||||
// Hot-reload: ensure cached state sees the latest config
|
||||
runtime.state.SetConfig(e.cfg)
|
||||
return runtime.state, nil
|
||||
}
|
||||
|
||||
@@ -175,6 +177,8 @@ func (e *GeminiWebExecutor) stateFor(auth *cliproxyauth.Auth) (*geminiwebapi.Gem
|
||||
defer e.mu.Unlock()
|
||||
|
||||
if runtime, ok := auth.Runtime.(*geminiWebRuntime); ok && runtime != nil && runtime.state != nil {
|
||||
// Hot-reload: ensure cached state sees the latest config
|
||||
runtime.state.SetConfig(e.cfg)
|
||||
return runtime.state, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -521,6 +521,9 @@ func (w *Watcher) reloadConfig() bool {
|
||||
if oldConfig.GeminiWeb.DisableContinuationHint != newConfig.GeminiWeb.DisableContinuationHint {
|
||||
log.Debugf(" gemini-web.disable-continuation-hint: %t -> %t", oldConfig.GeminiWeb.DisableContinuationHint, newConfig.GeminiWeb.DisableContinuationHint)
|
||||
}
|
||||
if oldConfig.GeminiWeb.GemMode != newConfig.GeminiWeb.GemMode {
|
||||
log.Debugf(" gemini-web.gem-mode: %s -> %s", oldConfig.GeminiWeb.GemMode, newConfig.GeminiWeb.GemMode)
|
||||
}
|
||||
if oldConfig.GeminiWeb.CodeMode != newConfig.GeminiWeb.CodeMode {
|
||||
log.Debugf(" gemini-web.code-mode: %t -> %t", oldConfig.GeminiWeb.CodeMode, newConfig.GeminiWeb.CodeMode)
|
||||
}
|
||||
|
||||
@@ -291,6 +291,17 @@ func (s *Service) ensureExecutorsForAuth(a *coreauth.Auth) {
|
||||
}
|
||||
}
|
||||
|
||||
// rebindExecutors refreshes provider executors so they observe the latest configuration.
|
||||
func (s *Service) rebindExecutors() {
|
||||
if s == nil || s.coreManager == nil {
|
||||
return
|
||||
}
|
||||
auths := s.coreManager.List()
|
||||
for _, auth := range auths {
|
||||
s.ensureExecutorsForAuth(auth)
|
||||
}
|
||||
}
|
||||
|
||||
// Run starts the service and blocks until the context is cancelled or the server stops.
|
||||
// It initializes all components including authentication, file watching, HTTP server,
|
||||
// and starts processing requests. The method blocks until the context is cancelled.
|
||||
@@ -389,6 +400,7 @@ func (s *Service) Run(ctx context.Context) error {
|
||||
s.cfgMu.Lock()
|
||||
s.cfg = newCfg
|
||||
s.cfgMu.Unlock()
|
||||
s.rebindExecutors()
|
||||
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user