luispater commited on
Commit
17c2266
·
unverified ·
1 Parent(s): f353d4f

feat(auth, docs): add SDK guides and local password support for management

Browse files

- Added extensive SDK usage guides for `cliproxy`, `sdk/access`, and watcher integration.
- Introduced `--password` flag for specifying local management access passwords.
- Enhanced management API with local password checks to secure localhost requests.
- Updated documentation to reflect the new password functionality.

cmd/server/main.go CHANGED
@@ -64,7 +64,7 @@ func (m *LogFormatter) Format(entry *log.Entry) ([]byte, error) {
64
  func init() {
65
  logDir := "logs"
66
  if err := os.MkdirAll(logDir, 0755); err != nil {
67
- fmt.Fprintf(os.Stderr, "failed to create log directory: %v\n", err)
68
  os.Exit(1)
69
  }
70
 
@@ -122,6 +122,7 @@ func main() {
122
  var noBrowser bool
123
  var projectID string
124
  var configPath string
 
125
 
126
  // Define command-line flags for different operation modes.
127
  flag.BoolVar(&login, "login", false, "Login Google Account")
@@ -132,6 +133,34 @@ func main() {
132
  flag.BoolVar(&noBrowser, "no-browser", false, "Don't open browser automatically for OAuth")
133
  flag.StringVar(&projectID, "project_id", "", "Project ID (Gemini only, not required)")
134
  flag.StringVar(&configPath, "config", "", "Configure File Path")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
135
 
136
  // Parse the command-line flags.
137
  flag.Parse()
@@ -206,6 +235,6 @@ func main() {
206
  cmd.DoGeminiWebAuth(cfg)
207
  } else {
208
  // Start the main proxy service
209
- cmd.StartService(cfg, configFilePath)
210
  }
211
  }
 
64
  func init() {
65
  logDir := "logs"
66
  if err := os.MkdirAll(logDir, 0755); err != nil {
67
+ _, _ = fmt.Fprintf(os.Stderr, "failed to create log directory: %v\n", err)
68
  os.Exit(1)
69
  }
70
 
 
122
  var noBrowser bool
123
  var projectID string
124
  var configPath string
125
+ var password string
126
 
127
  // Define command-line flags for different operation modes.
128
  flag.BoolVar(&login, "login", false, "Login Google Account")
 
133
  flag.BoolVar(&noBrowser, "no-browser", false, "Don't open browser automatically for OAuth")
134
  flag.StringVar(&projectID, "project_id", "", "Project ID (Gemini only, not required)")
135
  flag.StringVar(&configPath, "config", "", "Configure File Path")
136
+ flag.StringVar(&password, "password", "", "")
137
+
138
+ flag.CommandLine.Usage = func() {
139
+ out := flag.CommandLine.Output()
140
+ _, _ = fmt.Fprintf(out, "Usage of %s\n", os.Args[0])
141
+ flag.CommandLine.VisitAll(func(f *flag.Flag) {
142
+ if f.Name == "password" {
143
+ return
144
+ }
145
+ s := fmt.Sprintf(" -%s", f.Name)
146
+ name, usage := flag.UnquoteUsage(f)
147
+ if name != "" {
148
+ s += " " + name
149
+ }
150
+ if len(s) <= 4 {
151
+ s += " "
152
+ } else {
153
+ s += "\n "
154
+ }
155
+ if usage != "" {
156
+ s += usage
157
+ }
158
+ if f.DefValue != "" && f.DefValue != "false" && f.DefValue != "0" {
159
+ s += fmt.Sprintf(" (default %s)", f.DefValue)
160
+ }
161
+ _, _ = fmt.Fprint(out, s+"\n")
162
+ })
163
+ }
164
 
165
  // Parse the command-line flags.
166
  flag.Parse()
 
235
  cmd.DoGeminiWebAuth(cfg)
236
  } else {
237
  // Start the main proxy service
238
+ cmd.StartService(cfg, configFilePath, password)
239
  }
240
  }
docs/sdk-access.md ADDED
@@ -0,0 +1,176 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # @sdk/access SDK Reference
2
+
3
+ The `github.com/router-for-me/CLIProxyAPI/v6/sdk/access` package centralizes inbound request authentication for the proxy. It offers a lightweight manager that chains credential providers, so servers can reuse the same access control logic inside or outside the CLI runtime.
4
+
5
+ ## Importing
6
+
7
+ ```go
8
+ import (
9
+ sdkaccess "github.com/router-for-me/CLIProxyAPI/v6/sdk/access"
10
+ "github.com/router-for-me/CLIProxyAPI/v6/internal/config"
11
+ )
12
+ ```
13
+
14
+ Add the module with `go get github.com/router-for-me/CLIProxyAPI/v6/sdk/access`.
15
+
16
+ ## Manager Lifecycle
17
+
18
+ ```go
19
+ manager := sdkaccess.NewManager()
20
+ providers, err := sdkaccess.BuildProviders(cfg)
21
+ if err != nil {
22
+ return err
23
+ }
24
+ manager.SetProviders(providers)
25
+ ```
26
+
27
+ * `NewManager` constructs an empty manager.
28
+ * `SetProviders` replaces the provider slice using a defensive copy.
29
+ * `Providers` retrieves a snapshot that can be iterated safely from other goroutines.
30
+ * `BuildProviders` translates `config.Config` access declarations into runnable providers. When the config omits explicit providers but defines inline API keys, the helper auto-installs the built-in `config-api-key` provider.
31
+
32
+ ## Authenticating Requests
33
+
34
+ ```go
35
+ result, err := manager.Authenticate(ctx, req)
36
+ switch {
37
+ case err == nil:
38
+ // Authentication succeeded; result describes the provider and principal.
39
+ case errors.Is(err, sdkaccess.ErrNoCredentials):
40
+ // No recognizable credentials were supplied.
41
+ case errors.Is(err, sdkaccess.ErrInvalidCredential):
42
+ // Supplied credentials were present but rejected.
43
+ default:
44
+ // Transport-level failure was returned by a provider.
45
+ }
46
+ ```
47
+
48
+ `Manager.Authenticate` walks the configured providers in order. It returns on the first success, skips providers that surface `ErrNotHandled`, and tracks whether any provider reported `ErrNoCredentials` or `ErrInvalidCredential` for downstream error reporting.
49
+
50
+ If the manager itself is `nil` or no providers are registered, the call returns `nil, nil`, allowing callers to treat access control as disabled without branching on errors.
51
+
52
+ Each `Result` includes the provider identifier, the resolved principal, and optional metadata (for example, which header carried the credential).
53
+
54
+ ## Configuration Layout
55
+
56
+ The manager expects access providers under the `auth.providers` key inside `config.yaml`:
57
+
58
+ ```yaml
59
+ auth:
60
+ providers:
61
+ - name: inline-api
62
+ type: config-api-key
63
+ api-keys:
64
+ - sk-test-123
65
+ - sk-prod-456
66
+ ```
67
+
68
+ Fields map directly to `config.AccessProvider`: `name` labels the provider, `type` selects the registered factory, `sdk` can name an external module, `api-keys` seeds inline credentials, and `config` passes provider-specific options.
69
+
70
+ ### Loading providers from external SDK modules
71
+
72
+ To consume a provider shipped in another Go module, point the `sdk` field at the module path and import it for its registration side effect:
73
+
74
+ ```yaml
75
+ auth:
76
+ providers:
77
+ - name: partner-auth
78
+ type: partner-token
79
+ sdk: github.com/acme/xplatform/sdk/access/providers/partner
80
+ config:
81
+ region: us-west-2
82
+ audience: cli-proxy
83
+ ```
84
+
85
+ ```go
86
+ import (
87
+ _ "github.com/acme/xplatform/sdk/access/providers/partner" // registers partner-token
88
+ sdkaccess "github.com/router-for-me/CLIProxyAPI/v6/sdk/access"
89
+ )
90
+ ```
91
+
92
+ The blank identifier import ensures `init` runs so `sdkaccess.RegisterProvider` executes before `BuildProviders` is called.
93
+
94
+ ## Built-in Providers
95
+
96
+ The SDK ships with one provider out of the box:
97
+
98
+ - `config-api-key`: Validates API keys declared inline or under top-level `api-keys`. It accepts the key from `Authorization: Bearer`, `X-Goog-Api-Key`, `X-Api-Key`, or the `?key=` query string and reports `ErrInvalidCredential` when no match is found.
99
+
100
+ Additional providers can be delivered by third-party packages. When a provider package is imported, it registers itself with `sdkaccess.RegisterProvider`.
101
+
102
+ ### Metadata and auditing
103
+
104
+ `Result.Metadata` carries provider-specific context. The built-in `config-api-key` provider, for example, stores the credential source (`authorization`, `x-goog-api-key`, `x-api-key`, or `query-key`). Populate this map in custom providers to enrich logs and downstream auditing.
105
+
106
+ ## Writing Custom Providers
107
+
108
+ ```go
109
+ type customProvider struct{}
110
+
111
+ func (p *customProvider) Identifier() string { return "my-provider" }
112
+
113
+ func (p *customProvider) Authenticate(ctx context.Context, r *http.Request) (*sdkaccess.Result, error) {
114
+ token := r.Header.Get("X-Custom")
115
+ if token == "" {
116
+ return nil, sdkaccess.ErrNoCredentials
117
+ }
118
+ if token != "expected" {
119
+ return nil, sdkaccess.ErrInvalidCredential
120
+ }
121
+ return &sdkaccess.Result{
122
+ Provider: p.Identifier(),
123
+ Principal: "service-user",
124
+ Metadata: map[string]string{"source": "x-custom"},
125
+ }, nil
126
+ }
127
+
128
+ func init() {
129
+ sdkaccess.RegisterProvider("custom", func(cfg *config.AccessProvider, root *config.Config) (sdkaccess.Provider, error) {
130
+ return &customProvider{}, nil
131
+ })
132
+ }
133
+ ```
134
+
135
+ A provider must implement `Identifier()` and `Authenticate()`. To expose it to configuration, call `RegisterProvider` inside `init`. Provider factories receive the specific `AccessProvider` block plus the full root configuration for contextual needs.
136
+
137
+ ## Error Semantics
138
+
139
+ - `ErrNoCredentials`: no credentials were present or recognized by any provider.
140
+ - `ErrInvalidCredential`: at least one provider processed the credentials but rejected them.
141
+ - `ErrNotHandled`: instructs the manager to fall through to the next provider without affecting aggregate error reporting.
142
+
143
+ Return custom errors to surface transport failures; they propagate immediately to the caller instead of being masked.
144
+
145
+ ## Integration with cliproxy Service
146
+
147
+ `sdk/cliproxy` wires `@sdk/access` automatically when you build a CLI service via `cliproxy.NewBuilder`. Supplying a preconfigured manager allows you to extend or override the default providers:
148
+
149
+ ```go
150
+ coreCfg, _ := config.LoadConfig("config.yaml")
151
+ providers, _ := sdkaccess.BuildProviders(coreCfg)
152
+ manager := sdkaccess.NewManager()
153
+ manager.SetProviders(providers)
154
+
155
+ svc, _ := cliproxy.NewBuilder().
156
+ WithConfig(coreCfg).
157
+ WithAccessManager(manager).
158
+ Build()
159
+ ```
160
+
161
+ The service reuses the manager for every inbound request, ensuring consistent authentication across embedded deployments and the canonical CLI binary.
162
+
163
+ ### Hot reloading providers
164
+
165
+ When configuration changes, rebuild providers and swap them into the manager:
166
+
167
+ ```go
168
+ providers, err := sdkaccess.BuildProviders(newCfg)
169
+ if err != nil {
170
+ log.Errorf("reload auth providers failed: %v", err)
171
+ return
172
+ }
173
+ accessManager.SetProviders(providers)
174
+ ```
175
+
176
+ This mirrors the behaviour in `cliproxy.Service.refreshAccessProviders` and `api.Server.applyAccessConfig`, enabling runtime updates without restarting the process.
docs/sdk-access_CN.md ADDED
@@ -0,0 +1,176 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # @sdk/access 开发指引
2
+
3
+ `github.com/router-for-me/CLIProxyAPI/v6/sdk/access` 包负责代理的入站访问认证。它提供一个轻量的管理器,用于按顺序链接多种凭证校验实现,让服务器在 CLI 运行时内外都能复用相同的访问控制逻辑。
4
+
5
+ ## 引用方式
6
+
7
+ ```go
8
+ import (
9
+ sdkaccess "github.com/router-for-me/CLIProxyAPI/v6/sdk/access"
10
+ "github.com/router-for-me/CLIProxyAPI/v6/internal/config"
11
+ )
12
+ ```
13
+
14
+ 通过 `go get github.com/router-for-me/CLIProxyAPI/v6/sdk/access` 添加依赖。
15
+
16
+ ## 管理器生命周期
17
+
18
+ ```go
19
+ manager := sdkaccess.NewManager()
20
+ providers, err := sdkaccess.BuildProviders(cfg)
21
+ if err != nil {
22
+ return err
23
+ }
24
+ manager.SetProviders(providers)
25
+ ```
26
+
27
+ - `NewManager` 创建空管理器。
28
+ - `SetProviders` 替换提供者切片并做防御性拷贝。
29
+ - `Providers` 返回适合并发读取的快照。
30
+ - `BuildProviders` 将 `config.Config` 中的访问配置转换成可运行的提供者。当配置没有显式声明但包含顶层 `api-keys` 时,会自动挂载内建的 `config-api-key` 提供者。
31
+
32
+ ## 认证请求
33
+
34
+ ```go
35
+ result, err := manager.Authenticate(ctx, req)
36
+ switch {
37
+ case err == nil:
38
+ // Authentication succeeded; result carries provider and principal.
39
+ case errors.Is(err, sdkaccess.ErrNoCredentials):
40
+ // No recognizable credentials were supplied.
41
+ case errors.Is(err, sdkaccess.ErrInvalidCredential):
42
+ // Credentials were present but rejected.
43
+ default:
44
+ // Provider surfaced a transport-level failure.
45
+ }
46
+ ```
47
+
48
+ `Manager.Authenticate` 按配置顺序遍历提供者。遇到成功立即返回,`ErrNotHandled` 会继续尝试下一个;若发现 `ErrNoCredentials` 或 `ErrInvalidCredential`,会在遍历结束后汇总给调用方。
49
+
50
+ 若管理器本身为 `nil` 或尚未注册提供者,调用会返回 `nil, nil`,让调用方无需针对错误做额外分支即可关闭访问控制。
51
+
52
+ `Result` 提供认证提供者标识、解析出的主体以及可选元数据(例如凭证来源)。
53
+
54
+ ## 配置结构
55
+
56
+ 在 `config.yaml` 的 `auth.providers` 下定义访问提供者:
57
+
58
+ ```yaml
59
+ auth:
60
+ providers:
61
+ - name: inline-api
62
+ type: config-api-key
63
+ api-keys:
64
+ - sk-test-123
65
+ - sk-prod-456
66
+ ```
67
+
68
+ 条目映射到 `config.AccessProvider`:`name` 指定实例名,`type` 选择注册的工厂,`sdk` 可引用第三方模块,`api-keys` 提供内联凭证,`config` 用于传递特定选项。
69
+
70
+ ### 引入外部 SDK 提供者
71
+
72
+ 若要消费其它 Go 模块输出的访问提供者,可在配置里填写 `sdk` 字段并在代码中引入该包,利用其 `init` 注册过程:
73
+
74
+ ```yaml
75
+ auth:
76
+ providers:
77
+ - name: partner-auth
78
+ type: partner-token
79
+ sdk: github.com/acme/xplatform/sdk/access/providers/partner
80
+ config:
81
+ region: us-west-2
82
+ audience: cli-proxy
83
+ ```
84
+
85
+ ```go
86
+ import (
87
+ _ "github.com/acme/xplatform/sdk/access/providers/partner" // registers partner-token
88
+ sdkaccess "github.com/router-for-me/CLIProxyAPI/v6/sdk/access"
89
+ )
90
+ ```
91
+
92
+ 通过空白标识符导入即可确保 `init` 调用,先于 `BuildProviders` 完成 `sdkaccess.RegisterProvider`。
93
+
94
+ ## 内建提供者
95
+
96
+ 当前 SDK 默认内置:
97
+
98
+ - `config-api-key`:校验配置中的 API Key。它从 `Authorization: Bearer`、`X-Goog-Api-Key`、`X-Api-Key` 以及查询参数 `?key=` 提取凭证,不匹配时抛出 `ErrInvalidCredential`。
99
+
100
+ 导入第三方包即可通过 `sdkaccess.RegisterProvider` 注册更多类型。
101
+
102
+ ### 元数据与审计
103
+
104
+ `Result.Metadata` 用于携带提供者特定的上下文信息。内建的 `config-api-key` 会记录凭证来源(`authorization`、`x-goog-api-key`、`x-api-key` 或 `query-key`)。自定义提供者同样可以填充该 Map,以便丰富日志与审计场景。
105
+
106
+ ## 编写自定义提供者
107
+
108
+ ```go
109
+ type customProvider struct{}
110
+
111
+ func (p *customProvider) Identifier() string { return "my-provider" }
112
+
113
+ func (p *customProvider) Authenticate(ctx context.Context, r *http.Request) (*sdkaccess.Result, error) {
114
+ token := r.Header.Get("X-Custom")
115
+ if token == "" {
116
+ return nil, sdkaccess.ErrNoCredentials
117
+ }
118
+ if token != "expected" {
119
+ return nil, sdkaccess.ErrInvalidCredential
120
+ }
121
+ return &sdkaccess.Result{
122
+ Provider: p.Identifier(),
123
+ Principal: "service-user",
124
+ Metadata: map[string]string{"source": "x-custom"},
125
+ }, nil
126
+ }
127
+
128
+ func init() {
129
+ sdkaccess.RegisterProvider("custom", func(cfg *config.AccessProvider, root *config.Config) (sdkaccess.Provider, error) {
130
+ return &customProvider{}, nil
131
+ })
132
+ }
133
+ ```
134
+
135
+ 自定义提供者需要实现 `Identifier()` 与 `Authenticate()`。在 `init` 中调用 `RegisterProvider` 暴露给配置层,工厂函数既能读取当前条目,也能访问完整根配置。
136
+
137
+ ## 错误语义
138
+
139
+ - `ErrNoCredentials`:任何提供者都未识别到凭证。
140
+ - `ErrInvalidCredential`:至少一个提供者处理了凭证但判定无效。
141
+ - `ErrNotHandled`:告诉管理器跳到下一个提供者,不影响最终错误统计。
142
+
143
+ 自定义错误(例如网络异常)会马上冒泡返回。
144
+
145
+ ## 与 cliproxy 集成
146
+
147
+ 使用 `sdk/cliproxy` 构建服务时会自动接入 `@sdk/access`。如果需要扩展内置行为,可传入自定义管理器:
148
+
149
+ ```go
150
+ coreCfg, _ := config.LoadConfig("config.yaml")
151
+ providers, _ := sdkaccess.BuildProviders(coreCfg)
152
+ manager := sdkaccess.NewManager()
153
+ manager.SetProviders(providers)
154
+
155
+ svc, _ := cliproxy.NewBuilder().
156
+ WithConfig(coreCfg).
157
+ WithAccessManager(manager).
158
+ Build()
159
+ ```
160
+
161
+ 服务会复用该管理器处理每一个入站请求,实现与 CLI 二进制一致的访问控制体验。
162
+
163
+ ### 动态热更新提供者
164
+
165
+ 当配置发生变化时,可以重新构建提供者并替换当前列表:
166
+
167
+ ```go
168
+ providers, err := sdkaccess.BuildProviders(newCfg)
169
+ if err != nil {
170
+ log.Errorf("reload auth providers failed: %v", err)
171
+ return
172
+ }
173
+ accessManager.SetProviders(providers)
174
+ ```
175
+
176
+ 这一流程与 `cliproxy.Service.refreshAccessProviders` 和 `api.Server.applyAccessConfig` 保持一致,避免为更新访问策略而重启进程。
docs/sdk-advanced.md ADDED
@@ -0,0 +1,138 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # SDK Advanced: Executors & Translators
2
+
3
+ This guide explains how to extend the embedded proxy with custom providers and schemas using the SDK. You will:
4
+ - Implement a provider executor that talks to your upstream API
5
+ - Register request/response translators for schema conversion
6
+ - Register models so they appear in `/v1/models`
7
+
8
+ The examples use Go 1.24+ and the v6 module path.
9
+
10
+ ## Concepts
11
+
12
+ - Provider executor: a runtime component implementing `auth.ProviderExecutor` that performs outbound calls for a given provider key (e.g., `gemini`, `claude`, `codex`). Executors can also implement `RequestPreparer` to inject credentials on raw HTTP requests.
13
+ - Translator registry: schema conversion functions routed by `sdk/translator`. The built‑in handlers translate between OpenAI/Gemini/Claude/Codex formats; you can register new ones.
14
+ - Model registry: publishes the list of available models per client/provider to power `/v1/models` and routing hints.
15
+
16
+ ## 1) Implement a Provider Executor
17
+
18
+ Create a type that satisfies `auth.ProviderExecutor`.
19
+
20
+ ```go
21
+ package myprov
22
+
23
+ import (
24
+ "context"
25
+ "net/http"
26
+
27
+ coreauth "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth"
28
+ clipexec "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/executor"
29
+ )
30
+
31
+ type Executor struct{}
32
+
33
+ func (Executor) Identifier() string { return "myprov" }
34
+
35
+ // Optional: mutate outbound HTTP requests with credentials
36
+ func (Executor) PrepareRequest(req *http.Request, a *coreauth.Auth) error {
37
+ // Example: req.Header.Set("Authorization", "Bearer "+a.APIKey)
38
+ return nil
39
+ }
40
+
41
+ func (Executor) Execute(ctx context.Context, a *coreauth.Auth, req clipexec.Request, opts clipexec.Options) (clipexec.Response, error) {
42
+ // Build HTTP request based on req.Payload (already translated into provider format)
43
+ // Use per‑auth transport if provided: transport := a.RoundTripper // via RoundTripperProvider
44
+ // Perform call and return provider JSON payload
45
+ return clipexec.Response{Payload: []byte(`{"ok":true}`)}, nil
46
+ }
47
+
48
+ func (Executor) ExecuteStream(ctx context.Context, a *coreauth.Auth, req clipexec.Request, opts clipexec.Options) (<-chan clipexec.StreamChunk, error) {
49
+ ch := make(chan clipexec.StreamChunk, 1)
50
+ go func() { defer close(ch); ch <- clipexec.StreamChunk{Payload: []byte("data: {\"done\":true}\n\n")} }()
51
+ return ch, nil
52
+ }
53
+
54
+ func (Executor) Refresh(ctx context.Context, a *coreauth.Auth) (*coreauth.Auth, error) {
55
+ // Optionally refresh tokens and return updated auth
56
+ return a, nil
57
+ }
58
+ ```
59
+
60
+ Register the executor with the core manager before starting the service:
61
+
62
+ ```go
63
+ core := coreauth.NewManager(coreauth.NewFileStore(cfg.AuthDir), nil, nil)
64
+ core.RegisterExecutor(myprov.Executor{})
65
+ svc, _ := cliproxy.NewBuilder().WithConfig(cfg).WithConfigPath(cfgPath).WithCoreAuthManager(core).Build()
66
+ ```
67
+
68
+ If your auth entries use provider `"myprov"`, the manager routes requests to your executor.
69
+
70
+ ## 2) Register Translators
71
+
72
+ The handlers accept OpenAI/Gemini/Claude/Codex inputs. To support a new provider format, register translation functions in `sdk/translator`’s default registry.
73
+
74
+ Direction matters:
75
+ - Request: register from inbound schema to provider schema
76
+ - Response: register from provider schema back to inbound schema
77
+
78
+ Example: Convert OpenAI Chat → MyProv Chat and back.
79
+
80
+ ```go
81
+ package myprov
82
+
83
+ import (
84
+ "context"
85
+ sdktr "github.com/router-for-me/CLIProxyAPI/v6/sdk/translator"
86
+ )
87
+
88
+ const (
89
+ FOpenAI = sdktr.Format("openai.chat")
90
+ FMyProv = sdktr.Format("myprov.chat")
91
+ )
92
+
93
+ func init() {
94
+ sdktr.Register(FOpenAI, FMyProv,
95
+ // Request transform (model, rawJSON, stream)
96
+ func(model string, raw []byte, stream bool) []byte { return convertOpenAIToMyProv(model, raw, stream) },
97
+ // Response transform (stream & non‑stream)
98
+ sdktr.ResponseTransform{
99
+ Stream: func(ctx context.Context, model string, originalReq, translatedReq, raw []byte, param *any) []string {
100
+ return convertStreamMyProvToOpenAI(model, originalReq, translatedReq, raw)
101
+ },
102
+ NonStream: func(ctx context.Context, model string, originalReq, translatedReq, raw []byte, param *any) string {
103
+ return convertMyProvToOpenAI(model, originalReq, translatedReq, raw)
104
+ },
105
+ },
106
+ )
107
+ }
108
+ ```
109
+
110
+ When the OpenAI handler receives a request that should route to `myprov`, the pipeline uses the registered transforms automatically.
111
+
112
+ ## 3) Register Models
113
+
114
+ Expose models under `/v1/models` by registering them in the global model registry using the auth ID (client ID) and provider name.
115
+
116
+ ```go
117
+ models := []*cliproxy.ModelInfo{
118
+ { ID: "myprov-pro-1", Object: "model", Type: "myprov", DisplayName: "MyProv Pro 1" },
119
+ }
120
+ cliproxy.GlobalModelRegistry().RegisterClient(authID, "myprov", models)
121
+ ```
122
+
123
+ The embedded server calls this automatically for built‑in providers; for custom providers, register during startup (e.g., after loading auths) or upon auth registration hooks.
124
+
125
+ ## Credentials & Transports
126
+
127
+ - Use `Manager.SetRoundTripperProvider` to inject per‑auth `*http.Transport` (e.g., proxy):
128
+ ```go
129
+ core.SetRoundTripperProvider(myProvider) // returns transport per auth
130
+ ```
131
+ - For raw HTTP flows, implement `PrepareRequest` and/or call `Manager.InjectCredentials(req, authID)` to set headers.
132
+
133
+ ## Testing Tips
134
+
135
+ - Enable request logging: Management API GET/PUT `/v0/management/request-log`
136
+ - Toggle debug logs: Management API GET/PUT `/v0/management/debug`
137
+ - Hot reload changes in `config.yaml` and `auths/` are picked up automatically by the watcher
138
+
docs/sdk-advanced_CN.md ADDED
@@ -0,0 +1,131 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # SDK 高级指南:执行器与翻译器
2
+
3
+ 本文介绍如何使用 SDK 扩展内嵌代理:
4
+ - 实现自定义 Provider 执行器以调用你的上游 API
5
+ - 注册请求/响应翻译器进行协议转换
6
+ - 注册模型以出现在 `/v1/models`
7
+
8
+ 示例基于 Go 1.24+ 与 v6 模块路径。
9
+
10
+ ## 概念
11
+
12
+ - Provider 执行器:实现 `auth.ProviderExecutor` 的运行时组件,负责某个 provider key(如 `gemini`、`claude`、`codex`)的真正出站调用。若实现 `RequestPreparer` 接口,可在原始 HTTP 请求上注入凭据。
13
+ - 翻译器注册表:由 `sdk/translator` 驱动的协议转换函数。内置了 OpenAI/Gemini/Claude/Codex 的互转;你也可以注册新的格式转换。
14
+ - 模型注册表:对外发布可用模型列表,供 `/v1/models` 与路由参考。
15
+
16
+ ## 1) 实现 Provider 执行器
17
+
18
+ 创建类型满足 `auth.ProviderExecutor` 接口。
19
+
20
+ ```go
21
+ package myprov
22
+
23
+ import (
24
+ "context"
25
+ "net/http"
26
+
27
+ coreauth "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth"
28
+ clipexec "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/executor"
29
+ )
30
+
31
+ type Executor struct{}
32
+
33
+ func (Executor) Identifier() string { return "myprov" }
34
+
35
+ // 可选:在原始 HTTP 请求上注入凭据
36
+ func (Executor) PrepareRequest(req *http.Request, a *coreauth.Auth) error {
37
+ // 例如:req.Header.Set("Authorization", "Bearer "+a.Attributes["api_key"])
38
+ return nil
39
+ }
40
+
41
+ func (Executor) Execute(ctx context.Context, a *coreauth.Auth, req clipexec.Request, opts clipexec.Options) (clipexec.Response, error) {
42
+ // 基于 req.Payload 构造上游请求,返回上游 JSON 负载
43
+ return clipexec.Response{Payload: []byte(`{"ok":true}`)}, nil
44
+ }
45
+
46
+ func (Executor) ExecuteStream(ctx context.Context, a *coreauth.Auth, req clipexec.Request, opts clipexec.Options) (<-chan clipexec.StreamChunk, error) {
47
+ ch := make(chan clipexec.StreamChunk, 1)
48
+ go func() { defer close(ch); ch <- clipexec.StreamChunk{Payload: []byte("data: {\\"done\\":true}\\n\\n")} }()
49
+ return ch, nil
50
+ }
51
+
52
+ func (Executor) Refresh(ctx context.Context, a *coreauth.Auth) (*coreauth.Auth, error) { return a, nil }
53
+ ```
54
+
55
+ 在启动服务前将执行器注册到核心管理器:
56
+
57
+ ```go
58
+ core := coreauth.NewManager(coreauth.NewFileStore(cfg.AuthDir), nil, nil)
59
+ core.RegisterExecutor(myprov.Executor{})
60
+ svc, _ := cliproxy.NewBuilder().WithConfig(cfg).WithConfigPath(cfgPath).WithCoreAuthManager(core).Build()
61
+ ```
62
+
63
+ 当凭据的 `Provider` 为 `"myprov"` 时,管理器会将请求路由到你的执行器。
64
+
65
+ ## 2) 注册翻译器
66
+
67
+ 内置处理器接受 OpenAI/Gemini/Claude/Codex 的入站格式。要支持新的 provider 协议,需要在 `sdk/translator` 的默认注册表中注册转换函数。
68
+
69
+ 方向很重要:
70
+ - 请求:从“入站格式”转换为“provider 格式”
71
+ - 响应:从“provider 格式”转换回“入站格式”
72
+
73
+ 示例:OpenAI Chat → MyProv Chat 及其反向。
74
+
75
+ ```go
76
+ package myprov
77
+
78
+ import (
79
+ "context"
80
+ sdktr "github.com/router-for-me/CLIProxyAPI/v6/sdk/translator"
81
+ )
82
+
83
+ const (
84
+ FOpenAI = sdktr.Format("openai.chat")
85
+ FMyProv = sdktr.Format("myprov.chat")
86
+ )
87
+
88
+ func init() {
89
+ sdktr.Register(FOpenAI, FMyProv,
90
+ func(model string, raw []byte, stream bool) []byte { return convertOpenAIToMyProv(model, raw, stream) },
91
+ sdktr.ResponseTransform{
92
+ Stream: func(ctx context.Context, model string, originalReq, translatedReq, raw []byte, param *any) []string {
93
+ return convertStreamMyProvToOpenAI(model, originalReq, translatedReq, raw)
94
+ },
95
+ NonStream: func(ctx context.Context, model string, originalReq, translatedReq, raw []byte, param *any) string {
96
+ return convertMyProvToOpenAI(model, originalReq, translatedReq, raw)
97
+ },
98
+ },
99
+ )
100
+ }
101
+ ```
102
+
103
+ 当 OpenAI 处理器接到需要路由到 `myprov` 的请求时,流水线会自动应用已注册的转换。
104
+
105
+ ## 3) 注册模型
106
+
107
+ 通过全局模型注册表将模型暴露到 `/v1/models`:
108
+
109
+ ```go
110
+ models := []*cliproxy.ModelInfo{
111
+ { ID: "myprov-pro-1", Object: "model", Type: "myprov", DisplayName: "MyProv Pro 1" },
112
+ }
113
+ cliproxy.GlobalModelRegistry().RegisterClient(authID, "myprov", models)
114
+ ```
115
+
116
+ 内置 Provider 会自动注册;自定义 Provider 建议在启动时(例如加载到 Auth 后)或在 Auth 注册钩子中调用。
117
+
118
+ ## 凭据与传输
119
+
120
+ - 使用 `Manager.SetRoundTripperProvider` 注入按账户的 `*http.Transport`(例如代理):
121
+ ```go
122
+ core.SetRoundTripperProvider(myProvider) // 按账户返回 transport
123
+ ```
124
+ - 对于原始 HTTP 请求,若实现了 `PrepareRequest`,或通过 `Manager.InjectCredentials(req, authID)` 进行头部注入。
125
+
126
+ ## 测试建议
127
+
128
+ - 启用请求日志:管理 API GET/PUT `/v0/management/request-log`
129
+ - 切换调试日志:管理 API GET/PUT `/v0/management/debug`
130
+ - 热更新:`config.yaml` 与 `auths/` 变化会自动被侦测并应用
131
+
docs/sdk-usage.md ADDED
@@ -0,0 +1,163 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # CLI Proxy SDK Guide
2
+
3
+ The `sdk/cliproxy` module exposes the proxy as a reusable Go library so external programs can embed the routing, authentication, hot‑reload, and translation layers without depending on the CLI binary.
4
+
5
+ ## Install & Import
6
+
7
+ ```bash
8
+ go get github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy
9
+ ```
10
+
11
+ ```go
12
+ import (
13
+ "context"
14
+ "errors"
15
+ "time"
16
+
17
+ "github.com/router-for-me/CLIProxyAPI/v6/internal/config"
18
+ "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy"
19
+ )
20
+ ```
21
+
22
+ Note the `/v6` module path.
23
+
24
+ ## Minimal Embed
25
+
26
+ ```go
27
+ cfg, err := config.LoadConfig("config.yaml")
28
+ if err != nil { panic(err) }
29
+
30
+ svc, err := cliproxy.NewBuilder().
31
+ WithConfig(cfg).
32
+ WithConfigPath("config.yaml"). // absolute or working-dir relative
33
+ Build()
34
+ if err != nil { panic(err) }
35
+
36
+ ctx, cancel := context.WithCancel(context.Background())
37
+ defer cancel()
38
+
39
+ if err := svc.Run(ctx); err != nil && !errors.Is(err, context.Canceled) {
40
+ panic(err)
41
+ }
42
+ ```
43
+
44
+ The service manages config/auth watching, background token refresh, and graceful shutdown. Cancel the context to stop it.
45
+
46
+ ## Server Options (middleware, routes, logs)
47
+
48
+ The server accepts options via `WithServerOptions`:
49
+
50
+ ```go
51
+ svc, _ := cliproxy.NewBuilder().
52
+ WithConfig(cfg).
53
+ WithConfigPath("config.yaml").
54
+ WithServerOptions(
55
+ // Add global middleware
56
+ cliproxy.WithMiddleware(func(c *gin.Context) { c.Header("X-Embed", "1"); c.Next() }),
57
+ // Tweak gin engine early (CORS, trusted proxies, etc.)
58
+ cliproxy.WithEngineConfigurator(func(e *gin.Engine) { e.ForwardedByClientIP = true }),
59
+ // Add your own routes after defaults
60
+ cliproxy.WithRouterConfigurator(func(e *gin.Engine, _ *handlers.BaseAPIHandler, _ *config.Config) {
61
+ e.GET("/healthz", func(c *gin.Context) { c.String(200, "ok") })
62
+ }),
63
+ // Override request log writer/dir
64
+ cliproxy.WithRequestLoggerFactory(func(cfg *config.Config, cfgPath string) logging.RequestLogger {
65
+ return logging.NewFileRequestLogger(true, "logs", filepath.Dir(cfgPath))
66
+ }),
67
+ ).
68
+ Build()
69
+ ```
70
+
71
+ These options mirror the internals used by the CLI server.
72
+
73
+ ## Management API (when embedded)
74
+
75
+ - Management endpoints are mounted only when `remote-management.secret-key` is set in `config.yaml`.
76
+ - Remote access additionally requires `remote-management.allow-remote: true`.
77
+ - See MANAGEMENT_API.md for endpoints. Your embedded server exposes them under `/v0/management` on the configured port.
78
+
79
+ ## Using the Core Auth Manager
80
+
81
+ The service uses a core `auth.Manager` for selection, execution, and auto‑refresh. When embedding, you can provide your own manager to customize transports or hooks:
82
+
83
+ ```go
84
+ core := coreauth.NewManager(coreauth.NewFileStore(cfg.AuthDir), nil, nil)
85
+ core.SetRoundTripperProvider(myRTProvider) // per‑auth *http.Transport
86
+
87
+ svc, _ := cliproxy.NewBuilder().
88
+ WithConfig(cfg).
89
+ WithConfigPath("config.yaml").
90
+ WithCoreAuthManager(core).
91
+ Build()
92
+ ```
93
+
94
+ Implement a custom per‑auth transport:
95
+
96
+ ```go
97
+ type myRTProvider struct{}
98
+ func (myRTProvider) RoundTripperFor(a *coreauth.Auth) http.RoundTripper {
99
+ if a == nil || a.ProxyURL == "" { return nil }
100
+ u, _ := url.Parse(a.ProxyURL)
101
+ return &http.Transport{ Proxy: http.ProxyURL(u) }
102
+ }
103
+ ```
104
+
105
+ Programmatic execution is available on the manager:
106
+
107
+ ```go
108
+ // Non‑streaming
109
+ resp, err := core.Execute(ctx, []string{"gemini"}, req, opts)
110
+
111
+ // Streaming
112
+ chunks, err := core.ExecuteStream(ctx, []string{"gemini"}, req, opts)
113
+ for ch := range chunks { /* ... */ }
114
+ ```
115
+
116
+ Note: Built‑in provider executors are wired automatically when you run the `Service`. If you want to use `Manager` stand‑alone without the HTTP server, you must register your own executors that implement `auth.ProviderExecutor`.
117
+
118
+ ## Custom Client Sources
119
+
120
+ Replace the default loaders if your creds live outside the local filesystem:
121
+
122
+ ```go
123
+ type memoryTokenProvider struct{}
124
+ func (p *memoryTokenProvider) Load(ctx context.Context, cfg *config.Config) (*cliproxy.TokenClientResult, error) {
125
+ // Populate from memory/remote store and return counts
126
+ return &cliproxy.TokenClientResult{}, nil
127
+ }
128
+
129
+ svc, _ := cliproxy.NewBuilder().
130
+ WithConfig(cfg).
131
+ WithConfigPath("config.yaml").
132
+ WithTokenClientProvider(&memoryTokenProvider{}).
133
+ WithAPIKeyClientProvider(cliproxy.NewAPIKeyClientProvider()).
134
+ Build()
135
+ ```
136
+
137
+ ## Hooks
138
+
139
+ Observe lifecycle without patching internals:
140
+
141
+ ```go
142
+ hooks := cliproxy.Hooks{
143
+ OnBeforeStart: func(cfg *config.Config) { log.Infof("starting on :%d", cfg.Port) },
144
+ OnAfterStart: func(s *cliproxy.Service) { log.Info("ready") },
145
+ }
146
+ svc, _ := cliproxy.NewBuilder().WithConfig(cfg).WithConfigPath("config.yaml").WithHooks(hooks).Build()
147
+ ```
148
+
149
+ ## Shutdown
150
+
151
+ `Run` defers `Shutdown`, so cancelling the parent context is enough. To stop manually:
152
+
153
+ ```go
154
+ ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
155
+ defer cancel()
156
+ _ = svc.Shutdown(ctx)
157
+ ```
158
+
159
+ ## Notes
160
+
161
+ - Hot reload: changes to `config.yaml` and `auths/` are picked up automatically.
162
+ - Request logging can be toggled at runtime via the Management API.
163
+ - Gemini Web features (`gemini-web.*`) are honored in the embedded server.
docs/sdk-usage_CN.md ADDED
@@ -0,0 +1,164 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # CLI Proxy SDK 使用指南
2
+
3
+ `sdk/cliproxy` 模块将代理能力以 Go 库的形式对外暴露,方便在其它服务中内嵌路由、鉴权、热更新与翻译层,而无需依赖可执行的 CLI 程序。
4
+
5
+ ## 安装与导入
6
+
7
+ ```bash
8
+ go get github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy
9
+ ```
10
+
11
+ ```go
12
+ import (
13
+ "context"
14
+ "errors"
15
+ "time"
16
+
17
+ "github.com/router-for-me/CLIProxyAPI/v6/internal/config"
18
+ "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy"
19
+ )
20
+ ```
21
+
22
+ 注意模块路径包含 `/v6`。
23
+
24
+ ## 最小可用示例
25
+
26
+ ```go
27
+ cfg, err := config.LoadConfig("config.yaml")
28
+ if err != nil { panic(err) }
29
+
30
+ svc, err := cliproxy.NewBuilder().
31
+ WithConfig(cfg).
32
+ WithConfigPath("config.yaml"). // 绝对路径或工作目录相对路径
33
+ Build()
34
+ if err != nil { panic(err) }
35
+
36
+ ctx, cancel := context.WithCancel(context.Background())
37
+ defer cancel()
38
+
39
+ if err := svc.Run(ctx); err != nil && !errors.Is(err, context.Canceled) {
40
+ panic(err)
41
+ }
42
+ ```
43
+
44
+ 服务内部会管理配置与认证文件的监听、后台令牌刷新与优雅关闭。取消上下文即可停止服务。
45
+
46
+ ## 服务器可选项(中间件、路由、日志)
47
+
48
+ 通过 `WithServerOptions` 自定义:
49
+
50
+ ```go
51
+ svc, _ := cliproxy.NewBuilder().
52
+ WithConfig(cfg).
53
+ WithConfigPath("config.yaml").
54
+ WithServerOptions(
55
+ // 追加全局中间件
56
+ cliproxy.WithMiddleware(func(c *gin.Context) { c.Header("X-Embed", "1"); c.Next() }),
57
+ // 提前调整 gin 引擎(如 CORS、trusted proxies)
58
+ cliproxy.WithEngineConfigurator(func(e *gin.Engine) { e.ForwardedByClientIP = true }),
59
+ // 在默认路由之后追加自定义路由
60
+ cliproxy.WithRouterConfigurator(func(e *gin.Engine, _ *handlers.BaseAPIHandler, _ *config.Config) {
61
+ e.GET("/healthz", func(c *gin.Context) { c.String(200, "ok") })
62
+ }),
63
+ // 覆盖请求日志的创建(启用/目录)
64
+ cliproxy.WithRequestLoggerFactory(func(cfg *config.Config, cfgPath string) logging.RequestLogger {
65
+ return logging.NewFileRequestLogger(true, "logs", filepath.Dir(cfgPath))
66
+ }),
67
+ ).
68
+ Build()
69
+ ```
70
+
71
+ 这些选项与 CLI 服务器内部用法保持一致。
72
+
73
+ ## 管理 API(内嵌时)
74
+
75
+ - 仅当 `config.yaml` 中设置了 `remote-management.secret-key` 时才会挂载管理端点。
76
+ - 远程访问还需要 `remote-management.allow-remote: true`。
77
+ - 具体端点见 MANAGEMENT_API_CN.md。内嵌服务器会在配置端口下暴露 `/v0/management`。
78
+
79
+ ## 使用核心鉴权管理器
80
+
81
+ 服务内部使用核心 `auth.Manager` 负责选择、执行、自动刷新。内嵌时可自定义其传输或钩子:
82
+
83
+ ```go
84
+ core := coreauth.NewManager(coreauth.NewFileStore(cfg.AuthDir), nil, nil)
85
+ core.SetRoundTripperProvider(myRTProvider) // 按账户返回 *http.Transport
86
+
87
+ svc, _ := cliproxy.NewBuilder().
88
+ WithConfig(cfg).
89
+ WithConfigPath("config.yaml").
90
+ WithCoreAuthManager(core).
91
+ Build()
92
+ ```
93
+
94
+ 实现每个账户的自定义传输:
95
+
96
+ ```go
97
+ type myRTProvider struct{}
98
+ func (myRTProvider) RoundTripperFor(a *coreauth.Auth) http.RoundTripper {
99
+ if a == nil || a.ProxyURL == "" { return nil }
100
+ u, _ := url.Parse(a.ProxyURL)
101
+ return &http.Transport{ Proxy: http.ProxyURL(u) }
102
+ }
103
+ ```
104
+
105
+ 管理器提供编程式执行接口:
106
+
107
+ ```go
108
+ // 非流式
109
+ resp, err := core.Execute(ctx, []string{"gemini"}, req, opts)
110
+
111
+ // 流式
112
+ chunks, err := core.ExecuteStream(ctx, []string{"gemini"}, req, opts)
113
+ for ch := range chunks { /* ... */ }
114
+ ```
115
+
116
+ 说明:运行 `Service` 时会自动注册内置的提供商执行器;若仅单独使用 `Manager` 而不启动 HTTP 服务器,则需要自行实现并注册满足 `auth.ProviderExecutor` 的执行器。
117
+
118
+ ## 自定义凭据来源
119
+
120
+ 当凭据不在本地文件系统时,替换默认加载器:
121
+
122
+ ```go
123
+ type memoryTokenProvider struct{}
124
+ func (p *memoryTokenProvider) Load(ctx context.Context, cfg *config.Config) (*cliproxy.TokenClientResult, error) {
125
+ // 从内存/远端加载并返回数量统计
126
+ return &cliproxy.TokenClientResult{}, nil
127
+ }
128
+
129
+ svc, _ := cliproxy.NewBuilder().
130
+ WithConfig(cfg).
131
+ WithConfigPath("config.yaml").
132
+ WithTokenClientProvider(&memoryTokenProvider{}).
133
+ WithAPIKeyClientProvider(cliproxy.NewAPIKeyClientProvider()).
134
+ Build()
135
+ ```
136
+
137
+ ## 启动钩子
138
+
139
+ 无需修改内部代码即可观察生命周期:
140
+
141
+ ```go
142
+ hooks := cliproxy.Hooks{
143
+ OnBeforeStart: func(cfg *config.Config) { log.Infof("starting on :%d", cfg.Port) },
144
+ OnAfterStart: func(s *cliproxy.Service) { log.Info("ready") },
145
+ }
146
+ svc, _ := cliproxy.NewBuilder().WithConfig(cfg).WithConfigPath("config.yaml").WithHooks(hooks).Build()
147
+ ```
148
+
149
+ ## 关闭
150
+
151
+ `Run` 内部会延迟调用 `Shutdown`,因此只需取消父上下文即可。若需手动停止:
152
+
153
+ ```go
154
+ ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
155
+ defer cancel()
156
+ _ = svc.Shutdown(ctx)
157
+ ```
158
+
159
+ ## 说明
160
+
161
+ - 热更新:`config.yaml` 与 `auths/` 变化会被自动侦测并应用。
162
+ - 请求日志可通过管理 API 在运行时开关。
163
+ - `gemini-web.*` 相关配置在内嵌服务器中会被遵循。
164
+
docs/sdk-watcher.md ADDED
@@ -0,0 +1,32 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # SDK Watcher Integration
2
+
3
+ The SDK service exposes a watcher integration that surfaces granular auth updates without forcing a full reload. This document explains the queue contract, how the service consumes updates, and how high-frequency change bursts are handled.
4
+
5
+ ## Update Queue Contract
6
+
7
+ - `watcher.AuthUpdate` represents a single credential change. `Action` may be `add`, `modify`, or `delete`, and `ID` carries the credential identifier. For `add`/`modify` the `Auth` payload contains a fully populated clone of the credential; `delete` may omit `Auth`.
8
+ - `WatcherWrapper.SetAuthUpdateQueue(chan<- watcher.AuthUpdate)` wires the queue produced by the SDK service into the watcher. The queue must be created before the watcher starts.
9
+ - The service builds the queue via `ensureAuthUpdateQueue`, using a buffered channel (`capacity=256`) and a dedicated consumer goroutine (`consumeAuthUpdates`). The consumer drains bursts by looping through the backlog before reacquiring the select loop.
10
+
11
+ ## Watcher Behaviour
12
+
13
+ - `internal/watcher/watcher.go` keeps a shadow snapshot of auth state (`currentAuths`). Each filesystem or configuration event triggers a recomputation and a diff against the previous snapshot to produce minimal `AuthUpdate` entries that mirror adds, edits, and removals.
14
+ - Updates are coalesced per credential identifier. If multiple changes occur before dispatch (e.g., write followed by delete), only the final action is sent downstream.
15
+ - The watcher runs an internal dispatch loop that buffers pending updates in memory and forwards them asynchronously to the queue. Producers never block on channel capacity; they just enqueue into the in-memory buffer and signal the dispatcher. Dispatch cancellation happens when the watcher stops, guaranteeing goroutines exit cleanly.
16
+
17
+ ## High-Frequency Change Handling
18
+
19
+ - The dispatch loop and service consumer run independently, preventing filesystem watchers from blocking even when many updates arrive at once.
20
+ - Back-pressure is absorbed in two places:
21
+ - The dispatch buffer (map + order slice) coalesces repeated updates for the same credential until the consumer catches up.
22
+ - The service channel capacity (256) combined with the consumer drain loop ensures several bursts can be processed without oscillation.
23
+ - If the queue is saturated for an extended period, updates continue to be merged, so the latest state is eventually applied without replaying redundant intermediate states.
24
+
25
+ ## Usage Checklist
26
+
27
+ 1. Instantiate the SDK service (builder or manual construction).
28
+ 2. Call `ensureAuthUpdateQueue` before starting the watcher to allocate the shared channel.
29
+ 3. When the `WatcherWrapper` is created, call `SetAuthUpdateQueue` with the service queue, then start the watcher.
30
+ 4. Provide a reload callback that handles configuration updates; auth deltas will arrive via the queue and are applied by the service automatically through `handleAuthUpdate`.
31
+
32
+ Following this flow keeps auth changes responsive while avoiding full reloads for every edit.
docs/sdk-watcher_CN.md ADDED
@@ -0,0 +1,32 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # SDK Watcher集成说明
2
+
3
+ 本文档介绍SDK服务与文件监控器之间的增量更新队列,包括接口契约、高频变更下的处理策略以及接入步骤。
4
+
5
+ ## 更新队列契约
6
+
7
+ - `watcher.AuthUpdate`描述单条凭据变更,`Action`可能为`add`、`modify`或`delete`,`ID`是凭据标识。对于`add`/`modify`会携带完整的`Auth`克隆,`delete`可以省略`Auth`。
8
+ - `WatcherWrapper.SetAuthUpdateQueue(chan<- watcher.AuthUpdate)`用于将服务侧创建的队列注入watcher,必须在watcher启动前完成。
9
+ - 服务通过`ensureAuthUpdateQueue`创建容量为256的缓冲通道,并在`consumeAuthUpdates`中使用专职goroutine消费;消费侧会主动“抽干”积压事件,降低切换开销。
10
+
11
+ ## Watcher行为
12
+
13
+ - `internal/watcher/watcher.go`维护`currentAuths`快照,文件或配置事件触发后会重建快照并与旧快照对比,生成最小化的`AuthUpdate`列表。
14
+ - 以凭据ID为维度对更新进行合并,同一凭据在短时间内的多次变更只会保留最新状态(例如先写后删只会下发`delete`)。
15
+ - watcher内部运行异步分发循环:生产者只向内存缓冲追加事件并唤醒分发协程,即使通道暂时写满也不会阻塞文件事件线程。watcher停止时会取消分发循环,确保协程正常退出。
16
+
17
+ ## 高频变更处理
18
+
19
+ - 分发循环与服务消费协程相互独立,因此即便短时间内出现大量变更也不会阻塞watcher事件处理。
20
+ - 背压通过两级缓冲吸收:
21
+ - 分发缓冲(map + 顺序切片)会合并同一凭据的重复事件,直到消费者完成处理。
22
+ - 服务端通道的256容量加上消费侧的“抽干”逻辑,可平稳处理多个突发批次。
23
+ - 当通道长时间处于高压状态时,缓冲仍持续合并事件,从而在消费者恢复后一次性应用最新状态,避免重复处理无意义的中间状态。
24
+
25
+ ## 接入步骤
26
+
27
+ 1. 实例化SDK Service(构建器或手工创建)。
28
+ 2. 在启动watcher之前调用`ensureAuthUpdateQueue`创建共享通道。
29
+ 3. watcher通过工厂函数创建后立刻调用`SetAuthUpdateQueue`注入通道,然后再启动watcher。
30
+ 4. Reload回调专注于配置更新;认证增量会通过队列送达,并由`handleAuthUpdate`自动应用。
31
+
32
+ 遵循上述流程即可在避免全量重载的同时保持凭据变更的实时性。
internal/api/handlers/management/handler.go CHANGED
@@ -3,6 +3,7 @@
3
  package management
4
 
5
  import (
 
6
  "fmt"
7
  "net/http"
8
  "strings"
@@ -33,6 +34,8 @@ type Handler struct {
33
  authManager *coreauth.Manager
34
  usageStats *usage.RequestStatistics
35
  tokenStore sdkAuth.TokenStore
 
 
36
  }
37
 
38
  // NewHandler creates a new management handler instance.
@@ -56,6 +59,9 @@ func (h *Handler) SetAuthManager(manager *coreauth.Manager) { h.authManager = ma
56
  // SetUsageStatistics allows replacing the usage statistics reference.
57
  func (h *Handler) SetUsageStatistics(stats *usage.RequestStatistics) { h.usageStats = stats }
58
 
 
 
 
59
  // Middleware enforces access control for management endpoints.
60
  // All requests (local and remote) require a valid management key.
61
  // Additionally, remote access requires allow-remote-management=true.
@@ -65,10 +71,10 @@ func (h *Handler) Middleware() gin.HandlerFunc {
65
 
66
  return func(c *gin.Context) {
67
  clientIP := c.ClientIP()
 
68
 
69
- // For remote IPs, enforce allow-remote-management and ban checks
70
- if !(clientIP == "127.0.0.1" || clientIP == "::1") {
71
- // Check if IP is currently blocked
72
  h.attemptsMu.Lock()
73
  ai := h.failedAttempts[clientIP]
74
  if ai != nil {
@@ -86,11 +92,25 @@ func (h *Handler) Middleware() gin.HandlerFunc {
86
  }
87
  h.attemptsMu.Unlock()
88
 
89
- allowRemote := h.cfg.RemoteManagement.AllowRemote
90
- if !allowRemote {
91
  c.AbortWithStatusJSON(http.StatusForbidden, gin.H{"error": "remote management disabled"})
92
  return
93
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
94
  }
95
  secret := h.cfg.RemoteManagement.SecretKey
96
  if secret == "" {
@@ -112,36 +132,32 @@ func (h *Handler) Middleware() gin.HandlerFunc {
112
  provided = c.GetHeader("X-Management-Key")
113
  }
114
 
115
- if !(clientIP == "127.0.0.1" || clientIP == "::1") {
116
- // For remote IPs, enforce key and track failures
117
- fail := func() {
118
- h.attemptsMu.Lock()
119
- ai := h.failedAttempts[clientIP]
120
- if ai == nil {
121
- ai = &attemptInfo{}
122
- h.failedAttempts[clientIP] = ai
123
- }
124
- ai.count++
125
- if ai.count >= maxFailures {
126
- ai.blockedUntil = time.Now().Add(banDuration)
127
- ai.count = 0
128
- }
129
- h.attemptsMu.Unlock()
130
  }
 
 
 
131
 
132
- if provided == "" {
133
- fail()
134
- c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "missing management key"})
135
- return
 
 
136
  }
 
137
 
138
- if err := bcrypt.CompareHashAndPassword([]byte(secret), []byte(provided)); err != nil {
 
139
  fail()
140
- c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "invalid management key"})
141
- return
142
  }
 
 
 
143
 
144
- // Success: reset failed count for this IP
145
  h.attemptsMu.Lock()
146
  if ai := h.failedAttempts[clientIP]; ai != nil {
147
  ai.count = 0
 
3
  package management
4
 
5
  import (
6
+ "crypto/subtle"
7
  "fmt"
8
  "net/http"
9
  "strings"
 
34
  authManager *coreauth.Manager
35
  usageStats *usage.RequestStatistics
36
  tokenStore sdkAuth.TokenStore
37
+
38
+ localPassword string
39
  }
40
 
41
  // NewHandler creates a new management handler instance.
 
59
  // SetUsageStatistics allows replacing the usage statistics reference.
60
  func (h *Handler) SetUsageStatistics(stats *usage.RequestStatistics) { h.usageStats = stats }
61
 
62
+ // SetLocalPassword configures the runtime-local password accepted for localhost requests.
63
+ func (h *Handler) SetLocalPassword(password string) { h.localPassword = password }
64
+
65
  // Middleware enforces access control for management endpoints.
66
  // All requests (local and remote) require a valid management key.
67
  // Additionally, remote access requires allow-remote-management=true.
 
71
 
72
  return func(c *gin.Context) {
73
  clientIP := c.ClientIP()
74
+ localClient := clientIP == "127.0.0.1" || clientIP == "::1"
75
 
76
+ fail := func() {}
77
+ if !localClient {
 
78
  h.attemptsMu.Lock()
79
  ai := h.failedAttempts[clientIP]
80
  if ai != nil {
 
92
  }
93
  h.attemptsMu.Unlock()
94
 
95
+ if !h.cfg.RemoteManagement.AllowRemote {
 
96
  c.AbortWithStatusJSON(http.StatusForbidden, gin.H{"error": "remote management disabled"})
97
  return
98
  }
99
+
100
+ fail = func() {
101
+ h.attemptsMu.Lock()
102
+ aip := h.failedAttempts[clientIP]
103
+ if aip == nil {
104
+ aip = &attemptInfo{}
105
+ h.failedAttempts[clientIP] = aip
106
+ }
107
+ aip.count++
108
+ if aip.count >= maxFailures {
109
+ aip.blockedUntil = time.Now().Add(banDuration)
110
+ aip.count = 0
111
+ }
112
+ h.attemptsMu.Unlock()
113
+ }
114
  }
115
  secret := h.cfg.RemoteManagement.SecretKey
116
  if secret == "" {
 
132
  provided = c.GetHeader("X-Management-Key")
133
  }
134
 
135
+ if provided == "" {
136
+ if !localClient {
137
+ fail()
 
 
 
 
 
 
 
 
 
 
 
 
138
  }
139
+ c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "missing management key"})
140
+ return
141
+ }
142
 
143
+ if localClient {
144
+ if lp := h.localPassword; lp != "" {
145
+ if subtle.ConstantTimeCompare([]byte(provided), []byte(lp)) == 1 {
146
+ c.Next()
147
+ return
148
+ }
149
  }
150
+ }
151
 
152
+ if err := bcrypt.CompareHashAndPassword([]byte(secret), []byte(provided)); err != nil {
153
+ if !localClient {
154
  fail()
 
 
155
  }
156
+ c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "invalid management key"})
157
+ return
158
+ }
159
 
160
+ if !localClient {
161
  h.attemptsMu.Lock()
162
  if ai := h.failedAttempts[clientIP]; ai != nil {
163
  ai.count = 0
internal/api/server.go CHANGED
@@ -33,6 +33,7 @@ type serverOptionConfig struct {
33
  engineConfigurator func(*gin.Engine)
34
  routerConfigurator func(*gin.Engine, *handlers.BaseAPIHandler, *config.Config)
35
  requestLoggerFactory func(*config.Config, string) logging.RequestLogger
 
36
  }
37
 
38
  // ServerOption customises HTTP server construction.
@@ -63,6 +64,13 @@ func WithRouterConfigurator(fn func(*gin.Engine, *handlers.BaseAPIHandler, *conf
63
  }
64
  }
65
 
 
 
 
 
 
 
 
66
  // WithRequestLoggerFactory customises request logger creation.
67
  func WithRequestLoggerFactory(factory func(*config.Config, string) logging.RequestLogger) ServerOption {
68
  return func(cfg *serverOptionConfig) {
@@ -163,6 +171,9 @@ func NewServer(cfg *config.Config, authManager *auth.Manager, accessManager *sdk
163
  s.applyAccessConfig(cfg)
164
  // Initialize management handler
165
  s.mgmt = managementHandlers.NewHandler(cfg, configFilePath, authManager)
 
 
 
166
 
167
  // Setup routes
168
  s.setupRoutes()
 
33
  engineConfigurator func(*gin.Engine)
34
  routerConfigurator func(*gin.Engine, *handlers.BaseAPIHandler, *config.Config)
35
  requestLoggerFactory func(*config.Config, string) logging.RequestLogger
36
+ localPassword string
37
  }
38
 
39
  // ServerOption customises HTTP server construction.
 
64
  }
65
  }
66
 
67
+ // WithLocalManagementPassword stores a runtime-only management password accepted for localhost requests.
68
+ func WithLocalManagementPassword(password string) ServerOption {
69
+ return func(cfg *serverOptionConfig) {
70
+ cfg.localPassword = password
71
+ }
72
+ }
73
+
74
  // WithRequestLoggerFactory customises request logger creation.
75
  func WithRequestLoggerFactory(factory func(*config.Config, string) logging.RequestLogger) ServerOption {
76
  return func(cfg *serverOptionConfig) {
 
171
  s.applyAccessConfig(cfg)
172
  // Initialize management handler
173
  s.mgmt = managementHandlers.NewHandler(cfg, configFilePath, authManager)
174
+ if optionState.localPassword != "" {
175
+ s.mgmt.SetLocalPassword(optionState.localPassword)
176
+ }
177
 
178
  // Setup routes
179
  s.setupRoutes()
internal/cmd/run.go CHANGED
@@ -21,10 +21,12 @@ import (
21
  // Parameters:
22
  // - cfg: The application configuration
23
  // - configPath: The path to the configuration file
24
- func StartService(cfg *config.Config, configPath string) {
 
25
  service, err := cliproxy.NewBuilder().
26
  WithConfig(cfg).
27
  WithConfigPath(configPath).
 
28
  Build()
29
  if err != nil {
30
  log.Fatalf("failed to build proxy service: %v", err)
 
21
  // Parameters:
22
  // - cfg: The application configuration
23
  // - configPath: The path to the configuration file
24
+ // - localPassword: Optional password accepted for local management requests
25
+ func StartService(cfg *config.Config, configPath string, localPassword string) {
26
  service, err := cliproxy.NewBuilder().
27
  WithConfig(cfg).
28
  WithConfigPath(configPath).
29
+ WithLocalManagementPassword(localPassword).
30
  Build()
31
  if err != nil {
32
  log.Fatalf("failed to build proxy service: %v", err)
sdk/cliproxy/builder.go CHANGED
@@ -142,6 +142,15 @@ func (b *Builder) WithServerOptions(opts ...api.ServerOption) *Builder {
142
  return b
143
  }
144
 
 
 
 
 
 
 
 
 
 
145
  // Build validates inputs, applies defaults, and returns a ready-to-run service.
146
  func (b *Builder) Build() (*Service, error) {
147
  if b.cfg == nil {
 
142
  return b
143
  }
144
 
145
+ // WithLocalManagementPassword configures a password that is only accepted from localhost management requests.
146
+ func (b *Builder) WithLocalManagementPassword(password string) *Builder {
147
+ if password == "" {
148
+ return b
149
+ }
150
+ b.serverOptions = append(b.serverOptions, api.WithLocalManagementPassword(password))
151
+ return b
152
+ }
153
+
154
  // Build validates inputs, applies defaults, and returns a ready-to-run service.
155
  func (b *Builder) Build() (*Service, error) {
156
  if b.cfg == nil {