From d57e77bd72e871609b74989a23ecc61f735bdc26 Mon Sep 17 00:00:00 2001 From: spalen0 Date: Tue, 2 Jun 2026 13:07:25 +0200 Subject: [PATCH] Add Codex LLM provider --- .env.example | 5 +- pyproject.toml | 5 ++ tests/test_llm.py | 116 +++++++++++++++++++++++++++++++ utils/llm/README.md | 14 ++-- utils/llm/codex_provider.py | 132 ++++++++++++++++++++++++++++++++++++ utils/llm/factory.py | 35 ++++++++-- uv.lock | 34 ++++++++++ 7 files changed, 331 insertions(+), 10 deletions(-) create mode 100644 utils/llm/codex_provider.py diff --git a/.env.example b/.env.example index 5637bdde..b17e2659 100644 --- a/.env.example +++ b/.env.example @@ -56,11 +56,14 @@ TENDERLY_PROJECT=sam ETHERSCAN_TOKEN=your-etherscan-api-key # LLM provider for AI transaction explanations -# Supported: venice (default), openai, anthropic, or any OpenAI-compatible provider +# Supported: venice (default), openai, anthropic, codex, or any OpenAI-compatible provider LLM_PROVIDER=venice +# Optional for LLM_PROVIDER=codex when existing Codex auth is available. LLM_API_KEY=your-llm-api-key # LLM_BASE_URL=https://api.venice.ai/api/v1 # auto-set for known providers # LLM_MODEL=deepseek-v4-flash # auto-set for known providers +# LLM_CODEX_MODEL_PROVIDER=openai # optional Codex SDK override +# LLM_CODEX_CWD=/path/to/workspace # optional Codex runtime cwd # Dune (hourly large-transfer monitor) DUNE_API_KEY=your-dune-api-key diff --git a/pyproject.toml b/pyproject.toml index 1b51817a..533433cf 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -63,10 +63,12 @@ dependencies = [ [project.optional-dependencies] ai = [ "anthropic>=0.40.0", + "openai-codex>=0.1.0b2", "openai>=1.0.0", ] dev = [ "anthropic>=0.40.0", + "openai-codex>=0.1.0b2", "openai>=1.0.0", "mdformat==1.0.0", "mypy==2.0.0", @@ -86,6 +88,9 @@ dev = [ # or ISO 8601 (e.g. "P7D"). Applies to `uv pip install`, `uv lock`, # `uv sync`, etc. Also settable via `UV_EXCLUDE_NEWER=1 week`. exclude-newer = "1 week" +# Narrow exception for the beta OpenAI Codex SDK requested by the LLM provider. +# The SDK package pins this exact runtime package. +exclude-newer-package = { openai-codex = "2026-05-29T00:00:00Z", openai-codex-cli-bin = "2026-05-21T00:00:00Z" } [build-system] requires = ["setuptools>=42", "wheel"] diff --git a/tests/test_llm.py b/tests/test_llm.py index 2c6c9844..b59033d5 100644 --- a/tests/test_llm.py +++ b/tests/test_llm.py @@ -227,6 +227,107 @@ def test_complete_structured_uses_forced_tool(self, mock_anthropic_cls: MagicMoc self.assertEqual(kwargs["tool_choice"]["type"], "tool") +class TestCodexProvider(unittest.TestCase): + """Tests for the OpenAI Codex Python SDK provider.""" + + @patch("openai_codex.Codex") + def test_complete_success(self, mock_codex_cls: MagicMock) -> None: + mock_client = MagicMock() + mock_codex_cls.return_value = mock_client + + mock_thread = MagicMock() + mock_client.thread_start.return_value = mock_thread + mock_result = MagicMock() + mock_result.final_response = " Updates the cap. LOW. " + mock_thread.run.return_value = mock_result + + from utils.llm.codex_provider import CodexProvider + + provider = CodexProvider(api_key="sk-test", model="gpt-5.2-codex", model_provider="openai") + result = provider.complete("prompt", system_prompt="sys") + + self.assertEqual(result, "Updates the cap. LOW.") + mock_client.login_api_key.assert_called_once_with("sk-test") + thread_kwargs = mock_client.thread_start.call_args.kwargs + self.assertEqual(thread_kwargs["model"], "gpt-5.2-codex") + self.assertEqual(thread_kwargs["model_provider"], "openai") + self.assertTrue(thread_kwargs["ephemeral"]) + self.assertIn("sys", thread_kwargs["developer_instructions"]) + self.assertEqual(thread_kwargs["approval_mode"].value, "deny_all") + self.assertEqual(thread_kwargs["sandbox"].value, "read-only") + run_kwargs = mock_thread.run.call_args.kwargs + self.assertEqual(run_kwargs["model"], "gpt-5.2-codex") + self.assertIsNone(run_kwargs["output_schema"]) + self.assertEqual(run_kwargs["approval_mode"].value, "deny_all") + self.assertEqual(run_kwargs["sandbox"].value, "read-only") + + @patch("openai_codex.Codex") + def test_complete_without_api_key_reuses_existing_auth(self, mock_codex_cls: MagicMock) -> None: + mock_client = MagicMock() + mock_codex_cls.return_value = mock_client + mock_thread = MagicMock() + mock_client.thread_start.return_value = mock_thread + mock_result = MagicMock() + mock_result.final_response = "OK" + mock_thread.run.return_value = mock_result + + from utils.llm.codex_provider import CodexProvider + + provider = CodexProvider(api_key=None, model="gpt-5.2-codex") + self.assertEqual(provider.complete("prompt"), "OK") + mock_client.login_api_key.assert_not_called() + + @patch("openai_codex.Codex") + def test_complete_empty_response_raises(self, mock_codex_cls: MagicMock) -> None: + mock_client = MagicMock() + mock_codex_cls.return_value = mock_client + mock_thread = MagicMock() + mock_client.thread_start.return_value = mock_thread + mock_result = MagicMock() + mock_result.final_response = None + mock_thread.run.return_value = mock_result + + from utils.llm.codex_provider import CodexProvider + + provider = CodexProvider(api_key=None, model="gpt-5.2-codex") + with self.assertRaises(LLMError): + provider.complete("prompt") + + @patch("openai_codex.Codex") + def test_complete_structured_parses_json(self, mock_codex_cls: MagicMock) -> None: + mock_client = MagicMock() + mock_codex_cls.return_value = mock_client + mock_thread = MagicMock() + mock_client.thread_start.return_value = mock_thread + mock_result = MagicMock() + mock_result.final_response = '{"summary": "Updates. LOW.", "detail": "d", "risk_tag": "LOW"}' + mock_thread.run.return_value = mock_result + + from utils.llm.codex_provider import CodexProvider + + provider = CodexProvider(api_key=None, model="gpt-5.2-codex") + result = provider.complete_structured("prompt", {"type": "object"}) + + self.assertEqual(result["risk_tag"], "LOW") + self.assertEqual(mock_thread.run.call_args.kwargs["output_schema"], {"type": "object"}) + + @patch("openai_codex.Codex") + def test_complete_structured_invalid_json_raises(self, mock_codex_cls: MagicMock) -> None: + mock_client = MagicMock() + mock_codex_cls.return_value = mock_client + mock_thread = MagicMock() + mock_client.thread_start.return_value = mock_thread + mock_result = MagicMock() + mock_result.final_response = "not json" + mock_thread.run.return_value = mock_result + + from utils.llm.codex_provider import CodexProvider + + provider = CodexProvider(api_key=None, model="gpt-5.2-codex") + with self.assertRaises(LLMError): + provider.complete_structured("prompt", {"type": "object"}) + + class TestFactory(unittest.TestCase): """Tests for the LLM provider factory.""" @@ -264,6 +365,15 @@ def test_anthropic_defaults(self, mock_anthropic_cls: MagicMock) -> None: provider = get_llm_provider() self.assertEqual(provider.model_name, "claude-haiku-4-5-20251001") + @patch("openai_codex.Codex") + def test_codex_defaults_without_api_key(self, mock_codex_cls: MagicMock) -> None: + env = {"LLM_PROVIDER": "codex"} + with patch.dict(os.environ, env, clear=True): + provider = get_llm_provider() + self.assertEqual(provider.model_name, "gpt-5.2-codex") + self.assertTrue(provider.supports_structured_output) + mock_codex_cls.return_value.login_api_key.assert_not_called() + @patch("anthropic.Anthropic") def test_anthropic_custom_model(self, mock_anthropic_cls: MagicMock) -> None: env = {"LLM_PROVIDER": "anthropic", "LLM_API_KEY": "sk-ant-test", "LLM_MODEL": "claude-sonnet-4-6"} @@ -326,6 +436,12 @@ def test_structured_output_on_by_default_for_anthropic(self, mock_anthropic_cls: with patch.dict(os.environ, env, clear=True): self.assertTrue(get_llm_provider().supports_structured_output) + @patch("openai_codex.Codex") + def test_structured_output_on_by_default_for_codex(self, mock_codex_cls: MagicMock) -> None: + env = {"LLM_PROVIDER": "codex"} + with patch.dict(os.environ, env, clear=True): + self.assertTrue(get_llm_provider().supports_structured_output) + if __name__ == "__main__": unittest.main() diff --git a/utils/llm/README.md b/utils/llm/README.md index 7fbb8959..b2416510 100644 --- a/utils/llm/README.md +++ b/utils/llm/README.md @@ -233,7 +233,7 @@ Calls upgradeTo(address) on the AAVE pool proxy... `_parse_explanation()` splits this with tolerant regex (handles `### DETAIL`, `**TLDR:**`, etc.); if the format isn't followed, the whole response becomes the summary (backward compatible). -Structured output is controlled by `LLM_STRUCTURED_OUTPUT` (per-provider default: on for `anthropic`/`openai`/`venice` — all verified live — off for `groq`/custom, since JSON-schema support varies by backend). The refine pass (step 7) always uses the text path. +Structured output is controlled by `LLM_STRUCTURED_OUTPUT` (per-provider default: on for `anthropic`/`codex`/`openai`/`venice`; off for `groq`/custom, since JSON-schema support varies by backend). The refine pass (step 7) always uses the text path. ### 9. Output Formatting @@ -253,11 +253,13 @@ All configuration is via environment variables: | Variable | Default | Description | |---|---|---| -| `LLM_PROVIDER` | `venice` | Provider name: `venice`, `groq`, `openai`, `anthropic`, or custom | -| `LLM_API_KEY` | *(required)* | API key for the LLM provider | +| `LLM_PROVIDER` | `venice` | Provider name: `venice`, `groq`, `openai`, `anthropic`, `codex`, or custom | +| `LLM_API_KEY` | *(required except codex)* | API key for the LLM provider. For `codex`, omitted means reuse existing Codex auth | | `LLM_MODEL` | `deepseek-v4-flash` | Model identifier | | `LLM_BASE_URL` | *(per provider)* | API base URL (not needed for anthropic) | -| `LLM_STRUCTURED_OUTPUT` | *(per provider)* | `true`/`false` to force JSON-schema output. Default: on for anthropic/openai/venice (all verified live), off for groq/custom | +| `LLM_STRUCTURED_OUTPUT` | *(per provider)* | `true`/`false` to force JSON-schema output. Default: on for anthropic/codex/openai/venice, off for groq/custom | +| `LLM_CODEX_MODEL_PROVIDER` | *(unset)* | Optional Codex SDK model-provider override | +| `LLM_CODEX_CWD` | *(current process cwd)* | Optional Codex runtime working directory | | `ETHERSCAN_TOKEN` | *(optional)* | Etherscan v2 multichain API key for source context | | `TENDERLY_API_KEY` | *(optional)* | Tenderly API key for simulation | | `TENDERLY_ACCOUNT` | `yearn` | Tenderly account slug | @@ -271,9 +273,10 @@ All configuration is via environment variables: | Groq | `https://api.groq.com/openai/v1` | `openai/gpt-oss-safeguard-20b` | `openai` | | OpenAI | `https://api.openai.com/v1` | `gpt-4o-mini` | `openai` | | Anthropic | *(native API)* | `claude-haiku-4-5-20251001` | `anthropic` | +| Codex | *(native SDK)* | `gpt-5.2-codex` | `openai-codex` | | Custom | Set `LLM_BASE_URL` | Set `LLM_MODEL` | `openai` | -The `openai` and `anthropic` packages are optional dependencies. Install with: +The `openai`, `anthropic`, and `openai-codex` packages are optional dependencies. Install with: ```bash uv pip install 'monitoring-scripts-py[ai]' @@ -287,6 +290,7 @@ utils/llm/ ├── ai_explainer.py # Orchestrator: decode → fetch context → prompt → explain ├── anthropic_provider.py # Anthropic (Claude) native API provider ├── base.py # Abstract LLMProvider base class + LLMError +├── codex_provider.py # OpenAI Codex Python SDK provider ├── factory.py # Provider factory with env-based config + singleton ├── openai_compat.py # OpenAI-compatible provider (Venice, OpenAI, etc.) └── README.md # This file diff --git a/utils/llm/codex_provider.py b/utils/llm/codex_provider.py new file mode 100644 index 00000000..4193ca78 --- /dev/null +++ b/utils/llm/codex_provider.py @@ -0,0 +1,132 @@ +"""OpenAI Codex Python SDK LLM provider. + +Uses the native ``openai-codex`` SDK rather than the OpenAI HTTP API. Codex is +an agent runtime, so this adapter constrains it to direct text completion: +read-only sandbox, denied approvals, and one ephemeral thread per request. +""" + +import json +from typing import Any + +from utils.llm.base import LLMError, LLMProvider +from utils.logging import get_logger + +logger = get_logger("utils.llm.codex_provider") + +_COMPLETION_INSTRUCTIONS = """Act as a direct LLM completion backend for this application. +Use only the prompt content supplied in the turn. Do not inspect the workspace, +run commands, edit files, or describe tool limitations.""" + + +class CodexProvider(LLMProvider): + """LLM provider backed by the OpenAI Codex Python SDK.""" + + def __init__( + self, + api_key: str | None, + model: str, + structured_output: bool = True, + model_provider: str | None = None, + cwd: str | None = None, + ) -> None: + """Initialize the provider. + + Args: + api_key: Optional OpenAI API key. If omitted, Codex reuses existing + Codex authentication (for example from ``codex login``). + model: Codex model identifier. + structured_output: Whether to advertise Codex ``output_schema``. + model_provider: Optional Codex model provider override. + cwd: Optional runtime working directory for the Codex app-server. + """ + try: + from openai_codex import ApprovalMode, Codex, CodexConfig, Sandbox + except ImportError: + raise LLMError( + "openai-codex package not installed. Install with: uv pip install 'monitoring-scripts-py[ai]'" + ) + + self._model = model + self._model_provider = model_provider + self._structured_output = structured_output + self._approval_mode = ApprovalMode.deny_all + self._sandbox = Sandbox.read_only + config = CodexConfig(cwd=cwd) if cwd else None + client = Codex(config) + try: + if api_key: + client.login_api_key(api_key) + except Exception: + client.close() + raise + self._client = client + logger.info( + "Initialized Codex provider: model=%s model_provider=%s structured=%s cwd=%s", + model, + model_provider, + structured_output, + cwd, + ) + + def complete(self, prompt: str, system_prompt: str = "") -> str: + """Generate a completion using a fresh Codex thread.""" + try: + return self._run(prompt, system_prompt).strip() + except LLMError: + raise + except Exception as e: + raise LLMError(f"Codex SDK call failed: {e}") from e + + @property + def supports_structured_output(self) -> bool: + """Return whether Codex ``output_schema`` is enabled.""" + return self._structured_output + + def complete_structured(self, prompt: str, schema: dict[str, Any], system_prompt: str = "") -> dict[str, Any]: + """Request a schema-constrained Codex response and return it parsed.""" + try: + content = self._run(prompt, system_prompt, output_schema=schema) + parsed: dict[str, Any] = json.loads(content) + return parsed + except LLMError: + raise + except json.JSONDecodeError as e: + raise LLMError(f"Structured Codex response was not valid JSON: {e}") from e + except Exception as e: + raise LLMError(f"Codex structured call failed: {e}") from e + + def close(self) -> None: + """Close the Codex runtime process.""" + self._client.close() + + def _run(self, prompt: str, system_prompt: str, output_schema: dict[str, Any] | None = None) -> str: + """Run one stateless Codex turn and return the final response text.""" + thread = self._client.thread_start( + approval_mode=self._approval_mode, + developer_instructions=self._build_instructions(system_prompt), + ephemeral=True, + model=self._model, + model_provider=self._model_provider, + sandbox=self._sandbox, + ) + result = thread.run( + prompt, + approval_mode=self._approval_mode, + model=self._model, + output_schema=output_schema, + sandbox=self._sandbox, + ) + if not result.final_response: + raise LLMError("Empty response from Codex") + return result.final_response.strip() + + def _build_instructions(self, system_prompt: str) -> str: + """Combine adapter-level constraints with the caller's system prompt.""" + if not system_prompt: + return _COMPLETION_INSTRUCTIONS + return f"{_COMPLETION_INSTRUCTIONS}\n\n{system_prompt}" + + @property + def model_name(self) -> str: + """Return the model identifier.""" + return self._model diff --git a/utils/llm/factory.py b/utils/llm/factory.py index 44c0bf3a..a7fb6bef 100644 --- a/utils/llm/factory.py +++ b/utils/llm/factory.py @@ -2,17 +2,21 @@ Environment variables: LLM_PROVIDER: Provider name (default: "venice"). - LLM_API_KEY: API key for the provider (required). + LLM_API_KEY: API key for the provider (required except codex when reusing + existing Codex auth). LLM_BASE_URL: Base URL for the API (not needed for anthropic). LLM_MODEL: Model identifier to use. LLM_STRUCTURED_OUTPUT: "true"/"false" to force JSON-schema structured output on or off. Unset uses a per-provider default (on for anthropic/openai/venice). + LLM_CODEX_MODEL_PROVIDER: Optional Codex SDK model-provider override. + LLM_CODEX_CWD: Optional Codex runtime working directory. Provider defaults: venice: base_url=https://api.venice.ai/api/v1, model=deepseek-v4-flash groq: base_url=https://api.groq.com/openai/v1, model=openai/gpt-oss-safeguard-20b openai: base_url=https://api.openai.com/v1, model=gpt-4o-mini anthropic: model=claude-haiku-4-5-20251001 (uses native Anthropic API) + codex: model=gpt-5.2-codex (uses native Codex Python SDK) Custom: Set LLM_BASE_URL and LLM_MODEL explicitly. """ @@ -40,6 +44,9 @@ "anthropic": { "model": "claude-haiku-4-5-20251001", }, + "codex": { + "model": "gpt-5.2-codex", + }, } # Cached singleton instance @@ -50,9 +57,10 @@ # Verified live against venice/deepseek-v4-flash and openai. groq and custom # backends default off (support varies by model) and must opt in via # LLM_STRUCTURED_OUTPUT. Anthropic uses forced tool use — all Claude models -# support it — so it defaults on. +# support it — so it defaults on. Codex uses the SDK's output_schema. _STRUCTURED_OUTPUT_DEFAULTS: dict[str, bool] = { "anthropic": True, + "codex": True, "openai": True, "venice": True, "groq": False, @@ -68,7 +76,7 @@ def _env_bool(name: str, default: bool) -> bool: def _create_provider( - provider_name: str, api_key: str, model: str, base_url: str, structured_output: bool + provider_name: str, api_key: str | None, model: str, base_url: str, structured_output: bool ) -> LLMProvider: """Create the appropriate provider instance. @@ -85,10 +93,27 @@ def _create_provider( if provider_name == "anthropic": from utils.llm.anthropic_provider import AnthropicProvider + if not api_key: + raise LLMError("LLM_API_KEY environment variable is not set") return AnthropicProvider(api_key=api_key, model=model, structured_output=structured_output) + if provider_name == "codex": + from utils.llm.codex_provider import CodexProvider + + model_provider = os.getenv("LLM_CODEX_MODEL_PROVIDER") or None + cwd = os.getenv("LLM_CODEX_CWD") or None + return CodexProvider( + api_key=api_key, + model=model, + structured_output=structured_output, + model_provider=model_provider, + cwd=cwd, + ) + from utils.llm.openai_compat import OpenAICompatProvider + if not api_key: + raise LLMError("LLM_API_KEY environment variable is not set") if not base_url: raise LLMError( f"LLM_BASE_URL must be set for provider '{provider_name}'. " @@ -112,7 +137,7 @@ def get_llm_provider() -> LLMProvider: provider_name = (os.getenv("LLM_PROVIDER") or "venice").lower() api_key = os.getenv("LLM_API_KEY") - if not api_key: + if not api_key and provider_name != "codex": raise LLMError("LLM_API_KEY environment variable is not set") defaults = _PROVIDER_DEFAULTS.get(provider_name, {}) @@ -135,4 +160,6 @@ def get_llm_provider() -> LLMProvider: def reset_provider() -> None: """Reset the cached provider instance. Useful for testing.""" global _instance + if _instance is not None and hasattr(_instance, "close"): + _instance.close() # type: ignore[attr-defined] _instance = None diff --git a/uv.lock b/uv.lock index da165056..694de756 100644 --- a/uv.lock +++ b/uv.lock @@ -10,6 +10,10 @@ resolution-markers = [ exclude-newer = "0001-01-01T00:00:00Z" # This has no effect and is included for backwards compatibility when using relative exclude-newer values. exclude-newer-span = "P1W" +[options.exclude-newer-package] +openai-codex-cli-bin = "2026-05-21T00:00:00Z" +openai-codex = "2026-05-29T00:00:00Z" + [[package]] name = "aiohappyeyeballs" version = "2.6.1" @@ -1117,12 +1121,14 @@ dependencies = [ ai = [ { name = "anthropic" }, { name = "openai" }, + { name = "openai-codex" }, ] dev = [ { name = "anthropic" }, { name = "mdformat" }, { name = "mypy" }, { name = "openai" }, + { name = "openai-codex" }, { name = "pytest" }, { name = "ruff" }, { name = "types-deprecated" }, @@ -1170,6 +1176,8 @@ requires-dist = [ { name = "ndjson", specifier = "==0.3.1" }, { name = "openai", marker = "extra == 'ai'", specifier = ">=1.0.0" }, { name = "openai", marker = "extra == 'dev'", specifier = ">=1.0.0" }, + { name = "openai-codex", marker = "extra == 'ai'", specifier = ">=0.1.0b2" }, + { name = "openai-codex", marker = "extra == 'dev'", specifier = ">=0.1.0b2" }, { name = "packaging", specifier = "==26.2" }, { name = "parsimonious", specifier = "==0.10.0" }, { name = "propcache", specifier = "==0.4.1" }, @@ -1381,6 +1389,32 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d0/b1/35b6f9c8cf9318e3dbb7146cc82dab4cf61182a8d5406fc9b50864362895/openai-2.29.0-py3-none-any.whl", hash = "sha256:b7c5de513c3286d17c5e29b92c4c98ceaf0d775244ac8159aeb1bddf840eb42a", size = 1141533, upload-time = "2026-03-17T17:53:47.348Z" }, ] +[[package]] +name = "openai-codex" +version = "0.1.0b2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "openai-codex-cli-bin" }, + { name = "pydantic" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/6f/b7/216fa0e7725c33791fe97621c663a9ec0e6e98c2240bffae89661de55602/openai_codex-0.1.0b2.tar.gz", hash = "sha256:44dd51f004ce2ed51cf7b9f34e8b2e6a207ab06e8739b45d5a0cfe338e460587", size = 57665, upload-time = "2026-05-28T06:28:00.461Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b6/38/59e86bc20bdd4390faa979d2a0235d7be60ae895cd3ac6a92a49bfe080a7/openai_codex-0.1.0b2-py3-none-any.whl", hash = "sha256:9edf0871e9e0d6e3951cd6c3b9b187641e6247d9413402dfc66ac3a5f6ba66ff", size = 64285, upload-time = "2026-05-28T06:27:58.949Z" }, +] + +[[package]] +name = "openai-codex-cli-bin" +version = "0.132.0" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/be/a1/b92b7a1b73a83785d2e1dcd0faecd1b7f886a38cf02a30abe1c35f42f0f7/openai_codex_cli_bin-0.132.0-py3-none-macosx_10_9_x86_64.whl", hash = "sha256:1c22b51dbd679413f84f00b9d8fd4e5cf8a1c0d1c7cc8c42bcb3f9f1b33e2334", size = 89403211, upload-time = "2026-05-20T02:37:22.311Z" }, + { url = "https://files.pythonhosted.org/packages/5f/68/163272e582de55a7f460e2329281267908d75d0fbcbbbb2c6749a6329e6b/openai_codex_cli_bin-0.132.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:56217495e6635c8a5d96df820cc0da5f46cd9b6ec6f3a5f67f1607d69ef74256", size = 79058685, upload-time = "2026-05-20T02:37:27.165Z" }, + { url = "https://files.pythonhosted.org/packages/0b/18/a60c6b137e7cd3959cae16ba757f57ca5702979b0ea107a21f516ba15d98/openai_codex_cli_bin-0.132.0-py3-none-musllinux_1_1_aarch64.whl", hash = "sha256:09642e7578a3078bfccc82af4077b085d42b0022b529e4b5c645e0a0af3397a4", size = 78689038, upload-time = "2026-05-20T02:37:31.548Z" }, + { url = "https://files.pythonhosted.org/packages/f8/eb/1b184307a67c1006d59b61636bcfcea73a89aa95271f6516ed28dce554ca/openai_codex_cli_bin-0.132.0-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:85aec095f9d144d7a2d1aff39fce77b7240f42014580c35801ba74b9317aa5f7", size = 85528820, upload-time = "2026-05-20T02:37:36.559Z" }, + { url = "https://files.pythonhosted.org/packages/0e/e8/1b823a8bf7b96d1513905ad79b16a146d797f81a19a6bc350a2f95a16661/openai_codex_cli_bin-0.132.0-py3-none-win_amd64.whl", hash = "sha256:3cb5c90c55baa39bd5ddc890d2068d3e1322a57a54d1d0e623819009a205c7f5", size = 86916218, upload-time = "2026-05-20T02:37:41.886Z" }, + { url = "https://files.pythonhosted.org/packages/6b/e6/bb8634bd4f3adaea299c95d7b03105ac417e32dd6d8bc2af5dda141d6f28/openai_codex_cli_bin-0.132.0-py3-none-win_arm64.whl", hash = "sha256:74ef93d3deef7cb83c71d19fc667defe749cdab337ec331f59a23511561b6f6a", size = 79892931, upload-time = "2026-05-20T02:37:46.828Z" }, +] + [[package]] name = "packaging" version = "26.2"