Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions ringer.py
Original file line number Diff line number Diff line change
Expand Up @@ -9430,6 +9430,10 @@ def parse_env_file(path: Path) -> dict[str, str]:


def parse_token_count(text: str, token_regex: str | None = DEFAULT_TOKEN_REGEX) -> int | None:
# Engines colorize stdout when FORCE_COLOR is set (Claude Code, most CI),
# leaving escape bytes between the label and the number that no practical
# regex bridges — strip them once here so every regex sees plain text.
text = ANSI_RE.sub("", text)
if token_regex:
matches = list(re.finditer(token_regex, text, flags=re.IGNORECASE))
for match in reversed(matches):
Expand All @@ -9454,6 +9458,9 @@ def parse_token_count(text: str, token_regex: str | None = DEFAULT_TOKEN_REGEX)
def parse_reported_model(text: str, model_report_regex: str | None) -> str | None:
if not model_report_regex:
return None
# Same reason as parse_token_count: a colorized "model:" header breaks
# the ^model: anchor unless the ANSI escapes are stripped first.
text = ANSI_RE.sub("", text)
match = re.search(model_report_regex, text, flags=re.IGNORECASE)
if match is None or match.lastindex is None:
return None
Expand Down
8 changes: 8 additions & 0 deletions tests/test_identity_evidence.py
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,14 @@ def test_codex_report_regex_captures_synthetic_header(self) -> None:
output = "OpenAI Codex v0.144.0\n--------\nmodel: gpt-5.6-sol\nprovider: openai\n"
self.assertEqual("gpt-5.6-sol", parse_reported_model(output, engine.model_report_regex))

def test_codex_report_regex_captures_ansi_colorized_header(self) -> None:
# Real bytes from codex exec run with FORCE_COLOR set (as Claude Code
# and most CI systems do): the header is bold-wrapped, so the ^model:
# anchor never matches without stripping ANSI first.
engine = load_engines(None)["codex"]
output = "OpenAI Codex v0.146.0\n--------\n\x1b[1mmodel:\x1b[0m gpt-5.6-sol\nprovider: openai\n"
self.assertEqual("gpt-5.6-sol", parse_reported_model(output, engine.model_report_regex))

def test_reported_model_wins_and_resolved_model_is_fallback(self) -> None:
rows = self.log_attempts(
WorkerResult(0, False, 12, reported_model="gpt-5.7"),
Expand Down
13 changes: 13 additions & 0 deletions tests/test_ringer.py
Original file line number Diff line number Diff line change
Expand Up @@ -595,6 +595,19 @@ def test_token_count_parser_accepts_colon_and_newline_formats(self) -> None:
self.assertEqual(ringer.parse_token_count("tokens used: 1,234", r"tokens\s+used\s*:?\s*([0-9][0-9,]*)"), 1234)
self.assertEqual(ringer.parse_token_count("tokens used\n5,678", r"tokens\s+used\s*:?\s*([0-9][0-9,]*)"), 5678)

def test_token_count_parser_sees_through_ansi_colorized_output(self) -> None:
# Real bytes from codex exec run with FORCE_COLOR set (as Claude Code
# and most CI systems do): the label is dim-wrapped and the number is
# on the following line, so the escape sequences sit between "used"
# and the digits and an ANSI-blind \s* cannot bridge them.
colorized = "\x1b[2mtokens used\x1b[0m\n17,270\n"
self.assertEqual(ringer.parse_token_count(colorized, ringer.DEFAULT_TOKEN_REGEX), 17270)
# User-supplied custom regexes get the same stripping — the fix is in
# the parser, not the shipped pattern.
self.assertEqual(ringer.parse_token_count(colorized, r"tokens\s+used\s*:?\s*([0-9][0-9,]*)"), 17270)
# Uncolorized output keeps parsing exactly as before.
self.assertEqual(ringer.parse_token_count("tokens used: 1,234", ringer.DEFAULT_TOKEN_REGEX), 1234)


if __name__ == "__main__":
unittest.main()