codex-relay-server 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.
@@ -0,0 +1,17 @@
1
+ """OpenAI-compatible LAN relay server."""
2
+
3
+ from .config import AuthConfig, AuthMode, RelayConfig, ServerConfig, Settings, load_settings
4
+ from .relay import create_app
5
+
6
+ __version__ = "0.1.0"
7
+
8
+ __all__ = [
9
+ "AuthConfig",
10
+ "AuthMode",
11
+ "RelayConfig",
12
+ "ServerConfig",
13
+ "Settings",
14
+ "__version__",
15
+ "create_app",
16
+ "load_settings",
17
+ ]
@@ -0,0 +1,5 @@
1
+ from .cli import main
2
+
3
+
4
+ if __name__ == "__main__":
5
+ main()
@@ -0,0 +1,39 @@
1
+ from __future__ import annotations
2
+
3
+ from fastapi import HTTPException, status
4
+
5
+ from .config import AuthConfig, AuthMode
6
+
7
+
8
+ def resolve_authorization_header(incoming_authorization: str | None, config: AuthConfig) -> str | None:
9
+ if config.mode == AuthMode.DIRECT:
10
+ return incoming_authorization
11
+
12
+ client_key = extract_client_key(incoming_authorization)
13
+ upstream_secret = config.key_map.get(client_key or "") if client_key else None
14
+ if upstream_secret is None:
15
+ upstream_secret = config.default_key
16
+
17
+ if upstream_secret is None:
18
+ raise HTTPException(
19
+ status_code=status.HTTP_401_UNAUTHORIZED,
20
+ detail="No upstream API key is available for this client key.",
21
+ )
22
+
23
+ return f"Bearer {upstream_secret.get_secret_value()}"
24
+
25
+
26
+ def extract_client_key(authorization_header: str | None) -> str | None:
27
+ if authorization_header is None:
28
+ return None
29
+
30
+ authorization_header = authorization_header.strip()
31
+ if not authorization_header:
32
+ return None
33
+
34
+ scheme, separator, value = authorization_header.partition(" ")
35
+ if separator and scheme.lower() == "bearer":
36
+ value = value.strip()
37
+ return value or None
38
+
39
+ return authorization_header
@@ -0,0 +1,78 @@
1
+ from __future__ import annotations
2
+
3
+ import argparse
4
+ from pathlib import Path
5
+ from typing import Any
6
+
7
+ import uvicorn
8
+
9
+ from .config import load_settings
10
+ from .relay import create_app
11
+
12
+
13
+ def build_parser() -> argparse.ArgumentParser:
14
+ parser = argparse.ArgumentParser(description="Run a LAN OpenAI-compatible relay server.")
15
+ parser.add_argument(
16
+ "-c",
17
+ "--config",
18
+ type=Path,
19
+ default=None,
20
+ help="Path to a JSON config file.",
21
+ )
22
+ parser.add_argument("--host", help="Override server.host, for example 0.0.0.0 or a LAN IP.")
23
+ parser.add_argument("--port", type=int, help="Override server.port.")
24
+ parser.add_argument("--upstream-url", help="Override relay.upstream_url.")
25
+ parser.add_argument("--local-base-path", help="Override relay.local_base_path.")
26
+ parser.add_argument(
27
+ "--strip-local-base-path",
28
+ type=parse_bool,
29
+ help="Override relay.strip_local_base_path with true or false.",
30
+ )
31
+ parser.add_argument("--auth-mode", choices=("direct", "transform"), help="Override auth.mode.")
32
+ parser.add_argument("--log-level", help="Override server.log_level.")
33
+ return parser
34
+
35
+
36
+ def main() -> None:
37
+ parser = build_parser()
38
+ args = parser.parse_args()
39
+
40
+ settings = load_settings(args.config)
41
+ updates = {}
42
+ if args.host is not None:
43
+ updates.setdefault("server", {})["host"] = args.host
44
+ if args.port is not None:
45
+ updates.setdefault("server", {})["port"] = args.port
46
+ if args.log_level is not None:
47
+ updates.setdefault("server", {})["log_level"] = args.log_level
48
+ if args.upstream_url is not None:
49
+ updates.setdefault("relay", {})["upstream_url"] = args.upstream_url
50
+ if args.local_base_path is not None:
51
+ updates.setdefault("relay", {})["local_base_path"] = args.local_base_path
52
+ if args.strip_local_base_path is not None:
53
+ updates.setdefault("relay", {})["strip_local_base_path"] = args.strip_local_base_path
54
+ if args.auth_mode is not None:
55
+ updates.setdefault("auth", {})["mode"] = args.auth_mode
56
+ if updates:
57
+ settings = type(settings).model_validate(_deep_update(settings.model_dump(mode="python"), updates))
58
+
59
+ app = create_app(settings)
60
+ uvicorn.run(app, host=settings.server.host, port=settings.server.port, log_level=settings.server.log_level)
61
+
62
+
63
+ def parse_bool(value: str) -> bool:
64
+ normalized = value.strip().lower()
65
+ if normalized in {"1", "true", "yes", "y", "on"}:
66
+ return True
67
+ if normalized in {"0", "false", "no", "n", "off"}:
68
+ return False
69
+ raise argparse.ArgumentTypeError(f"invalid boolean value: {value!r}")
70
+
71
+
72
+ def _deep_update(original: dict[str, Any], updates: dict[str, Any]) -> dict[str, Any]:
73
+ for key, value in updates.items():
74
+ if isinstance(value, dict) and isinstance(original.get(key), dict):
75
+ original[key] = _deep_update(original[key], value)
76
+ else:
77
+ original[key] = value
78
+ return original
@@ -0,0 +1,107 @@
1
+ from __future__ import annotations
2
+
3
+ from copy import deepcopy
4
+ import json
5
+ from collections.abc import Mapping
6
+ from enum import StrEnum
7
+ from pathlib import Path
8
+ from typing import Any
9
+
10
+ from pydantic import BaseModel, Field, SecretStr, field_validator, model_validator
11
+
12
+
13
+ class AuthMode(StrEnum):
14
+ DIRECT = "direct"
15
+ TRANSFORM = "transform"
16
+
17
+
18
+ class ServerConfig(BaseModel):
19
+ host: str = "127.0.0.1"
20
+ port: int = 8000
21
+ log_level: str = "info"
22
+
23
+ @field_validator("port")
24
+ @classmethod
25
+ def validate_port(cls, value: int) -> int:
26
+ if not 1 <= value <= 65535:
27
+ raise ValueError("port must be between 1 and 65535")
28
+ return value
29
+
30
+
31
+ class RelayConfig(BaseModel):
32
+ upstream_url: str = "https://api.openai.com/v1"
33
+ local_base_path: str = "/"
34
+ strip_local_base_path: bool = True
35
+ timeout_seconds: float = 300
36
+ verify_ssl: bool = True
37
+
38
+ @model_validator(mode="before")
39
+ @classmethod
40
+ def migrate_old_base_path_names(cls, data: Any) -> Any:
41
+ if isinstance(data, dict):
42
+ if "local_base_path" not in data and "public_base_path" in data:
43
+ data["local_base_path"] = data["public_base_path"]
44
+ if "strip_local_base_path" not in data and "strip_public_base_path" in data:
45
+ data["strip_local_base_path"] = data["strip_public_base_path"]
46
+ return data
47
+
48
+ @field_validator("upstream_url")
49
+ @classmethod
50
+ def validate_upstream_url(cls, value: str) -> str:
51
+ value = value.strip()
52
+ if not value:
53
+ raise ValueError("upstream_url is required")
54
+ if not value.startswith(("http://", "https://")):
55
+ raise ValueError("upstream_url must start with http:// or https://")
56
+ return value.rstrip("/")
57
+
58
+ @field_validator("local_base_path")
59
+ @classmethod
60
+ def validate_local_base_path(cls, value: str) -> str:
61
+ value = value.strip()
62
+ if value in {"", "/"}:
63
+ return "/"
64
+ if not value.startswith("/"):
65
+ value = f"/{value}"
66
+ return value.rstrip("/")
67
+
68
+ @field_validator("timeout_seconds")
69
+ @classmethod
70
+ def validate_timeout(cls, value: float) -> float:
71
+ if value <= 0:
72
+ raise ValueError("timeout_seconds must be greater than 0")
73
+ return value
74
+
75
+
76
+ class AuthConfig(BaseModel):
77
+ mode: AuthMode = AuthMode.DIRECT
78
+ key_map: dict[str, SecretStr] = Field(default_factory=dict)
79
+ default_key: SecretStr | None = None
80
+
81
+
82
+ class Settings(BaseModel):
83
+ server: ServerConfig = Field(default_factory=ServerConfig)
84
+ relay: RelayConfig = Field(default_factory=RelayConfig)
85
+ auth: AuthConfig = Field(default_factory=AuthConfig)
86
+
87
+
88
+ ConfigInput = Settings | Mapping[str, Any] | str | Path | None
89
+
90
+
91
+ def load_settings(config: ConfigInput = None) -> Settings:
92
+ raw: dict[str, Any] = {}
93
+ if isinstance(config, Settings):
94
+ raw = config.model_dump(mode="python")
95
+ elif isinstance(config, Mapping):
96
+ raw = deepcopy(dict(config))
97
+ elif config:
98
+ path = Path(config)
99
+ if not path.exists():
100
+ raise FileNotFoundError(f"config file not found: {path}")
101
+ with path.open("r", encoding="utf-8") as file:
102
+ loaded = json.load(file)
103
+ if not isinstance(loaded, dict):
104
+ raise ValueError("config file must contain a JSON object")
105
+ raw = loaded
106
+
107
+ return Settings.model_validate(raw)
@@ -0,0 +1,180 @@
1
+ from __future__ import annotations
2
+
3
+ from collections.abc import AsyncIterator, Iterable
4
+ from contextlib import asynccontextmanager
5
+
6
+ import httpx
7
+ from fastapi import FastAPI, HTTPException, Request
8
+ from fastapi.responses import JSONResponse, Response, StreamingResponse
9
+ from starlette.background import BackgroundTask
10
+
11
+ from .auth import resolve_authorization_header
12
+ from .config import Settings
13
+
14
+ HOP_BY_HOP_HEADERS = {
15
+ "connection",
16
+ "keep-alive",
17
+ "proxy-authenticate",
18
+ "proxy-authorization",
19
+ "te",
20
+ "trailer",
21
+ "transfer-encoding",
22
+ "upgrade",
23
+ }
24
+
25
+ REQUEST_HEADERS_TO_DROP = HOP_BY_HOP_HEADERS | {
26
+ "host",
27
+ "content-length",
28
+ }
29
+
30
+ RESPONSE_HEADERS_TO_DROP = HOP_BY_HOP_HEADERS
31
+
32
+ ALL_METHODS = ["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS", "HEAD"]
33
+
34
+
35
+ def create_app(settings: Settings, transport: httpx.AsyncBaseTransport | None = None) -> FastAPI:
36
+ client = httpx.AsyncClient(
37
+ timeout=httpx.Timeout(settings.relay.timeout_seconds),
38
+ verify=settings.relay.verify_ssl,
39
+ transport=transport,
40
+ follow_redirects=False,
41
+ )
42
+
43
+ @asynccontextmanager
44
+ async def lifespan(app: FastAPI) -> AsyncIterator[None]:
45
+ yield
46
+ await client.aclose()
47
+
48
+ app = FastAPI(
49
+ title="Codex Relay Server",
50
+ version="0.1.0",
51
+ docs_url=None,
52
+ redoc_url=None,
53
+ openapi_url=None,
54
+ lifespan=lifespan,
55
+ )
56
+ app.state.settings = settings
57
+ app.state.http_client = client
58
+
59
+ @app.get("/healthz")
60
+ async def healthz() -> dict[str, str]:
61
+ return {"status": "ok"}
62
+
63
+ @app.api_route("/{path:path}", methods=ALL_METHODS, response_model=None)
64
+ async def relay_request(path: str, request: Request) -> Response:
65
+ try:
66
+ return await forward_request(request, path, settings, client)
67
+ except HTTPException:
68
+ raise
69
+ except httpx.TimeoutException:
70
+ return JSONResponse({"detail": "Upstream request timed out."}, status_code=504)
71
+ except httpx.TransportError as exc:
72
+ return JSONResponse({"detail": f"Upstream request failed: {exc.__class__.__name__}"}, status_code=502)
73
+
74
+ return app
75
+
76
+
77
+ async def forward_request(
78
+ request: Request,
79
+ incoming_path: str,
80
+ settings: Settings,
81
+ client: httpx.AsyncClient,
82
+ ) -> Response:
83
+ target_url = build_upstream_url(
84
+ settings.relay.upstream_url,
85
+ incoming_path,
86
+ request.url.query,
87
+ settings.relay.local_base_path,
88
+ settings.relay.strip_local_base_path,
89
+ )
90
+ headers = build_upstream_headers(request.headers.items(), settings)
91
+ body = await request.body()
92
+
93
+ upstream_request = client.build_request(
94
+ request.method,
95
+ target_url,
96
+ headers=headers,
97
+ content=body,
98
+ )
99
+ upstream_response = await client.send(upstream_request, stream=True)
100
+ response_headers = filter_response_headers(upstream_response.headers.items())
101
+
102
+ if upstream_response.is_stream_consumed:
103
+ return Response(
104
+ content=upstream_response.content,
105
+ status_code=upstream_response.status_code,
106
+ headers=response_headers,
107
+ media_type=upstream_response.headers.get("content-type"),
108
+ )
109
+
110
+ return StreamingResponse(
111
+ upstream_response.aiter_raw(),
112
+ status_code=upstream_response.status_code,
113
+ headers=response_headers,
114
+ background=BackgroundTask(upstream_response.aclose),
115
+ )
116
+
117
+
118
+ def build_upstream_url(
119
+ upstream_url: str,
120
+ incoming_path: str,
121
+ query_string: str | bytes,
122
+ local_base_path: str,
123
+ strip_local_base_path: bool,
124
+ ) -> str:
125
+ suffix = incoming_path.strip("/")
126
+
127
+ validate_local_path(incoming_path, local_base_path)
128
+
129
+ if strip_local_base_path and local_base_path:
130
+ normalized_path = f"/{suffix}" if suffix else "/"
131
+ prefix = local_base_path.rstrip("/")
132
+ if normalized_path == prefix:
133
+ suffix = ""
134
+ elif normalized_path.startswith(f"{prefix}/"):
135
+ suffix = normalized_path[len(prefix) :].lstrip("/")
136
+
137
+ target = upstream_url.rstrip("/")
138
+ if suffix:
139
+ target = f"{target}/{suffix}"
140
+
141
+ if query_string:
142
+ query = query_string.decode("utf-8") if isinstance(query_string, bytes) else query_string
143
+ target = f"{target}?{query}"
144
+
145
+ return target
146
+
147
+
148
+ def validate_local_path(incoming_path: str, local_base_path: str) -> None:
149
+ if not local_base_path:
150
+ return
151
+
152
+ normalized_path = f"/{incoming_path.strip('/')}" if incoming_path.strip("/") else "/"
153
+ prefix = local_base_path.rstrip("/")
154
+ if normalized_path == prefix or normalized_path.startswith(f"{prefix}/"):
155
+ return
156
+
157
+ raise HTTPException(status_code=404, detail="This path is outside the configured local base path.")
158
+
159
+
160
+ def build_upstream_headers(incoming_headers: Iterable[tuple[str, str]], settings: Settings) -> dict[str, str]:
161
+ headers = {
162
+ key: value
163
+ for key, value in incoming_headers
164
+ if key.lower() not in REQUEST_HEADERS_TO_DROP
165
+ }
166
+
167
+ authorization = resolve_authorization_header(headers.get("authorization"), settings.auth)
168
+ headers.pop("authorization", None)
169
+ if authorization:
170
+ headers["authorization"] = authorization
171
+
172
+ return headers
173
+
174
+
175
+ def filter_response_headers(incoming_headers: Iterable[tuple[str, str]]) -> dict[str, str]:
176
+ return {
177
+ key: value
178
+ for key, value in incoming_headers
179
+ if key.lower() not in RESPONSE_HEADERS_TO_DROP
180
+ }
@@ -0,0 +1,185 @@
1
+ Metadata-Version: 2.4
2
+ Name: codex-relay-server
3
+ Version: 0.1.0
4
+ Summary: A LAN OpenAI-compatible relay server.
5
+ License: MIT
6
+ Requires-Python: >=3.10
7
+ Description-Content-Type: text/markdown
8
+ License-File: LICENSE
9
+ Requires-Dist: fastapi>=0.110
10
+ Requires-Dist: httpx>=0.27
11
+ Requires-Dist: pydantic>=2.7
12
+ Requires-Dist: uvicorn[standard]>=0.29
13
+ Provides-Extra: dev
14
+ Requires-Dist: pytest>=8; extra == "dev"
15
+ Requires-Dist: pytest-asyncio>=0.23; extra == "dev"
16
+ Dynamic: license-file
17
+
18
+ # codex-relay-server
19
+
20
+ A LAN relay server for OpenAI-compatible HTTP APIs.
21
+
22
+ Use this when one computer can reach the upstream model provider, but other computers on the LAN can only reach that computer. LAN clients point their OpenAI SDKs at this relay; the relay forwards requests to your configured upstream API root and streams the upstream response back.
23
+
24
+ ## Features
25
+
26
+ - Bind to any local IP and port, such as `0.0.0.0:8000` or a specific LAN adapter IP.
27
+ - Use any upstream API root URL. The upstream does not need to use `/v1`.
28
+ - Configure the local client-facing base path, such as `/`, `/v1`, `/openai`, or `/api/openai`.
29
+ - Forward all endpoints with the same path rules, including `/chat/completions`, `/responses`, `/models`, and future API paths.
30
+ - Pass through normal JSON responses and streaming SSE responses.
31
+ - Support two API key modes:
32
+ - `direct`: forward the incoming `Authorization` header unchanged.
33
+ - `transform`: map client-visible keys to upstream keys, with an optional default upstream key.
34
+
35
+ ## Installation
36
+
37
+ ```powershell
38
+ .\venv\Scripts\python.exe -m pip install -e ".[dev]"
39
+ ```
40
+
41
+ ## Configuration
42
+
43
+ Copy the example config:
44
+
45
+ ```powershell
46
+ Copy-Item config.example.json config.json
47
+ ```
48
+
49
+ Edit `config.json`:
50
+
51
+ ```json
52
+ {
53
+ "server": {
54
+ "host": "0.0.0.0",
55
+ "port": 8000
56
+ },
57
+ "relay": {
58
+ "upstream_url": "https://your-upstream.example/custom-api",
59
+ "local_base_path": "/",
60
+ "strip_local_base_path": true
61
+ },
62
+ "auth": {
63
+ "mode": "direct"
64
+ }
65
+ }
66
+ ```
67
+
68
+ With that config, an incoming LAN request to `/responses` is forwarded to:
69
+
70
+ ```text
71
+ https://your-upstream.example/custom-api/responses
72
+ ```
73
+
74
+ The default `local_base_path` is `/`, so the relay accepts requests from the root path. Set `local_base_path` explicitly if you want clients to use a specific base URL such as `/v1` or `/openai`.
75
+
76
+ Set `strip_local_base_path` to `false` if you want the full local path appended to the upstream URL.
77
+
78
+ You can expose any local base path:
79
+
80
+ ```json
81
+ {
82
+ "relay": {
83
+ "upstream_url": "https://your-upstream.example/not-v1",
84
+ "local_base_path": "/openai",
85
+ "strip_local_base_path": true
86
+ }
87
+ }
88
+ ```
89
+
90
+ LAN clients can then use this base URL:
91
+
92
+ ```text
93
+ http://<relay-lan-ip>:8000/openai
94
+ ```
95
+
96
+ ### Transform Mode
97
+
98
+ ```json
99
+ {
100
+ "auth": {
101
+ "mode": "transform",
102
+ "key_map": {
103
+ "client-visible-key-a": "real-upstream-key-a",
104
+ "client-visible-key-b": "real-upstream-key-b"
105
+ },
106
+ "default_key": "real-default-upstream-key"
107
+ }
108
+ }
109
+ ```
110
+
111
+ In transform mode, if the client sends:
112
+
113
+ ```text
114
+ Authorization: Bearer client-visible-key-a
115
+ ```
116
+
117
+ The upstream receives:
118
+
119
+ ```text
120
+ Authorization: Bearer real-upstream-key-a
121
+ ```
122
+
123
+ If the client key is not in `key_map` and `default_key` is configured, the relay uses `default_key`. If no default key is available, the relay returns `401`.
124
+
125
+ Keep real upstream keys in local `config.json` or Python-provided configuration. Do not commit them. This repository ignores `config.json`, `*.local.json`, and `*.secrets.json`.
126
+
127
+ ### Python Configuration
128
+
129
+ You can also provide configuration from Python code instead of a JSON file:
130
+
131
+ ```python
132
+ from codex_relay_server import create_app, load_settings
133
+
134
+ settings = load_settings(
135
+ {
136
+ "server": {"host": "0.0.0.0", "port": 8000},
137
+ "relay": {
138
+ "upstream_url": "https://your-upstream.example/custom-api",
139
+ "local_base_path": "/openai",
140
+ "strip_local_base_path": True,
141
+ },
142
+ "auth": {"mode": "direct"},
143
+ }
144
+ )
145
+
146
+ app = create_app(settings)
147
+ ```
148
+
149
+ ## Run
150
+
151
+ ```powershell
152
+ .\venv\Scripts\python.exe -m codex_relay_server --config config.json
153
+ ```
154
+
155
+ You can temporarily override the bind address, port, upstream URL, and local base path:
156
+
157
+ ```powershell
158
+ .\venv\Scripts\python.exe -m codex_relay_server `
159
+ --config config.json `
160
+ --host 0.0.0.0 `
161
+ --port 8000 `
162
+ --upstream-url https://your-upstream.example/custom-api `
163
+ --local-base-path /openai
164
+ ```
165
+
166
+ LAN client settings:
167
+
168
+ ```text
169
+ base_url = http://<relay-lan-ip>:8000/<your-local-base-path>
170
+ api_key = <client-key>
171
+ ```
172
+
173
+ ## Tests
174
+
175
+ ```powershell
176
+ .\venv\Scripts\python.exe -m pytest
177
+ ```
178
+
179
+ Tests use a local mock upstream and do not need a real API key.
180
+
181
+ ## Security Notes
182
+
183
+ - Do not expose this service to the public internet, especially in `transform` mode with `default_key` enabled.
184
+ - If you bind to `0.0.0.0`, make sure your firewall only allows trusted LAN clients.
185
+ - The project does not store private keys in tracked files. Put real keys in ignored local config files or Python-provided configuration.
@@ -0,0 +1,12 @@
1
+ codex_relay_server/__init__.py,sha256=HHHfABAFGmkGWuqMxz60IO706rx-PLoz9TiVutY9epc,351
2
+ codex_relay_server/__main__.py,sha256=wu5N2wk8mvBgyvr2ghmQf4prezAe0_i-p123VVreyYc,62
3
+ codex_relay_server/auth.py,sha256=5jQ59n8eP9S2ahi3JmzL2j7oCKtYODROTo4YkSw4cg0,1235
4
+ codex_relay_server/cli.py,sha256=ztU_Dsfghqg_E0jRpEGaGImCco6R41TVKJn1XOo7Sho,2975
5
+ codex_relay_server/config.py,sha256=wmaWvBCeyRyIMMUcE_d6wC4bDFBdAAaPUC3xB_cgEHY,3394
6
+ codex_relay_server/relay.py,sha256=t2yLPX0c2XAaq7jgawXAoQY-rM57hYg3ISts1jyN0pQ,5467
7
+ codex_relay_server-0.1.0.dist-info/licenses/LICENSE,sha256=gmkEFqkF3KJjRnhXGoLSfX4xXtfPHT1ghq3YgCF7B3w,1086
8
+ codex_relay_server-0.1.0.dist-info/METADATA,sha256=R_3HKoK8H4r5N0BvBmNBnZQPfaHSo3ynzMPC1asHkPE,5164
9
+ codex_relay_server-0.1.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
10
+ codex_relay_server-0.1.0.dist-info/entry_points.txt,sha256=gztvLIknzFq2EZVDcmSzHpyxxWa2A-4UJFBftvZQ2A4,67
11
+ codex_relay_server-0.1.0.dist-info/top_level.txt,sha256=6kE5Ua6UGVeJNGCHu7Kwi9vjeSTK3wNsd4tGnXelk0U,19
12
+ codex_relay_server-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (82.0.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ codex-relay-server = codex_relay_server.cli:main
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 GGN_2015
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1 @@
1
+ codex_relay_server