feat: 新增 Gemini CLI provider adapter

- 新增 gemini_cli adapter 包(client/constants/envelope/plugin/quota)
- 实现 v1internal 协议封装、OAuth enrichment、loadCodeAssist/onboardUser 流程
- 实现配额耗尽检测与冷却元数据管理(RESOURCE_EXHAUSTED 解析)
- 新增 GeminiCliQuotaReader 支持按模型粒度的配额展示
- endpoint check 支持流式回退和 v1internal 响应解包
- health_policy 429 处理增加 Google 配额冷却解析
- error_handler 在限流时同步 Gemini CLI 配额状态
- 前端添加 Gemini CLI provider 类型选项和 OAuth 图标
- 新增 preset models(gemini-2.5-pro/flash, gemini-3-pro/flash, gemini-3.1-pro)
- 新增 gemini_cli quota 单元测试

Closes #216

Co-authored-by: Entropy.Xu <entropy.xu@cloudhabitatsh.com>
This commit is contained in:
fawney19
2026-03-10 23:07:50 +08:00
parent 85aa66c76d
commit 1e39ab3c2e
21 changed files with 1317 additions and 31 deletions

View File

@@ -0,0 +1,117 @@
from __future__ import annotations
from types import SimpleNamespace
from typing import Any, cast
from src.services.orchestration.error_handler import ErrorHandlerService
from src.services.provider.adapters.gemini_cli.quota import (
build_quota_exhausted_metadata,
extract_quota_cooldown_seconds,
)
class _FakeDB:
def __init__(self) -> None:
self.added: list[object] = []
self.commit_count = 0
def add(self, obj: object) -> None:
self.added.append(obj)
def commit(self) -> None:
self.commit_count += 1
def test_extract_quota_cooldown_seconds_from_message_reset_after() -> None:
error_text = """
{
"error": {
"code": 429,
"message": "You have exhausted your capacity on this model. Your quota will reset after 27s.",
"status": "RESOURCE_EXHAUSTED",
"details": [
{
"@type": "type.googleapis.com/google.rpc.ErrorInfo",
"reason": "RATE_LIMIT_EXCEEDED",
"domain": "cloudcode-pa.googleapis.com",
"metadata": {
"model": "gemini-3-pro-preview"
}
}
]
}
}
"""
assert extract_quota_cooldown_seconds(error_text, now_ts=1_700_000_000) == 27
def test_build_quota_exhausted_metadata_skips_unknown_reset_window() -> None:
error_text = """
{
"error": {
"code": 429,
"message": "No capacity available for model gemini-3.1-pro-preview on the server",
"status": "RESOURCE_EXHAUSTED",
"details": [
{
"@type": "type.googleapis.com/google.rpc.ErrorInfo",
"reason": "MODEL_CAPACITY_EXHAUSTED",
"domain": "cloudcode-pa.googleapis.com",
"metadata": {
"model": "gemini-3.1-pro-preview"
}
}
]
}
}
"""
assert (
build_quota_exhausted_metadata(
model_name="gemini-3.1-pro-preview",
error_text=error_text,
current_namespace=None,
now_ts=1_700_000_000,
)
is None
)
def test_sync_gemini_cli_quota_state_uses_error_model_not_global_model_id() -> None:
db = _FakeDB()
service = ErrorHandlerService(db=cast(Any, db))
key = SimpleNamespace(id="k1", upstream_metadata={})
provider = SimpleNamespace(provider_type="gemini_cli")
error_text = """
{
"error": {
"code": 429,
"message": "You have exhausted your capacity on this model. Your quota will reset after 27s.",
"status": "RESOURCE_EXHAUSTED",
"details": [
{
"@type": "type.googleapis.com/google.rpc.ErrorInfo",
"reason": "RATE_LIMIT_EXCEEDED",
"domain": "cloudcode-pa.googleapis.com",
"metadata": {
"model": "gemini-3-pro-preview"
}
}
]
}
}
"""
service._sync_gemini_cli_quota_state(
key=cast(Any, key),
provider=cast(Any, provider),
model_name="17855939-7164-44b3-9354-f87851b25ec6",
error_text=error_text,
request_id="req-1",
)
quota_by_model = key.upstream_metadata["gemini_cli"]["quota_by_model"]
assert "gemini-3-pro-preview" in quota_by_model
assert "17855939-7164-44b3-9354-f87851b25ec6" not in quota_by_model
assert db.commit_count == 1