codeer-cli 0.1.12__tar.gz → 0.1.14__tar.gz

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.
Files changed (40) hide show
  1. {codeer_cli-0.1.12 → codeer_cli-0.1.14}/PKG-INFO +30 -2
  2. {codeer_cli-0.1.12 → codeer_cli-0.1.14}/README.md +28 -0
  3. {codeer_cli-0.1.12 → codeer_cli-0.1.14}/pyproject.toml +1 -1
  4. {codeer_cli-0.1.12 → codeer_cli-0.1.14}/src/codeer_cli/agents.py +6 -0
  5. {codeer_cli-0.1.12 → codeer_cli-0.1.14}/src/codeer_cli/commands/agent.py +15 -1
  6. {codeer_cli-0.1.12 → codeer_cli-0.1.14}/src/codeer_cli/commands/eval_cmd.py +52 -5
  7. {codeer_cli-0.1.12 → codeer_cli-0.1.14}/src/codeer_cli/eval_.py +13 -0
  8. codeer_cli-0.1.14/tests/test_agent_model_settings.py +230 -0
  9. codeer_cli-0.1.14/tests/test_eval_evaluators.py +230 -0
  10. {codeer_cli-0.1.12 → codeer_cli-0.1.14}/uv.lock +1 -1
  11. {codeer_cli-0.1.12 → codeer_cli-0.1.14}/.gitignore +0 -0
  12. {codeer_cli-0.1.12 → codeer_cli-0.1.14}/API_REFERENCE.md +0 -0
  13. {codeer_cli-0.1.12 → codeer_cli-0.1.14}/src/codeer_cli/__init__.py +0 -0
  14. {codeer_cli-0.1.12 → codeer_cli-0.1.14}/src/codeer_cli/_validate.py +0 -0
  15. {codeer_cli-0.1.12 → codeer_cli-0.1.14}/src/codeer_cli/chats.py +0 -0
  16. {codeer_cli-0.1.12 → codeer_cli-0.1.14}/src/codeer_cli/cli.py +0 -0
  17. {codeer_cli-0.1.12 → codeer_cli-0.1.14}/src/codeer_cli/client.py +0 -0
  18. {codeer_cli-0.1.12 → codeer_cli-0.1.14}/src/codeer_cli/commands/__init__.py +0 -0
  19. {codeer_cli-0.1.12 → codeer_cli-0.1.14}/src/codeer_cli/commands/_util.py +0 -0
  20. {codeer_cli-0.1.12 → codeer_cli-0.1.14}/src/codeer_cli/commands/check.py +0 -0
  21. {codeer_cli-0.1.12 → codeer_cli-0.1.14}/src/codeer_cli/commands/history.py +0 -0
  22. {codeer_cli-0.1.12 → codeer_cli-0.1.14}/src/codeer_cli/commands/kb.py +0 -0
  23. {codeer_cli-0.1.12 → codeer_cli-0.1.14}/src/codeer_cli/commands/model.py +0 -0
  24. {codeer_cli-0.1.12 → codeer_cli-0.1.14}/src/codeer_cli/commands/profile.py +0 -0
  25. {codeer_cli-0.1.12 → codeer_cli-0.1.14}/src/codeer_cli/constants.py +0 -0
  26. {codeer_cli-0.1.12 → codeer_cli-0.1.14}/src/codeer_cli/histories.py +0 -0
  27. {codeer_cli-0.1.12 → codeer_cli-0.1.14}/src/codeer_cli/kb.py +0 -0
  28. {codeer_cli-0.1.12 → codeer_cli-0.1.14}/src/codeer_cli/models.py +0 -0
  29. {codeer_cli-0.1.12 → codeer_cli-0.1.14}/src/codeer_cli/parse.py +0 -0
  30. {codeer_cli-0.1.12 → codeer_cli-0.1.14}/tests/test_agent_handoff.py +0 -0
  31. {codeer_cli-0.1.12 → codeer_cli-0.1.14}/tests/test_chats_v2.py +0 -0
  32. {codeer_cli-0.1.12 → codeer_cli-0.1.14}/tests/test_client_transport.py +0 -0
  33. {codeer_cli-0.1.12 → codeer_cli-0.1.14}/tests/test_eval_labels.py +0 -0
  34. {codeer_cli-0.1.12 → codeer_cli-0.1.14}/tests/test_eval_pairs.py +0 -0
  35. {codeer_cli-0.1.12 → codeer_cli-0.1.14}/tests/test_history_read.py +0 -0
  36. {codeer_cli-0.1.12 → codeer_cli-0.1.14}/tests/test_history_send.py +0 -0
  37. {codeer_cli-0.1.12 → codeer_cli-0.1.14}/tests/test_kb_nodes.py +0 -0
  38. {codeer_cli-0.1.12 → codeer_cli-0.1.14}/tests/test_kb_ranges.py +0 -0
  39. {codeer_cli-0.1.12 → codeer_cli-0.1.14}/tests/test_models.py +0 -0
  40. {codeer_cli-0.1.12 → codeer_cli-0.1.14}/tests/test_util.py +0 -0
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.4
1
+ Metadata-Version: 2.5
2
2
  Name: codeer-cli
3
- Version: 0.1.12
3
+ Version: 0.1.14
4
4
  Summary: Command line tools for managing Codeer agents over the Codeer API.
5
5
  Project-URL: Homepage, https://www.codeer.ai
6
6
  Author: Codeer.AI
@@ -123,6 +123,34 @@ List the active cloud models without opening the Codeer web app:
123
123
  codeer model list --type text
124
124
  ```
125
125
 
126
+ ## Custom evaluator judge models
127
+
128
+ Custom evaluator create/update commands can select a judge LLM model by ID:
129
+
130
+ ```bash
131
+ codeer eval evaluator-create \
132
+ --name "Correctness" \
133
+ --system-prompt-template-file evaluator-prompt.txt \
134
+ --judge-model <model-id> \
135
+ --dry-run
136
+
137
+ codeer eval evaluator-update \
138
+ --evaluator <evaluator-id> \
139
+ --judge-model <model-id> \
140
+ --dry-run
141
+ ```
142
+
143
+ Omit the judge-model flags on update to leave the current setting unchanged.
144
+ Use `--clear-judge-model` to explicitly clear the override and return to the
145
+ system default:
146
+
147
+ ```bash
148
+ codeer eval evaluator-update \
149
+ --evaluator <evaluator-id> \
150
+ --clear-judge-model \
151
+ --dry-run
152
+ ```
153
+
126
154
  ## Agent human handoff
127
155
 
128
156
  `codeer agent apply` accepts the same `human_handoff` object as the Agent API.
@@ -105,6 +105,34 @@ List the active cloud models without opening the Codeer web app:
105
105
  codeer model list --type text
106
106
  ```
107
107
 
108
+ ## Custom evaluator judge models
109
+
110
+ Custom evaluator create/update commands can select a judge LLM model by ID:
111
+
112
+ ```bash
113
+ codeer eval evaluator-create \
114
+ --name "Correctness" \
115
+ --system-prompt-template-file evaluator-prompt.txt \
116
+ --judge-model <model-id> \
117
+ --dry-run
118
+
119
+ codeer eval evaluator-update \
120
+ --evaluator <evaluator-id> \
121
+ --judge-model <model-id> \
122
+ --dry-run
123
+ ```
124
+
125
+ Omit the judge-model flags on update to leave the current setting unchanged.
126
+ Use `--clear-judge-model` to explicitly clear the override and return to the
127
+ system default:
128
+
129
+ ```bash
130
+ codeer eval evaluator-update \
131
+ --evaluator <evaluator-id> \
132
+ --clear-judge-model \
133
+ --dry-run
134
+ ```
135
+
108
136
  ## Agent human handoff
109
137
 
110
138
  `codeer agent apply` accepts the same `human_handoff` object as the Agent API.
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "codeer-cli"
7
- version = "0.1.12"
7
+ version = "0.1.14"
8
8
  description = "Command line tools for managing Codeer agents over the Codeer API."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.11"
@@ -20,6 +20,7 @@ def create(
20
20
  unified_tools: Optional[List[dict]] = None,
21
21
  use_search: bool = False,
22
22
  llm_model: Optional[str] = None,
23
+ llm_model_settings: Optional[dict[str, dict[str, Any]]] = None,
23
24
  description: Optional[str] = None,
24
25
  suggested_questions: Optional[List[str]] = None,
25
26
  primary_object_ids: Optional[List[int]] = None,
@@ -41,6 +42,8 @@ def create(
41
42
  body["description"] = description
42
43
  if llm_model is not None:
43
44
  body["llm_model"] = llm_model
45
+ if llm_model_settings is not None:
46
+ body["llm_model_settings"] = llm_model_settings
44
47
  if validated_handoff is not None:
45
48
  body["human_handoff"] = validated_handoff
46
49
  return client.post("/external/agents", json=body)
@@ -57,6 +60,7 @@ def update(
57
60
  version_note: str = "",
58
61
  description: Optional[str] = None,
59
62
  llm_model: Optional[str] = None,
63
+ llm_model_settings: Optional[dict[str, dict[str, Any]]] = None,
60
64
  suggested_questions: Optional[List[str]] = None,
61
65
  primary_object_ids: Optional[List[int]] = None,
62
66
  attachment_ids: Optional[List[str]] = None,
@@ -79,6 +83,8 @@ def update(
79
83
  body["description"] = description
80
84
  if llm_model is not None:
81
85
  body["llm_model"] = llm_model
86
+ if llm_model_settings is not None:
87
+ body["llm_model_settings"] = llm_model_settings
82
88
  if validated_handoff is not None:
83
89
  body["human_handoff"] = validated_handoff
84
90
  return client.patch(f"/external/agents/{agent_id}", json=body)
@@ -89,7 +89,9 @@ def register(subparsers):
89
89
  def _tool_summary(tools: list[dict] | None) -> list[dict]:
90
90
  out = []
91
91
  for t in tools or []:
92
- form = t.get("custom_form_schema") if isinstance(t.get("custom_form_schema"), dict) else {}
92
+ form = t.get("custom_form_schema")
93
+ if not isinstance(form, dict):
94
+ form = {}
93
95
  out.append({
94
96
  "id": t.get("id"),
95
97
  "type": t.get("type"),
@@ -169,6 +171,8 @@ def run_apply(args, client) -> int:
169
171
  log(f"error: invalid agent payload: {exc}")
170
172
  return 2
171
173
 
174
+ llm_model_settings_provided = "llm_model_settings" in body
175
+
172
176
  if args.dry_run:
173
177
  operation = "update" if args.agent_id else "create"
174
178
  result = {
@@ -181,6 +185,8 @@ def run_apply(args, client) -> int:
181
185
  "tool_count": len(validated_tools),
182
186
  "use_search": body.get("use_search", False),
183
187
  "llm_model": body.get("llm_model"),
188
+ "llm_model_settings_provided": llm_model_settings_provided,
189
+ "llm_model_settings": body.get("llm_model_settings"),
184
190
  "human_handoff": {
185
191
  "enabled": bool((validated_handoff or {}).get("enabled")),
186
192
  "idle_timeout_minutes": (validated_handoff or {}).get("idle_timeout_minutes"),
@@ -193,6 +199,12 @@ def run_apply(args, client) -> int:
193
199
  print_json(result)
194
200
  return 0
195
201
 
202
+ model_settings_kwargs = (
203
+ {"llm_model_settings": body["llm_model_settings"]}
204
+ if llm_model_settings_provided
205
+ else {}
206
+ )
207
+
196
208
  if args.agent_id:
197
209
  body.pop("workspace_id", None)
198
210
  agents_mod.update(
@@ -208,6 +220,7 @@ def run_apply(args, client) -> int:
208
220
  primary_object_ids=body.get("primary_object_ids") or [],
209
221
  attachment_ids=body.get("attachment_ids") or [],
210
222
  human_handoff=validated_handoff,
223
+ **model_settings_kwargs,
211
224
  )
212
225
  agent_id = args.agent_id
213
226
  log(f"PUT /agents/{agent_id} ok")
@@ -227,6 +240,7 @@ def run_apply(args, client) -> int:
227
240
  primary_object_ids=body.get("primary_object_ids") or [],
228
241
  attachment_ids=body.get("attachment_ids") or [],
229
242
  human_handoff=validated_handoff,
243
+ **model_settings_kwargs,
230
244
  )
231
245
  agent_id = agent["id"]
232
246
  log(f"POST /agents ok, id={agent_id}")
@@ -128,6 +128,12 @@ def register(subparsers):
128
128
  g.add_argument("--system-prompt-template", help="Evaluator system prompt template text")
129
129
  g.add_argument("--system-prompt-template-file", help="Path to evaluator system prompt template")
130
130
  p.add_argument("--description", default=None)
131
+ p.add_argument(
132
+ "--judge-model",
133
+ default=None,
134
+ metavar="MODEL_ID",
135
+ help="Judge LLM model ID (default: system default)",
136
+ )
131
137
  p.add_argument("--dry-run", action="store_true",
132
138
  help="Validate inputs and print intended mutation without writing server state.")
133
139
  p.set_defaults(func=run_evaluator_create)
@@ -140,6 +146,18 @@ def register(subparsers):
140
146
  g.add_argument("--system-prompt-template", help="Evaluator system prompt template text")
141
147
  g.add_argument("--system-prompt-template-file", help="Path to evaluator system prompt template")
142
148
  p.add_argument("--description", default=None)
149
+ g = p.add_mutually_exclusive_group()
150
+ g.add_argument(
151
+ "--judge-model",
152
+ default=None,
153
+ metavar="MODEL_ID",
154
+ help="Set the judge LLM model ID",
155
+ )
156
+ g.add_argument(
157
+ "--clear-judge-model",
158
+ action="store_true",
159
+ help="Clear the evaluator override and use the system default judge model",
160
+ )
143
161
  p.add_argument("--dry-run", action="store_true",
144
162
  help="Validate inputs and print intended mutation without writing server state.")
145
163
  p.set_defaults(func=run_evaluator_update)
@@ -249,6 +267,7 @@ def _evaluator_summary(evaluator: dict, *, full: bool = False) -> dict:
249
267
  "id": evaluator.get("id"),
250
268
  "name": evaluator.get("name"),
251
269
  "description": evaluator.get("description"),
270
+ "judge_llm_model_id": evaluator.get("judge_llm_model_id"),
252
271
  "system_prompt_template_chars": len(template),
253
272
  "has_tool_steps_placeholder": "{tool_steps}" in template,
254
273
  "has_output_placeholder": "{output}" in template,
@@ -534,6 +553,10 @@ def run_evaluator_create(args, client) -> int:
534
553
  "workspace_id": workspace_id,
535
554
  "name": args.name,
536
555
  "description": args.description,
556
+ "judge_model": {
557
+ "action": "set" if args.judge_model is not None else "use_system_default",
558
+ "model_id": args.judge_model,
559
+ },
537
560
  "system_prompt_template_chars": len(system_prompt_template or ""),
538
561
  "would_write_server_state": True,
539
562
  "next_step": "Review this summary, then rerun without --dry-run after approval.",
@@ -545,6 +568,7 @@ def run_evaluator_create(args, client) -> int:
545
568
  name=args.name,
546
569
  system_prompt_template=system_prompt_template,
547
570
  description=args.description,
571
+ judge_llm_model_id=args.judge_model,
548
572
  )
549
573
  print_json(_evaluator_summary(evaluator, full=True))
550
574
  return 0
@@ -560,13 +584,27 @@ def run_evaluator_update(args, client) -> int:
560
584
  else:
561
585
  system_prompt_template = args.system_prompt_template
562
586
 
563
- if args.name is None and args.description is None and system_prompt_template is None:
587
+ if (
588
+ args.name is None
589
+ and args.description is None
590
+ and system_prompt_template is None
591
+ and args.judge_model is None
592
+ and not args.clear_judge_model
593
+ ):
564
594
  log(
565
595
  "error: provide at least one of --name, --description, "
566
- "--system-prompt-template, --system-prompt-template-file"
596
+ "--system-prompt-template, --system-prompt-template-file, "
597
+ "--judge-model, --clear-judge-model"
567
598
  )
568
599
  return 2
569
600
 
601
+ if args.clear_judge_model:
602
+ judge_model = {"action": "clear_to_system_default", "model_id": None}
603
+ elif args.judge_model is not None:
604
+ judge_model = {"action": "set", "model_id": args.judge_model}
605
+ else:
606
+ judge_model = {"action": "unchanged"}
607
+
570
608
  if args.dry_run:
571
609
  print_json({
572
610
  "dry_run": True,
@@ -574,6 +612,7 @@ def run_evaluator_update(args, client) -> int:
574
612
  "evaluator_id": args.evaluator,
575
613
  "name": args.name,
576
614
  "description": args.description,
615
+ "judge_model": judge_model,
577
616
  "system_prompt_template_chars": (
578
617
  len(system_prompt_template) if system_prompt_template is not None else None
579
618
  ),
@@ -582,12 +621,20 @@ def run_evaluator_update(args, client) -> int:
582
621
  })
583
622
  return 0
584
623
 
624
+ evaluator_kwargs: dict[str, Any] = {
625
+ "name": args.name,
626
+ "system_prompt_template": system_prompt_template,
627
+ "description": args.description,
628
+ }
629
+ if args.clear_judge_model:
630
+ evaluator_kwargs["judge_llm_model_id"] = None
631
+ elif args.judge_model is not None:
632
+ evaluator_kwargs["judge_llm_model_id"] = args.judge_model
633
+
585
634
  evaluator = eval_mod.update_evaluator(
586
635
  client,
587
636
  evaluator_id=args.evaluator,
588
- name=args.name,
589
- system_prompt_template=system_prompt_template,
590
- description=args.description,
637
+ **evaluator_kwargs,
591
638
  )
592
639
  print_json(_evaluator_summary(evaluator, full=True))
593
640
  return 0
@@ -12,6 +12,13 @@ from typing import Any, List, Optional
12
12
  from .client import CodeerClient
13
13
 
14
14
 
15
+ class _UnsetType:
16
+ pass
17
+
18
+
19
+ _UNSET = _UnsetType()
20
+
21
+
15
22
  # --- cases ----------------------------------------------------------------
16
23
 
17
24
  def create_case(
@@ -186,6 +193,7 @@ def create_evaluator(
186
193
  name: str,
187
194
  system_prompt_template: str,
188
195
  description: Optional[str] = None,
196
+ judge_llm_model_id: Optional[str] = None,
189
197
  ) -> dict:
190
198
  body: dict[str, Any] = {
191
199
  "name": name,
@@ -193,6 +201,8 @@ def create_evaluator(
193
201
  }
194
202
  if description is not None:
195
203
  body["description"] = description
204
+ if judge_llm_model_id is not None:
205
+ body["judge_llm_model_id"] = judge_llm_model_id
196
206
  return client.post("/external/eval/evaluators", json=body)
197
207
 
198
208
 
@@ -211,6 +221,7 @@ def update_evaluator(
211
221
  name: Optional[str] = None,
212
222
  system_prompt_template: Optional[str] = None,
213
223
  description: Optional[str] = None,
224
+ judge_llm_model_id: str | None | _UnsetType = _UNSET,
214
225
  ) -> dict:
215
226
  body: dict[str, Any] = {}
216
227
  if name is not None:
@@ -219,6 +230,8 @@ def update_evaluator(
219
230
  body["system_prompt_template"] = system_prompt_template
220
231
  if description is not None:
221
232
  body["description"] = description
233
+ if judge_llm_model_id is not _UNSET:
234
+ body["judge_llm_model_id"] = judge_llm_model_id
222
235
  return client.put(f"/external/eval/evaluators/{evaluator_id}", json=body)
223
236
 
224
237
 
@@ -0,0 +1,230 @@
1
+ from __future__ import annotations
2
+
3
+ import contextlib
4
+ import io
5
+ import json
6
+ import tempfile
7
+ import unittest
8
+ from pathlib import Path
9
+ from types import SimpleNamespace
10
+ from unittest.mock import patch
11
+
12
+ from codeer_cli import agents
13
+ from codeer_cli.commands import agent as agent_cmd
14
+
15
+
16
+ MODEL_ID = "openai/gpt-5.6-luna"
17
+ MODEL_SETTINGS = {MODEL_ID: {"thinking": "xhigh"}}
18
+
19
+
20
+ class FakeClient:
21
+ def __init__(self) -> None:
22
+ self.calls: list[tuple[str, str, dict]] = []
23
+
24
+ def post(self, path: str, **kwargs):
25
+ self.calls.append(("POST", path, kwargs))
26
+ return {"id": "agent-1"}
27
+
28
+ def patch(self, path: str, **kwargs):
29
+ self.calls.append(("PATCH", path, kwargs))
30
+ return {"id": "agent-1"}
31
+
32
+
33
+ class AgentModelSettingsPayloadTests(unittest.TestCase):
34
+ def test_create_forwards_model_settings(self) -> None:
35
+ client = FakeClient()
36
+
37
+ agents.create(
38
+ client, # type: ignore[arg-type]
39
+ workspace_id="workspace-1",
40
+ name="Agent",
41
+ system_prompt="Help safely.",
42
+ llm_model=MODEL_ID,
43
+ llm_model_settings=MODEL_SETTINGS,
44
+ )
45
+
46
+ self.assertEqual(client.calls[0][0:2], ("POST", "/external/agents"))
47
+ self.assertEqual(
48
+ client.calls[0][2]["json"]["llm_model_settings"],
49
+ MODEL_SETTINGS,
50
+ )
51
+
52
+ def test_update_forwards_model_settings(self) -> None:
53
+ client = FakeClient()
54
+
55
+ agents.update(
56
+ client, # type: ignore[arg-type]
57
+ "agent-1",
58
+ name="Agent",
59
+ system_prompt="Help safely.",
60
+ unified_tools=[],
61
+ use_search=False,
62
+ llm_model=MODEL_ID,
63
+ llm_model_settings=MODEL_SETTINGS,
64
+ )
65
+
66
+ self.assertEqual(
67
+ client.calls[0][0:2],
68
+ ("PATCH", "/external/agents/agent-1"),
69
+ )
70
+ self.assertEqual(
71
+ client.calls[0][2]["json"]["llm_model_settings"],
72
+ MODEL_SETTINGS,
73
+ )
74
+
75
+ def test_update_forwards_empty_map_to_clear_settings(self) -> None:
76
+ client = FakeClient()
77
+
78
+ agents.update(
79
+ client, # type: ignore[arg-type]
80
+ "agent-1",
81
+ name="Agent",
82
+ system_prompt="Help safely.",
83
+ unified_tools=[],
84
+ use_search=False,
85
+ llm_model_settings={},
86
+ )
87
+
88
+ self.assertEqual(client.calls[0][2]["json"]["llm_model_settings"], {})
89
+
90
+ def test_update_omits_model_settings_when_unspecified(self) -> None:
91
+ client = FakeClient()
92
+
93
+ agents.update(
94
+ client, # type: ignore[arg-type]
95
+ "agent-1",
96
+ name="Agent",
97
+ system_prompt="Help safely.",
98
+ unified_tools=[],
99
+ use_search=False,
100
+ )
101
+
102
+ self.assertNotIn("llm_model_settings", client.calls[0][2]["json"])
103
+
104
+
105
+ class AgentModelSettingsCommandTests(unittest.TestCase):
106
+ def _run_apply(
107
+ self,
108
+ payload: dict,
109
+ *,
110
+ agent_id: str | None = None,
111
+ dry_run: bool = False,
112
+ ) -> tuple[int, dict]:
113
+ with tempfile.TemporaryDirectory() as tmpdir:
114
+ payload_path = Path(tmpdir) / "agent.json"
115
+ payload_path.write_text(json.dumps(payload))
116
+ args = SimpleNamespace(
117
+ payload=str(payload_path),
118
+ agent_id=agent_id,
119
+ dry_run=dry_run,
120
+ note="settings regression",
121
+ out=None,
122
+ )
123
+ stdout = io.StringIO()
124
+ with contextlib.redirect_stdout(stdout):
125
+ status = agent_cmd.run_apply(args, object())
126
+ return status, json.loads(stdout.getvalue()) if stdout.getvalue() else {}
127
+
128
+ def test_apply_create_forwards_model_settings(self) -> None:
129
+ payload = {
130
+ "workspace_id": "workspace-1",
131
+ "name": "Agent",
132
+ "system_prompt": "Help safely.",
133
+ "llm_model": MODEL_ID,
134
+ "llm_model_settings": MODEL_SETTINGS,
135
+ }
136
+ history = [{"id": "history-1", "version_number": 1, "status": "draft"}]
137
+
138
+ with (
139
+ patch.object(
140
+ agent_cmd.agents_mod,
141
+ "create",
142
+ return_value={"id": "agent-1"},
143
+ ) as create,
144
+ patch.object(agent_cmd.agents_mod, "list_versions", return_value=history),
145
+ ):
146
+ status, _ = self._run_apply(payload)
147
+
148
+ self.assertEqual(status, 0)
149
+ self.assertEqual(
150
+ create.call_args.kwargs["llm_model_settings"],
151
+ MODEL_SETTINGS,
152
+ )
153
+
154
+ def test_apply_update_preserves_present_and_empty_settings(self) -> None:
155
+ history = [{"id": "history-2", "version_number": 2, "status": "draft"}]
156
+
157
+ for model_settings in (MODEL_SETTINGS, {}):
158
+ with self.subTest(model_settings=model_settings):
159
+ payload = {
160
+ "name": "Agent",
161
+ "system_prompt": "Help safely.",
162
+ "llm_model_settings": model_settings,
163
+ }
164
+ with (
165
+ patch.object(agent_cmd.agents_mod, "update") as update,
166
+ patch.object(
167
+ agent_cmd.agents_mod,
168
+ "list_versions",
169
+ return_value=history,
170
+ ),
171
+ ):
172
+ status, _ = self._run_apply(payload, agent_id="agent-1")
173
+
174
+ self.assertEqual(status, 0)
175
+ self.assertEqual(
176
+ update.call_args.kwargs["llm_model_settings"],
177
+ model_settings,
178
+ )
179
+
180
+ def test_apply_update_does_not_supply_omitted_settings(self) -> None:
181
+ payload = {
182
+ "name": "Agent",
183
+ "system_prompt": "Help safely.",
184
+ }
185
+ history = [{"id": "history-2", "version_number": 2, "status": "draft"}]
186
+
187
+ with (
188
+ patch.object(agent_cmd.agents_mod, "update") as update,
189
+ patch.object(agent_cmd.agents_mod, "list_versions", return_value=history),
190
+ ):
191
+ status, _ = self._run_apply(payload, agent_id="agent-1")
192
+
193
+ self.assertEqual(status, 0)
194
+ self.assertNotIn("llm_model_settings", update.call_args.kwargs)
195
+
196
+ def test_apply_dry_run_distinguishes_settings_states(self) -> None:
197
+ cases = (
198
+ (MODEL_SETTINGS, True, MODEL_SETTINGS),
199
+ ({}, True, {}),
200
+ (None, False, None),
201
+ )
202
+
203
+ for model_settings, expected_provided, expected_settings in cases:
204
+ with self.subTest(model_settings=model_settings):
205
+ payload: dict[str, object] = {
206
+ "name": "Agent",
207
+ "system_prompt": "Help safely.",
208
+ }
209
+ if expected_provided:
210
+ payload["llm_model_settings"] = model_settings
211
+
212
+ status, result = self._run_apply(
213
+ payload,
214
+ agent_id="agent-1",
215
+ dry_run=True,
216
+ )
217
+
218
+ self.assertEqual(status, 0)
219
+ self.assertEqual(
220
+ result["llm_model_settings_provided"],
221
+ expected_provided,
222
+ )
223
+ self.assertEqual(
224
+ result["llm_model_settings"],
225
+ expected_settings,
226
+ )
227
+
228
+
229
+ if __name__ == "__main__":
230
+ unittest.main()
@@ -0,0 +1,230 @@
1
+ from __future__ import annotations
2
+
3
+ import argparse
4
+ import io
5
+ import json
6
+ import unittest
7
+ from contextlib import redirect_stderr, redirect_stdout
8
+ from types import SimpleNamespace
9
+
10
+ from codeer_cli import eval_ as eval_mod
11
+ from codeer_cli.commands.eval_cmd import (
12
+ register,
13
+ run_evaluator_create,
14
+ run_evaluator_update,
15
+ )
16
+
17
+
18
+ class FakeClient:
19
+ def __init__(self) -> None:
20
+ self.calls: list[tuple[str, str, dict]] = []
21
+
22
+ def post(self, path: str, **kwargs):
23
+ self.calls.append(("POST", path, kwargs))
24
+ return {"id": "created", **kwargs["json"]}
25
+
26
+ def put(self, path: str, **kwargs):
27
+ self.calls.append(("PUT", path, kwargs))
28
+ return {"id": "updated", **kwargs["json"]}
29
+
30
+ def resolve_scope(self):
31
+ return "ws-1", "org-1"
32
+
33
+
34
+ class EvaluatorRequestTests(unittest.TestCase):
35
+ def test_create_evaluator_sends_judge_model(self) -> None:
36
+ client = FakeClient()
37
+
38
+ eval_mod.create_evaluator(
39
+ client, # type: ignore[arg-type]
40
+ workspace_id="ws-1",
41
+ name="Correctness",
42
+ system_prompt_template="Judge {output}",
43
+ judge_llm_model_id="model-1",
44
+ )
45
+
46
+ self.assertEqual(client.calls[0][0:2], ("POST", "/external/eval/evaluators"))
47
+ self.assertEqual(client.calls[0][2]["json"]["judge_llm_model_id"], "model-1")
48
+
49
+ def test_create_evaluator_omits_unspecified_judge_model(self) -> None:
50
+ client = FakeClient()
51
+
52
+ eval_mod.create_evaluator(
53
+ client, # type: ignore[arg-type]
54
+ workspace_id="ws-1",
55
+ name="Correctness",
56
+ system_prompt_template="Judge {output}",
57
+ )
58
+
59
+ self.assertNotIn("judge_llm_model_id", client.calls[0][2]["json"])
60
+
61
+ def test_update_evaluator_sends_judge_model(self) -> None:
62
+ client = FakeClient()
63
+
64
+ eval_mod.update_evaluator(
65
+ client, # type: ignore[arg-type]
66
+ "eval-1",
67
+ judge_llm_model_id="model-2",
68
+ )
69
+
70
+ self.assertEqual(client.calls[0][2]["json"], {"judge_llm_model_id": "model-2"})
71
+
72
+ def test_update_evaluator_omits_unspecified_judge_model(self) -> None:
73
+ client = FakeClient()
74
+
75
+ eval_mod.update_evaluator(
76
+ client, # type: ignore[arg-type]
77
+ "eval-1",
78
+ name="Updated",
79
+ )
80
+
81
+ self.assertEqual(client.calls[0][2]["json"], {"name": "Updated"})
82
+
83
+ def test_update_evaluator_sends_null_to_clear_judge_model(self) -> None:
84
+ client = FakeClient()
85
+
86
+ eval_mod.update_evaluator(
87
+ client, # type: ignore[arg-type]
88
+ "eval-1",
89
+ judge_llm_model_id=None,
90
+ )
91
+
92
+ self.assertEqual(client.calls[0][2]["json"], {"judge_llm_model_id": None})
93
+
94
+
95
+ class EvaluatorCliTests(unittest.TestCase):
96
+ def test_create_dry_run_reports_set_judge_model(self) -> None:
97
+ client = FakeClient()
98
+ args = SimpleNamespace(
99
+ name="Correctness",
100
+ system_prompt_template="Judge {output}",
101
+ system_prompt_template_file=None,
102
+ description=None,
103
+ judge_model="model-1",
104
+ dry_run=True,
105
+ )
106
+
107
+ report, result = self._capture(run_evaluator_create, args, client)
108
+
109
+ self.assertEqual(result, 0)
110
+ self.assertEqual(
111
+ report["judge_model"], {"action": "set", "model_id": "model-1"}
112
+ )
113
+ self.assertEqual(client.calls, [])
114
+
115
+ def test_create_dry_run_reports_system_default_when_model_is_omitted(self) -> None:
116
+ client = FakeClient()
117
+ args = SimpleNamespace(
118
+ name="Correctness",
119
+ system_prompt_template="Judge {output}",
120
+ system_prompt_template_file=None,
121
+ description=None,
122
+ judge_model=None,
123
+ dry_run=True,
124
+ )
125
+
126
+ report, result = self._capture(run_evaluator_create, args, client)
127
+
128
+ self.assertEqual(result, 0)
129
+ self.assertEqual(
130
+ report["judge_model"],
131
+ {"action": "use_system_default", "model_id": None},
132
+ )
133
+ self.assertEqual(client.calls, [])
134
+
135
+ def test_update_dry_run_reports_set_judge_model(self) -> None:
136
+ client = FakeClient()
137
+ args = self._update_args(judge_model="model-2")
138
+
139
+ report, result = self._capture(run_evaluator_update, args, client)
140
+
141
+ self.assertEqual(result, 0)
142
+ self.assertEqual(
143
+ report["judge_model"], {"action": "set", "model_id": "model-2"}
144
+ )
145
+ self.assertEqual(client.calls, [])
146
+
147
+ def test_update_dry_run_reports_unspecified_judge_model_as_unchanged(self) -> None:
148
+ client = FakeClient()
149
+ args = self._update_args(name="Updated")
150
+
151
+ report, result = self._capture(run_evaluator_update, args, client)
152
+
153
+ self.assertEqual(result, 0)
154
+ self.assertEqual(report["judge_model"], {"action": "unchanged"})
155
+ self.assertEqual(client.calls, [])
156
+
157
+ def test_update_dry_run_reports_explicit_clear(self) -> None:
158
+ client = FakeClient()
159
+ args = self._update_args(clear_judge_model=True)
160
+
161
+ report, result = self._capture(run_evaluator_update, args, client)
162
+
163
+ self.assertEqual(result, 0)
164
+ self.assertEqual(
165
+ report["judge_model"],
166
+ {"action": "clear_to_system_default", "model_id": None},
167
+ )
168
+ self.assertEqual(client.calls, [])
169
+
170
+ def test_update_clear_flag_sends_explicit_null(self) -> None:
171
+ client = FakeClient()
172
+ args = self._update_args(clear_judge_model=True, dry_run=False)
173
+
174
+ _, result = self._capture(run_evaluator_update, args, client)
175
+
176
+ self.assertEqual(result, 0)
177
+ self.assertEqual(client.calls[0][2]["json"], {"judge_llm_model_id": None})
178
+
179
+ def test_update_other_field_does_not_send_judge_model(self) -> None:
180
+ client = FakeClient()
181
+ args = self._update_args(name="Updated", dry_run=False)
182
+
183
+ _, result = self._capture(run_evaluator_update, args, client)
184
+
185
+ self.assertEqual(result, 0)
186
+ self.assertEqual(client.calls[0][2]["json"], {"name": "Updated"})
187
+
188
+ def test_parser_rejects_set_and_clear_together(self) -> None:
189
+ parser = argparse.ArgumentParser()
190
+ subparsers = parser.add_subparsers(dest="group")
191
+ register(subparsers)
192
+
193
+ with redirect_stderr(io.StringIO()), self.assertRaises(SystemExit):
194
+ parser.parse_args(
195
+ [
196
+ "eval",
197
+ "evaluator-update",
198
+ "--evaluator",
199
+ "eval-1",
200
+ "--judge-model",
201
+ "model-1",
202
+ "--clear-judge-model",
203
+ ]
204
+ )
205
+
206
+ @staticmethod
207
+ def _capture(func, args, client) -> tuple[dict, int]:
208
+ stdout = io.StringIO()
209
+ with redirect_stdout(stdout):
210
+ result = func(args, client)
211
+ return json.loads(stdout.getvalue()), result
212
+
213
+ @staticmethod
214
+ def _update_args(**overrides) -> SimpleNamespace:
215
+ values = {
216
+ "evaluator": "eval-1",
217
+ "name": None,
218
+ "system_prompt_template": None,
219
+ "system_prompt_template_file": None,
220
+ "description": None,
221
+ "judge_model": None,
222
+ "clear_judge_model": False,
223
+ "dry_run": True,
224
+ }
225
+ values.update(overrides)
226
+ return SimpleNamespace(**values)
227
+
228
+
229
+ if __name__ == "__main__":
230
+ unittest.main()
@@ -26,7 +26,7 @@ wheels = [
26
26
 
27
27
  [[package]]
28
28
  name = "codeer-cli"
29
- version = "0.1.12"
29
+ version = "0.1.14"
30
30
  source = { editable = "." }
31
31
  dependencies = [
32
32
  { name = "httpx" },
File without changes