Skip to content

Commit f8f9b8e

Browse files
authored
Merge pull request #10 from BraveNewCapital/copilot/update-repo-architect-models
Resolve repo-architect models from GitHub catalog with available-only fallback selection and preserve explicit override
2 parents 7c09c90 + bd6f8a8 commit f8f9b8e

File tree

3 files changed

+241
-12
lines changed

3 files changed

+241
-12
lines changed

.github/workflows/repo-architect.yml

Lines changed: 84 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -77,14 +77,96 @@ jobs:
7777
run: |
7878
mkdir -p .agent docs/repo_architect
7979
80+
- name: Resolve GitHub Models configuration
81+
env:
82+
GITHUB_TOKEN: ${{ github.token }}
83+
run: |
84+
python - <<'PY'
85+
import json
86+
import os
87+
import urllib.request
88+
89+
order = [
90+
"anthropic/claude-sonnet-4.6",
91+
"anthropic/claude-sonnet-4.5",
92+
"openai/gpt-4.1",
93+
]
94+
secondary = "google/gemini-3-pro"
95+
available = set()
96+
catalog_ok = False
97+
try:
98+
req = urllib.request.Request(
99+
"https://models.github.ai/catalog/models",
100+
headers={
101+
"Authorization": f"Bearer {os.environ['GITHUB_TOKEN']}",
102+
"Accept": "application/json",
103+
"User-Agent": "repo-architect-workflow",
104+
},
105+
)
106+
with urllib.request.urlopen(req, timeout=30) as resp:
107+
payload = json.loads(resp.read().decode("utf-8"))
108+
models = payload.get("data", payload) if isinstance(payload, dict) else payload
109+
if isinstance(models, list):
110+
catalog_ok = True
111+
for item in models:
112+
if isinstance(item, dict):
113+
model_id = item.get("id") or item.get("name") or item.get("model")
114+
if isinstance(model_id, str) and model_id:
115+
available.add(model_id)
116+
except Exception as exc:
117+
print(f"warning: GitHub Models catalog lookup failed; using defaults ({exc})")
118+
119+
def first_available(candidates):
120+
for candidate in candidates:
121+
if candidate in available:
122+
return candidate
123+
return None
124+
125+
def deterministic_available(exclude=None):
126+
candidates = sorted(m for m in available if m != exclude)
127+
return candidates[0] if candidates else None
128+
129+
if catalog_ok and available:
130+
preferred = (
131+
first_available(order)
132+
or (secondary if secondary in available else None)
133+
or deterministic_available()
134+
)
135+
else:
136+
preferred = order[0]
137+
138+
if catalog_ok and available:
139+
if secondary in available and secondary != preferred:
140+
fallback = secondary
141+
else:
142+
fallback = (
143+
first_available([c for c in order if c != preferred])
144+
or deterministic_available(exclude=preferred)
145+
or preferred
146+
)
147+
else:
148+
fallback = secondary
149+
150+
if not isinstance(preferred, str) or not preferred:
151+
preferred = order[0]
152+
if not isinstance(fallback, str) or not fallback:
153+
fallback = secondary if secondary != preferred else order[-1]
154+
155+
env_file = os.environ.get("GITHUB_ENV")
156+
if not env_file:
157+
raise RuntimeError("GITHUB_ENV is not set; this internal workflow step must run inside GitHub Actions with environment-file support.")
158+
with open(env_file, "a", encoding="utf-8") as fh:
159+
fh.write(f"REPO_ARCHITECT_PREFERRED_MODEL={preferred}\n")
160+
fh.write(f"REPO_ARCHITECT_FALLBACK_MODEL={fallback}\n")
161+
print(f"selected preferred={preferred} fallback={fallback}")
162+
PY
163+
80164
- name: Run repo architect
81165
env:
82166
GITHUB_TOKEN: ${{ github.token }}
83167
GITHUB_REPO: ${{ github.repository }}
84168
GITHUB_BASE_BRANCH: ${{ github.event.repository.default_branch }}
85169
REPO_ARCHITECT_BRANCH_SUFFIX: ${{ github.run_id }}-${{ github.run_attempt }}
86-
REPO_ARCHITECT_PREFERRED_MODEL: openai/gpt-5.4
87-
REPO_ARCHITECT_FALLBACK_MODEL: openai/gpt-4.1
88170
run: |
89171
MODE="${{ github.event.inputs.mode }}"
90172
MODEL="${{ github.event.inputs.github_model }}"

repo_architect.py

Lines changed: 90 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -60,8 +60,8 @@
6060
}
6161

6262
# Model selection defaults
63-
DEFAULT_PREFERRED_MODEL = "openai/gpt-5.4"
64-
DEFAULT_FALLBACK_MODEL = "openai/gpt-4.1"
63+
DEFAULT_PREFERRED_MODEL = "anthropic/claude-sonnet-4.6"
64+
DEFAULT_FALLBACK_MODEL = "google/gemini-3-pro"
6565
# Substrings in HTTP error bodies that indicate the model itself is unavailable (not a transient error)
6666
_MODEL_UNAVAILABLE_SIGNALS = frozenset({
6767
"unknown_model", "model_not_found", "unsupported_model", "unsupported model",
@@ -697,7 +697,7 @@ def build_analysis(root: pathlib.Path) -> Dict[str, Any]:
697697
# -----------------------------
698698

699699
def enrich_with_github_models(config: Config, analysis: Dict[str, Any]) -> Dict[str, Any]:
700-
preferred = config.preferred_model or config.github_model
700+
preferred = config.github_model or config.preferred_model
701701
fallback = config.fallback_model
702702
meta: Dict[str, Any] = {
703703
"enabled": False,
@@ -975,7 +975,7 @@ def build_parse_errors_plan(config: Config, analysis: Dict[str, Any]) -> Optiona
975975
errors = analysis.get("parse_error_files", [])
976976
if not errors:
977977
return None
978-
preferred = config.preferred_model or config.github_model
978+
preferred = config.github_model or config.preferred_model
979979
if not config.github_token or not preferred:
980980
return None
981981
fallback = config.fallback_model
@@ -1042,7 +1042,7 @@ def build_import_cycles_plan(config: Config, analysis: Dict[str, Any]) -> Option
10421042
cycles = analysis.get("cycles", [])
10431043
if not cycles:
10441044
return None
1045-
preferred = config.preferred_model or config.github_model
1045+
preferred = config.github_model or config.preferred_model
10461046
if not config.github_token or not preferred:
10471047
return None
10481048
fallback = config.fallback_model
@@ -1128,7 +1128,7 @@ def build_entrypoint_consolidation_plan(config: Config, analysis: Dict[str, Any]
11281128
backend_eps = clusters.get("backend_servers", [])
11291129
if len(backend_eps) < _ENTRYPOINT_CONSOLIDATION_THRESHOLD:
11301130
return None
1131-
preferred = config.preferred_model or config.github_model
1131+
preferred = config.github_model or config.preferred_model
11321132
if not config.github_token or not preferred:
11331133
return None
11341134
fallback = config.fallback_model
@@ -1482,14 +1482,96 @@ def workflow_yaml(secret_env_names: Sequence[str], cron: str, github_model: Opti
14821482
run: |
14831483
mkdir -p .agent docs/repo_architect
14841484
1485+
- name: Resolve GitHub Models configuration
1486+
env:
1487+
GITHUB_TOKEN: ${{{{ github.token }}}}
1488+
run: |
1489+
python - <<'PY'
1490+
import json
1491+
import os
1492+
import urllib.request
1493+
1494+
order = [
1495+
"anthropic/claude-sonnet-4.6",
1496+
"anthropic/claude-sonnet-4.5",
1497+
"openai/gpt-4.1",
1498+
]
1499+
secondary = "google/gemini-3-pro"
1500+
available = set()
1501+
catalog_ok = False
1502+
try:
1503+
req = urllib.request.Request(
1504+
"https://models.github.ai/catalog/models",
1505+
headers={{
1506+
"Authorization": f"Bearer {{os.environ['GITHUB_TOKEN']}}",
1507+
"Accept": "application/json",
1508+
"User-Agent": "repo-architect-workflow",
1509+
}},
1510+
)
1511+
with urllib.request.urlopen(req, timeout=30) as resp:
1512+
payload = json.loads(resp.read().decode("utf-8"))
1513+
models = payload.get("data", payload) if isinstance(payload, dict) else payload
1514+
if isinstance(models, list):
1515+
catalog_ok = True
1516+
for item in models:
1517+
if isinstance(item, dict):
1518+
model_id = item.get("id") or item.get("name") or item.get("model")
1519+
if isinstance(model_id, str) and model_id:
1520+
available.add(model_id)
1521+
except Exception as exc:
1522+
print(f"warning: GitHub Models catalog lookup failed; using defaults ({{exc}})")
1523+
1524+
def first_available(candidates):
1525+
for candidate in candidates:
1526+
if candidate in available:
1527+
return candidate
1528+
return None
1529+
1530+
def deterministic_available(exclude=None):
1531+
candidates = sorted(m for m in available if m != exclude)
1532+
return candidates[0] if candidates else None
1533+
1534+
if catalog_ok and available:
1535+
preferred = (
1536+
first_available(order)
1537+
or (secondary if secondary in available else None)
1538+
or deterministic_available()
1539+
)
1540+
else:
1541+
preferred = order[0]
1542+
1543+
if catalog_ok and available:
1544+
if secondary in available and secondary != preferred:
1545+
fallback = secondary
1546+
else:
1547+
fallback = (
1548+
first_available([c for c in order if c != preferred])
1549+
or deterministic_available(exclude=preferred)
1550+
or preferred
1551+
)
1552+
else:
1553+
fallback = secondary
1554+
1555+
if not isinstance(preferred, str) or not preferred:
1556+
preferred = order[0]
1557+
if not isinstance(fallback, str) or not fallback:
1558+
fallback = secondary if secondary != preferred else order[-1]
1559+
1560+
env_file = os.environ.get("GITHUB_ENV")
1561+
if not env_file:
1562+
raise RuntimeError("GITHUB_ENV is not set; this internal workflow step must run inside GitHub Actions with environment-file support.")
1563+
with open(env_file, "a", encoding="utf-8") as fh:
1564+
fh.write(f"REPO_ARCHITECT_PREFERRED_MODEL={{preferred}}\\n")
1565+
fh.write(f"REPO_ARCHITECT_FALLBACK_MODEL={{fallback}}\\n")
1566+
print(f"selected preferred={{preferred}} fallback={{fallback}}")
1567+
PY
1568+
14851569
- name: Run repo architect
14861570
env:
14871571
GITHUB_TOKEN: ${{{{ github.token }}}}
14881572
GITHUB_REPO: ${{{{ github.repository }}}}
14891573
GITHUB_BASE_BRANCH: ${{{{ github.event.repository.default_branch }}}}
14901574
REPO_ARCHITECT_BRANCH_SUFFIX: ${{{{ github.run_id }}}}-${{{{ github.run_attempt }}}}
1491-
REPO_ARCHITECT_PREFERRED_MODEL: openai/gpt-5.4
1492-
REPO_ARCHITECT_FALLBACK_MODEL: openai/gpt-4.1
14931575
{extra_env} run: |
14941576
MODE="${{{{ github.event.inputs.mode }}}}"
14951577
MODEL="${{{{ github.event.inputs.github_model }}}}"

tests/test_repo_architect.py

Lines changed: 67 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -250,7 +250,72 @@ def side_effect(token: str, model: str, messages: list) -> dict:
250250

251251

252252
# ---------------------------------------------------------------------------
253-
# 3. Syntax validation of generated Python (ast.parse gate)
253+
# 3. Model configuration behaviour
254+
# ---------------------------------------------------------------------------
255+
256+
class TestModelConfiguration(unittest.TestCase):
257+
def test_build_config_uses_env_models_when_github_model_blank(self) -> None:
258+
env = dict(os.environ)
259+
env.pop("GITHUB_MODEL", None)
260+
env["REPO_ARCHITECT_PREFERRED_MODEL"] = "anthropic/claude-sonnet-4.6"
261+
env["REPO_ARCHITECT_FALLBACK_MODEL"] = "google/gemini-3-pro"
262+
with patch.object(ra, "discover_git_root", return_value=pathlib.Path("/tmp/repo")):
263+
with patch.dict(os.environ, env, clear=True):
264+
config = ra.build_config(ra.parse_args([]))
265+
self.assertIsNone(config.github_model)
266+
self.assertEqual(config.preferred_model, "anthropic/claude-sonnet-4.6")
267+
self.assertEqual(config.fallback_model, "google/gemini-3-pro")
268+
269+
def test_github_model_override_takes_precedence_over_preferred(self) -> None:
270+
analysis = {
271+
"architecture_score": 0.8,
272+
"cycles": [],
273+
"parse_error_files": [],
274+
"entrypoint_paths": [],
275+
"roadmap": [],
276+
}
277+
response = {"choices": [{"message": {"content": "ok"}}], "model": "openai/manual-override"}
278+
with tempfile.TemporaryDirectory() as tmp:
279+
root = _make_git_root(tmp)
280+
config = _make_config(
281+
root,
282+
github_token="tok",
283+
github_model="openai/manual-override",
284+
preferred_model="anthropic/claude-sonnet-4.6",
285+
fallback_model="google/gemini-3-pro",
286+
)
287+
with patch.object(
288+
ra,
289+
"call_models_with_fallback_or_none",
290+
return_value=(response, "openai/manual-override", None, False),
291+
) as mocked_call:
292+
meta = ra.enrich_with_github_models(config, analysis)
293+
self.assertEqual(mocked_call.call_args.args[1], "openai/manual-override")
294+
self.assertEqual(mocked_call.call_args.args[2], "google/gemini-3-pro")
295+
self.assertEqual(meta["requested_model"], "openai/manual-override")
296+
self.assertEqual(meta["actual_model"], "openai/manual-override")
297+
298+
def test_workflow_yaml_resolves_models_via_catalog_and_keeps_blank_override_logic(self) -> None:
299+
workflow = ra.workflow_yaml([], "17 * * * *", None)
300+
self.assertIn("Resolve GitHub Models configuration", workflow)
301+
self.assertIn("https://models.github.ai/catalog/models", workflow)
302+
self.assertIn("catalog_ok = False", workflow)
303+
self.assertIn('"anthropic/claude-sonnet-4.6"', workflow)
304+
self.assertIn('"anthropic/claude-sonnet-4.5"', workflow)
305+
self.assertIn('"openai/gpt-4.1"', workflow)
306+
self.assertIn('secondary = "google/gemini-3-pro"', workflow)
307+
self.assertIn("def deterministic_available(exclude=None):", workflow)
308+
self.assertIn("or deterministic_available()", workflow)
309+
self.assertIn("or deterministic_available(exclude=preferred)", workflow)
310+
self.assertIn("or preferred", workflow)
311+
self.assertIn("REPO_ARCHITECT_PREFERRED_MODEL={preferred}", workflow)
312+
self.assertIn("REPO_ARCHITECT_FALLBACK_MODEL={fallback}", workflow)
313+
self.assertIn('if [ -n "$MODEL" ]; then EXTRA_ARGS="$EXTRA_ARGS --github-model $MODEL"; fi', workflow)
314+
self.assertIn("models: read", workflow)
315+
316+
317+
# ---------------------------------------------------------------------------
318+
# 4. Syntax validation of generated Python (ast.parse gate)
254319
# ---------------------------------------------------------------------------
255320

256321
class TestSyntaxValidationGate(unittest.TestCase):
@@ -313,7 +378,7 @@ def test_build_parse_errors_plan_accepts_valid_fix(self) -> None:
313378

314379

315380
# ---------------------------------------------------------------------------
316-
# 4. Campaign aggregation behaviour
381+
# 5. Campaign aggregation behaviour
317382
# ---------------------------------------------------------------------------
318383

319384
class TestCampaignAggregation(unittest.TestCase):

0 commit comments

Comments
 (0)