codex-api-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.
- codex_api_proxy/__init__.py +3 -0
- codex_api_proxy/app_server_runner.py +554 -0
- codex_api_proxy/cli.py +570 -0
- codex_api_proxy/codex_runner.py +278 -0
- codex_api_proxy/config.py +83 -0
- codex_api_proxy/main.py +561 -0
- codex_api_proxy/prompt.py +31 -0
- codex_api_proxy/schemas.py +48 -0
- codex_api_proxy-0.1.0.dist-info/METADATA +347 -0
- codex_api_proxy-0.1.0.dist-info/RECORD +13 -0
- codex_api_proxy-0.1.0.dist-info/WHEEL +5 -0
- codex_api_proxy-0.1.0.dist-info/entry_points.txt +2 -0
- codex_api_proxy-0.1.0.dist-info/top_level.txt +1 -0
codex_api_proxy/cli.py
ADDED
|
@@ -0,0 +1,570 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import json
|
|
5
|
+
import os
|
|
6
|
+
import signal
|
|
7
|
+
import subprocess
|
|
8
|
+
import sys
|
|
9
|
+
import time
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
|
|
12
|
+
import uvicorn
|
|
13
|
+
|
|
14
|
+
from .app_server_runner import default_app_server_codex_home as _default_app_server_codex_home
|
|
15
|
+
from .config import Settings
|
|
16
|
+
from .main import create_app
|
|
17
|
+
|
|
18
|
+
FAST_MODEL = None
|
|
19
|
+
FAST_CODEX_CONFIGS = ['model_reasoning_effort="low"']
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def default_state_dir() -> Path:
|
|
23
|
+
return Path.home() / ".codex-api-proxy"
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def default_pid_file() -> Path:
|
|
27
|
+
return default_state_dir() / "codex-api-proxy.pid"
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def default_log_file() -> Path:
|
|
31
|
+
return default_state_dir() / "codex-api-proxy.log"
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def default_state_file() -> Path:
|
|
35
|
+
return default_state_dir() / "codex-api-proxy.state.json"
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def default_workspace_dir() -> Path:
|
|
39
|
+
return default_state_dir() / "workspace"
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def default_app_server_codex_home() -> Path:
|
|
43
|
+
return _default_app_server_codex_home()
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _path(value: str) -> Path:
|
|
47
|
+
return Path(value).expanduser()
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def add_settings_args(parser: argparse.ArgumentParser, *, defaults: bool = True) -> None:
|
|
51
|
+
parser.add_argument("--host", default="127.0.0.1" if defaults else None, help="Bind host. Defaults to 127.0.0.1.")
|
|
52
|
+
parser.add_argument("--port", type=int, default=8765 if defaults else None, help="Bind port. Defaults to 8765.")
|
|
53
|
+
parser.add_argument("--api-key", default=None, help="Require Authorization: Bearer <api-key>.")
|
|
54
|
+
parser.add_argument("--codex-bin", default="codex" if defaults else None, help="Codex executable. Defaults to codex.")
|
|
55
|
+
parser.add_argument("--proxy", default=None, help="Proxy URL to pass to Codex as http_proxy and https_proxy.")
|
|
56
|
+
parser.add_argument("--model", default=None, help="Codex model to pass as --model.")
|
|
57
|
+
parser.add_argument(
|
|
58
|
+
"--engine",
|
|
59
|
+
choices=["exec", "app-server"],
|
|
60
|
+
default="exec" if defaults else None,
|
|
61
|
+
help="Execution engine. exec uses codex exec; app-server uses experimental long-lived workers.",
|
|
62
|
+
)
|
|
63
|
+
parser.add_argument(
|
|
64
|
+
"--workers",
|
|
65
|
+
type=int,
|
|
66
|
+
default=1 if defaults else None,
|
|
67
|
+
help="Number of long-lived app-server workers.",
|
|
68
|
+
)
|
|
69
|
+
parser.add_argument(
|
|
70
|
+
"--max-queue-size",
|
|
71
|
+
type=int,
|
|
72
|
+
default=64 if defaults else None,
|
|
73
|
+
help="Maximum queued app-server requests before returning 429.",
|
|
74
|
+
)
|
|
75
|
+
parser.add_argument(
|
|
76
|
+
"--queue-timeout-seconds",
|
|
77
|
+
type=float,
|
|
78
|
+
default=30.0 if defaults else None,
|
|
79
|
+
help="Maximum time to wait for an app-server worker.",
|
|
80
|
+
)
|
|
81
|
+
parser.add_argument(
|
|
82
|
+
"--app-server-codex-home",
|
|
83
|
+
type=_path,
|
|
84
|
+
default=default_app_server_codex_home() if defaults else None,
|
|
85
|
+
help="Isolated CODEX_HOME used by app-server workers.",
|
|
86
|
+
)
|
|
87
|
+
parser.add_argument(
|
|
88
|
+
"--codex-config",
|
|
89
|
+
action="append",
|
|
90
|
+
dest="codex_configs",
|
|
91
|
+
default=None,
|
|
92
|
+
help="Codex config override passed as -c key=value. May be repeated.",
|
|
93
|
+
)
|
|
94
|
+
parser.add_argument("--ephemeral", action="store_true", default=True, help="Run codex exec with --ephemeral.")
|
|
95
|
+
parser.add_argument("--fast", action="store_true", help="Use fast defaults: low reasoning config.")
|
|
96
|
+
parser.add_argument(
|
|
97
|
+
"--default-cwd",
|
|
98
|
+
type=_path,
|
|
99
|
+
default=default_workspace_dir() if defaults else None,
|
|
100
|
+
help="Default Codex working directory.",
|
|
101
|
+
)
|
|
102
|
+
parser.add_argument(
|
|
103
|
+
"--allowed-root",
|
|
104
|
+
type=_path,
|
|
105
|
+
action="append",
|
|
106
|
+
dest="allowed_roots",
|
|
107
|
+
default=None,
|
|
108
|
+
help="Allowed cwd root. May be repeated. Defaults to --default-cwd.",
|
|
109
|
+
)
|
|
110
|
+
parser.add_argument(
|
|
111
|
+
"--timeout-seconds",
|
|
112
|
+
type=float,
|
|
113
|
+
default=300.0 if defaults else None,
|
|
114
|
+
help="Per-request Codex timeout.",
|
|
115
|
+
)
|
|
116
|
+
parser.add_argument(
|
|
117
|
+
"--max-concurrency",
|
|
118
|
+
type=int,
|
|
119
|
+
default=1 if defaults else None,
|
|
120
|
+
help="Maximum concurrent Codex executions.",
|
|
121
|
+
)
|
|
122
|
+
parser.add_argument(
|
|
123
|
+
"--log-level",
|
|
124
|
+
choices=["debug", "info", "warning", "error"],
|
|
125
|
+
default="info" if defaults else None,
|
|
126
|
+
help="Uvicorn log level. Defaults to info.",
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def add_process_args(parser: argparse.ArgumentParser) -> None:
|
|
131
|
+
parser.add_argument("--pid-file", type=_path, default=default_pid_file(), help="Path to the daemon pid file.")
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def add_state_arg(parser: argparse.ArgumentParser) -> None:
|
|
135
|
+
parser.add_argument("--state-file", type=_path, default=default_state_file(), help="Path to the daemon state file.")
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
139
|
+
parser = argparse.ArgumentParser(prog="codex-api-proxy", description="Manage the local Codex proxy service.")
|
|
140
|
+
subparsers = parser.add_subparsers(dest="command", required=True)
|
|
141
|
+
|
|
142
|
+
start = subparsers.add_parser(
|
|
143
|
+
"start",
|
|
144
|
+
help="Start codex-api-proxy.",
|
|
145
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
146
|
+
epilog=(
|
|
147
|
+
"Examples:\n"
|
|
148
|
+
" codex-api-proxy start\n"
|
|
149
|
+
" codex-api-proxy start --host 0.0.0.0\n"
|
|
150
|
+
" codex-api-proxy start --proxy=http://127.0.0.1:8118 --fast --engine app-server --workers 4"
|
|
151
|
+
),
|
|
152
|
+
)
|
|
153
|
+
add_settings_args(start)
|
|
154
|
+
add_process_args(start)
|
|
155
|
+
add_state_arg(start)
|
|
156
|
+
start.add_argument("--log-file", type=_path, default=default_log_file(), help="Path to daemon log file.")
|
|
157
|
+
start.add_argument("--foreground", action="store_true", help="Run in the foreground instead of daemonizing.")
|
|
158
|
+
|
|
159
|
+
restart_parser = subparsers.add_parser("restart", help="Restart codex-api-proxy, reusing the last start settings.")
|
|
160
|
+
add_settings_args(restart_parser, defaults=False)
|
|
161
|
+
restart_parser.add_argument("--pid-file", type=_path, default=None, help="Override the daemon pid file.")
|
|
162
|
+
restart_parser.add_argument("--log-file", type=_path, default=None, help="Override the daemon log file.")
|
|
163
|
+
add_state_arg(restart_parser)
|
|
164
|
+
|
|
165
|
+
stop = subparsers.add_parser("stop", help="Stop a running codex-api-proxy daemon.")
|
|
166
|
+
add_process_args(stop)
|
|
167
|
+
|
|
168
|
+
status = subparsers.add_parser("status", help="Show codex-api-proxy daemon status.")
|
|
169
|
+
add_process_args(status)
|
|
170
|
+
add_state_arg(status)
|
|
171
|
+
status.add_argument("--verbose", action="store_true", help="Show saved runtime settings.")
|
|
172
|
+
|
|
173
|
+
return parser
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def build_serve_parser() -> argparse.ArgumentParser:
|
|
177
|
+
parser = argparse.ArgumentParser(prog="codex-api-proxy _serve", add_help=False)
|
|
178
|
+
parser.set_defaults(command="_serve")
|
|
179
|
+
add_settings_args(parser)
|
|
180
|
+
add_process_args(parser)
|
|
181
|
+
return parser
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def settings_from_args(args: argparse.Namespace) -> Settings:
|
|
185
|
+
default_cwd = args.default_cwd.expanduser()
|
|
186
|
+
allowed_roots = args.allowed_roots or [default_cwd]
|
|
187
|
+
model = args.model
|
|
188
|
+
codex_configs = list(args.codex_configs or [])
|
|
189
|
+
ephemeral = args.ephemeral
|
|
190
|
+
if args.fast:
|
|
191
|
+
if model is None:
|
|
192
|
+
model = FAST_MODEL
|
|
193
|
+
if not codex_configs:
|
|
194
|
+
codex_configs = list(FAST_CODEX_CONFIGS)
|
|
195
|
+
ephemeral = True
|
|
196
|
+
return Settings(
|
|
197
|
+
host=args.host,
|
|
198
|
+
port=args.port,
|
|
199
|
+
api_key=args.api_key,
|
|
200
|
+
codex_bin=args.codex_bin,
|
|
201
|
+
proxy=args.proxy,
|
|
202
|
+
model=model,
|
|
203
|
+
codex_configs=codex_configs,
|
|
204
|
+
ephemeral=ephemeral,
|
|
205
|
+
engine=args.engine,
|
|
206
|
+
workers=args.workers,
|
|
207
|
+
max_queue_size=args.max_queue_size,
|
|
208
|
+
queue_timeout_seconds=args.queue_timeout_seconds,
|
|
209
|
+
app_server_codex_home=args.app_server_codex_home,
|
|
210
|
+
default_cwd=default_cwd,
|
|
211
|
+
allowed_roots=allowed_roots,
|
|
212
|
+
request_timeout_seconds=args.timeout_seconds,
|
|
213
|
+
max_concurrency=args.max_concurrency,
|
|
214
|
+
log_level=args.log_level,
|
|
215
|
+
)
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def _path_or_none(value: object) -> Path | None:
|
|
219
|
+
return Path(str(value)).expanduser() if value else None
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def state_from_args(args: argparse.Namespace) -> dict[str, object]:
|
|
223
|
+
settings = settings_from_args(args)
|
|
224
|
+
return {
|
|
225
|
+
"host": settings.host,
|
|
226
|
+
"port": settings.port,
|
|
227
|
+
"api_key": settings.api_key,
|
|
228
|
+
"codex_bin": settings.codex_bin,
|
|
229
|
+
"proxy": settings.proxy,
|
|
230
|
+
"model": settings.model,
|
|
231
|
+
"codex_configs": settings.codex_configs or [],
|
|
232
|
+
"ephemeral": settings.ephemeral,
|
|
233
|
+
"engine": settings.engine,
|
|
234
|
+
"workers": settings.workers,
|
|
235
|
+
"max_queue_size": settings.max_queue_size,
|
|
236
|
+
"queue_timeout_seconds": settings.queue_timeout_seconds,
|
|
237
|
+
"app_server_codex_home": str(settings.app_server_codex_home) if settings.app_server_codex_home else None,
|
|
238
|
+
"default_cwd": str(settings.default_cwd),
|
|
239
|
+
"allowed_roots": [str(root) for root in (settings.allowed_roots or [])],
|
|
240
|
+
"timeout_seconds": settings.request_timeout_seconds,
|
|
241
|
+
"max_concurrency": settings.max_concurrency,
|
|
242
|
+
"log_level": settings.log_level,
|
|
243
|
+
"pid_file": str(args.pid_file),
|
|
244
|
+
"log_file": str(args.log_file),
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
def write_start_state(args: argparse.Namespace) -> None:
|
|
249
|
+
args.state_file.parent.mkdir(parents=True, exist_ok=True)
|
|
250
|
+
args.state_file.write_text(json.dumps(state_from_args(args), indent=2, sort_keys=True) + "\n", encoding="utf-8")
|
|
251
|
+
args.state_file.chmod(0o600)
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
def load_start_state(state_file: Path) -> dict[str, object]:
|
|
255
|
+
return json.loads(state_file.read_text(encoding="utf-8"))
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
def display_host(host: object) -> str:
|
|
259
|
+
value = str(host)
|
|
260
|
+
if value in {"0.0.0.0", "::", "[::]"}:
|
|
261
|
+
return "127.0.0.1"
|
|
262
|
+
return value
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
def endpoint_urls(host: object, port: object) -> tuple[str, str]:
|
|
266
|
+
base_url = f"http://{display_host(host)}:{port}"
|
|
267
|
+
return base_url, f"{base_url}/v1/chat/completions"
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
def print_endpoint_summary(host: object, port: object) -> None:
|
|
271
|
+
base_url, chat_url = endpoint_urls(host, port)
|
|
272
|
+
print("api endpoints:")
|
|
273
|
+
print(f" base_url: {base_url}")
|
|
274
|
+
print(f" chat_completions_url: {chat_url}")
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
def print_start_summary(args: argparse.Namespace, *, pid: int) -> None:
|
|
278
|
+
settings = settings_from_args(args)
|
|
279
|
+
allowed_roots = settings.allowed_roots or [settings.default_cwd]
|
|
280
|
+
print(f"codex-api-proxy started with pid {pid}")
|
|
281
|
+
print(f"state: {args.state_file}")
|
|
282
|
+
print(f"log: {args.log_file}")
|
|
283
|
+
print("effective parameters:")
|
|
284
|
+
print(f" host: {settings.host}")
|
|
285
|
+
print(f" port: {settings.port}")
|
|
286
|
+
print(f" api_key: {'<redacted>' if settings.api_key else '<none>'}")
|
|
287
|
+
print(f" codex_bin: {settings.codex_bin}")
|
|
288
|
+
print(f" proxy: {settings.proxy or '<none>'}")
|
|
289
|
+
print(f" model: {settings.model or '<default>'}")
|
|
290
|
+
print(f" engine: {settings.engine}")
|
|
291
|
+
print(f" workers: {settings.workers}")
|
|
292
|
+
print(f" max_queue_size: {settings.max_queue_size}")
|
|
293
|
+
print(f" queue_timeout_seconds: {settings.queue_timeout_seconds:g}")
|
|
294
|
+
print(f" app_server_codex_home: {settings.app_server_codex_home or '<default>'}")
|
|
295
|
+
print(f" codex_configs: {', '.join(settings.codex_configs or []) if settings.codex_configs else '<none>'}")
|
|
296
|
+
print(f" ephemeral: {settings.ephemeral}")
|
|
297
|
+
print(f" default_cwd: {settings.default_cwd}")
|
|
298
|
+
print(f" allowed_roots: {', '.join(str(root) for root in allowed_roots)}")
|
|
299
|
+
print(f" timeout_seconds: {settings.request_timeout_seconds}")
|
|
300
|
+
print(f" max_concurrency: {settings.max_concurrency}")
|
|
301
|
+
print(f" log_level: {settings.log_level}")
|
|
302
|
+
print(f" pid_file: {args.pid_file}")
|
|
303
|
+
print_endpoint_summary(settings.host, settings.port)
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
def print_public_bind_warning(args: argparse.Namespace) -> None:
|
|
307
|
+
if args.host in {"0.0.0.0", "::", "[::]"} and not args.api_key:
|
|
308
|
+
print(
|
|
309
|
+
"WARNING: --host 0.0.0.0 exposes codex-api-proxy beyond localhost. "
|
|
310
|
+
"Set --api-key before using it on untrusted networks."
|
|
311
|
+
)
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
def args_from_state(state: dict[str, object], overrides: argparse.Namespace) -> argparse.Namespace:
|
|
315
|
+
default_cwd = _path_or_none(overrides.default_cwd) or _path_or_none(state.get("default_cwd")) or Path.cwd()
|
|
316
|
+
pid_file = overrides.pid_file or _path_or_none(state.get("pid_file")) or default_pid_file()
|
|
317
|
+
log_file = overrides.log_file or _path_or_none(state.get("log_file")) or default_log_file()
|
|
318
|
+
allowed_roots = overrides.allowed_roots
|
|
319
|
+
if allowed_roots is None:
|
|
320
|
+
allowed_roots = [_path(str(root)) for root in state.get("allowed_roots", [])]
|
|
321
|
+
model = overrides.model if overrides.model is not None else state.get("model")
|
|
322
|
+
engine = overrides.engine if overrides.engine is not None else state.get("engine", "exec")
|
|
323
|
+
workers = overrides.workers if overrides.workers is not None else state.get("workers", 1)
|
|
324
|
+
max_queue_size = (
|
|
325
|
+
overrides.max_queue_size if overrides.max_queue_size is not None else state.get("max_queue_size", 64)
|
|
326
|
+
)
|
|
327
|
+
queue_timeout_seconds = (
|
|
328
|
+
overrides.queue_timeout_seconds
|
|
329
|
+
if overrides.queue_timeout_seconds is not None
|
|
330
|
+
else state.get("queue_timeout_seconds", 30.0)
|
|
331
|
+
)
|
|
332
|
+
app_server_codex_home = (
|
|
333
|
+
_path_or_none(overrides.app_server_codex_home)
|
|
334
|
+
or _path_or_none(state.get("app_server_codex_home"))
|
|
335
|
+
or default_app_server_codex_home()
|
|
336
|
+
)
|
|
337
|
+
codex_configs = overrides.codex_configs if overrides.codex_configs is not None else state.get("codex_configs", [])
|
|
338
|
+
ephemeral = overrides.ephemeral or bool(state.get("ephemeral", False))
|
|
339
|
+
if overrides.fast:
|
|
340
|
+
if overrides.model is None:
|
|
341
|
+
model = FAST_MODEL
|
|
342
|
+
if overrides.codex_configs is None:
|
|
343
|
+
codex_configs = list(FAST_CODEX_CONFIGS)
|
|
344
|
+
ephemeral = True
|
|
345
|
+
return argparse.Namespace(
|
|
346
|
+
command="start",
|
|
347
|
+
host=overrides.host if overrides.host is not None else state.get("host", "127.0.0.1"),
|
|
348
|
+
port=overrides.port if overrides.port is not None else state.get("port", 8765),
|
|
349
|
+
api_key=overrides.api_key if overrides.api_key is not None else state.get("api_key"),
|
|
350
|
+
codex_bin=overrides.codex_bin if overrides.codex_bin is not None else state.get("codex_bin", "codex"),
|
|
351
|
+
proxy=overrides.proxy if overrides.proxy is not None else state.get("proxy"),
|
|
352
|
+
model=model,
|
|
353
|
+
engine=engine,
|
|
354
|
+
workers=int(workers),
|
|
355
|
+
max_queue_size=int(max_queue_size),
|
|
356
|
+
queue_timeout_seconds=float(queue_timeout_seconds),
|
|
357
|
+
app_server_codex_home=app_server_codex_home,
|
|
358
|
+
codex_configs=list(codex_configs or []),
|
|
359
|
+
ephemeral=ephemeral,
|
|
360
|
+
default_cwd=default_cwd,
|
|
361
|
+
allowed_roots=allowed_roots or None,
|
|
362
|
+
timeout_seconds=(
|
|
363
|
+
overrides.timeout_seconds if overrides.timeout_seconds is not None else state.get("timeout_seconds", 300.0)
|
|
364
|
+
),
|
|
365
|
+
max_concurrency=(
|
|
366
|
+
overrides.max_concurrency if overrides.max_concurrency is not None else state.get("max_concurrency", 1)
|
|
367
|
+
),
|
|
368
|
+
log_level=overrides.log_level if overrides.log_level is not None else state.get("log_level", "info"),
|
|
369
|
+
pid_file=pid_file,
|
|
370
|
+
log_file=log_file,
|
|
371
|
+
state_file=overrides.state_file,
|
|
372
|
+
foreground=False,
|
|
373
|
+
fast=False,
|
|
374
|
+
)
|
|
375
|
+
|
|
376
|
+
|
|
377
|
+
def is_process_running(pid: int) -> bool:
|
|
378
|
+
try:
|
|
379
|
+
os.kill(pid, 0)
|
|
380
|
+
except ProcessLookupError:
|
|
381
|
+
return False
|
|
382
|
+
except PermissionError:
|
|
383
|
+
return True
|
|
384
|
+
return True
|
|
385
|
+
|
|
386
|
+
|
|
387
|
+
def read_pid(pid_file: Path) -> int | None:
|
|
388
|
+
try:
|
|
389
|
+
return int(pid_file.read_text(encoding="utf-8").strip())
|
|
390
|
+
except (FileNotFoundError, ValueError):
|
|
391
|
+
return None
|
|
392
|
+
|
|
393
|
+
|
|
394
|
+
def serve(args: argparse.Namespace) -> int:
|
|
395
|
+
args.default_cwd.mkdir(parents=True, exist_ok=True)
|
|
396
|
+
settings = settings_from_args(args)
|
|
397
|
+
app = create_app(settings)
|
|
398
|
+
uvicorn.run(app, host=settings.host, port=settings.port, log_level=settings.log_level)
|
|
399
|
+
return 0
|
|
400
|
+
|
|
401
|
+
|
|
402
|
+
def start(args: argparse.Namespace) -> int:
|
|
403
|
+
existing_pid = read_pid(args.pid_file)
|
|
404
|
+
if existing_pid and is_process_running(existing_pid):
|
|
405
|
+
print(f"codex-api-proxy already running with pid {existing_pid}")
|
|
406
|
+
return 0
|
|
407
|
+
|
|
408
|
+
print_public_bind_warning(args)
|
|
409
|
+
if args.foreground:
|
|
410
|
+
return serve(args)
|
|
411
|
+
|
|
412
|
+
args.default_cwd.mkdir(parents=True, exist_ok=True)
|
|
413
|
+
args.pid_file.parent.mkdir(parents=True, exist_ok=True)
|
|
414
|
+
args.log_file.parent.mkdir(parents=True, exist_ok=True)
|
|
415
|
+
write_start_state(args)
|
|
416
|
+
command = [
|
|
417
|
+
sys.executable,
|
|
418
|
+
"-m",
|
|
419
|
+
"codex_api_proxy.cli",
|
|
420
|
+
"_serve",
|
|
421
|
+
"--host",
|
|
422
|
+
args.host,
|
|
423
|
+
"--port",
|
|
424
|
+
str(args.port),
|
|
425
|
+
"--codex-bin",
|
|
426
|
+
args.codex_bin,
|
|
427
|
+
"--engine",
|
|
428
|
+
args.engine,
|
|
429
|
+
"--workers",
|
|
430
|
+
str(args.workers),
|
|
431
|
+
"--max-queue-size",
|
|
432
|
+
str(args.max_queue_size),
|
|
433
|
+
"--queue-timeout-seconds",
|
|
434
|
+
str(args.queue_timeout_seconds),
|
|
435
|
+
"--app-server-codex-home",
|
|
436
|
+
str(args.app_server_codex_home),
|
|
437
|
+
"--default-cwd",
|
|
438
|
+
str(args.default_cwd),
|
|
439
|
+
"--timeout-seconds",
|
|
440
|
+
str(args.timeout_seconds),
|
|
441
|
+
"--max-concurrency",
|
|
442
|
+
str(args.max_concurrency),
|
|
443
|
+
"--log-level",
|
|
444
|
+
args.log_level,
|
|
445
|
+
"--pid-file",
|
|
446
|
+
str(args.pid_file),
|
|
447
|
+
]
|
|
448
|
+
if args.model:
|
|
449
|
+
command.extend(["--model", args.model])
|
|
450
|
+
for config in args.codex_configs or []:
|
|
451
|
+
command.extend(["--codex-config", config])
|
|
452
|
+
if args.ephemeral:
|
|
453
|
+
command.append("--ephemeral")
|
|
454
|
+
if args.fast:
|
|
455
|
+
command.append("--fast")
|
|
456
|
+
if args.api_key:
|
|
457
|
+
command.extend(["--api-key", args.api_key])
|
|
458
|
+
if args.proxy:
|
|
459
|
+
command.extend(["--proxy", args.proxy])
|
|
460
|
+
for root in args.allowed_roots or []:
|
|
461
|
+
command.extend(["--allowed-root", str(root)])
|
|
462
|
+
|
|
463
|
+
with args.log_file.open("ab") as log:
|
|
464
|
+
process = subprocess.Popen(
|
|
465
|
+
command,
|
|
466
|
+
stdin=subprocess.DEVNULL,
|
|
467
|
+
stdout=log,
|
|
468
|
+
stderr=subprocess.STDOUT,
|
|
469
|
+
start_new_session=True,
|
|
470
|
+
)
|
|
471
|
+
args.pid_file.write_text(f"{process.pid}\n", encoding="utf-8")
|
|
472
|
+
print_start_summary(args, pid=process.pid)
|
|
473
|
+
return 0
|
|
474
|
+
|
|
475
|
+
|
|
476
|
+
def restart(args: argparse.Namespace) -> int:
|
|
477
|
+
try:
|
|
478
|
+
state = load_start_state(args.state_file)
|
|
479
|
+
except FileNotFoundError:
|
|
480
|
+
print(f"codex-api-proxy state file not found: {args.state_file}")
|
|
481
|
+
return 1
|
|
482
|
+
start_args = args_from_state(state, args)
|
|
483
|
+
stop_args = argparse.Namespace(pid_file=start_args.pid_file)
|
|
484
|
+
stopped = stop(stop_args)
|
|
485
|
+
if stopped != 0:
|
|
486
|
+
return stopped
|
|
487
|
+
return start(start_args)
|
|
488
|
+
|
|
489
|
+
|
|
490
|
+
def stop(args: argparse.Namespace) -> int:
|
|
491
|
+
pid = read_pid(args.pid_file)
|
|
492
|
+
if not pid:
|
|
493
|
+
print("codex-api-proxy is not running")
|
|
494
|
+
return 0
|
|
495
|
+
if not is_process_running(pid):
|
|
496
|
+
args.pid_file.unlink(missing_ok=True)
|
|
497
|
+
print("codex-api-proxy is not running")
|
|
498
|
+
return 0
|
|
499
|
+
|
|
500
|
+
try:
|
|
501
|
+
os.kill(pid, signal.SIGTERM)
|
|
502
|
+
except PermissionError:
|
|
503
|
+
print(f"permission denied stopping codex-api-proxy pid {pid}")
|
|
504
|
+
return 1
|
|
505
|
+
for _ in range(50):
|
|
506
|
+
if not is_process_running(pid):
|
|
507
|
+
args.pid_file.unlink(missing_ok=True)
|
|
508
|
+
print(f"codex-api-proxy stopped pid {pid}")
|
|
509
|
+
return 0
|
|
510
|
+
time.sleep(0.1)
|
|
511
|
+
print(f"codex-api-proxy pid {pid} did not stop after SIGTERM")
|
|
512
|
+
return 1
|
|
513
|
+
|
|
514
|
+
|
|
515
|
+
def status(args: argparse.Namespace) -> int:
|
|
516
|
+
pid = read_pid(args.pid_file)
|
|
517
|
+
if pid and is_process_running(pid):
|
|
518
|
+
print(f"codex-api-proxy running with pid {pid}")
|
|
519
|
+
try:
|
|
520
|
+
state = load_start_state(args.state_file)
|
|
521
|
+
except FileNotFoundError:
|
|
522
|
+
return 0
|
|
523
|
+
print_endpoint_summary(state.get("host", "127.0.0.1"), state.get("port", 8765))
|
|
524
|
+
if args.verbose:
|
|
525
|
+
print(f"state: {args.state_file}")
|
|
526
|
+
print(f"log: {state.get('log_file', default_log_file())}")
|
|
527
|
+
print("runtime:")
|
|
528
|
+
print(f" host: {state.get('host', '127.0.0.1')}")
|
|
529
|
+
print(f" port: {state.get('port', 8765)}")
|
|
530
|
+
print(f" api_key: {'<redacted>' if state.get('api_key') else '<none>'}")
|
|
531
|
+
print(f" codex_bin: {state.get('codex_bin', 'codex')}")
|
|
532
|
+
print(f" proxy: {state.get('proxy') or '<none>'}")
|
|
533
|
+
print(f" model: {state.get('model') or '<default>'}")
|
|
534
|
+
print(f" engine: {state.get('engine', 'exec')}")
|
|
535
|
+
print(f" workers: {state.get('workers', 1)}")
|
|
536
|
+
print(f" max_queue_size: {state.get('max_queue_size', 64)}")
|
|
537
|
+
print(f" queue_timeout_seconds: {state.get('queue_timeout_seconds', 30)}")
|
|
538
|
+
print(f" app_server_codex_home: {state.get('app_server_codex_home') or default_app_server_codex_home()}")
|
|
539
|
+
print(f" default_cwd: {state.get('default_cwd', '<unknown>')}")
|
|
540
|
+
print(f" allowed_roots: {', '.join(str(root) for root in state.get('allowed_roots', []))}")
|
|
541
|
+
print(f" timeout_seconds: {state.get('timeout_seconds', 300)}")
|
|
542
|
+
print(f" max_concurrency: {state.get('max_concurrency', 1)}")
|
|
543
|
+
print(f" log_level: {state.get('log_level', 'info')}")
|
|
544
|
+
return 0
|
|
545
|
+
print("codex-api-proxy stopped")
|
|
546
|
+
return 1
|
|
547
|
+
|
|
548
|
+
|
|
549
|
+
def main(argv: list[str] | None = None) -> int:
|
|
550
|
+
argv = list(sys.argv[1:] if argv is None else argv)
|
|
551
|
+
if argv and argv[0] == "_serve":
|
|
552
|
+
args = build_serve_parser().parse_args(argv[1:])
|
|
553
|
+
return serve(args)
|
|
554
|
+
|
|
555
|
+
parser = build_parser()
|
|
556
|
+
args = parser.parse_args(argv)
|
|
557
|
+
if args.command == "start":
|
|
558
|
+
return start(args)
|
|
559
|
+
if args.command == "restart":
|
|
560
|
+
return restart(args)
|
|
561
|
+
if args.command == "stop":
|
|
562
|
+
return stop(args)
|
|
563
|
+
if args.command == "status":
|
|
564
|
+
return status(args)
|
|
565
|
+
parser.error(f"unknown command: {args.command}")
|
|
566
|
+
return 2
|
|
567
|
+
|
|
568
|
+
|
|
569
|
+
if __name__ == "__main__":
|
|
570
|
+
raise SystemExit(main())
|