Skip to content

Commit f04921e

Browse files
patricka3125claude
andauthored
fix(providers): honor profile.model at terminal creation (#189)
* fix(providers): honor profile.model at terminal creation (#174) Claude Code, Codex, Gemini CLI, Copilot CLI, and Kimi CLI all accept a --model flag at launch, but each provider's command builder was dropping profile.model on the floor — only Q CLI and Kiro CLI used it (at install time, baked into the agent JSON). Add a BaseProvider._model_args(profile) helper and wire it into each of the five runtime command builders. Copilot gates the flag behind its existing --model capability probe and treats profile-load failures as non-fatal. Model values are passed verbatim so any alias or full ID supported by the target CLI works unchanged. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * refactor(providers): inline --model flag append in each provider Remove BaseProvider._model_args helper and inline the two-line conditional directly in each provider's command builder. Keeps provider-specific CLI flag details contained in the provider that owns them, so a future change to one CLI's syntax does not ripple through a shared helper. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * refactor(copilot): drop _supports_flag gate around --model The flag is documented in GitHub's Copilot CLI command reference, so the capability probe no longer earns its keep. Removing it also brings Copilot in line with the other four providers, which pass --model unconditionally and let the CLI surface a clear error if it's too old to understand the flag — better UX than silently ignoring the user's model preference. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * refactor(copilot): take model via constructor instead of re-loading profile terminal_service already loads the AgentProfile once to resolve allowedTools (terminal_service.py:148); Copilot was re-loading it inside _command() just to read profile.model. Thread the model value through create_provider into CopilotCliProvider.__init__ so the profile is read once per terminal. Scope is limited to Copilot because the other providers genuinely need profile.system_prompt and profile.mcpServers at command-build time, so removing their re-load is a larger refactor that can happen separately. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 86c9317 commit f04921e

13 files changed

Lines changed: 259 additions & 0 deletions

src/cli_agent_orchestrator/providers/claude_code.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,9 @@ def _build_claude_command(self) -> str:
9292
try:
9393
profile = load_agent_profile(self._agent_profile)
9494

95+
if profile.model:
96+
command_parts.extend(["--model", profile.model])
97+
9598
# Add system prompt - escape newlines to prevent tmux chunking issues
9699
system_prompt = profile.system_prompt if profile.system_prompt is not None else ""
97100
system_prompt = self._apply_skill_prompt(system_prompt)

src/cli_agent_orchestrator/providers/codex.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,9 @@ def _build_codex_command(self) -> str:
144144
try:
145145
profile = load_agent_profile(self._agent_profile)
146146

147+
if profile.model:
148+
command_parts.extend(["--model", profile.model])
149+
147150
system_prompt = profile.system_prompt if profile.system_prompt is not None else ""
148151
system_prompt = self._apply_skill_prompt(system_prompt)
149152

src/cli_agent_orchestrator/providers/copilot_cli.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,10 +51,12 @@ def __init__(
5151
window_name: str,
5252
agent_profile: Optional[str] = None,
5353
allowed_tools: Optional[list] = None,
54+
model: Optional[str] = None,
5455
):
5556
super().__init__(terminal_id, session_name, window_name, allowed_tools)
5657
self._initialized = False
5758
self._agent_profile = agent_profile
59+
self._model = model
5860
self._copilot_help_text_cache: Optional[str] = None
5961

6062
@property
@@ -129,6 +131,8 @@ def _command(self) -> str:
129131

130132
if self._agent_profile:
131133
command_parts.extend(["--agent", self._agent_profile])
134+
if self._model:
135+
command_parts.extend(["--model", self._model])
132136

133137
command_parts.extend(["--config-dir", str(config_dir)])
134138
try:

src/cli_agent_orchestrator/providers/gemini_cli.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -218,6 +218,9 @@ def _build_gemini_command(self) -> str:
218218
try:
219219
profile = load_agent_profile(self._agent_profile)
220220

221+
if profile.model:
222+
command_parts.extend(["--model", profile.model])
223+
221224
# System prompt injection: write to GEMINI.md so Gemini loads it
222225
# as persistent project context on startup.
223226
#

src/cli_agent_orchestrator/providers/kimi_cli.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,9 @@ def _build_kimi_command(self) -> str:
194194
try:
195195
profile = load_agent_profile(self._agent_profile)
196196

197+
if profile.model:
198+
command_parts.extend(["--model", profile.model])
199+
197200
# Build agent file from profile's system prompt.
198201
# Kimi uses YAML agent files with a system_prompt_path pointing
199202
# to a markdown file. We create both in the temp directory.

src/cli_agent_orchestrator/providers/manager.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ def create_provider(
3232
agent_profile: Optional[str] = None,
3333
allowed_tools: Optional[List[str]] = None,
3434
skill_prompt: Optional[str] = None,
35+
model: Optional[str] = None,
3536
) -> BaseProvider:
3637
"""Create and store provider instance."""
3738
try:
@@ -81,6 +82,7 @@ def create_provider(
8182
tmux_window,
8283
agent_profile,
8384
allowed_tools,
85+
model=model,
8486
)
8587
elif provider_type == ProviderType.GEMINI_CLI.value:
8688
provider = GeminiCliProvider(

src/cli_agent_orchestrator/services/terminal_service.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,7 @@ def create_terminal(
172172
agent_profile,
173173
allowed_tools,
174174
skill_prompt=skill_prompt if provider in RUNTIME_SKILL_PROMPT_PROVIDERS else None,
175+
model=profile.model if profile else None,
175176
)
176177
provider_instance.initialize()
177178

test/providers/test_claude_code_coverage.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ def test_mcp_server_with_model_dump(self, mock_load, provider):
3535
}
3636
# isinstance(mock_mcp, dict) returns False, so the model_dump branch triggers
3737
mock_profile = MagicMock()
38+
mock_profile.model = None
3839
mock_profile.system_prompt = "Test prompt"
3940
mock_profile.mcpServers = {"my-mcp": mock_mcp}
4041
mock_profile.allowedTools = None

test/providers/test_claude_code_unit.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@ def test_initialize_with_agent_profile(
7676
mock_wait_status.return_value = True
7777
mock_tmux.get_history.return_value = "Welcome to Claude Code v2.0"
7878
mock_profile = MagicMock()
79+
mock_profile.model = None
7980
mock_profile.system_prompt = "Test system prompt"
8081
mock_profile.mcpServers = None
8182
mock_load.return_value = mock_profile
@@ -113,6 +114,7 @@ def test_initialize_with_mcp_servers(
113114
mock_wait_status.return_value = True
114115
mock_tmux.get_history.return_value = "Welcome to Claude Code v2.0"
115116
mock_profile = MagicMock()
117+
mock_profile.model = None
116118
mock_profile.system_prompt = None
117119
mock_profile.mcpServers = {"server1": {"command": "test", "args": ["--flag"]}}
118120
mock_load.return_value = mock_profile
@@ -493,6 +495,7 @@ def test_build_claude_command_no_profile(self):
493495
def test_build_claude_command_with_system_prompt(self, mock_load):
494496
"""Test building Claude command with system prompt."""
495497
mock_profile = MagicMock()
498+
mock_profile.model = None
496499
mock_profile.system_prompt = "Test prompt\nwith newlines"
497500
mock_profile.mcpServers = None
498501
mock_load.return_value = mock_profile
@@ -507,6 +510,7 @@ def test_build_claude_command_with_system_prompt(self, mock_load):
507510
def test_build_command_mcp_injects_terminal_id(self, mock_load):
508511
"""Test that _build_claude_command injects CAO_TERMINAL_ID into MCP server env."""
509512
mock_profile = MagicMock()
513+
mock_profile.model = None
510514
mock_profile.system_prompt = None
511515
mock_profile.mcpServers = {
512516
"cao-mcp-server": {"command": "cao-mcp-server", "args": ["--port", "8080"]}
@@ -531,6 +535,7 @@ def test_build_command_mcp_injects_terminal_id(self, mock_load):
531535
def test_build_command_mcp_preserves_existing_env(self, mock_load):
532536
"""Test that existing env vars in MCP config are preserved when injecting CAO_TERMINAL_ID."""
533537
mock_profile = MagicMock()
538+
mock_profile.model = None
534539
mock_profile.system_prompt = None
535540
mock_profile.mcpServers = {
536541
"my-server": {
@@ -559,6 +564,7 @@ def test_build_command_mcp_preserves_existing_env(self, mock_load):
559564
def test_build_command_mcp_does_not_override_existing_terminal_id(self, mock_load):
560565
"""Test that an existing CAO_TERMINAL_ID in MCP env is NOT overwritten."""
561566
mock_profile = MagicMock()
567+
mock_profile.model = None
562568
mock_profile.system_prompt = None
563569
mock_profile.mcpServers = {
564570
"my-server": {
@@ -581,6 +587,36 @@ def test_build_command_mcp_does_not_override_existing_terminal_id(self, mock_loa
581587
assert server_env["CAO_TERMINAL_ID"] == "user-provided-id"
582588

583589

590+
class TestClaudeCodeProviderModelFlag:
591+
"""Tests that profile.model is forwarded to Claude Code via --model."""
592+
593+
@patch("cli_agent_orchestrator.providers.claude_code.load_agent_profile")
594+
def test_build_command_appends_model_when_set(self, mock_load):
595+
mock_profile = MagicMock()
596+
mock_profile.model = "sonnet"
597+
mock_profile.system_prompt = None
598+
mock_profile.mcpServers = None
599+
mock_load.return_value = mock_profile
600+
601+
provider = ClaudeCodeProvider("tid", "sess", "win", "agent")
602+
command = provider._build_claude_command()
603+
604+
assert "--model sonnet" in command
605+
606+
@patch("cli_agent_orchestrator.providers.claude_code.load_agent_profile")
607+
def test_build_command_omits_model_when_unset(self, mock_load):
608+
mock_profile = MagicMock()
609+
mock_profile.model = None
610+
mock_profile.system_prompt = None
611+
mock_profile.mcpServers = None
612+
mock_load.return_value = mock_profile
613+
614+
provider = ClaudeCodeProvider("tid", "sess", "win", "agent")
615+
command = provider._build_claude_command()
616+
617+
assert "--model" not in command
618+
619+
584620
class TestClaudeCodeProviderStartupPrompts:
585621
"""Tests for Claude Code startup prompt handling (trust + bypass)."""
586622

test/providers/test_codex_provider_unit.py

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ def test_build_command_no_profile(self):
7373
@patch("cli_agent_orchestrator.providers.codex.load_agent_profile")
7474
def test_build_command_with_skill_prompt(self, mock_load_profile):
7575
mock_profile = MagicMock()
76+
mock_profile.model = None
7677
mock_profile.system_prompt = "You are a supervisor."
7778
mock_profile.mcpServers = None
7879
mock_load_profile.return_value = mock_profile
@@ -94,6 +95,7 @@ def test_build_command_with_skill_prompt(self, mock_load_profile):
9495
@patch("cli_agent_orchestrator.providers.codex.load_agent_profile")
9596
def test_build_command_with_agent_profile(self, mock_load_profile):
9697
mock_profile = MagicMock()
98+
mock_profile.model = None
9799
mock_profile.system_prompt = "You are a code supervisor agent."
98100
mock_profile.mcpServers = None
99101
mock_load_profile.return_value = mock_profile
@@ -110,6 +112,7 @@ def test_build_command_with_agent_profile(self, mock_load_profile):
110112
@patch("cli_agent_orchestrator.providers.codex.load_agent_profile")
111113
def test_build_command_escapes_quotes(self, mock_load_profile):
112114
mock_profile = MagicMock()
115+
mock_profile.model = None
113116
mock_profile.system_prompt = 'Use "double quotes" carefully.'
114117
mock_profile.mcpServers = None
115118
mock_load_profile.return_value = mock_profile
@@ -122,6 +125,7 @@ def test_build_command_escapes_quotes(self, mock_load_profile):
122125
@patch("cli_agent_orchestrator.providers.codex.load_agent_profile")
123126
def test_build_command_escapes_newlines(self, mock_load_profile):
124127
mock_profile = MagicMock()
128+
mock_profile.model = None
125129
mock_profile.system_prompt = "Line one.\nLine two.\n\n## Section\n- Item"
126130
mock_profile.mcpServers = None
127131
mock_load_profile.return_value = mock_profile
@@ -137,6 +141,7 @@ def test_build_command_escapes_newlines(self, mock_load_profile):
137141
@patch("cli_agent_orchestrator.providers.codex.load_agent_profile")
138142
def test_build_command_with_mcp_servers(self, mock_load_profile):
139143
mock_profile = MagicMock()
144+
mock_profile.model = None
140145
mock_profile.system_prompt = "You are a supervisor."
141146
mock_profile.mcpServers = {
142147
"cao-mcp-server": {
@@ -163,6 +168,7 @@ def test_build_command_with_mcp_servers(self, mock_load_profile):
163168
@patch("cli_agent_orchestrator.providers.codex.load_agent_profile")
164169
def test_build_command_with_mcp_servers_env(self, mock_load_profile):
165170
mock_profile = MagicMock()
171+
mock_profile.model = None
166172
mock_profile.system_prompt = ""
167173
mock_profile.mcpServers = {
168174
"test-server": {
@@ -186,6 +192,7 @@ def test_build_command_with_mcp_servers_env(self, mock_load_profile):
186192
@patch("cli_agent_orchestrator.providers.codex.load_agent_profile")
187193
def test_build_command_mcp_preserves_existing_env_vars(self, mock_load_profile):
188194
mock_profile = MagicMock()
195+
mock_profile.model = None
189196
mock_profile.system_prompt = ""
190197
mock_profile.mcpServers = {
191198
"my-server": {
@@ -207,6 +214,7 @@ def test_build_command_mcp_preserves_existing_env_vars(self, mock_load_profile):
207214
@patch("cli_agent_orchestrator.providers.codex.load_agent_profile")
208215
def test_build_command_empty_system_prompt(self, mock_load_profile):
209216
mock_profile = MagicMock()
217+
mock_profile.model = None
210218
mock_profile.system_prompt = ""
211219
mock_profile.mcpServers = None
212220
mock_load_profile.return_value = mock_profile
@@ -220,6 +228,7 @@ def test_build_command_empty_system_prompt(self, mock_load_profile):
220228
@patch("cli_agent_orchestrator.providers.codex.load_agent_profile")
221229
def test_build_command_none_system_prompt(self, mock_load_profile):
222230
mock_profile = MagicMock()
231+
mock_profile.model = None
223232
mock_profile.system_prompt = None
224233
mock_profile.mcpServers = None
225234
mock_load_profile.return_value = mock_profile
@@ -249,6 +258,7 @@ def test_initialize_with_agent_profile(
249258
mock_wait_status.return_value = True
250259
mock_tmux.get_history.return_value = "OpenAI Codex (v0.98.0)"
251260
mock_profile = MagicMock()
261+
mock_profile.model = None
252262
mock_profile.system_prompt = "You are a supervisor."
253263
mock_profile.mcpServers = None
254264
mock_load_profile.return_value = mock_profile
@@ -263,6 +273,36 @@ def test_initialize_with_agent_profile(
263273
assert "You are a supervisor." in codex_call.args[2]
264274

265275

276+
class TestCodexProviderModelFlag:
277+
"""Tests that profile.model is forwarded to Codex via --model."""
278+
279+
@patch("cli_agent_orchestrator.providers.codex.load_agent_profile")
280+
def test_build_command_appends_model_when_set(self, mock_load):
281+
mock_profile = MagicMock()
282+
mock_profile.model = "gpt-5"
283+
mock_profile.system_prompt = None
284+
mock_profile.mcpServers = None
285+
mock_load.return_value = mock_profile
286+
287+
provider = CodexProvider("tid", "sess", "win", "agent")
288+
command = provider._build_codex_command()
289+
290+
assert "--model gpt-5" in command
291+
292+
@patch("cli_agent_orchestrator.providers.codex.load_agent_profile")
293+
def test_build_command_omits_model_when_unset(self, mock_load):
294+
mock_profile = MagicMock()
295+
mock_profile.model = None
296+
mock_profile.system_prompt = None
297+
mock_profile.mcpServers = None
298+
mock_load.return_value = mock_profile
299+
300+
provider = CodexProvider("tid", "sess", "win", "agent")
301+
command = provider._build_codex_command()
302+
303+
assert "--model" not in command
304+
305+
266306
class TestCodexProviderStatusDetection:
267307
@patch("cli_agent_orchestrator.providers.codex.tmux_client")
268308
def test_get_status_idle(self, mock_tmux):

0 commit comments

Comments
 (0)