coding-proxy 0.2.1a2__py3-none-any.whl → 0.2.2__py3-none-any.whl
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.
- coding/proxy/cli/__init__.py +37 -3
- coding/proxy/config/config.default.yaml +6 -1
- coding/proxy/logging/__init__.py +13 -9
- coding/proxy/logging/formatters.py +14 -36
- coding/proxy/routing/circuit_breaker.py +30 -7
- coding/proxy/routing/executor.py +59 -5
- coding/proxy/routing/router.py +60 -0
- coding/proxy/routing/tier.py +24 -2
- coding/proxy/server/app.py +3 -1
- coding/proxy/server/factory.py +4 -1
- coding/proxy/server/routes.py +44 -2
- coding/proxy/vendors/antigravity.py +134 -3
- {coding_proxy-0.2.1a2.dist-info → coding_proxy-0.2.2.dist-info}/METADATA +2 -2
- {coding_proxy-0.2.1a2.dist-info → coding_proxy-0.2.2.dist-info}/RECORD +17 -17
- {coding_proxy-0.2.1a2.dist-info → coding_proxy-0.2.2.dist-info}/WHEEL +0 -0
- {coding_proxy-0.2.1a2.dist-info → coding_proxy-0.2.2.dist-info}/entry_points.txt +0 -0
- {coding_proxy-0.2.1a2.dist-info → coding_proxy-0.2.2.dist-info}/licenses/LICENSE +0 -0
coding/proxy/cli/__init__.py
CHANGED
|
@@ -216,16 +216,50 @@ async def _run_usage(
|
|
|
216
216
|
@app.command()
|
|
217
217
|
def reset(
|
|
218
218
|
port: int = typer.Option(8046, "--port", "-p", help="代理服务端口"),
|
|
219
|
+
vendor: str | None = typer.Option(
|
|
220
|
+
None,
|
|
221
|
+
"--vendor",
|
|
222
|
+
"-v",
|
|
223
|
+
help="提升/重排序 vendor 优先级(单个或逗号分隔多个)",
|
|
224
|
+
),
|
|
219
225
|
) -> None:
|
|
220
|
-
"""
|
|
226
|
+
"""重置所有层级的熔断器和配额守卫.
|
|
227
|
+
|
|
228
|
+
可通过 -v 指定运行时 N-tier 链路重排序:
|
|
229
|
+
|
|
230
|
+
\b
|
|
231
|
+
-v zhipu 提升 zhipu 到最高优先级
|
|
232
|
+
-v zhipu,anthropic 替换整个 N-tier 链路顺序
|
|
233
|
+
"""
|
|
221
234
|
import httpx
|
|
222
235
|
|
|
236
|
+
# 构建请求 body
|
|
237
|
+
json_body: dict | None = None
|
|
238
|
+
if vendor:
|
|
239
|
+
parts = [v.strip() for v in vendor.split(",") if v.strip()]
|
|
240
|
+
if parts:
|
|
241
|
+
json_body = {"vendors": parts}
|
|
242
|
+
|
|
223
243
|
try:
|
|
224
|
-
resp = httpx.post(
|
|
244
|
+
resp = httpx.post(
|
|
245
|
+
f"http://127.0.0.1:{port}/api/reset",
|
|
246
|
+
json=json_body,
|
|
247
|
+
timeout=5,
|
|
248
|
+
)
|
|
225
249
|
if resp.status_code == 200:
|
|
250
|
+
data = resp.json()
|
|
226
251
|
console.print("[green]所有层级的熔断器和配额守卫已重置[/green]")
|
|
252
|
+
tier_order = data.get("tier_order")
|
|
253
|
+
if tier_order:
|
|
254
|
+
order_str = " → ".join(tier_order)
|
|
255
|
+
console.print(f"[cyan]当前链路顺序:[/] {order_str}")
|
|
227
256
|
else:
|
|
228
|
-
|
|
257
|
+
try:
|
|
258
|
+
err = resp.json()
|
|
259
|
+
msg = err.get("error", {}).get("message", resp.text)
|
|
260
|
+
except Exception:
|
|
261
|
+
msg = resp.text
|
|
262
|
+
console.print(f"[red]重置失败: {msg}[/red]")
|
|
229
263
|
except httpx.ConnectError:
|
|
230
264
|
console.print("[red]代理服务未运行[/red]")
|
|
231
265
|
|
|
@@ -111,7 +111,7 @@ vendors:
|
|
|
111
111
|
# 不配置 circuit_breaker → 自动成为终端层,不触发向下故障转移
|
|
112
112
|
circuit_breaker:
|
|
113
113
|
failure_threshold: 3
|
|
114
|
-
recovery_timeout_seconds:
|
|
114
|
+
recovery_timeout_seconds: 30
|
|
115
115
|
success_threshold: 2
|
|
116
116
|
quota_guard:
|
|
117
117
|
enabled: true # 启用后按 Premium Requests 配额管理
|
|
@@ -421,6 +421,11 @@ pricing:
|
|
|
421
421
|
input_cost_per_mtok: ¥0.80
|
|
422
422
|
output_cost_per_mtok: ¥2.00
|
|
423
423
|
cache_read_cost_per_mtok: ¥0.16
|
|
424
|
+
- vendor: zhipu
|
|
425
|
+
model: glm-4.7 # 待区分长短上下文定价
|
|
426
|
+
input_cost_per_mtok: ¥2.00
|
|
427
|
+
output_cost_per_mtok: ¥8.00
|
|
428
|
+
cache_read_cost_per_mtok: ¥0.40
|
|
424
429
|
- vendor: zhipu
|
|
425
430
|
model: glm-5v-turbo # 待区分长短上下文定价
|
|
426
431
|
input_cost_per_mtok: ¥5.00
|
coding/proxy/logging/__init__.py
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"""日志模块.
|
|
2
2
|
|
|
3
|
-
提供 uvicorn 兼容的 dictConfig
|
|
3
|
+
提供 uvicorn 兼容的 dictConfig 构建、文件日志字符串格式化器、
|
|
4
4
|
以及 gzip 压缩轮转支持。
|
|
5
5
|
"""
|
|
6
6
|
|
|
@@ -12,7 +12,7 @@ import logging.handlers
|
|
|
12
12
|
import os
|
|
13
13
|
from pathlib import Path
|
|
14
14
|
|
|
15
|
-
from .formatters import
|
|
15
|
+
from .formatters import FileFormatter
|
|
16
16
|
|
|
17
17
|
# ── 常量 ────────────────────────────────────────────────────────
|
|
18
18
|
|
|
@@ -83,7 +83,7 @@ def build_log_config(
|
|
|
83
83
|
|
|
84
84
|
双写行为:
|
|
85
85
|
- 控制台:人类可读格式,级别由 ``level`` 参数控制(handler 级别过滤)
|
|
86
|
-
-
|
|
86
|
+
- 文件:字符串格式(与控制台风格一致),固定 DEBUG 级别(捕获所有日志)
|
|
87
87
|
- 当 ``file_path`` 为 ``None`` 或空字符串时,退化为纯控制台模式(向后兼容)
|
|
88
88
|
"""
|
|
89
89
|
config: dict = {
|
|
@@ -118,7 +118,11 @@ def build_log_config(
|
|
|
118
118
|
},
|
|
119
119
|
"loggers": {
|
|
120
120
|
"uvicorn": {"handlers": ["default"], "level": level, "propagate": False},
|
|
121
|
-
"uvicorn.error": {
|
|
121
|
+
"uvicorn.error": {
|
|
122
|
+
"handlers": ["default"],
|
|
123
|
+
"level": level,
|
|
124
|
+
"propagate": False,
|
|
125
|
+
},
|
|
122
126
|
"uvicorn.access": {
|
|
123
127
|
"handlers": ["access"],
|
|
124
128
|
"level": "INFO",
|
|
@@ -138,16 +142,16 @@ def build_log_config(
|
|
|
138
142
|
log_file = Path(file_path)
|
|
139
143
|
log_file.parent.mkdir(parents=True, exist_ok=True)
|
|
140
144
|
|
|
141
|
-
#
|
|
142
|
-
config["formatters"]["
|
|
143
|
-
"()": "coding.proxy.logging.formatters.
|
|
145
|
+
# 注入文件日志字符串 formatter
|
|
146
|
+
config["formatters"]["file_fmt"] = {
|
|
147
|
+
"()": "coding.proxy.logging.formatters.FileFormatter",
|
|
144
148
|
}
|
|
145
149
|
|
|
146
150
|
# 注入 RotatingFileHandler(gzip 压缩轮转)
|
|
147
151
|
# 使用工厂函数(而非 class + namer/rotator kwargs),
|
|
148
152
|
# 因为 dictConfig 不支持将 namer/rotator 作为构造参数传递
|
|
149
153
|
config["handlers"]["file"] = {
|
|
150
|
-
"formatter": "
|
|
154
|
+
"formatter": "file_fmt",
|
|
151
155
|
"()": "coding.proxy.logging._create_rotating_file_handler",
|
|
152
156
|
"filename": str(log_file.resolve()),
|
|
153
157
|
"maxBytes": max_bytes,
|
|
@@ -182,7 +186,7 @@ def build_log_config(
|
|
|
182
186
|
|
|
183
187
|
__all__ = [
|
|
184
188
|
"build_log_config",
|
|
185
|
-
"
|
|
189
|
+
"FileFormatter",
|
|
186
190
|
"_gzip_namer",
|
|
187
191
|
"_gzip_rotator",
|
|
188
192
|
]
|
|
@@ -1,49 +1,27 @@
|
|
|
1
|
-
"""
|
|
1
|
+
"""文件日志格式化器(字符串输出).
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
为文件日志提供人类可读的字符串格式输出,
|
|
4
|
+
与控制台输出风格一致,便于人工阅读和 grep 检索。
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
7
|
from __future__ import annotations
|
|
8
8
|
|
|
9
|
-
import json
|
|
10
9
|
import logging
|
|
11
|
-
from datetime import UTC, datetime
|
|
12
10
|
|
|
13
11
|
|
|
14
|
-
class
|
|
15
|
-
"""将 LogRecord
|
|
12
|
+
class FileFormatter(logging.Formatter):
|
|
13
|
+
"""将 LogRecord 格式化为单行可读字符串.
|
|
16
14
|
|
|
17
|
-
|
|
18
|
-
- ``timestamp``: ISO 8601 UTC 时间戳
|
|
19
|
-
- ``level``: 日志级别名称(DEBUG/INFO/WARNING/ERROR)
|
|
20
|
-
- ``logger``: logger 名称(如 ``coding.proxy.routing.executor``)
|
|
21
|
-
- ``message``: 格式化后的日志消息
|
|
22
|
-
- ``exception``: 异常堆栈(仅当存在时)
|
|
15
|
+
输出格式:``2026-04-11 16:51:13 INFO ModelCall: vendor=zhipu ...``
|
|
23
16
|
|
|
24
17
|
设计要点:
|
|
25
|
-
-
|
|
26
|
-
-
|
|
27
|
-
-
|
|
28
|
-
- ``sort_keys=True`` 保证输出确定性,便于日志聚合工具处理
|
|
18
|
+
- 时间戳使用 ``yyyy-MM-dd HH:mm:ss`` 格式(与控制台一致)
|
|
19
|
+
- 日志级别左对齐 5 字符宽度,保证多行对齐美观
|
|
20
|
+
- 无 ANSI 颜色码(文件输出不需要终端转义)
|
|
29
21
|
"""
|
|
30
22
|
|
|
31
|
-
def
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
if record.exc_info and record.exc_info[0] is not None:
|
|
37
|
-
exception = self.formatException(record.exc_info)
|
|
38
|
-
|
|
39
|
-
log_entry: dict[str, object] = {
|
|
40
|
-
"timestamp": datetime.fromtimestamp(record.created, tz=UTC).isoformat(),
|
|
41
|
-
"level": record.levelname,
|
|
42
|
-
"logger": record.name,
|
|
43
|
-
"message": message,
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
if exception:
|
|
47
|
-
log_entry["exception"] = exception
|
|
48
|
-
|
|
49
|
-
return json.dumps(log_entry, ensure_ascii=False, sort_keys=True)
|
|
23
|
+
def __init__(self) -> None:
|
|
24
|
+
super().__init__(
|
|
25
|
+
fmt="%(asctime)s %(levelname)-5s %(message)s",
|
|
26
|
+
datefmt="%Y-%m-%d %H:%M:%S",
|
|
27
|
+
)
|
|
@@ -32,11 +32,14 @@ class CircuitBreaker:
|
|
|
32
32
|
recovery_timeout_seconds: int = 300,
|
|
33
33
|
success_threshold: int = 2,
|
|
34
34
|
max_recovery_seconds: int = 3600,
|
|
35
|
+
*,
|
|
36
|
+
vendor_name: str = "",
|
|
35
37
|
) -> None:
|
|
36
38
|
self._failure_threshold = failure_threshold
|
|
37
39
|
self._recovery_timeout = recovery_timeout_seconds
|
|
38
40
|
self._success_threshold = success_threshold
|
|
39
41
|
self._max_recovery = max_recovery_seconds
|
|
42
|
+
self._vendor_label = f" [{vendor_name}]" if vendor_name else ""
|
|
40
43
|
|
|
41
44
|
self._state = CircuitState.CLOSED
|
|
42
45
|
self._failure_count = 0
|
|
@@ -65,7 +68,13 @@ class CircuitBreaker:
|
|
|
65
68
|
self._success_count += 1
|
|
66
69
|
if self._success_count >= self._success_threshold:
|
|
67
70
|
self._transition_to(CircuitState.CLOSED)
|
|
68
|
-
logger.info(
|
|
71
|
+
logger.info(
|
|
72
|
+
"Circuit breaker%s: HALF_OPEN → CLOSED "
|
|
73
|
+
"(recovered, %d/%d consecutive successes)",
|
|
74
|
+
self._vendor_label,
|
|
75
|
+
self._success_count,
|
|
76
|
+
self._success_threshold,
|
|
77
|
+
)
|
|
69
78
|
elif self._state == CircuitState.CLOSED:
|
|
70
79
|
# 正常状态下成功,无需操作
|
|
71
80
|
pass
|
|
@@ -94,7 +103,10 @@ class CircuitBreaker:
|
|
|
94
103
|
self._transition_to(CircuitState.OPEN)
|
|
95
104
|
self._backoff_recovery(hint_seconds=retry_after_seconds)
|
|
96
105
|
logger.warning(
|
|
97
|
-
"Circuit breaker: HALF_OPEN → OPEN
|
|
106
|
+
"Circuit breaker%s: HALF_OPEN → OPEN "
|
|
107
|
+
"(recovery probe failed, backoff %ds → next retry in %ds)",
|
|
108
|
+
self._vendor_label,
|
|
109
|
+
self._current_recovery,
|
|
98
110
|
self._current_recovery,
|
|
99
111
|
)
|
|
100
112
|
elif self._state == CircuitState.CLOSED:
|
|
@@ -117,14 +129,17 @@ class CircuitBreaker:
|
|
|
117
129
|
)
|
|
118
130
|
if force_open:
|
|
119
131
|
logger.warning(
|
|
120
|
-
"Circuit breaker: CLOSED → OPEN "
|
|
121
|
-
"(forced, rate-limited, next retry in %ds)",
|
|
132
|
+
"Circuit breaker%s: CLOSED → OPEN "
|
|
133
|
+
"(forced, rate-limited, retry-after=%ss → next retry in %ds)",
|
|
134
|
+
self._vendor_label,
|
|
135
|
+
retry_after_seconds or "N/A",
|
|
122
136
|
self._current_recovery,
|
|
123
137
|
)
|
|
124
138
|
else:
|
|
125
139
|
logger.warning(
|
|
126
|
-
"Circuit breaker: CLOSED → OPEN "
|
|
140
|
+
"Circuit breaker%s: CLOSED → OPEN "
|
|
127
141
|
"(%d consecutive failures, next retry in %ds)",
|
|
142
|
+
self._vendor_label,
|
|
128
143
|
self._failure_count,
|
|
129
144
|
self._current_recovery,
|
|
130
145
|
)
|
|
@@ -134,7 +149,9 @@ class CircuitBreaker:
|
|
|
134
149
|
with self._lock:
|
|
135
150
|
self._transition_to(CircuitState.CLOSED)
|
|
136
151
|
self._current_recovery = self._recovery_timeout
|
|
137
|
-
logger.info(
|
|
152
|
+
logger.info(
|
|
153
|
+
"Circuit breaker%s: manually reset to CLOSED", self._vendor_label
|
|
154
|
+
)
|
|
138
155
|
|
|
139
156
|
def get_info(self) -> dict:
|
|
140
157
|
"""获取熔断器状态信息."""
|
|
@@ -157,7 +174,13 @@ class CircuitBreaker:
|
|
|
157
174
|
elapsed = time.monotonic() - self._last_failure_time
|
|
158
175
|
if elapsed >= self._current_recovery:
|
|
159
176
|
self._transition_to(CircuitState.HALF_OPEN)
|
|
160
|
-
|
|
177
|
+
elapsed_s = int(elapsed)
|
|
178
|
+
logger.info(
|
|
179
|
+
"Circuit breaker%s: OPEN → HALF_OPEN (recovery timeout, waited %ds/%ds)",
|
|
180
|
+
self._vendor_label,
|
|
181
|
+
elapsed_s,
|
|
182
|
+
self._current_recovery,
|
|
183
|
+
)
|
|
161
184
|
|
|
162
185
|
def _transition_to(self, new_state: CircuitState) -> None:
|
|
163
186
|
self._state = new_state
|
coding/proxy/routing/executor.py
CHANGED
|
@@ -48,9 +48,13 @@ logger = logging.getLogger(__name__)
|
|
|
48
48
|
|
|
49
49
|
|
|
50
50
|
def _log_http_error_detail(
|
|
51
|
-
tier_name: str,
|
|
51
|
+
tier_name: str,
|
|
52
|
+
exc: Exception,
|
|
53
|
+
*,
|
|
54
|
+
is_stream: bool = False,
|
|
55
|
+
tier: VendorTier | None = None,
|
|
52
56
|
) -> None:
|
|
53
|
-
"""记录 HTTP 错误的详细信息(状态码 / 响应体摘要 /
|
|
57
|
+
"""记录 HTTP 错误的详细信息(状态码 / 响应体摘要 / 异常类型 / 熔断器快照).
|
|
54
58
|
|
|
55
59
|
替代原先单行 ``logger.warning("Tier %s stream failed: %s", ...)``,
|
|
56
60
|
在非 200 响应时输出更丰富的诊断上下文,便于跟踪上游故障根因。
|
|
@@ -78,6 +82,14 @@ def _log_http_error_detail(
|
|
|
78
82
|
detail_parts.append(f" error_msg={err.get('message', 'N/A')[:200]}")
|
|
79
83
|
else:
|
|
80
84
|
detail_parts.append(f" message={str(exc)[:300]}")
|
|
85
|
+
# 熔断器状态快照
|
|
86
|
+
if tier and tier.circuit_breaker:
|
|
87
|
+
cb = tier.circuit_breaker
|
|
88
|
+
cb_info = cb.get_info()
|
|
89
|
+
detail_parts.append(
|
|
90
|
+
f" circuit_breaker={cb_info['state']} "
|
|
91
|
+
f"(failures={cb_info['failure_count']}/{cb._failure_threshold})"
|
|
92
|
+
)
|
|
81
93
|
logger.warning("\n".join(detail_parts))
|
|
82
94
|
|
|
83
95
|
|
|
@@ -268,6 +280,12 @@ class _RouteExecutor:
|
|
|
268
280
|
duration = int((time.monotonic() - start) * 1000)
|
|
269
281
|
model = body.get("model", "unknown")
|
|
270
282
|
model_served = usage.get("model_served") or tier.vendor.map_model(model)
|
|
283
|
+
if failed_tier_name is not None:
|
|
284
|
+
logger.info(
|
|
285
|
+
"Tier %s stream succeeded (took over from failed tier: %s)",
|
|
286
|
+
tier.name,
|
|
287
|
+
failed_tier_name,
|
|
288
|
+
)
|
|
271
289
|
self._recorder.log_model_call(
|
|
272
290
|
vendor=tier.name,
|
|
273
291
|
model_requested=model,
|
|
@@ -310,7 +328,7 @@ class _RouteExecutor:
|
|
|
310
328
|
httpx.ConnectError,
|
|
311
329
|
httpx.ReadError,
|
|
312
330
|
) as exc:
|
|
313
|
-
_log_http_error_detail(tier.name, exc, is_stream=True)
|
|
331
|
+
_log_http_error_detail(tier.name, exc, is_stream=True, tier=tier)
|
|
314
332
|
(
|
|
315
333
|
should_continue,
|
|
316
334
|
failed_tier_name,
|
|
@@ -325,6 +343,7 @@ class _RouteExecutor:
|
|
|
325
343
|
request_body=body,
|
|
326
344
|
)
|
|
327
345
|
if should_continue:
|
|
346
|
+
self._log_failover_transition(tier, exc, self._tiers, i)
|
|
328
347
|
continue
|
|
329
348
|
if is_last:
|
|
330
349
|
raise
|
|
@@ -399,6 +418,12 @@ class _RouteExecutor:
|
|
|
399
418
|
duration = int((time.monotonic() - start) * 1000)
|
|
400
419
|
model = body.get("model", "unknown")
|
|
401
420
|
model_served = resp.model_served or tier.vendor.map_model(model)
|
|
421
|
+
if failed_tier_name is not None:
|
|
422
|
+
logger.info(
|
|
423
|
+
"Tier %s message succeeded (took over from failed tier: %s)",
|
|
424
|
+
tier.name,
|
|
425
|
+
failed_tier_name,
|
|
426
|
+
)
|
|
402
427
|
self._recorder.log_model_call(
|
|
403
428
|
vendor=tier.name,
|
|
404
429
|
model_requested=model,
|
|
@@ -469,10 +494,15 @@ class _RouteExecutor:
|
|
|
469
494
|
rate_limit_deadline=compute_rate_limit_deadline(rl_info),
|
|
470
495
|
)
|
|
471
496
|
if not is_last:
|
|
497
|
+
next_tier = (
|
|
498
|
+
self._tiers[i + 1] if i + 1 < len(self._tiers) else None
|
|
499
|
+
)
|
|
500
|
+
next_info = f" → next: {next_tier.name}" if next_tier else ""
|
|
472
501
|
logger.warning(
|
|
473
|
-
"Tier %s error %d, failing over",
|
|
502
|
+
"Tier %s error %d, failing over%s",
|
|
474
503
|
tier.name,
|
|
475
504
|
resp.status_code,
|
|
505
|
+
next_info,
|
|
476
506
|
)
|
|
477
507
|
failed_tier_name = tier.name
|
|
478
508
|
continue
|
|
@@ -513,7 +543,7 @@ class _RouteExecutor:
|
|
|
513
543
|
continue
|
|
514
544
|
|
|
515
545
|
except (httpx.TimeoutException, httpx.ConnectError, httpx.ReadError) as exc:
|
|
516
|
-
_log_http_error_detail(tier.name, exc, is_stream=False)
|
|
546
|
+
_log_http_error_detail(tier.name, exc, is_stream=False, tier=tier)
|
|
517
547
|
tier.record_failure()
|
|
518
548
|
failed_tier_name = tier.name
|
|
519
549
|
if is_last:
|
|
@@ -694,6 +724,30 @@ class _RouteExecutor:
|
|
|
694
724
|
|
|
695
725
|
return False, tier.name, exc
|
|
696
726
|
|
|
727
|
+
@staticmethod
|
|
728
|
+
def _log_failover_transition(
|
|
729
|
+
current_tier: VendorTier,
|
|
730
|
+
exc: Exception,
|
|
731
|
+
tiers: list[VendorTier],
|
|
732
|
+
current_index: int,
|
|
733
|
+
) -> None:
|
|
734
|
+
"""记录 vendor 轮转摘要日志(谁 → 谁,原因)."""
|
|
735
|
+
next_tier = tiers[current_index + 1] if current_index + 1 < len(tiers) else None
|
|
736
|
+
if next_tier is None:
|
|
737
|
+
return
|
|
738
|
+
|
|
739
|
+
# 提取错误摘要
|
|
740
|
+
reason = type(exc).__name__
|
|
741
|
+
if isinstance(exc, httpx.HTTPStatusError) and exc.response is not None:
|
|
742
|
+
reason = f"HTTP {exc.response.status_code}"
|
|
743
|
+
|
|
744
|
+
logger.info(
|
|
745
|
+
"Failover: %s → %s (reason: %s)",
|
|
746
|
+
current_tier.name,
|
|
747
|
+
next_tier.name,
|
|
748
|
+
reason,
|
|
749
|
+
)
|
|
750
|
+
|
|
697
751
|
@staticmethod
|
|
698
752
|
def _is_cap_error(resp: VendorResponse) -> bool:
|
|
699
753
|
"""判断是否为订阅用量上限错误."""
|
coding/proxy/routing/router.py
CHANGED
|
@@ -68,6 +68,66 @@ class RequestRouter:
|
|
|
68
68
|
"""当前活跃供应商名称(由 Executor 在成功响应时写入)."""
|
|
69
69
|
return self._active_vendor_name
|
|
70
70
|
|
|
71
|
+
# ── 运行时 N-tier 链路重排序 ─────────────────────────────
|
|
72
|
+
|
|
73
|
+
def get_vendor_names(self) -> list[str]:
|
|
74
|
+
"""返回当前 tiers 的供应商名称列表(按优先级顺序)."""
|
|
75
|
+
return [t.name for t in self._tiers]
|
|
76
|
+
|
|
77
|
+
def reorder_tiers(self, vendor_names: list[str]) -> None:
|
|
78
|
+
"""原地重排序 N-tier 链路.
|
|
79
|
+
|
|
80
|
+
使用切片赋值保持列表引用同一性,使 ``_RouteExecutor`` 立即可见。
|
|
81
|
+
|
|
82
|
+
Args:
|
|
83
|
+
vendor_names: 新的供应商名称顺序(必须包含所有当前 tier)。
|
|
84
|
+
|
|
85
|
+
Raises:
|
|
86
|
+
ValueError: 名称不存在、有重复、或未覆盖所有 tier。
|
|
87
|
+
"""
|
|
88
|
+
name_to_tier = {t.name: t for t in self._tiers}
|
|
89
|
+
current_names = set(name_to_tier)
|
|
90
|
+
|
|
91
|
+
# 校验:重复
|
|
92
|
+
if len(vendor_names) != len(set(vendor_names)):
|
|
93
|
+
seen: set[str] = set()
|
|
94
|
+
dups = [n for n in vendor_names if n in seen or seen.add(n)] # type: ignore[func-returns-value]
|
|
95
|
+
raise ValueError(f"vendor 名称重复: {', '.join(dups)}")
|
|
96
|
+
|
|
97
|
+
# 校验:名称存在性
|
|
98
|
+
unknown = [n for n in vendor_names if n not in current_names]
|
|
99
|
+
if unknown:
|
|
100
|
+
raise ValueError(
|
|
101
|
+
f"未知 vendor: {', '.join(unknown)}; "
|
|
102
|
+
f"可用: {', '.join(sorted(current_names))}"
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
# 校验:全量覆盖
|
|
106
|
+
provided = set(vendor_names)
|
|
107
|
+
if provided != current_names:
|
|
108
|
+
missing = current_names - provided
|
|
109
|
+
raise ValueError(f"缺少 vendor: {', '.join(sorted(missing))}")
|
|
110
|
+
|
|
111
|
+
self._tiers[:] = [name_to_tier[n] for n in vendor_names]
|
|
112
|
+
|
|
113
|
+
def promote_vendor(self, vendor_name: str) -> None:
|
|
114
|
+
"""将指定 vendor 提升至最高优先级,其余保持相对顺序.
|
|
115
|
+
|
|
116
|
+
Args:
|
|
117
|
+
vendor_name: 要提升的供应商名称。
|
|
118
|
+
|
|
119
|
+
Raises:
|
|
120
|
+
ValueError: 名称不存在。
|
|
121
|
+
"""
|
|
122
|
+
current_names = self.get_vendor_names()
|
|
123
|
+
if vendor_name not in current_names:
|
|
124
|
+
available = sorted(t.name for t in self._tiers)
|
|
125
|
+
raise ValueError(
|
|
126
|
+
f"未知 vendor: {vendor_name}; 可用: {', '.join(available)}"
|
|
127
|
+
)
|
|
128
|
+
new_order = [vendor_name] + [n for n in current_names if n != vendor_name]
|
|
129
|
+
self.reorder_tiers(new_order)
|
|
130
|
+
|
|
71
131
|
# ── 公开路由接口(委托给 _RouteExecutor)───────────────
|
|
72
132
|
|
|
73
133
|
async def route_stream(
|
coding/proxy/routing/tier.py
CHANGED
|
@@ -155,11 +155,33 @@ class VendorTier:
|
|
|
155
155
|
if not is_probe_scenario:
|
|
156
156
|
return cb_allows and qg_allows and wqg_allows
|
|
157
157
|
|
|
158
|
+
# 构建探测上下文摘要
|
|
159
|
+
probe_context_parts: list[str] = []
|
|
160
|
+
if self.circuit_breaker:
|
|
161
|
+
cb_info = self.circuit_breaker.get_info()
|
|
162
|
+
probe_context_parts.append(
|
|
163
|
+
f"circuit_breaker={cb_info['state']}, "
|
|
164
|
+
f"failures={cb_info['failure_count']}"
|
|
165
|
+
)
|
|
166
|
+
if self._rate_limit_deadline > 0:
|
|
167
|
+
waited = int(time.monotonic() - self._rate_limit_deadline)
|
|
168
|
+
probe_context_parts.append(f"rate_limit_waited={waited}s")
|
|
169
|
+
probe_context = (
|
|
170
|
+
" (" + ", ".join(probe_context_parts) + ")" if probe_context_parts else ""
|
|
171
|
+
)
|
|
172
|
+
|
|
158
173
|
# ── 第二层: Health Check 门控 ──
|
|
159
|
-
logger.info(
|
|
174
|
+
logger.info(
|
|
175
|
+
"Tier %s: probe scenario%s, running health check",
|
|
176
|
+
self.name,
|
|
177
|
+
probe_context,
|
|
178
|
+
)
|
|
160
179
|
healthy = await self.vendor.check_health()
|
|
161
180
|
if not healthy:
|
|
162
|
-
logger.warning(
|
|
181
|
+
logger.warning(
|
|
182
|
+
"Tier %s: health check failed, staying degraded",
|
|
183
|
+
self.name,
|
|
184
|
+
)
|
|
163
185
|
self.record_failure()
|
|
164
186
|
return False
|
|
165
187
|
|
coding/proxy/server/app.py
CHANGED
|
@@ -106,7 +106,9 @@ def create_app(config: ProxyConfig | None = None) -> FastAPI:
|
|
|
106
106
|
vendor_cfg, config.failover, mapper, token_store
|
|
107
107
|
)
|
|
108
108
|
cb = (
|
|
109
|
-
_build_circuit_breaker(
|
|
109
|
+
_build_circuit_breaker(
|
|
110
|
+
vendor_cfg.circuit_breaker, vendor_name=vendor_cfg.vendor
|
|
111
|
+
)
|
|
110
112
|
if vendor_cfg.circuit_breaker
|
|
111
113
|
else None
|
|
112
114
|
)
|
coding/proxy/server/factory.py
CHANGED
|
@@ -91,13 +91,16 @@ def _find_copilot_vendor(router: Any) -> CopilotVendor | None:
|
|
|
91
91
|
return None
|
|
92
92
|
|
|
93
93
|
|
|
94
|
-
def _build_circuit_breaker(
|
|
94
|
+
def _build_circuit_breaker(
|
|
95
|
+
cfg: CircuitBreakerConfig, *, vendor_name: str = ""
|
|
96
|
+
) -> CircuitBreaker:
|
|
95
97
|
"""从配置构建熔断器实例."""
|
|
96
98
|
return CircuitBreaker(
|
|
97
99
|
failure_threshold=cfg.failure_threshold,
|
|
98
100
|
recovery_timeout_seconds=cfg.recovery_timeout_seconds,
|
|
99
101
|
success_threshold=cfg.success_threshold,
|
|
100
102
|
max_recovery_seconds=cfg.max_recovery_seconds,
|
|
103
|
+
vendor_name=vendor_name,
|
|
101
104
|
)
|
|
102
105
|
|
|
103
106
|
|
coding/proxy/server/routes.py
CHANGED
|
@@ -261,7 +261,40 @@ def register_admin_routes(app: Any, router: Any) -> None:
|
|
|
261
261
|
"""注册管理操作路由(重置等)."""
|
|
262
262
|
|
|
263
263
|
@app.post("/api/reset")
|
|
264
|
-
async def reset_circuit() ->
|
|
264
|
+
async def reset_circuit(request: Request) -> Response:
|
|
265
|
+
"""重置所有层级的熔断器/配额守卫/rate limit.
|
|
266
|
+
|
|
267
|
+
可选 JSON body ``{"vendors": ["v1", "v2", ...]}`` 支持运行时重排序:
|
|
268
|
+
- 单个 vendor → 提升至最高优先级,其余保持相对顺序
|
|
269
|
+
- 多个 vendor → 替换整个 N-tier 链路顺序(需覆盖所有 vendor)
|
|
270
|
+
"""
|
|
271
|
+
# 解析可选 body
|
|
272
|
+
vendor_names: list[str] | None = None
|
|
273
|
+
try:
|
|
274
|
+
body = await request.json()
|
|
275
|
+
if isinstance(body, dict):
|
|
276
|
+
raw = body.get("vendors")
|
|
277
|
+
if isinstance(raw, list) and raw:
|
|
278
|
+
vendor_names = [str(v) for v in raw]
|
|
279
|
+
except Exception:
|
|
280
|
+
# 无 body 或非 JSON → 仅 reset(向后兼容)
|
|
281
|
+
pass
|
|
282
|
+
|
|
283
|
+
# 重排序(如果指定)
|
|
284
|
+
if vendor_names is not None:
|
|
285
|
+
try:
|
|
286
|
+
if len(vendor_names) == 1:
|
|
287
|
+
router.promote_vendor(vendor_names[0])
|
|
288
|
+
else:
|
|
289
|
+
router.reorder_tiers(vendor_names)
|
|
290
|
+
except ValueError as exc:
|
|
291
|
+
return json_error_response(
|
|
292
|
+
400,
|
|
293
|
+
error_type="invalid_request_error",
|
|
294
|
+
message=str(exc),
|
|
295
|
+
)
|
|
296
|
+
|
|
297
|
+
# 全量 reset
|
|
265
298
|
for tier in router.tiers:
|
|
266
299
|
if tier.circuit_breaker:
|
|
267
300
|
tier.circuit_breaker.reset()
|
|
@@ -270,7 +303,16 @@ def register_admin_routes(app: Any, router: Any) -> None:
|
|
|
270
303
|
if tier.weekly_quota_guard:
|
|
271
304
|
tier.weekly_quota_guard.reset()
|
|
272
305
|
tier.reset_rate_limit()
|
|
273
|
-
|
|
306
|
+
|
|
307
|
+
result: dict[str, Any] = {"status": "ok"}
|
|
308
|
+
if vendor_names is not None:
|
|
309
|
+
result["tier_order"] = router.get_vendor_names()
|
|
310
|
+
|
|
311
|
+
return Response(
|
|
312
|
+
content=json.dumps(result, ensure_ascii=False).encode(),
|
|
313
|
+
status_code=200,
|
|
314
|
+
media_type="application/json",
|
|
315
|
+
)
|
|
274
316
|
|
|
275
317
|
|
|
276
318
|
def register_reauth_routes(app: Any, reauth_coordinator: Any) -> None:
|
|
@@ -37,6 +37,9 @@ _V1INTERNAL_USER_AGENT = (
|
|
|
37
37
|
"Antigravity/4.1.31 (Macintosh; Intel Mac OS X 10_15_7) "
|
|
38
38
|
"Chrome/132.0.6834.160 Electron/39.2.3"
|
|
39
39
|
)
|
|
40
|
+
# Cloud Resource Manager API(用于自动发现 GCP project_id)
|
|
41
|
+
_CRM_PROJECTS_URL = "https://cloudresourcemanager.googleapis.com/v1/projects"
|
|
42
|
+
_V1INTERNAL_BASE_URL = "https://cloudcode-pa.googleapis.com/v1internal"
|
|
40
43
|
|
|
41
44
|
|
|
42
45
|
# ── Google OAuth2 Token 管理器(原 antigravity_token_manager.py) ──
|
|
@@ -148,13 +151,119 @@ class AntigravityVendor(TokenBackendMixin, BaseVendor):
|
|
|
148
151
|
self._project_id: str = config.project_id
|
|
149
152
|
self._session_id: str = uuid.uuid4().hex[:16]
|
|
150
153
|
self._message_count: int = 0
|
|
154
|
+
# project_id 自动发现状态
|
|
155
|
+
self._project_id_discovered: str = ""
|
|
156
|
+
self._project_discovery_attempted: bool = False
|
|
151
157
|
|
|
152
158
|
def get_name(self) -> str:
|
|
153
159
|
return "antigravity"
|
|
154
160
|
|
|
155
161
|
def _is_v1internal_mode(self) -> bool:
|
|
156
162
|
"""检测是否启用 v1internal 协议模式(与 Antigravity-Manager 对齐)."""
|
|
157
|
-
return bool(self.
|
|
163
|
+
return bool(self._effective_project_id) and "v1internal" in self._base_url
|
|
164
|
+
|
|
165
|
+
@property
|
|
166
|
+
def _effective_project_id(self) -> str:
|
|
167
|
+
"""返回有效的 project_id:显式配置优先,否则使用自动发现的值."""
|
|
168
|
+
return self._project_id or self._project_id_discovered
|
|
169
|
+
|
|
170
|
+
async def _discover_project_id(self, access_token: str) -> str:
|
|
171
|
+
"""通过 Cloud Resource Manager API 自动发现用户的 GCP project_id.
|
|
172
|
+
|
|
173
|
+
利用已有的 cloud-platform OAuth scope 调用 CRM API 列出用户有权限的项目,
|
|
174
|
+
选择合适的 project_id 后自动切换至 v1internal 模式。
|
|
175
|
+
|
|
176
|
+
Args:
|
|
177
|
+
access_token: 当前有效的 Google OAuth access_token
|
|
178
|
+
|
|
179
|
+
Returns:
|
|
180
|
+
发现到的 project_id;失败返回空字符串
|
|
181
|
+
"""
|
|
182
|
+
if self._project_discovery_attempted:
|
|
183
|
+
return self._project_id_discovered
|
|
184
|
+
|
|
185
|
+
# 已手动配置则跳过
|
|
186
|
+
if self._project_id:
|
|
187
|
+
self._project_discovery_attempted = True
|
|
188
|
+
return ""
|
|
189
|
+
|
|
190
|
+
self._project_discovery_attempted = True
|
|
191
|
+
client = self._get_client()
|
|
192
|
+
try:
|
|
193
|
+
response = await client.get(
|
|
194
|
+
_CRM_PROJECTS_URL,
|
|
195
|
+
headers={"authorization": f"Bearer {access_token}"},
|
|
196
|
+
)
|
|
197
|
+
response.raise_for_status()
|
|
198
|
+
|
|
199
|
+
data = response.json()
|
|
200
|
+
projects = data.get("projects", [])
|
|
201
|
+
|
|
202
|
+
if not projects:
|
|
203
|
+
logger.warning(
|
|
204
|
+
"GCP 项目自动发现完成但未找到任何项目,"
|
|
205
|
+
"回退至标准 GLA 模式。请手动配置 project_id 或确认账号已关联 GCP 项目。"
|
|
206
|
+
)
|
|
207
|
+
return ""
|
|
208
|
+
|
|
209
|
+
# 选择策略:优先 ACTIVE 状态的项目
|
|
210
|
+
selected = None
|
|
211
|
+
for p in projects:
|
|
212
|
+
if p.get("lifecycleState", "") == "ACTIVE":
|
|
213
|
+
selected = p
|
|
214
|
+
break
|
|
215
|
+
if selected is None:
|
|
216
|
+
for p in projects:
|
|
217
|
+
if p.get("lifecycleState", "") != "DELETE_REQUESTED":
|
|
218
|
+
selected = p
|
|
219
|
+
break
|
|
220
|
+
if selected is None:
|
|
221
|
+
selected = projects[0]
|
|
222
|
+
|
|
223
|
+
project_id = selected.get("projectId", "")
|
|
224
|
+
if not project_id:
|
|
225
|
+
logger.warning(
|
|
226
|
+
"GCP 项目 '%s' 缺少 projectId 字段,跳过。",
|
|
227
|
+
selected.get("name", "unknown"),
|
|
228
|
+
)
|
|
229
|
+
return ""
|
|
230
|
+
|
|
231
|
+
# 发现成功:原子性切换到 v1internal 模式
|
|
232
|
+
self._base_url = _V1INTERNAL_BASE_URL
|
|
233
|
+
self._project_id_discovered = project_id
|
|
234
|
+
|
|
235
|
+
# 重建 HTTP 客户端(base_url 是初始化参数)
|
|
236
|
+
if self._client is not None and not self._client.is_closed:
|
|
237
|
+
await self._client.aclose()
|
|
238
|
+
self._client = None
|
|
239
|
+
|
|
240
|
+
logger.info(
|
|
241
|
+
"GCP 项目自动发现成功: project_id=%s, name=%s, "
|
|
242
|
+
"已自动切换至 v1internal 协议模式",
|
|
243
|
+
project_id,
|
|
244
|
+
selected.get("name", "unknown"),
|
|
245
|
+
)
|
|
246
|
+
return project_id
|
|
247
|
+
|
|
248
|
+
except httpx.HTTPStatusError as exc:
|
|
249
|
+
logger.error(
|
|
250
|
+
"GCP 项目自动发现 API 错误 (HTTP %d),回退至标准 GLA 模式。%s",
|
|
251
|
+
exc.response.status_code,
|
|
252
|
+
exc,
|
|
253
|
+
)
|
|
254
|
+
return ""
|
|
255
|
+
except (httpx.TimeoutException, httpx.ConnectError) as exc:
|
|
256
|
+
logger.warning(
|
|
257
|
+
"GCP 项目自动发现网络异常,回退至标准 GLA 模式。%s",
|
|
258
|
+
exc,
|
|
259
|
+
)
|
|
260
|
+
return ""
|
|
261
|
+
except Exception as exc:
|
|
262
|
+
logger.error(
|
|
263
|
+
"GCP 项目自动发现未知异常,回退至标准 GLA 模式。%s",
|
|
264
|
+
exc,
|
|
265
|
+
)
|
|
266
|
+
return ""
|
|
158
267
|
|
|
159
268
|
def get_capabilities(self) -> VendorCapabilities:
|
|
160
269
|
return VendorCapabilities(
|
|
@@ -230,6 +339,19 @@ class AntigravityVendor(TokenBackendMixin, BaseVendor):
|
|
|
230
339
|
self._last_request_adaptations = converted.adaptations
|
|
231
340
|
token = await self._token_manager.get_token()
|
|
232
341
|
|
|
342
|
+
# 懒加载:未配置 project_id 时自动发现并切换 v1internal 模式
|
|
343
|
+
if not self._project_id and not self._project_discovery_attempted:
|
|
344
|
+
discovered = await self._discover_project_id(token)
|
|
345
|
+
if discovered:
|
|
346
|
+
logger.info(
|
|
347
|
+
"已自动启用 v1internal 协议模式(project_id=%s)", discovered
|
|
348
|
+
)
|
|
349
|
+
else:
|
|
350
|
+
logger.info(
|
|
351
|
+
"无法自动发现 GCP project_id,继续使用标准 GLA 模式。"
|
|
352
|
+
"如需启用 v1internal 协议,请在配置中手动指定 project_id。"
|
|
353
|
+
)
|
|
354
|
+
|
|
233
355
|
if self._is_v1internal_mode():
|
|
234
356
|
return self._prepare_v1internal_request(gemini_body, resolved_model, token)
|
|
235
357
|
|
|
@@ -259,7 +381,7 @@ class AntigravityVendor(TokenBackendMixin, BaseVendor):
|
|
|
259
381
|
"""
|
|
260
382
|
self._message_count += 1
|
|
261
383
|
envelope = {
|
|
262
|
-
"project": self.
|
|
384
|
+
"project": self._effective_project_id,
|
|
263
385
|
"requestId": f"agent/antigravity/{self._session_id}/{self._message_count}",
|
|
264
386
|
"request": gemini_body,
|
|
265
387
|
"model": resolved_model,
|
|
@@ -277,7 +399,7 @@ class AntigravityVendor(TokenBackendMixin, BaseVendor):
|
|
|
277
399
|
"_prepare_v1internal_request: model=%s -> %s, project=%s, requestId=%s",
|
|
278
400
|
resolved_model,
|
|
279
401
|
resolved_model,
|
|
280
|
-
self.
|
|
402
|
+
self._effective_project_id,
|
|
281
403
|
envelope["requestId"],
|
|
282
404
|
)
|
|
283
405
|
return envelope, new_headers
|
|
@@ -308,6 +430,15 @@ class AntigravityVendor(TokenBackendMixin, BaseVendor):
|
|
|
308
430
|
result["resolved_model"] = self._last_resolved_model
|
|
309
431
|
if self._last_model_resolution_reason:
|
|
310
432
|
result["model_resolution_reason"] = self._last_model_resolution_reason
|
|
433
|
+
# project_id 发现诊断
|
|
434
|
+
result["project_id_source"] = (
|
|
435
|
+
"configured"
|
|
436
|
+
if self._project_id
|
|
437
|
+
else ("discovered" if self._project_id_discovered else "none")
|
|
438
|
+
)
|
|
439
|
+
if self._project_id_discovered:
|
|
440
|
+
result["discovered_project_id"] = self._project_id_discovered
|
|
441
|
+
result["is_v1internal_mode"] = self._is_v1internal_mode()
|
|
311
442
|
return result
|
|
312
443
|
|
|
313
444
|
async def send_message(
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: coding-proxy
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.2
|
|
4
4
|
Summary: A High-Availability, Transparent, and Smart Multi-Vendor Proxy for Claude Code. Support Claude Plans, GitHub Copilot, Google Antigravity, ZAI/GLM, MiniMax, Qwen, Xiaomi, Kimi, Doubao...
|
|
5
5
|
Project-URL: Source Code, https://github.com/ThreeFish-AI/coding-proxy
|
|
6
6
|
Project-URL: User Guide, https://github.com/ThreeFish-AI/coding-proxy/blob/master/docs/user-guide.md
|
|
@@ -56,7 +56,7 @@ When you're deeply immersed in your coding "zone" with **Claude Code** (or any A
|
|
|
56
56
|
|
|
57
57
|
## 🌟 Core Features
|
|
58
58
|
|
|
59
|
-
- **⛓️ N-tier Chained Failover**:
|
|
59
|
+
- **⛓️ N-tier Chained Failover**: Autonomous descending sequence, supporting Claude's official plans, as well as Coding Plans from GitHub Copilot, Z AI, MiniMax, Alibaba Qwen, Xiaomi, Kimi, Doubao, etc.
|
|
60
60
|
- **🛡️ Smart Resilience & Quota Guardians**: Every single vendor node comes fully armed with an independent **Circuit Breaker** and **Quota Guard** to proactively dodge avalanches without breaking a sweat.
|
|
61
61
|
- **👻 Phantom-like Transparency**: **100% transparent** to the client! No code tweaks required. Overwrite `ANTHROPIC_BASE_URL` with a single line, and you're good to go.
|
|
62
62
|
- **🔄 Universal Alchemy (Formats & Models)**: Native support for two-way request/streaming (SSE) translations between Anthropic ←→ Gemini. Plus, auto/DIY model name mapping (e.g., effortlessly morphing `claude-*` into `glm-*`).
|
|
@@ -9,7 +9,7 @@ coding/proxy/auth/providers/__init__.py,sha256=hnGL_aSHcMxiZHK6HKPWD0_tWSPcwuxwQ
|
|
|
9
9
|
coding/proxy/auth/providers/base.py,sha256=TOtCi2b81-UBdEoWMv7HbYSmRxAjGSvnF1yBxazqai0,1092
|
|
10
10
|
coding/proxy/auth/providers/github.py,sha256=xH6yygAzt8vI-MXV8EHl2tg4RcOmLaLBns93PBCyVG8,4835
|
|
11
11
|
coding/proxy/auth/providers/google.py,sha256=MhbM5PXmgwQBgWCMDkhK_7GYyvS-IdPV4ATxKYH6WXM,8402
|
|
12
|
-
coding/proxy/cli/__init__.py,sha256=
|
|
12
|
+
coding/proxy/cli/__init__.py,sha256=wkec3lIOglEOMN44ssGPzixCJ2hiTZ2wYL4_JtBc4Nw,8911
|
|
13
13
|
coding/proxy/cli/auth_commands.py,sha256=tN5jWJo8sJP8jgoN4WH5iTl9rhi4xWPLMptgeTHIFpE,9212
|
|
14
14
|
coding/proxy/cli/banner.py,sha256=gat7QGpfocL0wiAw3-MSUsCviyc59Sf-XswOp7Pg6LI,1694
|
|
15
15
|
coding/proxy/compat/__init__.py,sha256=-msRszArsk2YJJtOeaidYDd1tAQEzeE91LvCi1CyMis,721
|
|
@@ -17,7 +17,7 @@ coding/proxy/compat/canonical.py,sha256=-zcuEwZ402xeH3C545RmuYRkT2HDuvFloyFydDv8
|
|
|
17
17
|
coding/proxy/compat/session_store.py,sha256=B9IFjjQJnHMg1244m__jG9gnqGWi26-JyEtwopEri6Q,5244
|
|
18
18
|
coding/proxy/config/__init__.py,sha256=hzgU5noJGecjj13UY38cC_p6jpWO3GO7okSW-A2XkJ0,127
|
|
19
19
|
coding/proxy/config/auth_schema.py,sha256=LYrJQU_fgW-6AoQdjXt4-MgPJjXjv9HrghlCcZwotnA,696
|
|
20
|
-
coding/proxy/config/config.default.yaml,sha256=
|
|
20
|
+
coding/proxy/config/config.default.yaml,sha256=WT5wK3T9h3ODyz7ydicieC4PGZTHq-419FV__dxdj4M,16575
|
|
21
21
|
coding/proxy/config/loader.py,sha256=1J_RBJgjuC8RwB2mwewLMS7vy9WEhUqttZG2igeDm-w,8984
|
|
22
22
|
coding/proxy/config/resiliency.py,sha256=GnzY-LoyfFqXFM1l6xEru418v-cKlv97-HM0noGPdks,1308
|
|
23
23
|
coding/proxy/config/routing.py,sha256=aJMhfCRyoZIvemM3Q2_KV9rpWxUsFnoY0ZdCr4TwSs8,11765
|
|
@@ -30,9 +30,9 @@ coding/proxy/convert/anthropic_to_openai.py,sha256=crE8gi780Tr-dI58hce75BiaKzlh_
|
|
|
30
30
|
coding/proxy/convert/gemini_sse_adapter.py,sha256=b7zQ9wBBn1bbG46WpLrAYOl0ExpyMTzpTorxoTadWaM,7395
|
|
31
31
|
coding/proxy/convert/gemini_to_anthropic.py,sha256=EE_rUsLmWTsF2QSYWqIYzsjyk1mKXFp2sziqj_S2W5U,3446
|
|
32
32
|
coding/proxy/convert/openai_to_anthropic.py,sha256=8rg8NZs_1up27MixrXRJAfmSiYrOkh_lkAJuytDGZow,3694
|
|
33
|
-
coding/proxy/logging/__init__.py,sha256=
|
|
33
|
+
coding/proxy/logging/__init__.py,sha256=6zEh2CELMJ5aZvM6PNCOFlZ55De8xoC6QNyMAa9WeCA,6755
|
|
34
34
|
coding/proxy/logging/db.py,sha256=uARNaaAXWxemwfyybp660ujpi6UikNtFokG0pzXYwR8,19058
|
|
35
|
-
coding/proxy/logging/formatters.py,sha256=
|
|
35
|
+
coding/proxy/logging/formatters.py,sha256=8LDqiNAxGcfn9fsHqZzdK9TKGqOGNAOls75B1byBjnM,823
|
|
36
36
|
coding/proxy/logging/stats.py,sha256=JlhJC4a2RmuCjbaseSUONScbcgn41KNR_sNsZ0OScbw,9203
|
|
37
37
|
coding/proxy/model/__init__.py,sha256=E8E59yabP-CF5GIwHE4OEGPLiJj3yTLjFWmCorkQTxc,3947
|
|
38
38
|
coding/proxy/model/auth.py,sha256=NiiIPdLdxPtuHNrsGoxFASUXwJU5pkDuVRorQe_1Z-Q,934
|
|
@@ -42,30 +42,30 @@ coding/proxy/model/pricing.py,sha256=KHYsLQJJkEEuAeX8iB182znfpfSPTf9KI1hPchphqnc
|
|
|
42
42
|
coding/proxy/model/token.py,sha256=yCh-66_nFDAk7wKbYy-9ymdgae3SaJ47-fkYfYLO3Oo,1818
|
|
43
43
|
coding/proxy/model/vendor.py,sha256=Yg4AYN7tgjHaGWRGEccCs3AdO8vBc-KxIelnqRowpyo,7898
|
|
44
44
|
coding/proxy/routing/__init__.py,sha256=2Tpc6MQ5Oy_w-YlEWQVDSgcSgiHp3OwKOABIjj5kfHA,1595
|
|
45
|
-
coding/proxy/routing/circuit_breaker.py,sha256=
|
|
45
|
+
coding/proxy/routing/circuit_breaker.py,sha256=hOyoY_RWB5dTVbipyuiWfAelnva3cZRLbXrSmSn8KTs,8096
|
|
46
46
|
coding/proxy/routing/error_classifier.py,sha256=oDxp69sCb-pdfe1hWce29A9tsN3jAbhqfl2fRECBtCo,3747
|
|
47
|
-
coding/proxy/routing/executor.py,sha256=
|
|
47
|
+
coding/proxy/routing/executor.py,sha256=vnXmmaWw0w49HhKsNine37Hnye04NSZ0vc9ghMzqLdQ,30268
|
|
48
48
|
coding/proxy/routing/model_mapper.py,sha256=b72CGRdusLAwoq7p8-8TWAv9-Zv8BozssiZC8XnNTx4,3574
|
|
49
49
|
coding/proxy/routing/quota_guard.py,sha256=TRJs3mjdRMc_u0HCw-hZn--osha_m11dEtAUxOO2P5A,6806
|
|
50
50
|
coding/proxy/routing/rate_limit.py,sha256=i2cCqtbmXrP6wjRUTWXLP4DdYwVj0C4o59QdYGkPQj0,5122
|
|
51
51
|
coding/proxy/routing/retry.py,sha256=KEYVToBkBOtXdqY7He70RrtVWFqIpnOQf2VVksdLrlk,2386
|
|
52
|
-
coding/proxy/routing/router.py,sha256=
|
|
52
|
+
coding/proxy/routing/router.py,sha256=x5x6cjL5BOnRXZZuzK3GCnNtaMVjw75xw2OQ-DoO9_s,5830
|
|
53
53
|
coding/proxy/routing/session_manager.py,sha256=kpEPCo67CdsELX71hoe6G7sb2dcda7gTNL0FtpgRK7I,2950
|
|
54
|
-
coding/proxy/routing/tier.py,sha256=
|
|
54
|
+
coding/proxy/routing/tier.py,sha256=OBrJiMxfLCxXvZfARD5oNmuhpYASKnyrW9sOCHLdQ1g,7777
|
|
55
55
|
coding/proxy/routing/usage_parser.py,sha256=j4G0ArFduQ2D4Yeuad94DVlwdc-JvSh7SJLVzc-hLcs,8480
|
|
56
56
|
coding/proxy/routing/usage_recorder.py,sha256=pObOrX2yIITTiyojl1fJcqO0yWWpbP4KqsJvFdmlt04,6273
|
|
57
57
|
coding/proxy/server/__init__.py,sha256=KeH7mEu36v9v27m3VgeSxSeFz9sLTJrxS_EVATJ7Vks,20
|
|
58
|
-
coding/proxy/server/app.py,sha256=
|
|
59
|
-
coding/proxy/server/factory.py,sha256=
|
|
58
|
+
coding/proxy/server/app.py,sha256=kRGgb772dZu8200LPnn7Nt0IU5oagcbBf_Vc5ynxxzE,5599
|
|
59
|
+
coding/proxy/server/factory.py,sha256=w8VFvxoogw9K9sO8MlT6bIP7xM7mR6sCohrZle9y_Gg,9985
|
|
60
60
|
coding/proxy/server/request_normalizer.py,sha256=F_mgxxuA3i7cN1CEWlto7Mm9VT_QG2QNspiUWeYVBpM,10511
|
|
61
61
|
coding/proxy/server/responses.py,sha256=i0ugnLRNOdRYGHEWxkwsxR35ChmdMQsSaD8AjRluTn4,2167
|
|
62
|
-
coding/proxy/server/routes.py,sha256=
|
|
62
|
+
coding/proxy/server/routes.py,sha256=_CkeOEcnMe21RcLZ-raxgknUbdclV-Uf9f_B2Xz9_a8,13325
|
|
63
63
|
coding/proxy/streaming/__init__.py,sha256=0al5TC-zJt8Bz0CibTmd8WPza-Cs-qcuZWvT2fKPqHI,23
|
|
64
64
|
coding/proxy/streaming/anthropic_compat.py,sha256=cycYiJ-glN88OmfLTR7FroHRethONlEcK0PT78KqGbA,21202
|
|
65
65
|
coding/proxy/vendors/__init__.py,sha256=fehSpQ2-kUlZY6N8cNMGVJYMXspQmi3V4e0FgC4Ay4k,1011
|
|
66
66
|
coding/proxy/vendors/alibaba.py,sha256=vYhvk8hZEeOpqUrtGT-uaK89y6uolrDUAT2BvTD_IQY,876
|
|
67
67
|
coding/proxy/vendors/anthropic.py,sha256=KA_fvOxagGBggH2pc3l9Zoyl_XYUyRdfR8bI-eVj_e4,6328
|
|
68
|
-
coding/proxy/vendors/antigravity.py,sha256=
|
|
68
|
+
coding/proxy/vendors/antigravity.py,sha256=sz2-TSIUe9YDVen7A4d9K6lmhyh4jGWN3dppfabSJg0,21431
|
|
69
69
|
coding/proxy/vendors/base.py,sha256=vrAaI_4uLtzaFcPWtBStVMHU5AgkCWTNLEgVeoHHgLE,14781
|
|
70
70
|
coding/proxy/vendors/copilot.py,sha256=qAFTO2xMqnawaFlvZS2a9-7f_UEkfODIFfDlgOBiQl4,29590
|
|
71
71
|
coding/proxy/vendors/copilot_models.py,sha256=UckIQYA4qLyNwEA09koQGBOZ65Nxhuo3ytUuDNqtzRU,15514
|
|
@@ -79,8 +79,8 @@ coding/proxy/vendors/native_anthropic.py,sha256=SxtM71PDci0gqLqiwCrFnT410SnSoD7F
|
|
|
79
79
|
coding/proxy/vendors/token_manager.py,sha256=s10t4Com0jNnKGkPyJ_HpG5SjHrCEJvfArEOAaPKA_k,4189
|
|
80
80
|
coding/proxy/vendors/xiaomi.py,sha256=E-GcmJBZh7GOtDFonxZmlf0hKRhrlrXzL0IxHFRYcRo,860
|
|
81
81
|
coding/proxy/vendors/zhipu.py,sha256=3j_rqNFu1CX-B5ugtrL6Y1OeWSy9yiqsVa9Bi1ssaAA,1062
|
|
82
|
-
coding_proxy-0.2.
|
|
83
|
-
coding_proxy-0.2.
|
|
84
|
-
coding_proxy-0.2.
|
|
85
|
-
coding_proxy-0.2.
|
|
86
|
-
coding_proxy-0.2.
|
|
82
|
+
coding_proxy-0.2.2.dist-info/METADATA,sha256=4eYTzmULjk3sHv87lB4NnGv4RQ2I_VNADAeLVJGB9mQ,10817
|
|
83
|
+
coding_proxy-0.2.2.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
|
|
84
|
+
coding_proxy-0.2.2.dist-info/entry_points.txt,sha256=moIVzt5ho0Wk9B47LOo2SEAbhzuDDHWi-EfM30U0XBg,54
|
|
85
|
+
coding_proxy-0.2.2.dist-info/licenses/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
|
|
86
|
+
coding_proxy-0.2.2.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|