Skip to content

Commit ca08c4a

Browse files
lorenss-mclaude
andcommitted
fix: consistent model resolution across TrainingClient and create_agent
- TrainingClient._resolve_model_id now falls back to list_gateway_models() when /v2/models/resolve 404s, so display names work the same as slugs/UUIDs - env.py _inner_move resolves inner model via gateway list (same path as create_agent) so return_token_ids extension is triggered correctly - train.py Job.start hardcoded to 'c4-selfplay' instead of raw model string - Fixed inner move column parsing: prefer explicit 'column N' match, fall back to last \b[0-6]\b digit to avoid mis-picking row indices or counts Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
1 parent 9b106eb commit ca08c4a

4 files changed

Lines changed: 77 additions & 11 deletions

File tree

cookbooks/connect4-selfplay/env.py

Lines changed: 35 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@
2828
from hud.graders import EvaluationResult
2929

3030
_INNER_MODEL: str = "c4-selfplay"
31+
_INNER_MODEL_NAME: str = "c4-selfplay" # resolved model_name (gateway routing key)
32+
_INNER_PROVIDER: str = "openai"
3133
_OUTER_MARK: str = "X" # set per game; "X" drops first, "O" drops second
3234

3335
# Per-game inner model samples (reset at game start, read at game end).
@@ -112,6 +114,26 @@ def render(self) -> str:
112114

113115
game = ConnectFour()
114116

117+
118+
def _resolve_inner_model(model: str) -> tuple[str, str]:
119+
"""Return (model_name, provider) by looking up the model in the gateway registry.
120+
121+
create_agent() does this same lookup (agents/__init__.py:36-65) so that the
122+
gateway receives the resolved model_name, not a raw slug/UUID. The token-id
123+
extensions (return_token_ids) are gated on the model routing as a known
124+
trainable model; passing a raw id or slug can miss that path silently.
125+
"""
126+
from hud.utils.gateway import list_gateway_models
127+
128+
try:
129+
for gm in list_gateway_models():
130+
if model in (gm.id, gm.name, gm.model_name):
131+
return gm.model_name or model, gm.provider.name or "openai"
132+
except Exception:
133+
pass
134+
return model, "openai"
135+
136+
115137
# ── MCP server ─────────────────────────────────────────────────────────────────
116138

117139

@@ -134,12 +156,12 @@ async def _inner_move(inner_mark: str) -> int:
134156
"""
135157
from hud.utils.gateway import build_gateway_client
136158

137-
client = build_gateway_client("openai")
159+
client = build_gateway_client(_INNER_PROVIDER)
138160
available = game.available()
139161

140162
try:
141163
resp = await client.chat.completions.create(
142-
model=_INNER_MODEL,
164+
model=_INNER_MODEL_NAME,
143165
messages=[
144166
{
145167
"role": "system",
@@ -171,10 +193,16 @@ async def _inner_move(inner_mark: str) -> int:
171193
"output_logprobs": [tok.logprob for tok in content_lp] if content_lp else [],
172194
}
173195
)
174-
# The model may reason before answering, so take the LAST valid column it
175-
# names, not the first integer it mentions.
176196
text = choice.message.content or ""
177-
for tok in reversed(re.findall(r"\d+", text)):
197+
# Prefer an explicit "column N" mention; fall back to the last bare digit
198+
# in range. Using \b[0-6]\b avoids mis-picking row indices or counts like
199+
# "4 in a row" that appear after the stated column.
200+
explicit = re.search(r"\bcol(?:umn)?\s*([0-6])\b", text, re.IGNORECASE)
201+
if explicit:
202+
col = int(explicit.group(1))
203+
if col in available:
204+
return col
205+
for tok in reversed(re.findall(r"\b([0-6])\b", text)):
178206
col = int(tok)
179207
if col in available:
180208
return col
@@ -259,8 +287,9 @@ async def _down() -> None:
259287
@env.template()
260288
async def play_self(model: str = _INNER_MODEL, seed: int = 0) -> None:
261289
"""Self-play game. seed % 2 decides who drops first: even → outer is X, odd → outer is O."""
262-
global _INNER_MODEL, _OUTER_MARK, _inner_samples
290+
global _INNER_MODEL, _INNER_MODEL_NAME, _INNER_PROVIDER, _OUTER_MARK, _inner_samples
263291
_INNER_MODEL = model
292+
_INNER_MODEL_NAME, _INNER_PROVIDER = _resolve_inner_model(model)
264293
_OUTER_MARK = "X" if seed % 2 == 0 else "O"
265294
inner_mark = "O" if _OUTER_MARK == "X" else "X"
266295

cookbooks/connect4-selfplay/train.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -97,15 +97,22 @@ async def main(
9797
val_opponent: str,
9898
val_games: int,
9999
) -> None:
100+
# Keep reasoning short so the agent doesn't burn its step budget on analysis.
101+
# Each turn: one sentence of reasoning, then call make_move immediately.
102+
system_prompt = (
103+
"You are playing Connect Four. Think in ONE sentence, then immediately call make_move. "
104+
"Do not write long analysis. Just pick the best column and call the tool."
105+
)
100106
# return_token_ids: gateway returns token ids + per-token logprobs for training
101107
agent = create_agent(
102108
model,
103109
max_steps=30,
110+
system_prompt=system_prompt,
104111
completion_kwargs={"extra_body": {"return_token_ids": True}},
105112
)
106113
trainer = TrainingClient(model)
107114
tasks = make_tasks(model)
108-
session = await Job.start(model, group=group)
115+
session = await Job.start("c4-selfplay", group=group)
109116

110117
val_curve: list[tuple[int, float]] = []
111118

cookbooks/connect4-selfplay/validate.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,14 @@ async def run_validation(trained: str, opponent: str, games: int) -> None:
3333
# Alternate who goes first: even seeds → trained is X (first), odd → trained is O.
3434
tasks = [play_self(model=opponent, seed=s) for s in range(games)]
3535
taskset = Taskset("c4-validate", tasks)
36-
agent = create_agent(trained, max_steps=30)
36+
agent = create_agent(
37+
trained,
38+
max_steps=30,
39+
system_prompt=(
40+
"You are playing Connect Four. Think in ONE sentence, then immediately call make_move. "
41+
"Do not write long analysis. Just pick the best column and call the tool."
42+
),
43+
)
3744

3845
print(f"Validation: {trained} (outer) vs {opponent} (inner fixed)")
3946
print(f"Games: {games} ({games // 2} as X, {games - games // 2} as O)\n")

hud/train/base.py

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -39,15 +39,38 @@ def __init__(
3939

4040
async def _resolve_model_id(self) -> str:
4141
"""Resolve ``self.model`` to the id the service keys on: a uuid is used
42-
directly, a slug is looked up once via the catalog and cached."""
42+
directly, a slug/name is looked up once via the catalog and cached.
43+
44+
Resolution order:
45+
1. UUID → used directly.
46+
2. Slug (model_name) → GET /v2/models/resolve (fast path).
47+
3. Display name or unknown string → fall back to list_gateway_models(),
48+
which matches id | name | model_name — same logic as create_agent().
49+
"""
4350
if self._model_id is not None:
4451
return self._model_id
4552
try:
4653
self._model_id = str(UUID(self.model))
4754
except ValueError:
4855
url = f"{self._api_url}/v2/models/resolve?model={quote(self.model, safe='')}"
49-
data = await make_request("GET", url, api_key=self._api_key)
50-
self._model_id = str(data["id"])
56+
try:
57+
data = await make_request("GET", url, api_key=self._api_key)
58+
self._model_id = str(data["id"])
59+
except Exception as exc:
60+
# /v2/models/resolve only matches model_name (slug), not the
61+
# display name. Fall back to the full model list, which matches
62+
# id | name | model_name — consistent with create_agent().
63+
from hud.utils.gateway import list_gateway_models
64+
65+
for gm in list_gateway_models():
66+
if self.model in (gm.id, gm.name, gm.model_name):
67+
self._model_id = gm.id
68+
break
69+
else:
70+
raise ValueError(
71+
f"Model {self.model!r} not found. "
72+
"Run `hud models` to list available models."
73+
) from exc
5174
return self._model_id
5275

5376
async def _train_url(self, suffix: str) -> str:

0 commit comments

Comments
 (0)