codex-proxy 3.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.
- codex_proxy/__init__.py +3 -0
- codex_proxy/__main__.py +66 -0
- codex_proxy/circuit_breaker.py +83 -0
- codex_proxy/compaction.py +42 -0
- codex_proxy/config.py +313 -0
- codex_proxy/key_rotation.py +108 -0
- codex_proxy/plugins.py +110 -0
- codex_proxy/plugins_builtin.py +34 -0
- codex_proxy/providers.py +130 -0
- codex_proxy/server.py +647 -0
- codex_proxy/store.py +97 -0
- codex_proxy/translator.py +360 -0
- codex_proxy/tui.py +262 -0
- codex_proxy-3.1.0.dist-info/METADATA +25 -0
- codex_proxy-3.1.0.dist-info/RECORD +18 -0
- codex_proxy-3.1.0.dist-info/WHEEL +4 -0
- codex_proxy-3.1.0.dist-info/entry_points.txt +2 -0
- codex_proxy-3.1.0.dist-info/licenses/LICENSE +21 -0
codex_proxy/plugins.py
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
"""Plugin system — hook-based extensibility for codex-proxy."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import importlib
|
|
6
|
+
import logging
|
|
7
|
+
from dataclasses import dataclass, field
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
logger = logging.getLogger("codex-proxy")
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@dataclass
|
|
14
|
+
class PluginContext:
|
|
15
|
+
"""Safe context passed to plugin hooks."""
|
|
16
|
+
|
|
17
|
+
request_id: str
|
|
18
|
+
method: str # "http" or "ws"
|
|
19
|
+
model: str
|
|
20
|
+
provider: str
|
|
21
|
+
api_key_masked: str
|
|
22
|
+
stream: bool
|
|
23
|
+
status_code: int | None = None
|
|
24
|
+
error: str | None = None
|
|
25
|
+
duration_ms: float | None = None
|
|
26
|
+
metadata: dict[str, Any] = field(default_factory=dict)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class Plugin:
|
|
30
|
+
"""Base class for plugins. Override only the hooks you need."""
|
|
31
|
+
|
|
32
|
+
name: str = "unnamed"
|
|
33
|
+
|
|
34
|
+
async def on_startup(self, config: Any) -> None:
|
|
35
|
+
pass
|
|
36
|
+
|
|
37
|
+
async def on_shutdown(self) -> None:
|
|
38
|
+
pass
|
|
39
|
+
|
|
40
|
+
async def on_request(self, ctx: PluginContext) -> PluginContext:
|
|
41
|
+
return ctx
|
|
42
|
+
|
|
43
|
+
async def on_response(self, ctx: PluginContext) -> None:
|
|
44
|
+
pass
|
|
45
|
+
|
|
46
|
+
async def on_error(self, ctx: PluginContext) -> None:
|
|
47
|
+
pass
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class PluginRegistry:
|
|
51
|
+
"""Loads and manages plugins from dotted module paths."""
|
|
52
|
+
|
|
53
|
+
def __init__(self) -> None:
|
|
54
|
+
self._plugins: list[Plugin] = []
|
|
55
|
+
|
|
56
|
+
def load(self, plugin_paths: list[str]) -> None:
|
|
57
|
+
"""Load plugins from dotted module.class paths."""
|
|
58
|
+
for path in plugin_paths:
|
|
59
|
+
try:
|
|
60
|
+
module_path, class_name = path.rsplit(".", 1)
|
|
61
|
+
module = importlib.import_module(module_path)
|
|
62
|
+
cls = getattr(module, class_name)
|
|
63
|
+
instance: Plugin = cls()
|
|
64
|
+
instance.name = getattr(instance, "name", class_name)
|
|
65
|
+
self._plugins.append(instance)
|
|
66
|
+
logger.info("Plugin loaded: %s", instance.name)
|
|
67
|
+
except Exception as e:
|
|
68
|
+
logger.error("Failed to load plugin %s: %s", path, e)
|
|
69
|
+
|
|
70
|
+
async def on_startup(self, config: Any) -> None:
|
|
71
|
+
for p in self._plugins:
|
|
72
|
+
try:
|
|
73
|
+
await p.on_startup(config)
|
|
74
|
+
except Exception as e:
|
|
75
|
+
logger.error("Plugin %s on_startup error: %s", p.name, e)
|
|
76
|
+
|
|
77
|
+
async def on_shutdown(self) -> None:
|
|
78
|
+
for p in self._plugins:
|
|
79
|
+
try:
|
|
80
|
+
await p.on_shutdown()
|
|
81
|
+
except Exception as e:
|
|
82
|
+
logger.error("Plugin %s on_shutdown error: %s", p.name, e)
|
|
83
|
+
|
|
84
|
+
async def on_request(self, ctx: PluginContext) -> PluginContext:
|
|
85
|
+
for p in self._plugins:
|
|
86
|
+
try:
|
|
87
|
+
ctx = await p.on_request(ctx)
|
|
88
|
+
except Exception as e:
|
|
89
|
+
logger.error("Plugin %s on_request error: %s", p.name, e)
|
|
90
|
+
return ctx
|
|
91
|
+
|
|
92
|
+
async def on_response(self, ctx: PluginContext) -> None:
|
|
93
|
+
for p in self._plugins:
|
|
94
|
+
try:
|
|
95
|
+
await p.on_response(ctx)
|
|
96
|
+
except Exception as e:
|
|
97
|
+
logger.error("Plugin %s on_response error: %s", p.name, e)
|
|
98
|
+
|
|
99
|
+
async def on_error(self, ctx: PluginContext) -> None:
|
|
100
|
+
for p in self._plugins:
|
|
101
|
+
try:
|
|
102
|
+
await p.on_error(ctx)
|
|
103
|
+
except Exception as e:
|
|
104
|
+
logger.error("Plugin %s on_error error: %s", p.name, e)
|
|
105
|
+
|
|
106
|
+
def list_plugins(self) -> list[str]:
|
|
107
|
+
return [p.name for p in self._plugins]
|
|
108
|
+
|
|
109
|
+
def reset(self) -> None:
|
|
110
|
+
self._plugins.clear()
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"""Built-in plugins for codex-proxy."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
|
|
7
|
+
from .plugins import Plugin, PluginContext
|
|
8
|
+
|
|
9
|
+
logger = logging.getLogger("codex-proxy")
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class LoggingPlugin(Plugin):
|
|
13
|
+
"""Structured request/response logging plugin."""
|
|
14
|
+
|
|
15
|
+
name = "logging"
|
|
16
|
+
|
|
17
|
+
async def on_request(self, ctx: PluginContext) -> PluginContext:
|
|
18
|
+
logger.info(
|
|
19
|
+
"[plugin:logging] request id=%s method=%s model=%s provider=%s stream=%s",
|
|
20
|
+
ctx.request_id, ctx.method, ctx.model, ctx.provider, ctx.stream,
|
|
21
|
+
)
|
|
22
|
+
return ctx
|
|
23
|
+
|
|
24
|
+
async def on_response(self, ctx: PluginContext) -> None:
|
|
25
|
+
logger.info(
|
|
26
|
+
"[plugin:logging] response id=%s status=%d duration=%.1fms",
|
|
27
|
+
ctx.request_id, ctx.status_code or 0, ctx.duration_ms or 0,
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
async def on_error(self, ctx: PluginContext) -> None:
|
|
31
|
+
logger.error(
|
|
32
|
+
"[plugin:logging] error id=%s status=%d error=%s",
|
|
33
|
+
ctx.request_id, ctx.status_code or 0, ctx.error,
|
|
34
|
+
)
|
codex_proxy/providers.py
ADDED
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
"""Provider-specific adapters for backend quirks."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@dataclass
|
|
9
|
+
class ProviderAdapter:
|
|
10
|
+
"""Base adapter — no modifications."""
|
|
11
|
+
|
|
12
|
+
name: str = "default"
|
|
13
|
+
|
|
14
|
+
def adjust_request(self, cc_body: dict) -> dict:
|
|
15
|
+
"""Modify the Chat Completions request before sending."""
|
|
16
|
+
return cc_body
|
|
17
|
+
|
|
18
|
+
def adjust_headers(self, headers: dict) -> dict:
|
|
19
|
+
"""Modify HTTP headers before sending."""
|
|
20
|
+
return headers
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _strip_stream_options(cc_body: dict) -> dict:
|
|
24
|
+
cc_body.pop("stream_options", None)
|
|
25
|
+
return cc_body
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class OllamaAdapter(ProviderAdapter):
|
|
29
|
+
"""Ollama doesn't support stream_options and doesn't need a real API key."""
|
|
30
|
+
|
|
31
|
+
name: str = "ollama"
|
|
32
|
+
|
|
33
|
+
def adjust_request(self, cc_body: dict) -> dict:
|
|
34
|
+
return _strip_stream_options(cc_body)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class OpenRouterAdapter(ProviderAdapter):
|
|
38
|
+
"""OpenRouter requires HTTP-Referer header for rankings."""
|
|
39
|
+
|
|
40
|
+
name: str = "openrouter"
|
|
41
|
+
|
|
42
|
+
def adjust_headers(self, headers: dict) -> dict:
|
|
43
|
+
headers.setdefault("HTTP-Referer", "https://github.com/ZiryaNoov/codex-proxy")
|
|
44
|
+
headers.setdefault("X-Title", "codex-proxy")
|
|
45
|
+
return headers
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class GroqAdapter(ProviderAdapter):
|
|
49
|
+
"""Groq has strict rate limits — no special request modifications needed yet."""
|
|
50
|
+
|
|
51
|
+
name: str = "groq"
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class AnthropicAdapter(ProviderAdapter):
|
|
55
|
+
"""Anthropic uses x-api-key header and anthropic-version."""
|
|
56
|
+
|
|
57
|
+
name: str = "anthropic"
|
|
58
|
+
|
|
59
|
+
def adjust_request(self, cc_body: dict) -> dict:
|
|
60
|
+
return _strip_stream_options(cc_body)
|
|
61
|
+
|
|
62
|
+
def adjust_headers(self, headers: dict) -> dict:
|
|
63
|
+
api_key = headers.pop("Authorization", "").removeprefix("Bearer ").strip()
|
|
64
|
+
headers["x-api-key"] = api_key
|
|
65
|
+
headers["anthropic-version"] = "2023-06-01"
|
|
66
|
+
return headers
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
class GeminiAdapter(ProviderAdapter):
|
|
70
|
+
"""Google Gemini OpenAI-compatible endpoint."""
|
|
71
|
+
|
|
72
|
+
name: str = "gemini"
|
|
73
|
+
|
|
74
|
+
def adjust_request(self, cc_body: dict) -> dict:
|
|
75
|
+
return _strip_stream_options(cc_body)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
class DeepSeekAdapter(ProviderAdapter):
|
|
79
|
+
"""DeepSeek is OpenAI-compatible but doesn't support stream_options."""
|
|
80
|
+
|
|
81
|
+
name: str = "deepseek"
|
|
82
|
+
|
|
83
|
+
def adjust_request(self, cc_body: dict) -> dict:
|
|
84
|
+
return _strip_stream_options(cc_body)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
class MistralAdapter(ProviderAdapter):
|
|
88
|
+
"""Mistral is OpenAI-compatible but doesn't support stream_options."""
|
|
89
|
+
|
|
90
|
+
name: str = "mistral"
|
|
91
|
+
|
|
92
|
+
def adjust_request(self, cc_body: dict) -> dict:
|
|
93
|
+
return _strip_stream_options(cc_body)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
class CohereAdapter(ProviderAdapter):
|
|
97
|
+
"""Cohere OpenAI-compatible endpoint."""
|
|
98
|
+
|
|
99
|
+
name: str = "cohere"
|
|
100
|
+
|
|
101
|
+
def adjust_request(self, cc_body: dict) -> dict:
|
|
102
|
+
return _strip_stream_options(cc_body)
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
class NvidiaAdapter(ProviderAdapter):
|
|
106
|
+
"""NVIDIA NIM OpenAI-compatible endpoint."""
|
|
107
|
+
|
|
108
|
+
name: str = "nvidia"
|
|
109
|
+
|
|
110
|
+
def adjust_request(self, cc_body: dict) -> dict:
|
|
111
|
+
return _strip_stream_options(cc_body)
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
_ADAPTERS: dict[str, type[ProviderAdapter]] = {
|
|
115
|
+
"ollama": OllamaAdapter,
|
|
116
|
+
"openrouter": OpenRouterAdapter,
|
|
117
|
+
"groq": GroqAdapter,
|
|
118
|
+
"anthropic": AnthropicAdapter,
|
|
119
|
+
"gemini": GeminiAdapter,
|
|
120
|
+
"deepseek": DeepSeekAdapter,
|
|
121
|
+
"mistral": MistralAdapter,
|
|
122
|
+
"cohere": CohereAdapter,
|
|
123
|
+
"nvidia": NvidiaAdapter,
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def get_adapter(provider_name: str) -> ProviderAdapter:
|
|
128
|
+
"""Get the adapter for a provider by name. Returns base adapter if unknown."""
|
|
129
|
+
cls = _ADAPTERS.get(provider_name, ProviderAdapter)
|
|
130
|
+
return cls(name=provider_name)
|