flexgate 0.11.0__tar.gz → 0.11.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. {flexgate-0.11.0 → flexgate-0.11.2}/CLAUDE.md +3 -3
  2. {flexgate-0.11.0 → flexgate-0.11.2}/PKG-INFO +6 -17
  3. {flexgate-0.11.0 → flexgate-0.11.2}/README.md +5 -16
  4. {flexgate-0.11.0 → flexgate-0.11.2}/config.yaml.template +4 -20
  5. {flexgate-0.11.0 → flexgate-0.11.2}/docs/cli.md +5 -1
  6. {flexgate-0.11.0 → flexgate-0.11.2}/docs/configuration.md +4 -15
  7. {flexgate-0.11.0 → flexgate-0.11.2}/docs/development.md +1 -1
  8. {flexgate-0.11.0 → flexgate-0.11.2}/docs/usage.md +8 -4
  9. {flexgate-0.11.0 → flexgate-0.11.2}/flexgate/__init__.py +1 -1
  10. {flexgate-0.11.0 → flexgate-0.11.2}/flexgate/cli.py +6 -1
  11. {flexgate-0.11.0 → flexgate-0.11.2}/flexgate/config.py +6 -22
  12. {flexgate-0.11.0 → flexgate-0.11.2}/flexgate/server.py +60 -5
  13. {flexgate-0.11.0 → flexgate-0.11.2}/flexgate/usage.py +15 -5
  14. {flexgate-0.11.0 → flexgate-0.11.2}/.github/workflows/docs.yml +0 -0
  15. {flexgate-0.11.0 → flexgate-0.11.2}/.github/workflows/release.yml +0 -0
  16. {flexgate-0.11.0 → flexgate-0.11.2}/.gitignore +0 -0
  17. {flexgate-0.11.0 → flexgate-0.11.2}/docs/Makefile +0 -0
  18. {flexgate-0.11.0 → flexgate-0.11.2}/docs/_static/.gitkeep +0 -0
  19. {flexgate-0.11.0 → flexgate-0.11.2}/docs/api.md +0 -0
  20. {flexgate-0.11.0 → flexgate-0.11.2}/docs/conf.py +0 -0
  21. {flexgate-0.11.0 → flexgate-0.11.2}/docs/index.md +0 -0
  22. {flexgate-0.11.0 → flexgate-0.11.2}/docs/quickstart.md +0 -0
  23. {flexgate-0.11.0 → flexgate-0.11.2}/docs/requirements.txt +0 -0
  24. {flexgate-0.11.0 → flexgate-0.11.2}/flexgate/__main__.py +0 -0
  25. {flexgate-0.11.0 → flexgate-0.11.2}/flexgate/doctor.py +0 -0
  26. {flexgate-0.11.0 → flexgate-0.11.2}/flexgate/fallback.py +0 -0
  27. {flexgate-0.11.0 → flexgate-0.11.2}/flexgate/healthcheck.py +0 -0
  28. {flexgate-0.11.0 → flexgate-0.11.2}/flexgate/images.py +0 -0
  29. {flexgate-0.11.0 → flexgate-0.11.2}/flexgate/main.py +0 -0
  30. {flexgate-0.11.0 → flexgate-0.11.2}/flexgate/migrate.py +0 -0
  31. {flexgate-0.11.0 → flexgate-0.11.2}/flexgate/proxy.py +0 -0
  32. {flexgate-0.11.0 → flexgate-0.11.2}/flexgate/router.py +0 -0
  33. {flexgate-0.11.0 → flexgate-0.11.2}/flexgate/service.py +0 -0
  34. {flexgate-0.11.0 → flexgate-0.11.2}/flexgate/settings.py +0 -0
  35. {flexgate-0.11.0 → flexgate-0.11.2}/flexgate/sync.py +0 -0
  36. {flexgate-0.11.0 → flexgate-0.11.2}/flexgate/ui.py +0 -0
  37. {flexgate-0.11.0 → flexgate-0.11.2}/flexgate/update.py +0 -0
  38. {flexgate-0.11.0 → flexgate-0.11.2}/pyproject.toml +0 -0
  39. {flexgate-0.11.0 → flexgate-0.11.2}/test.png +0 -0
  40. {flexgate-0.11.0 → flexgate-0.11.2}/test_multimodal.py +0 -0
  41. {flexgate-0.11.0 → flexgate-0.11.2}/test_multimodal_gateway.py +0 -0
@@ -95,7 +95,7 @@ OpenAI-style client → POST /v1/images/generations
95
95
  | `config.py` | Pydantic-like dataclasses (`GatewayConfig`, `ProviderConfig`, `RouteConfig`, `ScheduleEntry`), YAML load/save, `TIER_PATTERNS` regex map, `is_placeholder_key` |
96
96
  | `router.py` | `resolve(config, model)` — schedule-first then default routes, first regex match wins; model aliases normalized before matching |
97
97
  | `proxy.py` | `handle_request()` — httpx async proxy, per-key fallback retry loop, SSE streaming + JSON pass-through |
98
- | `usage.py` | `flexgate status` usage queries — per-platform adapters (MiniMax coding_plan API, Kimi Code usages API, GLM coding-plan/team quota APIs, LiteLLM `/key/info`, DeepSeek balance API) plus a minimal chat probe fallback; every adapter also fills a normalized `QuotaInfo` (plan + 5h/weekly `WindowQuota`: remaining percent, reset epoch) used by the unified summary and `--json`; unreported platform data ("?") stays None → displayed as 0% / no countdown |
98
+ | `usage.py` | `flexgate status` usage queries — per-platform adapters (MiniMax coding_plan API, Kimi Code usages API, GLM coding-plan/team quota APIs, LiteLLM `/key/info`, DeepSeek balance API) plus a minimal chat probe fallback; every adapter also fills a normalized `QuotaInfo` (plan + 5h/weekly `WindowQuota`: remaining percent, reset epoch; plus a `balance` text for prepaid credit) used by the unified summary and `--json`; unreported platform data ("?") stays None → displayed as 0% / no countdown |
99
99
  | `server.py` | Starlette app creation, `POST /v1/messages` + `POST /v1/images/{generations,edits}` endpoints, `SIGUSR1` lifespan reload |
100
100
  | `images.py` | OpenAI Images API → MiniMax `/v1/image_generation` translation (`image-01`, size→width/height clamped to 512–2048 mult of 8, `data.image_base64[]`→`data[].b64_json`); edits endpoint returns 501; unknown image model names get 501 `model_not_supported` |
101
101
  | `fallback.py` | Catch-all route: merged Anthropic/OpenAI-shaped JSON errors for every unserved path — 404 `route_not_found` (lists valid routes), 405 `method_not_allowed`, 501 `endpoint_not_supported` per recognized API family with an actionable reason |
@@ -114,8 +114,8 @@ OpenAI-style client → POST /v1/images/generations
114
114
  - **Model alias normalization** (`router.py`): before matching, bare aliases (`sonnet`/`opus`/`haiku`/`default`, case-insensitive, optional `[plan]` suffix) map to `claude-<tier>` prefixes (`default` → sonnet, Claude Code's default tier), and legacy numbered names (`claude-3-7-sonnet-latest`) reduce to their tier prefix; everything else passes through unchanged, so custom patterns and the catch-all behave exactly as before.
115
115
  - **Model resolution & `available_models` fallback**: A route may omit `model`; `router.resolve()` then falls back to the provider's first `available_models` entry, so `model_override` handed to the proxy is always a concrete name. `config._parse_routes` rejects routes that omit `model` on a provider with no `available_models` — so adding a provider without models requires an explicit `model` on every route using it.
116
116
  - **Proxy rewrite contract** (`proxy.py`): the upstream request gets the provider's `x-api-key` plus a fixed header set, and the JSON `model` field is rewritten only when the route set an override. Streaming responses are forwarded as raw bytes (`aiter_bytes`), never parsed.
117
- - **Key fallback** (`proxy.py`): a provider's `api_keys` list holds one or more keys for the same upstream (entries are key strings or `{key, note}` mappings — plus optional `organization`/`project` on GLM team keys, used only by the usage check; `note` is a free-form label shown by `flexgate status` and in fallback logs). Multiple accounts on one upstream — e.g. several MiniMax subscriptions — belong in ONE provider, not separate ones. GLM coding plans are the documented exception: z.ai (api.z.ai) and zhipu (open.bigmodel.cn) share one account system — the same keys work on both endpoints — so the same key may legitimately sit under a `zai` and a `zhipu` provider (or `zai-team`/`zhipu-team`) to keep a fallback endpoint. Each *route* has an active-key pointer (`active_key` in YAML, 1-based; `RouteConfig.key_index` 0-based, default first key): requests on that route start from the pointed key, and on a retryable failure (HTTP 401/402/403/429/500/502/503/529, connect error, or timeout) the next key is tried and the pointer advances with it (in memory only, wrapping around — it is not written back to the file); every key is tried at most once and a full circle of failures returns the last error. The pointer is set interactively via `flexgate config edit` → "api keys" (pick a route, with live per-key usage) or by editing the route's `active_key` directly. `resolve()` (`router.py`) returns the matched `RouteConfig` so the proxy can read/advance its pointer. Streaming requests can only fall back before the upstream returns 200. `_regular_proxy`/`_stream_proxy` return `(response, retryable)`; `handle_request` owns the loop and returns the last error when all keys fail. The legacy `api_key` + `fallback_keys` schema is still parsed (`config._parse_api_keys`); migration v3 → v4 rewrites it to `api_keys`. `ProviderConfig.api_key` remains as a first-key property (getter/setter) for the settings import code.
118
- - **Usage inspection** (`usage.py`): `flexgate status` maps `base_url` to a platform adapter (`_ADAPTERS`): MiniMax → `{origin}/v1/api/openplatform/coding_plan/remains` (only the "general" text-model quota entry is used; its counts are usually 0/0, so usage is read from the `*_remaining_percent` fields), Kimi Code → `{base}/v1/usages` (undocumented, same endpoint as the CLI's `/usage`; reports weekly quota, 5-hour window and parallel limit), GLM (z.ai / bigmodel.cn, one account system — same keys on both endpoints) → adapter picked per key entry: without `organization`/`project` it is a personal coding-plan key → `{origin}/api/monitor/usage/quota/limit` (undocumented but used by z.ai's own plugin; both endpoints serve it), with them it is a team key → the same endpoint with `?type=2` plus `bigmodel-organization`/`bigmodel-project` headers and the team subscription detail for plan name/expiry (both endpoints serve it too), USTC/LiteLLM → `{origin}/key/info`, DeepSeek → `{origin}/user/balance`. Hosts in `_PROBE_ONLY_MARKERS` (e.g. xiaomimimo.com — no key-based usage API) and unknown platforms get the minimal chat probe: `POST /v1/messages` with input `"hi"`, `max_tokens=128`. A failed adapter call also degrades to the probe. Any key whose usage query errored — a hard failure (adapter AND probe), or an adapter error merely recovered by the probe — is recorded in `~/.flexgate/usage-cache.json` (sha256 fingerprint of `base_url|key`, never key material — scoped by endpoint so the same key under two providers caches independently; entry keeps the `ok` flag and output lines) and skipped by later `status`/`usage` runs, shown as `cached (… ago)` / `cached failure (… ago)` with a hint; `flexgate usage --force` rechecks them and a clean success clears the entry. Placeholder keys are never cached.
117
+ - **Key fallback** (`proxy.py`): a provider's `api_keys` list holds one or more keys for the same upstream (entries are key strings or `{key, note}` mappings — plus optional `organization`/`project` on GLM team keys, used only by the usage check; `note` is a free-form label shown by `flexgate status` and in fallback logs). Multiple accounts on one upstream — e.g. several MiniMax subscriptions — belong in ONE provider, not separate ones. GLM coding plans are the documented exception: z.ai (api.z.ai) and zhipu (open.bigmodel.cn) share one account system — the same keys work on both endpoints, so a provider's `base_url` can be swapped freely when one endpoint is unstable (convention: one provider per key set, `zhipu`/`zhipu-team` on bigmodel). Each *route* has an active-key pointer (`active_key` in YAML, 1-based; `RouteConfig.key_index` 0-based, default first key): requests on that route start from the pointed key, and on a retryable failure (HTTP 401/402/403/429/500/502/503/529, connect error, or timeout) the next key is tried and the pointer advances with it (in memory only, wrapping around — it is not written back to the file); every key is tried at most once and a full circle of failures returns the last error. The pointer is set interactively via `flexgate config edit` → "api keys" (pick a route, with live per-key usage) or by editing the route's `active_key` directly. `resolve()` (`router.py`) returns the matched `RouteConfig` so the proxy can read/advance its pointer. Streaming requests can only fall back before the upstream returns 200. `_regular_proxy`/`_stream_proxy` return `(response, retryable)`; `handle_request` owns the loop and returns the last error when all keys fail. The legacy `api_key` + `fallback_keys` schema is still parsed (`config._parse_api_keys`); migration v3 → v4 rewrites it to `api_keys`. `ProviderConfig.api_key` remains as a first-key property (getter/setter) for the settings import code.
118
+ - **Usage inspection** (`usage.py`): `flexgate status` maps `base_url` to a platform adapter (`_ADAPTERS`): MiniMax → `{origin}/v1/api/openplatform/coding_plan/remains` (only the "general" text-model quota entry is used; its counts are usually 0/0, so usage is read from the `*_remaining_percent` fields), Kimi Code → `{base}/v1/usages` (undocumented, same endpoint as the CLI's `/usage`; reports weekly quota, 5-hour window and parallel limit), GLM (z.ai / bigmodel.cn, one account system — same keys on both endpoints) → adapter picked per key entry: without `organization`/`project` it is a personal coding-plan key → `{origin}/api/monitor/usage/quota/limit` (undocumented but used by z.ai's own plugin; both endpoints serve it), with them it is a team key → the same endpoint with `?type=2` plus `bigmodel-organization`/`bigmodel-project` headers and the team subscription detail for plan name/expiry (both endpoints serve it too), USTC/LiteLLM → `{origin}/key/info`, DeepSeek → `{origin}/user/balance` (prepaid total per currency normalized into `QuotaInfo.balance`, shown next to the key in the unified summary). Hosts in `_PROBE_ONLY_MARKERS` (e.g. xiaomimimo.com — no key-based usage API) and unknown platforms get the minimal chat probe: `POST /v1/messages` with input `"hi"`, `max_tokens=128`. A failed adapter call also degrades to the probe. Any key whose usage query errored — a hard failure (adapter AND probe), or an adapter error merely recovered by the probe — is recorded in `~/.flexgate/usage-cache.json` (sha256 fingerprint of `base_url|key`, never key material — scoped by endpoint so the same key under two providers caches independently; entry keeps the `ok` flag and output lines) and skipped by later `status`/`usage` runs, shown as `cached (… ago)` / `cached failure (… ago)` with a hint; `flexgate usage --force` rechecks them and a clean success clears the entry. Placeholder keys are never cached.
119
119
  - **Multimodal degradation**: `MULTIMODAL_MODELS` (currently `{"MiniMax-M3", "glm-4.6v"}`) is the allowlist. Requests carrying image blocks aimed at any other model have images stripped and a `[flexgate]` text note injected into both the outgoing request and the returned response, so non-multimodal backends don't 4xx.
120
120
  - **Schedule-based overrides**: Optional time windows (e.g. 22:00-06:00) override default routes. Overnight wrap is supported.
121
121
  - **Service-first lifecycle**: `flexgate.service` is the sole persistent runtime. systemd owns restart, boot startup, logs, and process state.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: flexgate
3
- Version: 0.11.0
3
+ Version: 0.11.2
4
4
  Summary: Flexible local API gateway for Claude Code — route models to different providers
5
5
  Project-URL: Homepage, https://github.com/Agony5757/flexible-gateway
6
6
  Project-URL: Repository, https://github.com/Agony5757/flexible-gateway
@@ -204,7 +204,7 @@ Usage (timeout 15s per key)
204
204
  | GLM 个人版(api.z.ai / open.bigmodel.cn) | `GET /api/monitor/usage/quota/limit`(Authorization 直接带 key) | 未公开文档、但 z.ai 官方 coding 插件在用的接口,返回各窗口已用百分比与重置时间。z.ai 和智谱是同一账号体系,**两个端点都响应**;仅对个人版 coding plan 订阅有效,按量付费 key 退化为 probe |
205
205
  | GLM 团队版(api.z.ai / open.bigmodel.cn) | `GET /api/monitor/usage/quota/limit?type=2`,带 `bigmodel-organization`/`bigmodel-project` 头(取自 key 条目的 `organization`/`project` 字段) | 同一接口的团队版变体,返回 5 小时/每周窗口的积分用量;另查 `querySubscribeDetail` 显示套餐名与有效期,**两个端点都响应** |
206
206
  | USTC(api.llm.ustc.edu.cn,LiteLLM) | `GET /key/info`(Bearer 认证) | LiteLLM proxy 自带的 key 信息接口,返回 spend / max_budget / 限速等 |
207
- | DeepSeek(api.deepseek.com) | `GET /user/balance`(Bearer 认证) | 官方文档接口,返回预付费账户余额(总额 = 充值余额 + 未过期赠金,CNY/USD);查询不消耗余额,明细见 `flexgate usage --verbose` |
207
+ | DeepSeek(api.deepseek.com) | `GET /user/balance`(Bearer 认证) | 官方文档接口,返回预付费账户余额(总额 = 充值余额 + 未过期赠金,CNY/USD);查询不消耗余额,统一摘要直接显示总额(如 `balance 489.61 CNY`),明细见 `flexgate usage --verbose` |
208
208
  | 小米 MiMo(token-plan-cn.xiaomimimo.com) | **无 key 可用的官方接口**(控制台内部接口需要浏览器 cookie,不采用) | 退化为 minimal probe |
209
209
  | 其他/未知平台 | minimal chat probe | 发一条输入 `"hi"`、`max_tokens=128` 的最小 `/v1/messages` 请求,验证 key 是否还能正常服务(会消耗极少量额度) |
210
210
 
@@ -353,8 +353,8 @@ server:
353
353
  port: 8765
354
354
 
355
355
  providers:
356
- zai: # GLM 个人版 key,走 z.ai 端点
357
- base_url: "https://api.z.ai/api/anthropic"
356
+ zhipu: # GLM 个人版 key(z.ai 端点同体系,base_url 可换)
357
+ base_url: "https://open.bigmodel.cn/api/anthropic"
358
358
  api_keys:
359
359
  - "your-glm-personal-api-key"
360
360
  minimax:
@@ -365,10 +365,6 @@ providers:
365
365
  - key: "your-minimax-api-key-2"
366
366
  note: "备用账号"
367
367
  - "your-minimax-api-key-3" # 纯字符串写法(无备注)
368
- zhipu: # 同一把 GLM 个人版 key 也可走智谱端点(同一账号体系)
369
- base_url: "https://open.bigmodel.cn/api/anthropic"
370
- api_keys:
371
- - "your-glm-personal-api-key"
372
368
  zhipu-team: # GLM 团队版 key
373
369
  base_url: "https://open.bigmodel.cn/api/anthropic"
374
370
  api_keys:
@@ -376,13 +372,6 @@ providers:
376
372
  note: "队友名"
377
373
  organization: "org-XXXXXXXX" # GLM 团队版 key:组织/项目 ID
378
374
  project: "proj_XXXXXXXX" # (flexgate usage 查团队套餐额度用)
379
- zai-team: # 团队版 key 同样可用于 z.ai 端点
380
- base_url: "https://api.z.ai/api/anthropic"
381
- api_keys:
382
- - key: "your-glm-team-api-key"
383
- note: "队友名"
384
- organization: "org-XXXXXXXX"
385
- project: "proj_XXXXXXXX"
386
375
 
387
376
  claude_settings:
388
377
  default_opus_model: "claude-opus-4-7"
@@ -397,12 +386,12 @@ claude_settings:
397
386
  # end: "06:00"
398
387
  # routes:
399
388
  # - pattern: "^claude-sonnet"
400
- # provider: zai
389
+ # provider: zhipu
401
390
  # model: "glm-5.1"
402
391
 
403
392
  routes: # 从上到下匹配,首个命中生效
404
393
  - pattern: "^claude-opus"
405
- provider: zai
394
+ provider: zhipu
406
395
  model: "glm-5.1" # 可选,发给 provider 的实际模型名
407
396
  - pattern: "^claude-sonnet"
408
397
  provider: minimax
@@ -187,7 +187,7 @@ Usage (timeout 15s per key)
187
187
  | GLM 个人版(api.z.ai / open.bigmodel.cn) | `GET /api/monitor/usage/quota/limit`(Authorization 直接带 key) | 未公开文档、但 z.ai 官方 coding 插件在用的接口,返回各窗口已用百分比与重置时间。z.ai 和智谱是同一账号体系,**两个端点都响应**;仅对个人版 coding plan 订阅有效,按量付费 key 退化为 probe |
188
188
  | GLM 团队版(api.z.ai / open.bigmodel.cn) | `GET /api/monitor/usage/quota/limit?type=2`,带 `bigmodel-organization`/`bigmodel-project` 头(取自 key 条目的 `organization`/`project` 字段) | 同一接口的团队版变体,返回 5 小时/每周窗口的积分用量;另查 `querySubscribeDetail` 显示套餐名与有效期,**两个端点都响应** |
189
189
  | USTC(api.llm.ustc.edu.cn,LiteLLM) | `GET /key/info`(Bearer 认证) | LiteLLM proxy 自带的 key 信息接口,返回 spend / max_budget / 限速等 |
190
- | DeepSeek(api.deepseek.com) | `GET /user/balance`(Bearer 认证) | 官方文档接口,返回预付费账户余额(总额 = 充值余额 + 未过期赠金,CNY/USD);查询不消耗余额,明细见 `flexgate usage --verbose` |
190
+ | DeepSeek(api.deepseek.com) | `GET /user/balance`(Bearer 认证) | 官方文档接口,返回预付费账户余额(总额 = 充值余额 + 未过期赠金,CNY/USD);查询不消耗余额,统一摘要直接显示总额(如 `balance 489.61 CNY`),明细见 `flexgate usage --verbose` |
191
191
  | 小米 MiMo(token-plan-cn.xiaomimimo.com) | **无 key 可用的官方接口**(控制台内部接口需要浏览器 cookie,不采用) | 退化为 minimal probe |
192
192
  | 其他/未知平台 | minimal chat probe | 发一条输入 `"hi"`、`max_tokens=128` 的最小 `/v1/messages` 请求,验证 key 是否还能正常服务(会消耗极少量额度) |
193
193
 
@@ -336,8 +336,8 @@ server:
336
336
  port: 8765
337
337
 
338
338
  providers:
339
- zai: # GLM 个人版 key,走 z.ai 端点
340
- base_url: "https://api.z.ai/api/anthropic"
339
+ zhipu: # GLM 个人版 key(z.ai 端点同体系,base_url 可换)
340
+ base_url: "https://open.bigmodel.cn/api/anthropic"
341
341
  api_keys:
342
342
  - "your-glm-personal-api-key"
343
343
  minimax:
@@ -348,10 +348,6 @@ providers:
348
348
  - key: "your-minimax-api-key-2"
349
349
  note: "备用账号"
350
350
  - "your-minimax-api-key-3" # 纯字符串写法(无备注)
351
- zhipu: # 同一把 GLM 个人版 key 也可走智谱端点(同一账号体系)
352
- base_url: "https://open.bigmodel.cn/api/anthropic"
353
- api_keys:
354
- - "your-glm-personal-api-key"
355
351
  zhipu-team: # GLM 团队版 key
356
352
  base_url: "https://open.bigmodel.cn/api/anthropic"
357
353
  api_keys:
@@ -359,13 +355,6 @@ providers:
359
355
  note: "队友名"
360
356
  organization: "org-XXXXXXXX" # GLM 团队版 key:组织/项目 ID
361
357
  project: "proj_XXXXXXXX" # (flexgate usage 查团队套餐额度用)
362
- zai-team: # 团队版 key 同样可用于 z.ai 端点
363
- base_url: "https://api.z.ai/api/anthropic"
364
- api_keys:
365
- - key: "your-glm-team-api-key"
366
- note: "队友名"
367
- organization: "org-XXXXXXXX"
368
- project: "proj_XXXXXXXX"
369
358
 
370
359
  claude_settings:
371
360
  default_opus_model: "claude-opus-4-7"
@@ -380,12 +369,12 @@ claude_settings:
380
369
  # end: "06:00"
381
370
  # routes:
382
371
  # - pattern: "^claude-sonnet"
383
- # provider: zai
372
+ # provider: zhipu
384
373
  # model: "glm-5.1"
385
374
 
386
375
  routes: # 从上到下匹配,首个命中生效
387
376
  - pattern: "^claude-opus"
388
- provider: zai
377
+ provider: zhipu
389
378
  model: "glm-5.1" # 可选,发给 provider 的实际模型名
390
379
  - pattern: "^claude-sonnet"
391
380
  provider: minimax
@@ -24,32 +24,16 @@ providers:
24
24
  available_models:
25
25
  - "MiniMax-M3"
26
26
  # GLM Coding Plan:z.ai(api.z.ai)和智谱(open.bigmodel.cn)是同一账号
27
- # 体系,同一把 key 在两个端点都可用,因此同一把 key 可以同时配在 zai 和
28
- # zhipu 两个 provider 下(例如留一个备用端点)。provider 按端点选择;
27
+ # 体系,同一把 key 在两个端点都可用(哪个端点不稳就换 base_url)。
29
28
  # usage 检查按 key 条目区分:未配 organization/project 的是个人版
30
29
  # coding plan key,配了的是团队版 key。
31
- zai:
32
- base_url: "https://api.z.ai/api/anthropic"
33
- api_keys:
34
- - "your-glm-personal-api-key"
35
- available_models:
36
- - "glm-5.3" # 路由省略 model 时的回退模型
37
- - "glm-4.6v"
38
- zai-team:
39
- base_url: "https://api.z.ai/api/anthropic"
40
- api_keys:
41
- - key: "your-glm-team-api-key"
42
- note: "队友名"
43
- organization: "org-XXXXXXXX" # GLM 团队版:组织 ID
44
- project: "proj_XXXXXXXX" # GLM 团队版:项目 ID
45
- available_models:
46
- - "glm-5.3"
47
30
  zhipu:
48
31
  base_url: "https://open.bigmodel.cn/api/anthropic"
49
32
  api_keys:
50
33
  - "your-glm-personal-api-key"
51
34
  available_models:
52
- - "glm-5.3"
35
+ - "glm-5.3" # 路由省略 model 时的回退模型
36
+ - "glm-4.6v"
53
37
  zhipu-team:
54
38
  base_url: "https://open.bigmodel.cn/api/anthropic"
55
39
  api_keys:
@@ -85,7 +69,7 @@ claude_settings:
85
69
  # end: "24:00"
86
70
  # routes:
87
71
  # - pattern: "^claude-opus"
88
- # provider: zai
72
+ # provider: zhipu
89
73
  # model: "glm-5.3"
90
74
  # - pattern: "^claude-sonnet"
91
75
  # provider: xiaomi
@@ -72,6 +72,9 @@ flexgate service uninstall # 停止、禁用并删除 unit
72
72
  - `service reload` 和 `config set/edit` 会在仅路由变化时发送 SIGUSR1;
73
73
  如果 endpoint 变化,则先检查再 restart。若只改 host、仍复用当前 port,
74
74
  为避免误停服务会要求先执行 `service stop`,再执行 `service start`。
75
+ - 网关进程自身每 2 秒监视 config.yaml:无论文件被谁修改(`flexgate config`、
76
+ 编辑器、`flexgate sync pull`),路由变更都会在数秒内自动热重载生效,
77
+ 不依赖编辑端能否触达 systemd;解析失败的半成品文件保留旧配置继续服务。
75
78
  - 查看日志:`flexgate log`(`-f` 跟随、`--since`/`--grep` 过滤、`-r` 只看路由决策行)。
76
79
 
77
80
  ## 状态总览与用量查询
@@ -211,7 +214,8 @@ Flexgate config — ~/.flexgate/config.yaml
211
214
  (与 `flexgate status` 相同),并标注当前 active 的 key。
212
215
  - model 列表包含:`available_models` 中的各个模型、「使用 provider 默认
213
216
  (首个可用模型,不写死 model)」、以及「自定义模型…」(手动输入)。
214
- - 按 `s` 保存(并向运行中的网关发送 SIGUSR1 热重载,**无需重启即生效**),
217
+ - 按 `s` 保存(**无需重启即生效**:网关监视配置文件,数秒内自动热重载;
218
+ SIGUSR1 仍可用作立即触发),
215
219
  按 `q` 退出(有未保存改动时会询问 "Config changed — activate now?":
216
220
  选 Yes 立即保存并热重载生效,选 No 放弃改动);子菜单中按 `Esc`/`←`
217
221
  返回上一级。
@@ -25,8 +25,8 @@ server:
25
25
  port: 8765
26
26
 
27
27
  providers:
28
- zai: # GLM 个人版 key,走 z.ai 端点
29
- base_url: "https://api.z.ai/api/anthropic"
28
+ zhipu: # GLM 个人版 key(z.ai 端点同体系,base_url 可换)
29
+ base_url: "https://open.bigmodel.cn/api/anthropic"
30
30
  api_keys:
31
31
  - "your-glm-personal-api-key"
32
32
  minimax:
@@ -37,10 +37,6 @@ providers:
37
37
  - key: "your-minimax-api-key-2"
38
38
  note: "备用账号"
39
39
  - "your-minimax-api-key-3" # 纯字符串写法(无备注)
40
- zhipu: # 同一把 GLM 个人版 key 也可走智谱端点(同一账号体系)
41
- base_url: "https://open.bigmodel.cn/api/anthropic"
42
- api_keys:
43
- - "your-glm-personal-api-key"
44
40
  zhipu-team: # GLM 团队版 key
45
41
  base_url: "https://open.bigmodel.cn/api/anthropic"
46
42
  api_keys:
@@ -48,13 +44,6 @@ providers:
48
44
  note: "队友名"
49
45
  organization: "org-XXXXXXXX" # GLM 团队版 key:组织/项目 ID
50
46
  project: "proj_XXXXXXXX" # (flexgate usage 查团队套餐额度用)
51
- zai-team: # 团队版 key 同样可用于 z.ai 端点
52
- base_url: "https://api.z.ai/api/anthropic"
53
- api_keys:
54
- - key: "your-glm-team-api-key"
55
- note: "队友名"
56
- organization: "org-XXXXXXXX"
57
- project: "proj_XXXXXXXX"
58
47
 
59
48
  claude_settings:
60
49
  default_opus_model: "claude-opus-4-7"
@@ -69,12 +58,12 @@ claude_settings:
69
58
  # end: "06:00"
70
59
  # routes:
71
60
  # - pattern: "^claude-sonnet"
72
- # provider: zai
61
+ # provider: zhipu
73
62
  # model: "glm-5.1"
74
63
 
75
64
  routes: # 从上到下匹配,首个命中生效
76
65
  - pattern: "^claude-opus"
77
- provider: zai
66
+ provider: zhipu
78
67
  model: "glm-5.1" # 可选,发给 provider 的实际模型名
79
68
  - pattern: "^claude-sonnet"
80
69
  provider: minimax
@@ -10,7 +10,7 @@
10
10
  | `flexgate/router.py` | `resolve(config, model)` — 先定时路由后默认路由,首个正则命中生效;匹配前归一化模型别名(`sonnet`/`claude-3-7-sonnet-latest` → `^claude-<tier>`) |
11
11
  | `flexgate/proxy.py` | `handle_request()` — httpx 异步代理,per-key fallback 重试,SSE 流式 + JSON 透传;记录每个请求由哪把 key 服务 |
12
12
  | `flexgate/usage.py` | `flexgate status` / `flexgate usage` 的用量查询 — 各平台适配器 + minimal chat probe 兜底 + 查询失败缓存(`usage --force` 强制重查) |
13
- | `flexgate/server.py` | Starlette 应用,`POST /v1/messages` + `POST /v1/images/{generations,edits}` 端点,SIGUSR1 热重载 |
13
+ | `flexgate/server.py` | Starlette 应用,`POST /v1/messages` + `POST /v1/images/{generations,edits}` 端点,SIGUSR1 热重载 + 配置文件 watcher 自动重载 |
14
14
  | `flexgate/images.py` | OpenAI Images API → MiniMax `/v1/image_generation` 翻译(模型名映射、size 钳制、`b64_json` 透传;edits 返回 501) |
15
15
  | `flexgate/fallback.py` | 兜底路由:所有未服务路径返回 Anthropic/OpenAI 双兼容的结构化 JSON 错误(404 `route_not_found` / 405 / 501 `endpoint_not_supported`) |
16
16
  | `flexgate/main.py` | 启动引导:加载配置 → 创建 app → 跑 uvicorn |
@@ -16,7 +16,7 @@
16
16
  | GLM 个人版(api.z.ai / open.bigmodel.cn) | `GET /api/monitor/usage/quota/limit`(Authorization 直接带 key) | 未公开文档、但 z.ai 官方 coding 插件在用的接口,返回各窗口已用百分比与重置时间。z.ai 和智谱(bigmodel.cn)是同一账号体系,**两个端点都响应**此接口。仅对 GLM Coding Plan 个人订阅有效:按量付费 key 或团队 key 会返回"当前用户不存在coding plan"并退化为 probe(团队 key 需在条目上配 `organization`/`project`) |
17
17
  | GLM 团队版(api.z.ai / open.bigmodel.cn) | `GET /api/monitor/usage/quota/limit?type=2`,带 `bigmodel-organization`/`bigmodel-project` 头(取自 key 条目的 `organization`/`project` 字段) | 同一 monitor 接口的团队版变体,返回 5 小时/每周窗口的积分用量(绝对值 + 百分比 + 重置时间);另查 `querySubscribeDetail` 显示套餐名与有效期。**两个端点都响应**;key 条目未配 `organization`/`project` 时按个人版 key 处理 |
18
18
  | USTC(api.llm.ustc.edu.cn,LiteLLM) | `GET /key/info`(Bearer 认证) | LiteLLM proxy 自带的 key 信息接口,返回 spend / max_budget / 限速等 |
19
- | DeepSeek(api.deepseek.com) | `GET /user/balance`(Bearer 认证) | 官方文档接口,返回预付费账户余额(总额 = 充值余额 + 未过期赠金,按 CNY/USD 分别列出);查询本身不消耗余额。余额不属于窗口配额,统一摘要中只显示方法名,具体金额见 `--verbose` / `--json --verbose` |
19
+ | DeepSeek(api.deepseek.com) | `GET /user/balance`(Bearer 认证) | 官方文档接口,返回预付费账户余额(总额 = 充值余额 + 未过期赠金,按 CNY/USD 分别列出);查询本身不消耗余额。余额不属于窗口配额,统一摘要在 key 旁直接显示总额(如 `balance 489.61 CNY`),充值/赠金明细见 `--verbose` |
20
20
  | 小米 MiMo(token-plan-cn.xiaomimimo.com) | **无 key 可用的官方接口**(控制台内部接口需要浏览器 cookie,不采用) | 退化为 minimal probe |
21
21
  | 其他/未知平台 | minimal chat probe | 发一条输入 `"hi"`、`max_tokens=128` 的最小 `/v1/messages` 请求,验证 key 是否还能正常服务(会消耗极少量额度) |
22
22
 
@@ -31,9 +31,11 @@
31
31
 
32
32
  ```text
33
33
  Usage (timeout 15s per key)
34
- zai
34
+ zhipu
35
35
  ✓ #1 cbc4***sL7n [main] plan pro 5h 88% · 4h 36m weekly 81% · 6d 16h 15m
36
36
  ✓ #2 302b***RLBh [gsj] plan max 5h 100% · — weekly 99% · 6d 17h 46m
37
+ deepseek
38
+ ✓ #1 sk-c***9e13 [official] balance 489.61 CNY
37
39
  ustc
38
40
  ✗ #1 sk-G***jJYg LiteLLM /key/info failed (ConnectError: ); probe: ConnectError:
39
41
  ```
@@ -45,8 +47,8 @@ Usage (timeout 15s per key)
45
47
  - `dhm`(默认):`4d 4h 23m` / `2h 47m` / `23m`
46
48
  - `hours`:`52.1h`
47
49
  - `minutes`:`3125m`
48
- - 无窗口配额的 key(probe / 缓存失败 / 仅余额型接口如 DeepSeek、LiteLLM)
49
- 只显示一行方法或错误摘要。
50
+ - 无窗口配额的 key 只显示一行文本:probe / 缓存失败 / LiteLLM 显示方法
51
+ 或错误摘要;余额型平台(DeepSeek)显示 `balance 489.61 CNY`。
50
52
 
51
53
  ### "?" 值的语义
52
54
 
@@ -122,6 +124,8 @@ Usage (timeout 15s per key)
122
124
 
123
125
  - 字段语义与统一摘要一致:余额未知 → `remaining_percent: 0.0`;
124
126
  重置时间未知 → `remaining_seconds`/`reset_at` 为 `null`。
127
+ - DeepSeek 的余额在顶层 `balance` 字段(如 `"487.77 CNY"`,多币种以
128
+ ` · ` 连接)。
125
129
  - 平台额外信息保留在 `window_hours`(如 5.0)和 `used`(原始用量文本)。
126
130
  - key 一律掩码显示,永不输出明文。
127
131
 
@@ -1,3 +1,3 @@
1
1
  """Flexgate — Flexible API Gateway for Claude Code."""
2
2
 
3
- __version__ = "0.11.0"
3
+ __version__ = "0.11.2"
@@ -313,7 +313,12 @@ def _print_usage_summary(usage: dict, time_format: str) -> None:
313
313
  if quota is not None and (quota.plan or quota.five_hour or quota.weekly):
314
314
  rows.append((r, key, quota.plan or "—", quota.five_hour, quota.weekly, None))
315
315
  else:
316
- detail = r.method if r.ok else (r.lines[0] if r.lines else r.method)
316
+ if quota is not None and quota.balance:
317
+ detail = f"balance {quota.balance}"
318
+ elif r.ok:
319
+ detail = r.method
320
+ else:
321
+ detail = r.lines[0] if r.lines else r.method
317
322
  rows.append((r, key, None, None, None, detail))
318
323
  key_w = max(len(row[1]) for row in rows)
319
324
  plan_w = max((len(row[2]) for row in rows if row[2] is not None), default=0)
@@ -386,39 +386,23 @@ providers:
386
386
  available_models:
387
387
  - "MiniMax-M3"
388
388
  # GLM Coding Plan keys: z.ai (api.z.ai) and zhipu (open.bigmodel.cn) are
389
- # one account system — the same keys work on both endpoints, so a key may
390
- # be listed under a zai and a zhipu provider at the same time (e.g. to
391
- # keep a fallback endpoint). Pick the provider by endpoint; the usage
389
+ # one account system — the same keys work on both endpoints, so either
390
+ # base_url can be used (swap it if one endpoint is unstable). The usage
392
391
  # adapter is picked per key entry: without organization/project it is a
393
392
  # personal coding-plan key, with them a team key.
394
- zai:
395
- base_url: "https://api.z.ai/api/anthropic"
396
- api_keys:
397
- - "your-glm-personal-api-key"
398
- available_models:
399
- - "glm-5.3" # used as fallback when a route omits `model`
400
- - "glm-4.6v"
401
- zai-team:
402
- base_url: "https://api.z.ai/api/anthropic"
403
- api_keys:
404
- - key: "your-glm-team-api-key"
405
- note: "teammate name"
406
- organization: "org-XXXXXXXX" # GLM team IDs (see comment above)
407
- project: "proj_XXXXXXXX"
408
- available_models:
409
- - "glm-5.3"
410
393
  zhipu:
411
394
  base_url: "https://open.bigmodel.cn/api/anthropic"
412
395
  api_keys:
413
396
  - "your-glm-personal-api-key"
414
397
  available_models:
415
- - "glm-5.3"
398
+ - "glm-5.3" # used as fallback when a route omits `model`
399
+ - "glm-4.6v"
416
400
  zhipu-team:
417
401
  base_url: "https://open.bigmodel.cn/api/anthropic"
418
402
  api_keys:
419
403
  - key: "your-glm-team-api-key"
420
404
  note: "teammate name"
421
- organization: "org-XXXXXXXX"
405
+ organization: "org-XXXXXXXX" # GLM team IDs (see comment above)
422
406
  project: "proj_XXXXXXXX"
423
407
  available_models:
424
408
  - "glm-5.3"
@@ -449,7 +433,7 @@ claude_settings:
449
433
  # end: "24:00"
450
434
  # routes:
451
435
  # - pattern: "^claude-opus"
452
- # provider: zai
436
+ # provider: zhipu
453
437
  # model: "glm-5.3"
454
438
  # - pattern: "^claude-sonnet"
455
439
  # provider: xiaomi
@@ -1,7 +1,9 @@
1
1
  from __future__ import annotations
2
2
 
3
+ import asyncio
3
4
  import json
4
5
  import logging
6
+ import os
5
7
  import signal
6
8
  import time
7
9
  from collections.abc import AsyncGenerator
@@ -62,6 +64,56 @@ async def messages(request: Request) -> JSONResponse:
62
64
  return resp
63
65
 
64
66
 
67
+ def _load_new_config(config_path: str) -> GatewayConfig | None:
68
+ try:
69
+ return load_config(config_path)
70
+ except Exception as e:
71
+ logger.error("Failed to reload config: %s", e)
72
+ return None
73
+
74
+
75
+ # The gateway watches its config file so edits take effect within seconds no
76
+ # matter how the file was changed (flexgate config, an editor, sync). The
77
+ # CLI-side SIGUSR1 reload depends on the editing process reaching the systemd
78
+ # user bus, which is not always available (SSH sessions, agent shells).
79
+ CONFIG_WATCH_INTERVAL = 2.0
80
+
81
+
82
+ async def _watch_config(app, config_path: str | None) -> None:
83
+ last_mtime: float | None = None
84
+ if config_path:
85
+ try:
86
+ last_mtime = os.path.getmtime(config_path)
87
+ except OSError:
88
+ pass
89
+ while True:
90
+ await asyncio.sleep(CONFIG_WATCH_INTERVAL)
91
+ try:
92
+ if not config_path:
93
+ continue
94
+ try:
95
+ mtime = os.path.getmtime(config_path)
96
+ except OSError:
97
+ continue
98
+ if last_mtime is None:
99
+ last_mtime = mtime
100
+ continue
101
+ if mtime == last_mtime:
102
+ continue
103
+ new_config = _load_new_config(config_path)
104
+ if new_config is not None:
105
+ app.state.config = new_config
106
+ logger.info("Config auto-reloaded (file changed)")
107
+ # Advance the marker even on failure so a broken file logs once,
108
+ # not every poll; the corrected save rewrites the file (new mtime)
109
+ # and reloads again.
110
+ last_mtime = mtime
111
+ except asyncio.CancelledError:
112
+ raise
113
+ except Exception:
114
+ logger.exception("Config watcher error")
115
+
116
+
65
117
  def create_app(config: GatewayConfig, config_path: str | None = None) -> Starlette:
66
118
  client = httpx.AsyncClient(
67
119
  timeout=httpx.Timeout(connect=10, read=3600, write=10, pool=10),
@@ -82,16 +134,19 @@ def create_app(config: GatewayConfig, config_path: str | None = None) -> Starlet
82
134
  async def lifespan(app: Starlette) -> AsyncGenerator[None, None]:
83
135
  app.state.config = config
84
136
  app.state.client = client
137
+ watcher = asyncio.create_task(_watch_config(app, config_path))
85
138
 
86
139
  def _handle_sigusr1(signum, frame):
87
- try:
88
- app.state.config = load_config(config_path)
140
+ new_config = _load_new_config(config_path)
141
+ if new_config is not None:
142
+ app.state.config = new_config
89
143
  logger.info("Config reloaded via SIGUSR1")
90
- except Exception as e:
91
- logger.error("Failed to reload config: %s", e)
92
144
 
93
145
  signal.signal(signal.SIGUSR1, _handle_sigusr1)
94
- yield
146
+ try:
147
+ yield
148
+ finally:
149
+ watcher.cancel()
95
150
  await client.aclose()
96
151
 
97
152
  return Starlette(routes=routes, lifespan=lifespan)
@@ -29,16 +29,18 @@ queries it with the provider's own API key:
29
29
  * DeepSeek → GET {origin}/user/balance
30
30
  (officially documented endpoint; reports the prepaid
31
31
  pay-as-you-go balance — topped-up plus granted — per
32
- currency. A balance is not a windowed quota, so like
33
- LiteLLM it stays in the raw lines only)
32
+ currency. A balance is not a windowed quota, so there are
33
+ no 5h/weekly windows; the total per currency is normalized
34
+ into ``QuotaInfo.balance`` and shown in the unified summary)
34
35
  * anything else (e.g. Xiaomi MiMo, which has no key-based usage API)
35
36
  → minimal chat probe: POST /v1/messages with input "hi" and
36
37
  max_tokens=128 to verify the key can still serve requests
37
38
 
38
39
  Alongside the human-readable ``lines``, every adapter fills a normalized
39
40
  ``QuotaInfo`` (plan name + 5-hour/weekly ``WindowQuota`` with remaining
40
- percent and reset epoch) so the CLI can render one unified summary and
41
- ``--json`` consumers get structured fields. Data a platform did not report
41
+ percent and reset epoch, plus a ``balance`` text for prepaid credit) so
42
+ the CLI can render one unified summary and ``--json`` consumers get
43
+ structured fields. Data a platform did not report
42
44
  (the "?" cases) stays None: an unknown balance is displayed as 0% and an
43
45
  unknown reset time means no countdown is active.
44
46
  """
@@ -96,6 +98,10 @@ class QuotaInfo:
96
98
  plan: str | None = None
97
99
  five_hour: WindowQuota | None = None
98
100
  weekly: WindowQuota | None = None
101
+ # Pay-as-you-go balance text (e.g. "489.61 CNY") — DeepSeek-style prepaid
102
+ # credit, not a windowed quota; shown in the unified summary next to the
103
+ # key instead of the method name.
104
+ balance: str | None = None
99
105
 
100
106
 
101
107
  @dataclass
@@ -507,6 +513,7 @@ async def _usage_deepseek(
507
513
  if err:
508
514
  return [], None, err
509
515
  lines: list[str] = []
516
+ balances: list[str] = []
510
517
  if not data.get("is_available"):
511
518
  lines.append("NO balance available for API calls")
512
519
  for info in data.get("balance_infos", []):
@@ -517,7 +524,9 @@ async def _usage_deepseek(
517
524
  f"(topped-up {info.get('topped_up_balance', '?')}, "
518
525
  f"granted {info.get('granted_balance', '?')})"
519
526
  )
520
- return lines or ["no balance data"], None, None
527
+ balances.append(f"{info.get('total_balance', '?')} {info.get('currency', '?')}")
528
+ quota = QuotaInfo(balance=" · ".join(balances)) if data.get("is_available") and balances else None
529
+ return lines or ["no balance data"], quota, None
521
530
 
522
531
 
523
532
  async def _usage_kimi(
@@ -816,6 +825,7 @@ def result_to_dict(result: UsageResult, *, verbose: bool = False, now: float | N
816
825
  "method": result.method,
817
826
  "adapter_error": result.adapter_error,
818
827
  "plan": quota.plan if quota else None,
828
+ "balance": quota.balance if quota else None,
819
829
  "five_hour": window(quota.five_hour) if quota else None,
820
830
  "weekly": window(quota.weekly) if quota else None,
821
831
  }
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes