130 lines
4.4 KiB
Python
130 lines
4.4 KiB
Python
#!/usr/bin/env python3
|
|
"""Apply a model profile to agent definition files.
|
|
|
|
Supports:
|
|
- OpenCode YAML frontmatter agents in .opencode/agents/*.md
|
|
- Codex TOML agents in codex_adapter_templates/codex/agents/*.toml
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import re
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
|
|
REPO_ROOT = Path(__file__).resolve().parent.parent
|
|
if str(REPO_ROOT) not in sys.path:
|
|
sys.path.insert(0, str(REPO_ROOT))
|
|
|
|
from scripts.lib.model_config import (
|
|
ModelConfigError,
|
|
parse_model_overrides,
|
|
resolve_model_profile,
|
|
)
|
|
|
|
OPENCODE_ROLE_TO_FILE = {
|
|
"dr_plan": ".opencode/agents/dr-plan.md",
|
|
"dr_pm": ".opencode/agents/dr-pm.md",
|
|
"dr_searcher": ".opencode/agents/dr-searcher.md",
|
|
"dr_analyst": ".opencode/agents/dr-analyst.md",
|
|
"dr_verifier": ".opencode/agents/dr-verifier.md",
|
|
"dr_chief_editor": ".opencode/agents/dr-chief-editor.md",
|
|
"dr_editor_in_chief": ".opencode/agents/dr-editor-in-chief.md",
|
|
"dr_reporter": ".opencode/agents/dr-reporter.md",
|
|
}
|
|
|
|
CODEX_ROLE_TO_FILE = {
|
|
"dr_plan": "codex_adapter_templates/codex/agents/dr-plan.toml",
|
|
"dr_pm": "codex_adapter_templates/codex/agents/dr-pm.toml",
|
|
"dr_searcher": "codex_adapter_templates/codex/agents/dr-searcher.toml",
|
|
"dr_analyst": "codex_adapter_templates/codex/agents/dr-analyst.toml",
|
|
"dr_verifier": "codex_adapter_templates/codex/agents/dr-verifier.toml",
|
|
"dr_chief_editor": "codex_adapter_templates/codex/agents/dr-chief-editor.toml",
|
|
"dr_editor_in_chief": "codex_adapter_templates/codex/agents/dr-editor-in-chief.toml",
|
|
"dr_reporter": "codex_adapter_templates/codex/agents/dr-reporter.toml",
|
|
}
|
|
|
|
|
|
def replace_opencode_model(path: Path, model: str) -> bool:
|
|
text = path.read_text(encoding="utf-8")
|
|
new_text, count = re.subn(r"(?m)^model:\s*.+$", f"model: {model}", text, count=1)
|
|
if count == 0:
|
|
raise SystemExit(f"failed to locate model field: {path}")
|
|
if new_text == text:
|
|
return False
|
|
path.write_text(new_text, encoding="utf-8")
|
|
return True
|
|
|
|
|
|
def replace_codex_model(path: Path, model: str) -> bool:
|
|
text = path.read_text(encoding="utf-8")
|
|
new_text, count = re.subn(r'(?m)^model\s*=\s*"[^"]+"$', f'model = "{model}"', text, count=1)
|
|
if count == 0:
|
|
raise SystemExit(f"failed to locate model field: {path}")
|
|
if new_text == text:
|
|
return False
|
|
path.write_text(new_text, encoding="utf-8")
|
|
return True
|
|
|
|
|
|
def main() -> int:
|
|
parser = argparse.ArgumentParser(description="Apply model profile to agent files")
|
|
parser.add_argument("--profile", required=True, help="Profile name from configs/models.yaml")
|
|
parser.add_argument("--target", choices=["opencode", "codex", "both"], default="both")
|
|
parser.add_argument(
|
|
"--model-override",
|
|
action="append",
|
|
default=[],
|
|
metavar="ROLE=MODEL",
|
|
help="Override one role model, repeatable",
|
|
)
|
|
parser.add_argument("--dry-run", action="store_true")
|
|
args = parser.parse_args()
|
|
|
|
try:
|
|
resolved = resolve_model_profile(
|
|
profile=args.profile,
|
|
overrides=parse_model_overrides(args.model_override),
|
|
)
|
|
except ModelConfigError as exc:
|
|
raise SystemExit(f"model profile resolution failed: {exc}") from exc
|
|
|
|
roles = resolved["roles"]
|
|
changed: list[str] = []
|
|
|
|
def apply_map(mapping: dict[str, str], mode: str) -> None:
|
|
for role, rel_path in mapping.items():
|
|
model = roles.get(role)
|
|
if not model:
|
|
continue
|
|
file_path = REPO_ROOT / rel_path
|
|
if not file_path.exists():
|
|
continue
|
|
if args.dry_run:
|
|
changed.append(f"{mode}:{rel_path} -> {model}")
|
|
continue
|
|
did_change = replace_opencode_model(file_path, model) if mode == "opencode" else replace_codex_model(file_path, model)
|
|
if did_change:
|
|
changed.append(f"{mode}:{rel_path} -> {model}")
|
|
|
|
if args.target in ("opencode", "both"):
|
|
apply_map(OPENCODE_ROLE_TO_FILE, "opencode")
|
|
if args.target in ("codex", "both"):
|
|
apply_map(CODEX_ROLE_TO_FILE, "codex")
|
|
|
|
print(f"Profile applied: {resolved['profile']}")
|
|
print(f"Target: {args.target}")
|
|
if changed:
|
|
print("Updated:")
|
|
for item in changed:
|
|
print(f" - {item}")
|
|
else:
|
|
print("No files changed.")
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|