flexgate 0.8.0__tar.gz → 0.9.0__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.
- {flexgate-0.8.0 → flexgate-0.9.0}/CLAUDE.md +3 -3
- {flexgate-0.8.0 → flexgate-0.9.0}/PKG-INFO +13 -2
- {flexgate-0.8.0 → flexgate-0.9.0}/README.md +12 -1
- {flexgate-0.8.0 → flexgate-0.9.0}/config.yaml.template +12 -0
- {flexgate-0.8.0 → flexgate-0.9.0}/docs/configuration.md +12 -1
- {flexgate-0.8.0 → flexgate-0.9.0}/docs/usage.md +13 -6
- {flexgate-0.8.0 → flexgate-0.9.0}/flexgate/__init__.py +1 -1
- {flexgate-0.8.0 → flexgate-0.9.0}/flexgate/config.py +54 -8
- {flexgate-0.8.0 → flexgate-0.9.0}/flexgate/usage.py +101 -10
- {flexgate-0.8.0 → flexgate-0.9.0}/.github/workflows/docs.yml +0 -0
- {flexgate-0.8.0 → flexgate-0.9.0}/.github/workflows/release.yml +0 -0
- {flexgate-0.8.0 → flexgate-0.9.0}/.gitignore +0 -0
- {flexgate-0.8.0 → flexgate-0.9.0}/docs/Makefile +0 -0
- {flexgate-0.8.0 → flexgate-0.9.0}/docs/_static/.gitkeep +0 -0
- {flexgate-0.8.0 → flexgate-0.9.0}/docs/api.md +0 -0
- {flexgate-0.8.0 → flexgate-0.9.0}/docs/cli.md +0 -0
- {flexgate-0.8.0 → flexgate-0.9.0}/docs/conf.py +0 -0
- {flexgate-0.8.0 → flexgate-0.9.0}/docs/development.md +0 -0
- {flexgate-0.8.0 → flexgate-0.9.0}/docs/index.md +0 -0
- {flexgate-0.8.0 → flexgate-0.9.0}/docs/quickstart.md +0 -0
- {flexgate-0.8.0 → flexgate-0.9.0}/docs/requirements.txt +0 -0
- {flexgate-0.8.0 → flexgate-0.9.0}/flexgate/__main__.py +0 -0
- {flexgate-0.8.0 → flexgate-0.9.0}/flexgate/cli.py +0 -0
- {flexgate-0.8.0 → flexgate-0.9.0}/flexgate/doctor.py +0 -0
- {flexgate-0.8.0 → flexgate-0.9.0}/flexgate/fallback.py +0 -0
- {flexgate-0.8.0 → flexgate-0.9.0}/flexgate/healthcheck.py +0 -0
- {flexgate-0.8.0 → flexgate-0.9.0}/flexgate/images.py +0 -0
- {flexgate-0.8.0 → flexgate-0.9.0}/flexgate/main.py +0 -0
- {flexgate-0.8.0 → flexgate-0.9.0}/flexgate/migrate.py +0 -0
- {flexgate-0.8.0 → flexgate-0.9.0}/flexgate/proxy.py +0 -0
- {flexgate-0.8.0 → flexgate-0.9.0}/flexgate/router.py +0 -0
- {flexgate-0.8.0 → flexgate-0.9.0}/flexgate/server.py +0 -0
- {flexgate-0.8.0 → flexgate-0.9.0}/flexgate/service.py +0 -0
- {flexgate-0.8.0 → flexgate-0.9.0}/flexgate/settings.py +0 -0
- {flexgate-0.8.0 → flexgate-0.9.0}/flexgate/sync.py +0 -0
- {flexgate-0.8.0 → flexgate-0.9.0}/flexgate/ui.py +0 -0
- {flexgate-0.8.0 → flexgate-0.9.0}/flexgate/update.py +0 -0
- {flexgate-0.8.0 → flexgate-0.9.0}/pyproject.toml +0 -0
- {flexgate-0.8.0 → flexgate-0.9.0}/test.png +0 -0
- {flexgate-0.8.0 → flexgate-0.9.0}/test_multimodal.py +0 -0
- {flexgate-0.8.0 → flexgate-0.9.0}/test_multimodal_gateway.py +0 -0
|
@@ -92,7 +92,7 @@ OpenAI-style client → POST /v1/images/generations
|
|
|
92
92
|
| `config.py` | Pydantic-like dataclasses (`GatewayConfig`, `ProviderConfig`, `RouteConfig`, `ScheduleEntry`), YAML load/save, `TIER_PATTERNS` regex map, `is_placeholder_key` |
|
|
93
93
|
| `router.py` | `resolve(config, model)` — schedule-first then default routes, first regex match wins; model aliases normalized before matching |
|
|
94
94
|
| `proxy.py` | `handle_request()` — httpx async proxy, per-key fallback retry loop, SSE streaming + JSON pass-through |
|
|
95
|
-
| `usage.py` | `flexgate status` usage queries — per-platform adapters (MiniMax coding_plan API, Kimi Code usages API, z.ai quota API, LiteLLM `/key/info`) plus a minimal chat probe fallback |
|
|
95
|
+
| `usage.py` | `flexgate status` usage queries — per-platform adapters (MiniMax coding_plan API, Kimi Code usages API, z.ai quota API, zhipu team quota API, LiteLLM `/key/info`) plus a minimal chat probe fallback |
|
|
96
96
|
| `server.py` | Starlette app creation, `POST /v1/messages` + `POST /v1/images/{generations,edits}` endpoints, `SIGUSR1` lifespan reload |
|
|
97
97
|
| `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` |
|
|
98
98
|
| `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 |
|
|
@@ -111,8 +111,8 @@ OpenAI-style client → POST /v1/images/generations
|
|
|
111
111
|
- **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.
|
|
112
112
|
- **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.
|
|
113
113
|
- **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.
|
|
114
|
-
- **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; `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. 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.
|
|
115
|
-
- **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), z.ai
|
|
114
|
+
- **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 zhipu 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. 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.
|
|
115
|
+
- **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), z.ai → `{origin}/api/monitor/usage/quota/limit` (undocumented but used by z.ai's own plugin; personal coding plans only), bigmodel.cn → the same endpoint with `?type=2` plus `bigmodel-organization`/`bigmodel-project` headers read from the key entry's `organization`/`project` fields (team plans; also fetches the team subscription detail for plan name/expiry — a key entry without both fields gets no adapter at all and goes straight to the probe, with a hint line), USTC/LiteLLM → `{origin}/key/info`. 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, never key material; 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.
|
|
116
116
|
- **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.
|
|
117
117
|
- **Schedule-based overrides**: Optional time windows (e.g. 22:00-06:00) override default routes. Overnight wrap is supported.
|
|
118
118
|
- **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.
|
|
3
|
+
Version: 0.9.0
|
|
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
|
|
@@ -337,6 +337,13 @@ providers:
|
|
|
337
337
|
- key: "your-minimax-api-key-2"
|
|
338
338
|
note: "备用账号"
|
|
339
339
|
- "your-minimax-api-key-3" # 纯字符串写法(无备注)
|
|
340
|
+
zhipu-team:
|
|
341
|
+
base_url: "https://open.bigmodel.cn/api/anthropic"
|
|
342
|
+
api_keys:
|
|
343
|
+
- key: "your-zhipu-team-api-key"
|
|
344
|
+
note: "队友名"
|
|
345
|
+
organization: "org-XXXXXXXX" # 智谱团队版 key:组织/项目 ID
|
|
346
|
+
project: "proj_XXXXXXXX" # (flexgate usage 查团队套餐额度用)
|
|
340
347
|
|
|
341
348
|
claude_settings:
|
|
342
349
|
default_opus_model: "claude-opus-4-7"
|
|
@@ -376,7 +383,7 @@ routes: # 从上到下匹配,首个命中生效
|
|
|
376
383
|
|------|------|
|
|
377
384
|
| `server.host/port` | 网关监听地址 |
|
|
378
385
|
| `providers.<name>.base_url` | Provider 的 API 地址 |
|
|
379
|
-
| `providers.<name>.api_keys` | API key 列表(一个或多个,互为 fallback);每项为 key 字符串或 `{key, note}
|
|
386
|
+
| `providers.<name>.api_keys` | API key 列表(一个或多个,互为 fallback);每项为 key 字符串或 `{key, note}`(`note` 是备注)。智谱团队版 key 还可加 `organization`/`project`(团队组织/项目 ID),供 `flexgate usage` 查询团队套餐额度 |
|
|
380
387
|
| `providers.<name>.available_models` | 该 provider 的可用模型列表,首个条目作为路由省略 `model` 时的回退模型 |
|
|
381
388
|
| `claude_settings.*` | 写入 settings.json 的模型和超时配置 |
|
|
382
389
|
| `routes[].pattern` | 正则匹配请求中的 model 字段(匹配前归一化裸别名 `sonnet`/`opus`/`haiku`/`default` 与旧编号名) |
|
|
@@ -410,6 +417,10 @@ routes: # 从上到下匹配,首个命中生效
|
|
|
410
417
|
- 每次切换都会在服务日志中留下记录(key 只显示前后各 4 位)。
|
|
411
418
|
- 用 `flexgate status` 可以查看每个 provider 的 fallback 链和每个 key 的
|
|
412
419
|
实时用量。
|
|
420
|
+
- 智谱 GLM Coding Plan **团队版** key 在条目上配 `organization`/`project`
|
|
421
|
+
(团队组织/项目 ID)后,`flexgate status`/`usage` 会用团队版接口查询
|
|
422
|
+
套餐名、有效期和 5 小时/每周积分窗口;未配置这两个字段的 key 只做
|
|
423
|
+
连通性探测。
|
|
413
424
|
- 旧版 `api_key` + `fallback_keys` 写法仍然兼容,`flexgate update` 会
|
|
414
425
|
自动迁移为 `api_keys` 列表(config_version 3 → 4)。
|
|
415
426
|
|
|
@@ -320,6 +320,13 @@ providers:
|
|
|
320
320
|
- key: "your-minimax-api-key-2"
|
|
321
321
|
note: "备用账号"
|
|
322
322
|
- "your-minimax-api-key-3" # 纯字符串写法(无备注)
|
|
323
|
+
zhipu-team:
|
|
324
|
+
base_url: "https://open.bigmodel.cn/api/anthropic"
|
|
325
|
+
api_keys:
|
|
326
|
+
- key: "your-zhipu-team-api-key"
|
|
327
|
+
note: "队友名"
|
|
328
|
+
organization: "org-XXXXXXXX" # 智谱团队版 key:组织/项目 ID
|
|
329
|
+
project: "proj_XXXXXXXX" # (flexgate usage 查团队套餐额度用)
|
|
323
330
|
|
|
324
331
|
claude_settings:
|
|
325
332
|
default_opus_model: "claude-opus-4-7"
|
|
@@ -359,7 +366,7 @@ routes: # 从上到下匹配,首个命中生效
|
|
|
359
366
|
|------|------|
|
|
360
367
|
| `server.host/port` | 网关监听地址 |
|
|
361
368
|
| `providers.<name>.base_url` | Provider 的 API 地址 |
|
|
362
|
-
| `providers.<name>.api_keys` | API key 列表(一个或多个,互为 fallback);每项为 key 字符串或 `{key, note}
|
|
369
|
+
| `providers.<name>.api_keys` | API key 列表(一个或多个,互为 fallback);每项为 key 字符串或 `{key, note}`(`note` 是备注)。智谱团队版 key 还可加 `organization`/`project`(团队组织/项目 ID),供 `flexgate usage` 查询团队套餐额度 |
|
|
363
370
|
| `providers.<name>.available_models` | 该 provider 的可用模型列表,首个条目作为路由省略 `model` 时的回退模型 |
|
|
364
371
|
| `claude_settings.*` | 写入 settings.json 的模型和超时配置 |
|
|
365
372
|
| `routes[].pattern` | 正则匹配请求中的 model 字段(匹配前归一化裸别名 `sonnet`/`opus`/`haiku`/`default` 与旧编号名) |
|
|
@@ -393,6 +400,10 @@ routes: # 从上到下匹配,首个命中生效
|
|
|
393
400
|
- 每次切换都会在服务日志中留下记录(key 只显示前后各 4 位)。
|
|
394
401
|
- 用 `flexgate status` 可以查看每个 provider 的 fallback 链和每个 key 的
|
|
395
402
|
实时用量。
|
|
403
|
+
- 智谱 GLM Coding Plan **团队版** key 在条目上配 `organization`/`project`
|
|
404
|
+
(团队组织/项目 ID)后,`flexgate status`/`usage` 会用团队版接口查询
|
|
405
|
+
套餐名、有效期和 5 小时/每周积分窗口;未配置这两个字段的 key 只做
|
|
406
|
+
连通性探测。
|
|
396
407
|
- 旧版 `api_key` + `fallback_keys` 写法仍然兼容,`flexgate update` 会
|
|
397
408
|
自动迁移为 `api_keys` 列表(config_version 3 → 4)。
|
|
398
409
|
|
|
@@ -8,6 +8,9 @@ server:
|
|
|
8
8
|
# 日志都会显示)。某个 key 遇到 401/402/403/429/5xx/529 或连接错误(如额度
|
|
9
9
|
# 用尽、平台过载)时,按顺序自动切换到下一个 key 重试。同一上游的多个账号
|
|
10
10
|
# 应这样配置,而不是拆成多个 provider。
|
|
11
|
+
# 智谱 GLM Coding Plan 团队版 key 还需在 key 条目上配置 organization 和
|
|
12
|
+
# project(团队组织/项目 ID,从团队用量页的浏览器请求头里一次性获取);
|
|
13
|
+
# flexgate usage 依赖这两个字段查询团队套餐额度,未配置则只做连通性探测。
|
|
11
14
|
providers:
|
|
12
15
|
minimax:
|
|
13
16
|
base_url: "https://api.minimaxi.com/anthropic"
|
|
@@ -26,6 +29,15 @@ providers:
|
|
|
26
29
|
available_models:
|
|
27
30
|
- "glm-5.3" # 路由省略 model 时的回退模型
|
|
28
31
|
- "glm-4.6v"
|
|
32
|
+
zhipu-team:
|
|
33
|
+
base_url: "https://open.bigmodel.cn/api/anthropic"
|
|
34
|
+
api_keys:
|
|
35
|
+
- key: "your-zhipu-team-api-key"
|
|
36
|
+
note: "队友名"
|
|
37
|
+
organization: "org-XXXXXXXX" # 智谱团队版:组织 ID
|
|
38
|
+
project: "proj_XXXXXXXX" # 智谱团队版:项目 ID
|
|
39
|
+
available_models:
|
|
40
|
+
- "glm-5.3"
|
|
29
41
|
xiaomi:
|
|
30
42
|
base_url: "https://token-plan-cn.xiaomimimo.com/anthropic"
|
|
31
43
|
api_keys:
|
|
@@ -37,6 +37,13 @@ providers:
|
|
|
37
37
|
- key: "your-minimax-api-key-2"
|
|
38
38
|
note: "备用账号"
|
|
39
39
|
- "your-minimax-api-key-3" # 纯字符串写法(无备注)
|
|
40
|
+
zhipu-team:
|
|
41
|
+
base_url: "https://open.bigmodel.cn/api/anthropic"
|
|
42
|
+
api_keys:
|
|
43
|
+
- key: "your-zhipu-team-api-key"
|
|
44
|
+
note: "队友名"
|
|
45
|
+
organization: "org-XXXXXXXX" # 智谱团队版 key:组织/项目 ID
|
|
46
|
+
project: "proj_XXXXXXXX" # (flexgate usage 查团队套餐额度用)
|
|
40
47
|
|
|
41
48
|
claude_settings:
|
|
42
49
|
default_opus_model: "claude-opus-4-7"
|
|
@@ -76,7 +83,7 @@ routes: # 从上到下匹配,首个命中生效
|
|
|
76
83
|
|------|------|
|
|
77
84
|
| `server.host/port` | 网关监听地址 |
|
|
78
85
|
| `providers.<name>.base_url` | Provider 的 API 地址 |
|
|
79
|
-
| `providers.<name>.api_keys` | API key 列表(一个或多个,互为 fallback);每项为 key 字符串或 `{key, note}
|
|
86
|
+
| `providers.<name>.api_keys` | API key 列表(一个或多个,互为 fallback);每项为 key 字符串或 `{key, note}`(`note` 是备注)。智谱团队版 key 还可加 `organization`/`project`(团队组织/项目 ID),供 `flexgate usage` 查询团队套餐额度 |
|
|
80
87
|
| `providers.<name>.available_models` | 该 provider 的可用模型列表,首个条目作为路由省略 `model` 时的回退模型 |
|
|
81
88
|
| `claude_settings.*` | 写入 settings.json 的模型和超时配置 |
|
|
82
89
|
| `routes[].pattern` | 正则匹配请求中的 model 字段 |
|
|
@@ -109,6 +116,10 @@ routes: # 从上到下匹配,首个命中生效
|
|
|
109
116
|
当前使用的 key 和每个 key 的实时用量;在 `flexgate config edit` 交互
|
|
110
117
|
界面的「api keys」入口可以按路由查看各 key 的实时用量/有效性并切换
|
|
111
118
|
active key。
|
|
119
|
+
- 智谱 GLM Coding Plan **团队版** key 在条目上配 `organization`/`project`
|
|
120
|
+
(团队组织/项目 ID)后,`flexgate status`/`usage` 会用团队版接口查询
|
|
121
|
+
套餐名、有效期和 5 小时/每周积分窗口;未配置这两个字段的 key 只做
|
|
122
|
+
连通性探测(团队 key 不适用个人版查询接口)。
|
|
112
123
|
- 旧版 `api_key` + `fallback_keys` 写法仍然兼容,`flexgate update` 会
|
|
113
124
|
自动迁移为 `api_keys` 列表(config_version 3 → 4);路由上的
|
|
114
125
|
`active_key` 字段在 config_version 5 引入,旧配置的每条路由默认指向
|
|
@@ -8,7 +8,8 @@
|
|
|
8
8
|
|------|----------|------|
|
|
9
9
|
| MiniMax(api.minimaxi.com / api.minimax.io) | `GET /v1/api/openplatform/coding_plan/remains`(Bearer 认证,复用 provider key) | 官方 token plan 接口,返回 5 小时窗口和周窗口的**剩余**次数(注意响应里 `*_usage_count` 字段实际是剩余量) |
|
|
10
10
|
| Kimi Code(api.kimi.com) | `GET {base}/v1/usages`(Bearer 认证,复用 provider key) | 未公开文档、与 Kimi Code CLI `/usage` 相同的接口,返回周配额、5 小时滚动窗口剩余量和并发上限 |
|
|
11
|
-
| z.ai
|
|
11
|
+
| z.ai 个人版(api.z.ai) | `GET /api/monitor/usage/quota/limit`(Authorization 直接带 key) | 未公开文档、但 z.ai 官方 coding 插件在用的接口,返回各窗口已用百分比与重置时间。**仅对 GLM Coding Plan 个人订阅有效**:按量付费 key 会返回"当前用户不存在coding plan"并退化为 probe |
|
|
12
|
+
| 智谱团队版(open.bigmodel.cn) | `GET /api/monitor/usage/quota/limit?type=2`,带 `bigmodel-organization`/`bigmodel-project` 头(取自 key 条目的 `organization`/`project` 字段) | 同一 monitor 接口的团队版变体,返回 5 小时/每周窗口的积分用量(绝对值 + 百分比 + 重置时间);另查 `querySubscribeDetail` 显示套餐名与有效期。**key 条目未配 `organization`/`project` 时不查询**,退化为 probe(团队 key 不适用个人版接口) |
|
|
12
13
|
| USTC(api.llm.ustc.edu.cn,LiteLLM) | `GET /key/info`(Bearer 认证) | LiteLLM proxy 自带的 key 信息接口,返回 spend / max_budget / 限速等 |
|
|
13
14
|
| 小米 MiMo(token-plan-cn.xiaomimimo.com) | **无 key 可用的官方接口**(控制台内部接口需要浏览器 cookie,不采用) | 退化为 minimal probe |
|
|
14
15
|
| 其他/未知平台 | minimal chat probe | 发一条输入 `"hi"`、`max_tokens=128` 的最小 `/v1/messages` 请求,验证 key 是否还能正常服务(会消耗极少量额度) |
|
|
@@ -31,8 +32,8 @@
|
|
|
31
32
|
```text
|
|
32
33
|
Usage (timeout 15s per key)
|
|
33
34
|
2 key(s) skipped — last check failed (recheck with: flexgate usage --force)
|
|
34
|
-
|
|
35
|
-
✓ key #1 (
|
|
35
|
+
zai
|
|
36
|
+
✓ key #1 (9f2c***abCD) cached (15s ago)
|
|
36
37
|
z.ai quota API (unofficial) failed (platform error: 当前用户不存在coding plan); probe: key can serve glm-5.3
|
|
37
38
|
recheck with: flexgate usage --force
|
|
38
39
|
ustc
|
|
@@ -60,11 +61,17 @@ Usage (timeout 15s per key):
|
|
|
60
61
|
weekly remaining 63/100 (reset 08-28 22:17)
|
|
61
62
|
5h window remaining 74/100 (reset 08-23 14:17)
|
|
62
63
|
parallel limit: 30
|
|
64
|
+
zhipu-team:
|
|
65
|
+
✓ key #2 (ddb6***oZoj) [zhangsiyi] zhipu team quota API (unofficial)
|
|
66
|
+
plan: 团队套餐高级版 (until 2026-10-14, auto-renew off)
|
|
67
|
+
5h window: used 264/35000 credits (1%), reset 09-25 14:00
|
|
68
|
+
weekly window: used 93043/155000 credits (60%), reset 09-28 16:17
|
|
63
69
|
```
|
|
64
70
|
|
|
65
71
|
## 新增平台适配器
|
|
66
72
|
|
|
67
73
|
适配器登记在 `flexgate/usage.py` 的 `_ADAPTERS` 列表中:每条是
|
|
68
|
-
`(url 子串, 适配器协程, 方法标签)
|
|
69
|
-
|
|
70
|
-
|
|
74
|
+
`(url 子串, 适配器协程, 方法标签)`,首个命中生效。bigmodel.cn 的团队版
|
|
75
|
+
适配器额外要求 key 条目上配置 `organization`/`project`,否则视为无适配
|
|
76
|
+
接口。已知没有 key 可用接口的平台放入 `_PROBE_ONLY_MARKERS`,直接走
|
|
77
|
+
probe,避免浪费一次失败的 HTTP 请求。
|
|
@@ -35,9 +35,16 @@ def is_placeholder_key(key: str) -> bool:
|
|
|
35
35
|
|
|
36
36
|
@dataclass
|
|
37
37
|
class ApiKey:
|
|
38
|
-
"""One API key for a provider, with an optional human-readable note.
|
|
38
|
+
"""One API key for a provider, with an optional human-readable note.
|
|
39
|
+
|
|
40
|
+
``organization``/``project`` identify the team a zhipu (bigmodel.cn)
|
|
41
|
+
GLM Coding Plan *team* key belongs to; the usage check passes them as the
|
|
42
|
+
bigmodel-organization/bigmodel-project headers of the team quota query.
|
|
43
|
+
"""
|
|
39
44
|
key: str
|
|
40
45
|
note: str = ""
|
|
46
|
+
organization: str = ""
|
|
47
|
+
project: str = ""
|
|
41
48
|
|
|
42
49
|
|
|
43
50
|
@dataclass
|
|
@@ -122,7 +129,9 @@ def _parse_api_keys(name: str, prov: dict) -> list[ApiKey]:
|
|
|
122
129
|
"""Parse a provider's key list.
|
|
123
130
|
|
|
124
131
|
Current schema: ``api_keys`` — a list whose entries are either a plain
|
|
125
|
-
key string or a mapping ``{key: ..., note:
|
|
132
|
+
key string or a mapping ``{key: ..., note: ..., organization: ...,
|
|
133
|
+
project: ...}`` (``note`` free-form; ``organization``/``project`` are the
|
|
134
|
+
zhipu team IDs used by the usage check). The legacy schema
|
|
126
135
|
(``api_key`` plus optional ``fallback_keys``) is still accepted.
|
|
127
136
|
"""
|
|
128
137
|
raw = prov.get("api_keys")
|
|
@@ -136,7 +145,19 @@ def _parse_api_keys(name: str, prov: dict) -> list[ApiKey]:
|
|
|
136
145
|
if isinstance(item, str):
|
|
137
146
|
keys.append(ApiKey(key=item))
|
|
138
147
|
elif isinstance(item, dict) and isinstance(item.get("key"), str):
|
|
139
|
-
|
|
148
|
+
organization = str(item.get("organization") or "")
|
|
149
|
+
project = str(item.get("project") or "")
|
|
150
|
+
if bool(organization) != bool(project):
|
|
151
|
+
raise ValueError(
|
|
152
|
+
f"Provider '{name}': api_keys entry '{item['key'][:4]}***' sets "
|
|
153
|
+
f"only one of 'organization'/'project' — set both or neither"
|
|
154
|
+
)
|
|
155
|
+
keys.append(ApiKey(
|
|
156
|
+
key=item["key"],
|
|
157
|
+
note=str(item.get("note") or ""),
|
|
158
|
+
organization=organization,
|
|
159
|
+
project=project,
|
|
160
|
+
))
|
|
140
161
|
else:
|
|
141
162
|
raise ValueError(
|
|
142
163
|
f"Provider '{name}': each api_keys entry must be a key string "
|
|
@@ -262,6 +283,21 @@ def _format_hhmm(minutes: int) -> str:
|
|
|
262
283
|
return f"{minutes // 60:02d}:{minutes % 60:02d}"
|
|
263
284
|
|
|
264
285
|
|
|
286
|
+
def _serialize_key(k: ApiKey) -> dict | str:
|
|
287
|
+
"""Key entry → YAML value: a plain string, or a mapping that keeps the
|
|
288
|
+
note and the zhipu-team organization/project fields."""
|
|
289
|
+
if not (k.note or k.organization or k.project):
|
|
290
|
+
return k.key
|
|
291
|
+
data: dict = {"key": k.key}
|
|
292
|
+
if k.note:
|
|
293
|
+
data["note"] = k.note
|
|
294
|
+
if k.organization:
|
|
295
|
+
data["organization"] = k.organization
|
|
296
|
+
if k.project:
|
|
297
|
+
data["project"] = k.project
|
|
298
|
+
return data
|
|
299
|
+
|
|
300
|
+
|
|
265
301
|
def save_config(cfg: GatewayConfig, path: str | None = None) -> None:
|
|
266
302
|
import yaml
|
|
267
303
|
|
|
@@ -287,10 +323,7 @@ def save_config(cfg: GatewayConfig, path: str | None = None) -> None:
|
|
|
287
323
|
for name, prov in cfg.providers.items():
|
|
288
324
|
entry: dict = {
|
|
289
325
|
"base_url": prov.base_url,
|
|
290
|
-
"api_keys": [
|
|
291
|
-
{"key": k.key, "note": k.note} if k.note else k.key
|
|
292
|
-
for k in prov.api_keys
|
|
293
|
-
],
|
|
326
|
+
"api_keys": [_serialize_key(k) for k in prov.api_keys],
|
|
294
327
|
}
|
|
295
328
|
if prov.available_models:
|
|
296
329
|
entry["available_models"] = list(prov.available_models)
|
|
@@ -329,7 +362,11 @@ server:
|
|
|
329
362
|
# a plain key string or {key, note} where `note` is a free-form label stored
|
|
330
363
|
# in the config (shown by `flexgate status`, included in fallback logs).
|
|
331
364
|
# Multiple accounts on the same upstream should be configured this way
|
|
332
|
-
# instead of as separate providers.
|
|
365
|
+
# instead of as separate providers. zhipu GLM Coding Plan *team* keys also
|
|
366
|
+
# carry `organization`/`project` — the team IDs copied once from the team
|
|
367
|
+
# usage page's request headers; `flexgate usage` needs them to query the
|
|
368
|
+
# team plan quota, and without them the key only gets an availability probe.
|
|
369
|
+
# A route's `active_key` (see below)
|
|
333
370
|
# selects which key its requests start from; when that key fails with a
|
|
334
371
|
# retryable error (401/402/403/429/5xx/529 or a connection error — e.g.
|
|
335
372
|
# quota exhausted or provider overloaded), the request is retried with the
|
|
@@ -353,6 +390,15 @@ providers:
|
|
|
353
390
|
available_models:
|
|
354
391
|
- "glm-5.3" # used as fallback when a route omits `model`
|
|
355
392
|
- "glm-4.6v"
|
|
393
|
+
zhipu-team:
|
|
394
|
+
base_url: "https://open.bigmodel.cn/api/anthropic"
|
|
395
|
+
api_keys:
|
|
396
|
+
- key: "your-zhipu-team-api-key"
|
|
397
|
+
note: "teammate name"
|
|
398
|
+
organization: "org-XXXXXXXX" # zhipu team IDs (see comment above)
|
|
399
|
+
project: "proj_XXXXXXXX"
|
|
400
|
+
available_models:
|
|
401
|
+
- "glm-5.3"
|
|
356
402
|
xiaomi:
|
|
357
403
|
base_url: "https://token-plan-cn.xiaomimimo.com/anthropic"
|
|
358
404
|
api_keys:
|
|
@@ -14,6 +14,11 @@ known adapter and queries it with the provider's own API key:
|
|
|
14
14
|
returns weekly quota, 5-hour window and parallel limit)
|
|
15
15
|
* z.ai → GET {origin}/api/monitor/usage/quota/limit
|
|
16
16
|
(undocumented but used by z.ai's own coding plugin)
|
|
17
|
+
* zhipu team→ GET {origin}/api/monitor/usage/quota/limit?type=2 with
|
|
18
|
+
bigmodel-organization/bigmodel-project headers taken from the
|
|
19
|
+
key entry's organization/project fields, plus the team
|
|
20
|
+
subscription detail for the plan name/expiry; keys without
|
|
21
|
+
both fields cannot be queried and get the plain probe
|
|
17
22
|
* LiteLLM → GET {origin}/key/info (e.g. USTC api.llm.ustc.edu.cn)
|
|
18
23
|
* anything else (e.g. Xiaomi MiMo, which has no key-based usage API)
|
|
19
24
|
→ minimal chat probe: POST /v1/messages with input "hi" and
|
|
@@ -176,8 +181,9 @@ async def _get_json(
|
|
|
176
181
|
|
|
177
182
|
|
|
178
183
|
async def _usage_minimax(
|
|
179
|
-
client: httpx.AsyncClient, provider: ProviderConfig,
|
|
184
|
+
client: httpx.AsyncClient, provider: ProviderConfig, entry: ApiKey, timeout: float
|
|
180
185
|
) -> tuple[list[str], str | None]:
|
|
186
|
+
key = entry.key
|
|
181
187
|
url = f"{_origin(provider.base_url)}/v1/api/openplatform/coding_plan/remains"
|
|
182
188
|
headers = {"Authorization": f"Bearer {key}", "Content-Type": "application/json"}
|
|
183
189
|
data, err = await _get_json(client, url, headers, timeout)
|
|
@@ -217,8 +223,9 @@ async def _usage_minimax(
|
|
|
217
223
|
|
|
218
224
|
|
|
219
225
|
async def _usage_zai(
|
|
220
|
-
client: httpx.AsyncClient, provider: ProviderConfig,
|
|
226
|
+
client: httpx.AsyncClient, provider: ProviderConfig, entry: ApiKey, timeout: float
|
|
221
227
|
) -> tuple[list[str], str | None]:
|
|
228
|
+
key = entry.key
|
|
222
229
|
url = f"{_origin(provider.base_url)}/api/monitor/usage/quota/limit"
|
|
223
230
|
headers = {"Authorization": key, "Content-Type": "application/json"}
|
|
224
231
|
data, err = await _get_json(client, url, headers, timeout)
|
|
@@ -247,9 +254,75 @@ async def _usage_zai(
|
|
|
247
254
|
return lines or ["no quota data"], None
|
|
248
255
|
|
|
249
256
|
|
|
257
|
+
def _zhipu_team_headers(entry: ApiKey) -> dict[str, str]:
|
|
258
|
+
return {
|
|
259
|
+
"Authorization": entry.key,
|
|
260
|
+
"Content-Type": "application/json",
|
|
261
|
+
"bigmodel-organization": entry.organization,
|
|
262
|
+
"bigmodel-project": entry.project,
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
async def _zhipu_team_plan(
|
|
267
|
+
client: httpx.AsyncClient, provider: ProviderConfig, entry: ApiKey, timeout: float
|
|
268
|
+
) -> str | None:
|
|
269
|
+
"""Best-effort team subscription detail: plan name and expiry."""
|
|
270
|
+
url = f"{_origin(provider.base_url)}/api/biz/team/subscribe/product/querySubscribeDetail"
|
|
271
|
+
data, err = await _get_json(client, url, _zhipu_team_headers(entry), timeout)
|
|
272
|
+
if err or not data.get("success", False):
|
|
273
|
+
return None
|
|
274
|
+
detail = data.get("data") or {}
|
|
275
|
+
name = detail.get("productName")
|
|
276
|
+
if not name:
|
|
277
|
+
return None
|
|
278
|
+
line = f"plan: {name}"
|
|
279
|
+
end = detail.get("subscribeEndTime")
|
|
280
|
+
if end:
|
|
281
|
+
line += f" (until {end}"
|
|
282
|
+
if detail.get("autoRenew") == 0:
|
|
283
|
+
line += ", auto-renew off"
|
|
284
|
+
line += ")"
|
|
285
|
+
return line
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
async def _usage_zhipu_team(
|
|
289
|
+
client: httpx.AsyncClient, provider: ProviderConfig, entry: ApiKey, timeout: float
|
|
290
|
+
) -> tuple[list[str], str | None]:
|
|
291
|
+
url = f"{_origin(provider.base_url)}/api/monitor/usage/quota/limit?type=2"
|
|
292
|
+
data, err = await _get_json(client, url, _zhipu_team_headers(entry), timeout)
|
|
293
|
+
if err:
|
|
294
|
+
return [], err
|
|
295
|
+
if not data.get("success", False):
|
|
296
|
+
return [], f"platform error: {data.get('msg', 'unknown')}"
|
|
297
|
+
payload = data.get("data", {}) or {}
|
|
298
|
+
limits = payload.get("limits") or []
|
|
299
|
+
if not limits:
|
|
300
|
+
# type=2 answers success with empty limits when the key does not
|
|
301
|
+
# belong to the configured organization/project.
|
|
302
|
+
return [], "empty limits — key is not a member of the configured organization/project"
|
|
303
|
+
lines: list[str] = []
|
|
304
|
+
plan = await _zhipu_team_plan(client, provider, entry, timeout)
|
|
305
|
+
if plan:
|
|
306
|
+
lines.append(plan)
|
|
307
|
+
for limit in limits:
|
|
308
|
+
if not isinstance(limit, dict):
|
|
309
|
+
continue
|
|
310
|
+
# team plans are credit-metered; keep TOKENS_LIMIT handling anyway
|
|
311
|
+
if limit.get("type") not in ("CREDIT_LIMIT", "TOKENS_LIMIT"):
|
|
312
|
+
continue
|
|
313
|
+
# unit 3 = N-hour rolling window, unit 6 = weekly window
|
|
314
|
+
window = f"{limit.get('number', '?')}h" if limit.get("unit") == 3 else "weekly"
|
|
315
|
+
lines.append(
|
|
316
|
+
f"{window} window: used {limit.get('currentValue', '?')}/{limit.get('usage', '?')} "
|
|
317
|
+
f"credits ({limit.get('percentage', '?')}%), reset {_fmt_epoch_ms(limit.get('nextResetTime'))}"
|
|
318
|
+
)
|
|
319
|
+
return lines or ["no quota data"], None
|
|
320
|
+
|
|
321
|
+
|
|
250
322
|
async def _usage_litellm(
|
|
251
|
-
client: httpx.AsyncClient, provider: ProviderConfig,
|
|
323
|
+
client: httpx.AsyncClient, provider: ProviderConfig, entry: ApiKey, timeout: float
|
|
252
324
|
) -> tuple[list[str], str | None]:
|
|
325
|
+
key = entry.key
|
|
253
326
|
url = f"{_origin(provider.base_url)}/key/info"
|
|
254
327
|
headers = {"Authorization": f"Bearer {key}"}
|
|
255
328
|
data, err = await _get_json(client, url, headers, timeout)
|
|
@@ -278,8 +351,9 @@ async def _usage_litellm(
|
|
|
278
351
|
|
|
279
352
|
|
|
280
353
|
async def _usage_kimi(
|
|
281
|
-
client: httpx.AsyncClient, provider: ProviderConfig,
|
|
354
|
+
client: httpx.AsyncClient, provider: ProviderConfig, entry: ApiKey, timeout: float
|
|
282
355
|
) -> tuple[list[str], str | None]:
|
|
356
|
+
key = entry.key
|
|
283
357
|
base = provider.base_url.rstrip("/")
|
|
284
358
|
url = f"{base}/usages" if base.endswith("/v1") else f"{base}/v1/usages"
|
|
285
359
|
headers = {"Authorization": f"Bearer {key}"}
|
|
@@ -323,18 +397,31 @@ _ADAPTERS = [
|
|
|
323
397
|
("minimax.io", _usage_minimax, "MiniMax coding_plan API"),
|
|
324
398
|
("api.kimi.com", _usage_kimi, "Kimi Code usages API (unofficial)"),
|
|
325
399
|
("z.ai", _usage_zai, "z.ai quota API (unofficial)"),
|
|
326
|
-
("bigmodel.cn",
|
|
400
|
+
("bigmodel.cn", _usage_zhipu_team, "zhipu team quota API (unofficial)"),
|
|
327
401
|
("llm.ustc.edu.cn", _usage_litellm, "LiteLLM /key/info"),
|
|
328
402
|
]
|
|
329
403
|
|
|
330
404
|
# Platforms known to have NO key-based usage API — go straight to the probe.
|
|
331
405
|
_PROBE_ONLY_MARKERS = ("xiaomimimo.com",)
|
|
332
406
|
|
|
407
|
+
# bigmodel.cn marker, reused to explain why a key got no quota adapter.
|
|
408
|
+
_ZHIPU_MARKER = "bigmodel.cn"
|
|
409
|
+
|
|
333
410
|
|
|
334
|
-
def _find_adapter(base_url: str):
|
|
411
|
+
def _find_adapter(base_url: str, entry: ApiKey):
|
|
412
|
+
"""First matching adapter for a base_url. Returns (adapter, label).
|
|
413
|
+
|
|
414
|
+
A bigmodel.cn (zhipu) team key whose entry lacks organization/project has
|
|
415
|
+
no quota endpoint to query — the personal coding-plan endpoint does not
|
|
416
|
+
apply to team keys — so it returns (None, None) and falls through to the
|
|
417
|
+
availability probe.
|
|
418
|
+
"""
|
|
335
419
|
for marker, adapter, label in _ADAPTERS:
|
|
336
|
-
if marker in base_url:
|
|
337
|
-
|
|
420
|
+
if marker not in base_url:
|
|
421
|
+
continue
|
|
422
|
+
if adapter is _usage_zhipu_team and not (entry.organization and entry.project):
|
|
423
|
+
return None, None
|
|
424
|
+
return adapter, label
|
|
338
425
|
return None, None
|
|
339
426
|
|
|
340
427
|
|
|
@@ -391,10 +478,10 @@ async def check_key_usage(
|
|
|
391
478
|
if any(marker in provider.base_url for marker in _PROBE_ONLY_MARKERS):
|
|
392
479
|
adapter, method = None, None
|
|
393
480
|
else:
|
|
394
|
-
adapter, method = _find_adapter(provider.base_url)
|
|
481
|
+
adapter, method = _find_adapter(provider.base_url, entry)
|
|
395
482
|
|
|
396
483
|
if adapter is not None:
|
|
397
|
-
lines, err = await adapter(client, provider,
|
|
484
|
+
lines, err = await adapter(client, provider, entry, timeout)
|
|
398
485
|
if err is None:
|
|
399
486
|
return UsageResult(provider.name, label, method, True, lines)
|
|
400
487
|
logger.debug("usage adapter %s failed for %s: %s; falling back to probe", method, provider.name, err)
|
|
@@ -410,6 +497,10 @@ async def check_key_usage(
|
|
|
410
497
|
lines, err = await _probe_chat(client, provider, key, timeout)
|
|
411
498
|
method = f'minimal chat probe ("{_PROBE_INPUT}", max_tokens={_PROBE_MAX_TOKENS})'
|
|
412
499
|
if err is None:
|
|
500
|
+
if _ZHIPU_MARKER in provider.base_url and not (entry.organization and entry.project):
|
|
501
|
+
lines = lines + [
|
|
502
|
+
"team quota not queried: set organization/project on this key entry"
|
|
503
|
+
]
|
|
413
504
|
return UsageResult(provider.name, label, method, True, lines)
|
|
414
505
|
return UsageResult(provider.name, label, method, False, [err])
|
|
415
506
|
|
|
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
|
|
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
|