coding-proxy 0.1.0__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.
Files changed (77) hide show
  1. coding/__init__.py +0 -0
  2. coding/proxy/__init__.py +3 -0
  3. coding/proxy/__main__.py +5 -0
  4. coding/proxy/auth/__init__.py +13 -0
  5. coding/proxy/auth/providers/__init__.py +6 -0
  6. coding/proxy/auth/providers/base.py +35 -0
  7. coding/proxy/auth/providers/github.py +133 -0
  8. coding/proxy/auth/providers/google.py +237 -0
  9. coding/proxy/auth/runtime.py +122 -0
  10. coding/proxy/auth/store.py +74 -0
  11. coding/proxy/cli/__init__.py +151 -0
  12. coding/proxy/cli/auth_commands.py +224 -0
  13. coding/proxy/compat/__init__.py +30 -0
  14. coding/proxy/compat/canonical.py +193 -0
  15. coding/proxy/compat/session_store.py +137 -0
  16. coding/proxy/config/__init__.py +6 -0
  17. coding/proxy/config/auth_schema.py +24 -0
  18. coding/proxy/config/loader.py +139 -0
  19. coding/proxy/config/resiliency.py +46 -0
  20. coding/proxy/config/routing.py +279 -0
  21. coding/proxy/config/schema.py +280 -0
  22. coding/proxy/config/server.py +23 -0
  23. coding/proxy/config/vendors.py +53 -0
  24. coding/proxy/convert/__init__.py +14 -0
  25. coding/proxy/convert/anthropic_to_gemini.py +352 -0
  26. coding/proxy/convert/anthropic_to_openai.py +352 -0
  27. coding/proxy/convert/gemini_sse_adapter.py +169 -0
  28. coding/proxy/convert/gemini_to_anthropic.py +98 -0
  29. coding/proxy/convert/openai_to_anthropic.py +88 -0
  30. coding/proxy/logging/__init__.py +49 -0
  31. coding/proxy/logging/db.py +308 -0
  32. coding/proxy/logging/stats.py +129 -0
  33. coding/proxy/model/__init__.py +93 -0
  34. coding/proxy/model/auth.py +32 -0
  35. coding/proxy/model/compat.py +153 -0
  36. coding/proxy/model/constants.py +21 -0
  37. coding/proxy/model/pricing.py +70 -0
  38. coding/proxy/model/token.py +64 -0
  39. coding/proxy/model/vendor.py +218 -0
  40. coding/proxy/pricing.py +100 -0
  41. coding/proxy/routing/__init__.py +47 -0
  42. coding/proxy/routing/circuit_breaker.py +152 -0
  43. coding/proxy/routing/error_classifier.py +67 -0
  44. coding/proxy/routing/executor.py +453 -0
  45. coding/proxy/routing/model_mapper.py +90 -0
  46. coding/proxy/routing/quota_guard.py +169 -0
  47. coding/proxy/routing/rate_limit.py +159 -0
  48. coding/proxy/routing/retry.py +82 -0
  49. coding/proxy/routing/router.py +84 -0
  50. coding/proxy/routing/session_manager.py +62 -0
  51. coding/proxy/routing/tier.py +171 -0
  52. coding/proxy/routing/usage_parser.py +193 -0
  53. coding/proxy/routing/usage_recorder.py +131 -0
  54. coding/proxy/server/__init__.py +1 -0
  55. coding/proxy/server/app.py +142 -0
  56. coding/proxy/server/factory.py +175 -0
  57. coding/proxy/server/request_normalizer.py +139 -0
  58. coding/proxy/server/responses.py +74 -0
  59. coding/proxy/server/routes.py +264 -0
  60. coding/proxy/streaming/__init__.py +1 -0
  61. coding/proxy/streaming/anthropic_compat.py +484 -0
  62. coding/proxy/vendors/__init__.py +29 -0
  63. coding/proxy/vendors/anthropic.py +44 -0
  64. coding/proxy/vendors/antigravity.py +328 -0
  65. coding/proxy/vendors/base.py +353 -0
  66. coding/proxy/vendors/copilot.py +702 -0
  67. coding/proxy/vendors/copilot_models.py +438 -0
  68. coding/proxy/vendors/copilot_token_manager.py +167 -0
  69. coding/proxy/vendors/copilot_urls.py +16 -0
  70. coding/proxy/vendors/mixins.py +71 -0
  71. coding/proxy/vendors/token_manager.py +128 -0
  72. coding/proxy/vendors/zhipu.py +243 -0
  73. coding_proxy-0.1.0.dist-info/METADATA +184 -0
  74. coding_proxy-0.1.0.dist-info/RECORD +77 -0
  75. coding_proxy-0.1.0.dist-info/WHEEL +4 -0
  76. coding_proxy-0.1.0.dist-info/entry_points.txt +2 -0
  77. coding_proxy-0.1.0.dist-info/licenses/LICENSE +201 -0
@@ -0,0 +1,47 @@
1
+ """路由模块."""
2
+
3
+ from .circuit_breaker import CircuitBreaker, CircuitState
4
+ from .error_classifier import (
5
+ build_request_capabilities,
6
+ extract_error_payload_from_http_status,
7
+ is_semantic_rejection,
8
+ )
9
+ from .executor import _RouteExecutor
10
+ from .model_mapper import ModelMapper
11
+ from .quota_guard import QuotaGuard, QuotaState
12
+ from .rate_limit import RateLimitInfo
13
+ from .rate_limit import (
14
+ compute_effective_retry_seconds,
15
+ compute_rate_limit_deadline,
16
+ parse_rate_limit_headers,
17
+ )
18
+ from .retry import RetryConfig, calculate_delay, is_retryable_error
19
+ from .router import RequestRouter
20
+ from .session_manager import RouteSessionManager
21
+ from .tier import BackendTier
22
+ from .usage_parser import (
23
+ build_usage_evidence_records,
24
+ has_missing_input_usage_signals,
25
+ parse_usage_from_chunk,
26
+ )
27
+ from .usage_recorder import UsageRecorder
28
+
29
+ __all__ = [
30
+ # Core routing
31
+ "CircuitBreaker", "CircuitState",
32
+ "ModelMapper", "RequestRouter", "BackendTier",
33
+ # Decomposed components (internal use)
34
+ "_RouteExecutor", "UsageRecorder", "RouteSessionManager",
35
+ # Resiliency
36
+ "QuotaGuard", "QuotaState", "RateLimitInfo",
37
+ "RetryConfig",
38
+ "parse_rate_limit_headers", "compute_effective_retry_seconds",
39
+ "compute_rate_limit_deadline",
40
+ "is_retryable_error", "calculate_delay",
41
+ # Error classification
42
+ "build_request_capabilities", "is_semantic_rejection",
43
+ "extract_error_payload_from_http_status",
44
+ # Usage parsing
45
+ "build_usage_evidence_records", "has_missing_input_usage_signals",
46
+ "parse_usage_from_chunk",
47
+ ]
@@ -0,0 +1,152 @@
1
+ """熔断器 (Circuit Breaker) — 状态机实现."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import logging
6
+ import threading
7
+ import time
8
+ from enum import Enum
9
+
10
+ logger = logging.getLogger(__name__)
11
+
12
+
13
+ class CircuitState(Enum):
14
+ CLOSED = "closed" # 正常:使用主后端
15
+ OPEN = "open" # 故障:使用备选后端
16
+ HALF_OPEN = "half_open" # 试探:测试主后端是否恢复
17
+
18
+
19
+ class CircuitBreaker:
20
+ """线程安全的熔断器.
21
+
22
+ 状态转换:
23
+ - CLOSED → OPEN: 连续 failure_threshold 次失败
24
+ - OPEN → HALF_OPEN: recovery_timeout 后
25
+ - HALF_OPEN → CLOSED: 连续 success_threshold 次成功
26
+ - HALF_OPEN → OPEN: 任意一次失败(指数退避)
27
+ """
28
+
29
+ def __init__(
30
+ self,
31
+ failure_threshold: int = 3,
32
+ recovery_timeout_seconds: int = 300,
33
+ success_threshold: int = 2,
34
+ max_recovery_seconds: int = 3600,
35
+ ) -> None:
36
+ self._failure_threshold = failure_threshold
37
+ self._recovery_timeout = recovery_timeout_seconds
38
+ self._success_threshold = success_threshold
39
+ self._max_recovery = max_recovery_seconds
40
+
41
+ self._state = CircuitState.CLOSED
42
+ self._failure_count = 0
43
+ self._success_count = 0
44
+ self._last_failure_time: float | None = None
45
+ self._current_recovery = recovery_timeout_seconds
46
+ self._lock = threading.Lock()
47
+
48
+ @property
49
+ def state(self) -> CircuitState:
50
+ with self._lock:
51
+ self._check_recovery()
52
+ return self._state
53
+
54
+ def can_execute(self) -> bool:
55
+ """判断是否可以在主后端上执行请求."""
56
+ with self._lock:
57
+ self._check_recovery()
58
+ return self._state in (CircuitState.CLOSED, CircuitState.HALF_OPEN)
59
+
60
+ def record_success(self) -> None:
61
+ """记录一次成功调用."""
62
+ with self._lock:
63
+ self._failure_count = 0
64
+ if self._state == CircuitState.HALF_OPEN:
65
+ self._success_count += 1
66
+ if self._success_count >= self._success_threshold:
67
+ self._transition_to(CircuitState.CLOSED)
68
+ logger.info("Circuit breaker: HALF_OPEN → CLOSED (recovered)")
69
+ elif self._state == CircuitState.CLOSED:
70
+ # 正常状态下成功,无需操作
71
+ pass
72
+
73
+ def record_failure(self, retry_after_seconds: float | None = None) -> None:
74
+ """记录一次失败调用.
75
+
76
+ Args:
77
+ retry_after_seconds: 从响应头解析出的建议恢复时间(秒)。
78
+ 若提供且大于当前指数退避值,将覆盖以避免过早探测。
79
+ """
80
+ with self._lock:
81
+ self._failure_count += 1
82
+ self._success_count = 0
83
+ self._last_failure_time = time.monotonic()
84
+
85
+ if self._state == CircuitState.HALF_OPEN:
86
+ self._transition_to(CircuitState.OPEN)
87
+ self._backoff_recovery(hint_seconds=retry_after_seconds)
88
+ logger.warning(
89
+ "Circuit breaker: HALF_OPEN → OPEN (recovery failed, next retry in %ds)",
90
+ self._current_recovery,
91
+ )
92
+ elif self._state == CircuitState.CLOSED:
93
+ if self._failure_count >= self._failure_threshold:
94
+ self._transition_to(CircuitState.OPEN)
95
+ if retry_after_seconds and retry_after_seconds > self._current_recovery:
96
+ self._current_recovery = min(
97
+ retry_after_seconds, self._max_recovery,
98
+ )
99
+ logger.warning(
100
+ "Circuit breaker: CLOSED → OPEN (%d consecutive failures, next retry in %ds)",
101
+ self._failure_count,
102
+ self._current_recovery,
103
+ )
104
+
105
+ def reset(self) -> None:
106
+ """手动重置熔断器为 CLOSED 状态."""
107
+ with self._lock:
108
+ self._transition_to(CircuitState.CLOSED)
109
+ self._current_recovery = self._recovery_timeout
110
+ logger.info("Circuit breaker: manually reset to CLOSED")
111
+
112
+ def get_info(self) -> dict:
113
+ """获取熔断器状态信息."""
114
+ with self._lock:
115
+ self._check_recovery()
116
+ return {
117
+ "state": self._state.value,
118
+ "failure_count": self._failure_count,
119
+ "success_count": self._success_count,
120
+ "current_recovery_seconds": self._current_recovery,
121
+ "last_failure_time": self._last_failure_time,
122
+ }
123
+
124
+ def _check_recovery(self) -> None:
125
+ """检查是否应从 OPEN 转为 HALF_OPEN."""
126
+ if self._state != CircuitState.OPEN:
127
+ return
128
+ if self._last_failure_time is None:
129
+ return
130
+ elapsed = time.monotonic() - self._last_failure_time
131
+ if elapsed >= self._current_recovery:
132
+ self._transition_to(CircuitState.HALF_OPEN)
133
+ logger.info("Circuit breaker: OPEN → HALF_OPEN (recovery timeout)")
134
+
135
+ def _transition_to(self, new_state: CircuitState) -> None:
136
+ old = self._state
137
+ self._state = new_state
138
+ if new_state == CircuitState.CLOSED:
139
+ self._failure_count = 0
140
+ self._success_count = 0
141
+ self._current_recovery = self._recovery_timeout
142
+ elif new_state == CircuitState.HALF_OPEN:
143
+ self._success_count = 0
144
+
145
+ def _backoff_recovery(self, hint_seconds: float | None = None) -> None:
146
+ """指数退避恢复超时,支持 server-hinted 覆盖."""
147
+ exponential = min(self._current_recovery * 2, self._max_recovery)
148
+ if hint_seconds is not None and hint_seconds > exponential:
149
+ # Server 告知的恢复时间优先于指数退避
150
+ self._current_recovery = min(hint_seconds, self._max_recovery)
151
+ else:
152
+ self._current_recovery = exponential
@@ -0,0 +1,67 @@
1
+ """HTTP 错误分类与请求能力画像提取."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ from typing import Any
7
+
8
+ import httpx
9
+
10
+ from ..vendors.base import RequestCapabilities
11
+
12
+
13
+ def extract_error_payload_from_http_status(exc: httpx.HTTPStatusError) -> dict[str, Any] | None:
14
+ response = exc.response
15
+ if response is None or not response.content:
16
+ return None
17
+ try:
18
+ payload = response.json()
19
+ except (json.JSONDecodeError, UnicodeDecodeError, TypeError, ValueError):
20
+ return None
21
+ return payload if isinstance(payload, dict) else None
22
+
23
+
24
+ def is_semantic_rejection(
25
+ *,
26
+ status_code: int,
27
+ error_type: str | None = None,
28
+ error_message: str | None = None,
29
+ ) -> bool:
30
+ if status_code != 400:
31
+ return False
32
+ normalized_type = (error_type or "").strip().lower()
33
+ if normalized_type == "invalid_request_error":
34
+ return True
35
+ normalized_message = (error_message or "").lower()
36
+ return any(
37
+ marker in normalized_message
38
+ for marker in (
39
+ "invalid_request_error",
40
+ "should match pattern",
41
+ "validation",
42
+ "tool_use_id",
43
+ "server_tool_use",
44
+ )
45
+ )
46
+
47
+
48
+ def build_request_capabilities(body: dict[str, Any]) -> RequestCapabilities:
49
+ """从请求体提取能力画像."""
50
+ has_images = False
51
+ for msg in body.get("messages", []):
52
+ content = msg.get("content")
53
+ if not isinstance(content, list):
54
+ continue
55
+ if any(
56
+ isinstance(block, dict) and block.get("type") == "image"
57
+ for block in content
58
+ ):
59
+ has_images = True
60
+ break
61
+
62
+ return RequestCapabilities(
63
+ has_tools=bool(body.get("tools") or body.get("tool_choice")),
64
+ has_thinking=bool(body.get("thinking") or body.get("extended_thinking")),
65
+ has_images=has_images,
66
+ has_metadata=bool(body.get("metadata")),
67
+ )