fix(cli): preserve Codex proxy config with subcommand overrides

This commit is contained in:
Mingyang Wu 2026-09-11 17:18:10 +08:00
parent 9a715df212
commit fbe7df0d20
2 changed files with 177 additions and 5 deletions

View file

@ -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:

View file

@ -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 = {}