From fbe7df0d20f05b85f018bf6ce32364d960133faa Mon Sep 17 00:00:00 2001 From: Mingyang Wu <129849514+aprylewu@users.noreply.github.com> Date: Fri, 11 Sep 2026 17:18:10 +0800 Subject: [PATCH] fix(cli): preserve Codex proxy config with subcommand overrides --- litellm/proxy/client/cli/commands/agents.py | 40 ++++- .../proxy/client/cli/test_agents.py | 142 +++++++++++++++++- 2 files changed, 177 insertions(+), 5 deletions(-) diff --git a/litellm/proxy/client/cli/commands/agents.py b/litellm/proxy/client/cli/commands/agents.py index bf2a784f590..ed9f60ee4aa 100644 --- a/litellm/proxy/client/cli/commands/agents.py +++ b/litellm/proxy/client/cli/commands/agents.py @@ -2,7 +2,7 @@ import os import shutil import subprocess import sys -from collections.abc import Callable, Mapping, Sequence +from collections.abc import Callable, Iterator, Mapping, Sequence from dataclasses import dataclass from pathlib import Path from types import MappingProxyType @@ -206,6 +206,37 @@ def agent_launch_args(command: str, base_url: str) -> list[str]: return builder(base_url) if builder else [] +def _codex_arg_groups(user_args: Sequence[str]) -> Iterator[tuple[bool, tuple[str, ...]]]: + tokens: Final = iter(user_args) + for token in tokens: + if token == "--": + yield False, (token, *tokens) + return + if token in ("-c", "--config"): + if (value := next(tokens, None)) is None: + yield False, (token,) + return + if value.startswith("-"): + yield False, (token, value, *tokens) + return + yield True, (token, value) + else: + yield token.startswith(("-c", "--config=")), (token,) + + +def _codex_launch_args(user_args: Sequence[str], extra_args: Sequence[str]) -> tuple[str, ...]: + # Codex discards root -c values when a subcommand has its own. Replay the + # defaults and earlier user overrides at the last config option's scope. + groups: Final = tuple(_codex_arg_groups(user_args)) + last_config: Final = max((index for index, (is_config, _) in enumerate(groups) if is_config), default=0) + return ( + *(arg for _, args in groups[:last_config] for arg in args), + *extra_args, + *(arg for is_config, args in groups[:last_config] if is_config for arg in args), + *(arg for _, args in groups[last_config:] for arg in args), + ) + + class ListedModel(BaseModel): """The fields of a /v1/models entry that an OpenCode model entry is built from.""" @@ -513,9 +544,14 @@ def run_agent( } ) extra_args: Final = (*agent_launch_args(command[0], base_url), *prepared_args) + launch_args: Final = ( + _codex_launch_args(command[1:], extra_args) + if os.path.basename(command[0]) == "codex" + else (*extra_args, *command[1:]) + ) if reattach_terminal is not None: reattach_terminal() - launcher(binary, [command[0], *extra_args, *command[1:]], env) + launcher(binary, [command[0], *launch_args], env) def _is_interactive() -> bool: diff --git a/tests/test_litellm/proxy/client/cli/test_agents.py b/tests/test_litellm/proxy/client/cli/test_agents.py index bebea285edc..711161ef712 100644 --- a/tests/test_litellm/proxy/client/cli/test_agents.py +++ b/tests/test_litellm/proxy/client/cli/test_agents.py @@ -9,8 +9,6 @@ import pytest import requests from click.testing import CliRunner - - from litellm.proxy.client.cli.commands.agents import ( AgentRunError, ModelSyncSkipped, @@ -582,9 +580,147 @@ class TestRunAgent: assert args[-2:] == ("exec", "do a thing") assert 'model_provider="litellm"' in args assert 'model_providers.litellm.base_url="http://localhost:4000/v1"' in args - # overrides must precede the codex subcommand so codex parses them assert args.index('model_provider="litellm"') < args.index("exec") + @pytest.mark.parametrize( + "subcommand", + [ + ("exec",), + ("e",), + ("resume", "--last"), + ("review",), + ("fork", "--last"), + ("exec", "resume", "--last"), + ("exec", "review"), + ("exec", "fork", "--last"), + ], + ) + @pytest.mark.parametrize( + "config_args", + [ + ("-c", "model_reasoning_effort=low"), + ("--config", "model_reasoning_effort=low"), + ("--config=model_reasoning_effort=low",), + ("-cmodel_reasoning_effort=low",), + ("-c=model_reasoning_effort=low",), + ], + ) + def test_codex_injects_proxy_overrides_at_subcommand_config_scope(self, subcommand, config_args): + launcher = _Recorder() + run_agent( + "http://localhost:4000", + "sk-key", + ("/usr/local/bin/codex", *subcommand, *config_args, "hello"), + skip_verify=True, + base_env={}, + which=lambda name: name, + launcher=launcher, + ) + + binary, args, env = launcher.calls[0] + assert binary == "/usr/local/bin/codex" + assert args == [ + "/usr/local/bin/codex", + *subcommand, + *agent_launch_args("codex", "http://localhost:4000"), + *config_args, + "hello", + ] + assert env["OPENAI_API_KEY"] == "sk-key" + + @pytest.mark.parametrize("first_scope", [(), ("exec",)]) + def test_codex_replays_user_overrides_after_proxy_defaults(self, first_scope): + launcher = _Recorder() + provider = ("-c", 'model_provider="custom"') + base_url = ("--config", 'model_providers.litellm.base_url="http://other/v1"') + last_config = ("-c", "model_reasoning_effort=low") + child_scope = () if first_scope else ("exec",) + prefix = (*first_scope, *provider, *child_scope, *base_url) + run_agent( + "http://localhost:4000", + "sk-key", + ("codex", *prefix, *last_config, "hello"), + skip_verify=True, + base_env={}, + which=lambda name: name, + launcher=launcher, + ) + assert launcher.calls[0][1] == [ + "codex", + *prefix, + *agent_launch_args("codex", "http://localhost:4000"), + *provider, + *base_url, + *last_config, + "hello", + ] + + @pytest.mark.parametrize( + "user_args", + [ + ("exec", "--", "--config=model_provider=custom"), + ("exec", "--", "-c"), + ("exec", "explain --config and -c"), + ("exec", "-c"), + ("exec", "--config"), + ("exec", "-c", "--", "--config=literal"), + ("exec", "--config", "--help"), + ("exec", "-mcustom-model"), + ("exec", "--configurations=literal"), + ], + ) + def test_codex_preserves_literals_and_missing_config_values(self, user_args): + launcher = _Recorder() + run_agent( + "http://localhost:4000", + "sk-key", + ("codex", *user_args), + skip_verify=True, + base_env={}, + which=lambda name: name, + launcher=launcher, + ) + assert launcher.calls[0][1] == [ + "codex", + *agent_launch_args("codex", "http://localhost:4000"), + *user_args, + ] + + def test_codex_keeps_config_flag_after_option_with_missing_operand(self): + launcher = _Recorder() + run_agent( + "http://localhost:4000", + "sk-key", + ("codex", "exec", "--output-schema", "-c", "model_reasoning_effort=low", "hello"), + skip_verify=True, + base_env={}, + which=lambda name: name, + launcher=launcher, + ) + assert launcher.calls[0][1] == [ + "codex", + "exec", + "--output-schema", + *agent_launch_args("codex", "http://localhost:4000"), + "-c", + "model_reasoning_effort=low", + "hello", + ] + + def test_other_agents_keep_config_arguments_in_place(self): + launcher = _Recorder() + command = ("mytool", "exec", "-c", "model=test", "--", "--config=prompt") + run_agent( + "http://localhost:4000", + "sk-key", + command, + skip_verify=True, + base_env={}, + which=lambda name: name, + launcher=launcher, + ) + assert launcher.calls[0][1] == list(command) + def test_pi_preparer_runs_after_verify_and_before_launch(self): order = [] captured = {}