Skip to content

fix(release): guard against dated Anthropic model IDs rotting out of a published wheel (#1112) - #1125

Merged
frankbria merged 4 commits into
mainfrom
fix/1112-release-model-guard
Aug 10, 2026
Merged

fix(release): guard against dated Anthropic model IDs rotting out of a published wheel (#1112)#1125
frankbria merged 4 commits into
mainfrom
fix/1112-release-model-guard

Conversation

@frankbria

Copy link
Copy Markdown
Owner

Closes #1112.

What was actually broken

codeframe-ai==0.9.1 on PyPI pins five retired Anthropic model IDs, so every
LLM-backed command 404s on a fresh install — with a valid API key. The IDs were
already corrected on main and never released. The broken thing is the
artifact, not the code
, so the fix is a release plus a guard that stops the
same rot recurring.

Changes

scripts/check_model_defaults.py — the release guard. Three rules:

  1. Every DEFAULT_*_MODEL is a date-less alias. Anthropic repoints aliases
    (claude-haiku-4-5); it retires dated IDs (claude-3-5-haiku-20241022).
    Pinning a date is the bug.
  2. No live call site hardcodes a dated ID.
  3. When a key is available, each default must actually resolve via
    models.retrieve() — this catches an alias being retired too.

.github/workflows/release.yml — runs the guard before uv build, with
MODEL_GUARD_REQUIRE_LIVE=1. That flag matters: without it, an unconfigured
ANTHROPIC_API_KEY secret would silently skip the strongest check, which is the
same failure mode one level up. With it, a missing or broken secret fails the
release.

Two live call sites the guard caught, neither of which I went looking for:

  • codeframe/cli/auth_commands.py:163 validated API keys against
    claude-3-haiku-20240307. That model is already retired — so cf auth
    was reporting valid keys as invalid. This was a live bug, not just a latent one.
  • codeframe/ui/routers/settings_v2.py:376 verified Anthropic keys against
    claude-haiku-4-5-20251001 — still resolvable today, same trajectory.

Both now use DEFAULT_GENERATION_MODEL.

pyproject.toml — 0.9.1 → 0.9.2.

Pricing lookup tables (metrics_tracker.py, streaming_chat.py) are explicitly
excluded: they key on the dated names the API reports back, which is the
opposite problem.

Evidence

Path Result
Guard on this branch, offline exit=0, "release guard passed"
Guard with a default regressed to claude-3-5-haiku-20241022 flags it
Guard with auth_commands.py reverted to main exit=1, names file:line
MODEL_GUARD_REQUIRE_LIVE=1 with no key exit=1, names the missing secret
Live models.retrieve against the real API 5/5 defaults resolve; claude-3-5-haiku-20241022 and claude-3-haiku-20240307 both NotFoundError — the guard has teeth on exactly the IDs that broke 0.9.1

6 new tests. Third-party review (codex review --base main): no blocking issues.

Acceptance criteria — what this PR does and does not close

  • A release check fails the build if any DEFAULT_*_MODEL is not
    resolvable.
    Done, both offline and live.
  • Cut and publish 0.9.2. This PR makes it cuttable. The release fires on
    a v* tag, and I deliberately did not push one — publishing to PyPI is
    irreversible. Run git tag v0.9.2 && git push origin v0.9.2 when ready.
  • Cleanroom run.sh (no --source) reaches cf prd generate. Only
    verifiable against the published package, so it is gated on the tag above.
  • Yank or mark 0.9.1. Needs the PyPI project owner. Recommended: yank
    it — it cannot work for anyone.

The three unchecked items are all operator actions on PyPI, not code.

Known limitations

  • The live check needs ANTHROPIC_API_KEY configured as a repo secret for the
    pypi release job. If it is not, the release now fails loudly rather than
    publishing unverified — that is the intended behaviour, but it does mean the
    secret must exist before the next tag.
  • The guard's call-site scan is a regex over core/cli/ui/adapters, with a
    two-file exclusion list for pricing tables. A new pricing table elsewhere
    would trip it; the fix is to add the file to EXCLUDED_FILES.

…a published wheel (#1112)

0.9.1 on PyPI pins five now-retired model IDs, so every LLM-backed command
404s on a fresh install with a perfectly good API key. The IDs were fixed on
main but never released — the broken thing is the artifact, not the code.

What changes here is the part that stops it recurring, plus the version bump
that makes the release cuttable:

- scripts/check_model_defaults.py: every DEFAULT_*_MODEL must be a date-less
  alias (aliases get repointed; dated IDs get retired), no live call site may
  hardcode a dated ID, and — when a key is available — each default must
  actually resolve via models.retrieve().
- release.yml runs it before the build with MODEL_GUARD_REQUIRE_LIVE=1, so an
  unconfigured ANTHROPIC_API_KEY secret fails the release instead of silently
  disabling the strongest check.
- Two live call sites were still pinning dated IDs and the guard caught them:
  cf auth's key validation used claude-3-haiku-20240307, which is *already*
  retired — a valid key was being reported as invalid. The settings API's
  Anthropic key verification used claude-haiku-4-5-20251001, same trajectory.
- Version 0.9.1 -> 0.9.2.

The tag push (and any yank of 0.9.1) is deliberately left to the operator:
publishing to PyPI is irreversible.
@github-actions

github-actions Bot commented Aug 10, 2026

Copy link
Copy Markdown
Contributor

Claude finished @frankbria's task in 4m 35s —— View job


✅ GLM precision review: no defects found

Scoped to concrete defects only (logic errors, crashes/data loss, security, contract breaks). Style/coverage handled by the general review. Per the no-defects outcome, no inline comments were posted.

Verified against the checked-out tree (not the diff alone):

  • All 5 DEFAULT_*_MODEL are dateless aliasescodeframe/adapters/llm/base.py:47-51 (claude-haiku-4-5, claude-sonnet-4-5). The regex claude-[a-z0-9.-]*?-(20\d{6})\b correctly rejects these and matches every dated form (claude-3-5-haiku-20241022, claude-haiku-4-5-20251001, claude-3-haiku-20240307).
  • EXCLUDED_FILES paths actually exist and are the right onescodeframe/core/adapters/streaming_chat.py:585 carries the dated pricing key claude-3-5-haiku-20241022, and codeframe/lib/metrics_tracker.py carries dated IDs in normalization comments. A repo-wide grep for the dated pattern under codeframe/ returns matches only in those two excluded files, so the offline guard passes consistently (no latent false-positive waiting to fire).
  • Live check is sound against the pinned SDKuv.lock pins anthropic==0.70.0, which exposes both client.models.retrieve and anthropic.NotFoundError. The except NotFoundError → violation / except Exception → fail-loud under MODEL_GUARD_REQUIRE_LIVE chain is correct, and a missing secret yields an empty (falsy) env var that the guard treats as a hard failure — the intended loud behavior.
  • Both call-site fixes import safelysettings_v2.py adds a top-level from codeframe.adapters.llm.base import ...; base.py imports only stdlib, so no circular import. auth_commands.py:160 imports inside the existing try, which already has a broad except fallback. Both functions still return the documented (bool, str) tuples.
  • Version bump is consistentpyproject.toml and uv.lock both move 0.9.1 → 0.9.2.
  • scripts/__init__.py is absent but not a defectpython -m scripts.check_model_defaults resolves via PEP 420 namespace packages from the repo root, which is the cwd in both the release step and test_script_exits_zero_offline; that test asserts the -m invocation returns 0.

Not flagged (already documented by the author as Known limitations, not shipped defects): the call-site scan is a regex with a manual two-file exclusion list, so a new pricing table elsewhere would trip it — but that fails loud (release blocked), the safe direction, and the fix is a one-line EXCLUDED_FILES entry.

One non-blocking observation outside this PR's defect scope: validate_anthropic_credential now probes with claude-haiku-4-5 instead of the retired claude-3-haiku-20240307, so it is correct rather than cheaper — no action needed.

Clean.

@claude

claude Bot commented Aug 10, 2026

Copy link
Copy Markdown

Review

Solid, well-scoped fix. The root-cause framing (broken artifact, not broken code) is right, and the guard is a reasonable structural fix rather than a one-off patch. A few notes:

Correctness

scripts/check_model_defaults.py:104-119 — a mid-loop transient error silently discards already-found violations.

In check_defaults_resolve(), violations accumulates across the for name, value in sorted(_defaults().items()) loop, but the first non-NotFoundError exception hit anywhere in that loop causes an early return — either [f"live model check could not complete — {detail}"] (with MODEL_GUARD_REQUIRE_LIVE) or [] (without it). Either way, any NotFoundError violations already appended for earlier defaults in the same run are thrown away instead of being combined with the new message/returned list.

Concretely: if DEFAULT_CORRECTION_MODEL fails with NotFoundError (real violation, appended) and then DEFAULT_EXECUTION_MODEL hits a transient network blip (generic exception), the function returns either a single generic "could not complete" message or an empty list — the real, already-detected CORRECTION violation is lost. In REQUIRE_LIVE mode the build still fails (good), but the diagnostic naming which default is actually broken is gone, replaced by a network-blip message that could send whoever's investigating down the wrong path. In non-REQUIRE_LIVE mode (local dev with a key configured but no MODEL_GUARD_REQUIRE_LIVE) it's worse: a real, already-detected violation is silently swallowed and the function returns [], i.e. "passed."

This is exactly the class of bug the PR is trying to eliminate (a check that can silently pass despite real breakage), so it seems worth closing: accumulate the "could not complete" message alongside violations rather than replacing it, e.g. return violations + [detail-message] instead of return [detail-message] / return [].

Not blocking — check_defaults_are_aliases() (offline regex) independently catches dated-ID mistakes in defaults, so 0.9.1-style regressions are still caught even if this path misfires. The gap is narrower: it only affects the "Anthropic retired an undated alias" case the live check exists specifically to catch, combined with a same-run transient failure on a different default.

Minor / non-blocking

  • EXCLUDED_FILES in scripts/check_model_defaults.py:44-48 lists codeframe/lib/metrics_tracker.py, but LIVE_CALL_SITES doesn't include codeframe/lib at all — that exclusion entry is currently dead (never reached by the scan). Harmless, but slightly misleading about what's actually being guarded against; either drop the entry or add codeframe/lib to the scanned areas if pricing tables could ever live somewhere the scan does reach.
  • Verified independently: the four LIVE_CALL_SITES directories only contain dated model IDs in the two already-excluded pricing-table files (metrics_tracker.py, streaming_chat.py) — the guard's current scope is sufficient for the codebase as it stands today.

What's good

  • The two live call-site fixes (auth_commands.py, settings_v2.py) are real bugs independent of the release issue — claude-3-haiku-20240307 is already retired, so cf auth validate was reporting valid keys as invalid. Good catch, and both now correctly use DEFAULT_GENERATION_MODEL via lazy/module-level imports that respect the core/CLI/UI layering (no FastAPI creeping into core).
  • MODEL_GUARD_REQUIRE_LIVE=1 failing loudly on a missing secret rather than silently downgrading to the offline-only check is the right call — matches the PR's own stated principle of not letting failures go quiet.
  • Test coverage for the guard itself is good (alias detection, call-site scan, require-live-without-key, quiet-skip-without-require-live, offline exit code) — the one gap is the multi-default partial-failure scenario above, which isn't exercised.
  • Docs are honest about scope: the PR explicitly separates "code is fixed" from "0.9.2 is tagged/published/yanked," which are correctly left as operator actions given PyPI publishes are irreversible.

…1112)

tests/test_root_docs_950.py pins README and CHANGELOG to the pyproject version,
which the bump to 0.9.2 broke. The existing Unreleased body is what 0.9.2 ships,
so it becomes the 0.9.2 section rather than a new one above it.
@github-actions

github-actions Bot commented Aug 10, 2026

Copy link
Copy Markdown
Contributor

Claude finished @frankbria's task in 5m 10s —— View job


✅ GLM precision review: no new defects found

Scoped to concrete defects only (logic errors, crashes/data loss, security, contract breaks, resource leaks). Style/coverage handled by the general review. Verified against the checked-out tree, not the diff alone.

  • Pull full diff and PR context
  • Read existing comments — avoid repeating findings
  • Verify each guard rule + call-site fix against the checked-out tree
  • Post inline comments for confirmed defects
  • Summary

What I verified independently:

Area Check Result
base.py:47-51 All 5 DEFAULT_*_MODEL are dateless aliases ✅ offline rule passes
DATED_MODEL_RE Matches every dated form (claude-3-5-haiku-20241022, claude-haiku-4-5-20251001, claude-3-haiku-20240307), rejects aliases ✅ leftmost-match always yields the full model id in group(0)
Repo-wide grep claude-…-20YYYYMMDD under codeframe/ Only hits are core/adapters/streaming_chat.py:585 (active exclusion) + lib/metrics_tracker.py (comments, unscanned area) ✅ no latent false-positive; offline guard has teeth today
uv.lock anthropic==0.70.0 exposes client.models.retrieve and anthropic.NotFoundError ✅ live check is sound; except NotFoundError → violation, generic except → fail-loud under MODEL_GUARD_REQUIRE_LIVE
settings_v2.py:55 Module-level from codeframe.adapters.llm.base import DEFAULT_GENERATION_MODEL ✅ no NameError, no circular import (base.py is stdlib-only)
auth_commands.py:160 Import is inside the existing try whose broad except already covers it ✅ still returns (bool, str)
Version bump pyproject.toml + uv.lock both 0.9.1 → 0.9.2 ✅ consistent

On the two issues already on record (general review, not repeated here per the no-duplicate rule):

  1. check_defaults_resolve() — a mid-loop generic exception returns and discards violations already appended for earlier defaults. In non-REQUIRE_LIVE mode that can return [] ("passed") despite a real retired-alias NotFoundError. This is in defect scope (silent pass on a real breakage), but it's already posted with the exact fix (return violations + [detail]), so I'm not posting it again. The narrower real-world gap: only the "alias retired" case (which the offline regex can't see) combined with a same-run transient error on another default. MODEL_GUARD_REQUIRE_LIVE=1 in the release job keeps the build failing — the loss is diagnostic, not the gate.
  2. EXCLUDED_FILES lists codeframe/lib/metrics_tracker.py, but LIVE_CALL_SITES doesn't include codeframe/lib — dead entry, zero behavioral effect. Cleanliness, not a defect.

Both are non-blocking; no inline comments were posted.

@claude

claude Bot commented Aug 10, 2026

Copy link
Copy Markdown

Review (follow-up)

I reviewed this branch earlier (see my comment above) after the first commit. The second commit only touches CHANGELOG.md/README.md (folding the [Unreleased] body into a [0.9.2] section and bumping the beta banner, to satisfy test_root_docs_950.py's version pin) — scripts/check_model_defaults.py and the two call-site fixes are unchanged since that review. Confirmed by re-reading the current tree: the mid-loop transient-error issue I flagged in check_defaults_resolve() (lines 108-119 — a generic exception on one default's models.retrieve() call discards already-collected NotFoundError violations from earlier defaults in the same loop, either replacing them with a single "could not complete" message under MODEL_GUARD_REQUIRE_LIVE or silently returning [] without it) is still present. Restating my earlier assessment: not blocking, since check_defaults_are_aliases() independently catches the 0.9.1-style dated-ID regression via the offline regex regardless of what the live check does — the gap only narrows to "Anthropic retires an undated alias, and a different default hits a transient error in the same run," which is a real but small hole worth closing opportunistically (return violations + [detail] instead of replacing).

The CHANGELOG/README changes in the second commit are accurate and consistent with the PR description — the [Unreleased][0.9.2] restructure correctly preserves the existing body rather than duplicating it, and the new ### Added/### Fixed entries for #1112 match what actually shipped (release guard, cf auth false-negative fix, settings API alias fix).

No new issues found. This remains a solid, well-scoped release-hygiene fix with honest scope boundaries (code fix vs. the still-open operator actions: tag, cleanroom verification, yanking 0.9.1).

#1112)

Both from PR review.

The live loop returned early on any non-NotFoundError, discarding violations it
had already collected. Without MODEL_GUARD_REQUIRE_LIVE that returned [] — i.e.
"passed" — despite a model the API had just said does not exist, which is the
exact class of silent pass this guard exists to prevent. With REQUIRE_LIVE the
build still failed, but the message named a network blip instead of the broken
model. Now the accumulated violations survive either way.

Also: EXCLUDED_FILES listed codeframe/lib/metrics_tracker.py while LIVE_CALL_SITES
never scanned codeframe/lib, so that entry was dead. Scanning lib now, which
widens coverage and makes the exclusion mean what it says.
@frankbria

Copy link
Copy Markdown
Owner Author

Both review findings were real and are fixed in 5654be5.

The mid-loop early return — confirmed exactly as described. Without MODEL_GUARD_REQUIRE_LIVE the function returned [] ("passed") despite a model the API had just 404'd, which is the same silent-pass shape this guard exists to prevent. It now returns violations + [detail] under REQUIRE_LIVE and violations without it.

Two tests cover it, and I verified they are not tautological — reverting the fix fails both:

FAILED ...TestAPartialLiveFailureKeepsWhatItFound::test_the_confirmed_404_survives_under_require_live
FAILED ...TestAPartialLiveFailureKeepsWhatItFound::test_the_confirmed_404_is_not_silently_dropped_without_require_live

The dead EXCLUDED_FILES entry — also correct. Rather than dropping the entry I added codeframe/lib to LIVE_CALL_SITES, which widens the scan and makes the exclusion mean what it says. Guard still passes.

Everything else in the review matches my own reading, including that the operator actions (tag, cleanroom verification against the published wheel, yanking 0.9.1) stay open by design.

@github-actions

github-actions Bot commented Aug 10, 2026

Copy link
Copy Markdown
Contributor

Claude finished @frankbria's task in 4m 28s —— View job


✅ GLM precision review: no defects found

Scoped to concrete defects only — logic errors, crashes/data loss, security, contract breaks, resource leaks. Style/architecture/coverage handled by the general review. Verified against the checked-out tree at 5654be5 (base 9420cee), not the diff alone.

  • Pull full diff + PR context
  • Read prior reviews — both prior findings fixed in 5654be5; not re-posted
  • Verify the two fixes landed correctly
  • Verify each guard rule + call-site fix against the checked-out tree
  • Hunt for new defects introduced by the fix commit
  • Summary — no defects; no inline comments posted

Both prior findings are correctly fixed:

  1. Mid-loop early return (scripts/check_model_defaults.py:114-124) — now return violations + [could-not-complete] under MODEL_GUARD_REQUIRE_LIVE and return violations without it, so a confirmed 404 survives a later transient error instead of being replaced/swallowed. Confirmed the two new tests are non-tautological: the mock counter spans one client across the sorted-defaults loop (first default → NotFoundError, second → ConnectionError), so reverting the fix would drop the "does not resolve" message both tests assert on.
  2. Dead EXCLUDED_FILES entry — fixed by adding codeframe/lib to LIVE_CALL_SITES rather than dropping the entry. I grepped codeframe/ for the dated pattern: the only lib/ hits are in metrics_tracker.py (lines 121/128/133/190/193 — all comments/docstrings), which is in EXCLUDED_FILES, so widening the scan introduces no false positive. The guard still passes.

New-defect hunt on the fix commit — nothing found:

Area Check Result
base.py:7-12 Stdlib-only imports (asyncio, os, abc, dataclasses, enum, typing) ✅ no circular import from settings_v2.py's new module-level import
base.py:49 DEFAULT_GENERATION_MODEL = "claude-haiku-4-5" ✅ dateless alias; both call-site fixes validate against a non-retiring alias
auth_commands.py:160 from codeframe.adapters.llm.base import DEFAULT_GENERATION_MODEL inside existing try ✅ covered by except ImportError / broad except; still returns (bool, str)
settings_v2.py:365-387 _verify_anthropic_sync runs via run_in_threadpool; pure model-string swap ✅ exception handling unchanged and sound
check_defaults_resolve except ordering except NotFoundError precedes except Exception ✅ specific 404 branch matched first; transient → fail-loud / preserve-violations
Repo-wide dated-ID grep under codeframe/ Only metrics_tracker.py (comments) + streaming_chat.py:585 (pricing) ✅ both excluded; no latent false-positive waiting to fire

One residual non-blocking observation (not a defect — already documented and not re-flagged): the transient-error branch still returns early, so defaults after the failing one aren't checked in that same run. That is not the silent-pass class of bug the guard exists to prevent — the build still fails (non-empty violations, or the could-not-complete message under REQUIRE_LIVE), a network-recovered retry re-checks all defaults, and check_defaults_are_aliases() independently catches dated-ID-in-default regressions offline. Acknowledged as non-blocking by the prior reviewer; no action needed.

Clean. The operator actions (tag v0.9.2, cleanroom verify against the published wheel, yank 0.9.1) remain correctly out of scope for this code PR.

@claude

claude Bot commented Aug 10, 2026

Copy link
Copy Markdown

Review (follow-up — verifying the fix commit)

I reviewed the first two commits earlier (see my comments above). This adds the third commit (5654be5), which addresses both issues I raised. I re-verified against the current tree rather than trusting the description:

Mid-loop early return — confirmed fixed. check_defaults_resolve() (scripts/check_model_defaults.py:104-122) now does return violations + [f"live model check could not complete — {detail}"] under MODEL_GUARD_REQUIRE_LIVE, and return violations (not []) otherwise — a NotFoundError collected earlier in the loop survives a later transient error instead of being discarded. The two new tests in TestAPartialLiveFailureKeepsWhatItFound exercise exactly this (first call 404s, second call raises a generic ConnectionError), and the author's own repro (reverting the fix fails both tests) checks out against the logic.

Dead EXCLUDED_FILES entry — confirmed fixed. LIVE_CALL_SITES now includes codeframe/lib, so the codeframe/lib/metrics_tracker.py exclusion is reachable. I grepped codeframe/ for the dated-ID pattern independently: the only two hits are core/adapters/streaming_chat.py:585 and lib/metrics_tracker.py, both in EXCLUDED_FILES — no latent false positive, no live call site missed.

Also re-checked, unchanged since my first pass:

  • All 5 DEFAULT_*_MODEL in codeframe/adapters/llm/base.py are date-less aliases.
  • auth_commands.py and settings_v2.py both now import DEFAULT_GENERATION_MODEL and use it in place of the retired/dated literals, with no circular-import risk (base.py is stdlib-only).
  • pyproject.toml/uv.lock version bump is consistent; the 0.9.1 strings remaining elsewhere (test_version_flag.py, test_root_docs_950.py, quickstart docs) are comments/docstrings/historical references, not assertions that would break.
  • CHANGELOG restructure ([Unreleased][0.9.2]) preserves the existing body correctly and the new ### Added/### Fixed entries match what actually shipped.

No new issues found. Both previously-flagged points are resolved with real test coverage, not just asserted fixed. This is ready from a code-review standpoint — the remaining checklist items (tag push, cleanroom verification, yanking 0.9.1) are explicitly scoped as operator actions outside this PR, which I agree with.

@github-actions

github-actions Bot commented Aug 10, 2026

Copy link
Copy Markdown
Contributor

Claude finished @frankbria's task in 2m 10s —— View job


✅ GLM precision review: no defects found (synchronize — merge from main)

Scoped to concrete defects only — logic errors, crashes/data loss, security, contract breaks, resource leaks. Style/architecture/coverage handled by the general review. Verified against the checked-out tree at 50e387d (merge of 5654be5 + d5f30e2), not the diff alone.

The new commit is a merge that brings origin/main into the branch. It touches only lock files (uv.lock, web-ui/package-lock.json, web-ui/package.json) — no conflict resolution in any source file this PR changes. The risk a merge carries here is the one the guard exists for: main's code (#1103 schema refactor, #1094#1101 fixes, dep bumps) sneaking a dated model ID into a scanned area and silently breaking the offline guard. Verified it did not.

Area Check Result
Merge scope git diff 5654be5 50e387d --stat ✅ only uv.lock + web-ui locks; zero source conflicts
Dated-ID scan (the merge-risk) Repo-wide grep claude-…-20YYYYMMDD under scanned areas (adapters/llm, cli, core, lib, ui) ✅ only hits are metrics_tracker.py (comments) + streaming_chat.py:585 (pricing) — both in EXCLUDED_FILES; no merged-in code added a dated ID
Version bump pyproject.toml:7 + uv.lock:579 (codeframe-ai) ✅ both 0.9.2 post-merge
SDK pin uv.lock:181 anthropic==0.70.0 still exposes models.retrieve + NotFoundError
check_defaults_resolve (the 5654be5 fix) scripts/check_model_defaults.py:122,124 return violations + […] under REQUIRE_LIVE / return violations otherwise — confirmed-404 survives a later transient error; both prior findings still resolved
Defaults base.py:47-51 ✅ all 5 are dateless aliases
Call-site fix auth_commands.py:160,166 (import inside try, returns (bool, str)) ✅ intact; covered by except ImportError/broad except
Call-site fix settings_v2.py:55,378 (module-level import) ✅ intact; no circular import (base.py is stdlib-only)
Release wiring release.yml:52-56 ✅ guard runs with MODEL_GUARD_REQUIRE_LIVE=1 before uv build

On prior findings: both were fixed in 5654be5 and remain fixed through this merge — the merge did not regress them (it didn't touch the guard or the call sites). Not re-posted.

No new defects introduced by the merge. Clean. The operator actions (tag v0.9.2, cleanroom verify against the published wheel, yank 0.9.1) remain correctly out of scope for this code PR.

@claude

claude Bot commented Aug 10, 2026

Copy link
Copy Markdown

Review (final pass — post-merge-commit)

I reviewed this PR across the first three commits already (see comments above); this pass covers the fourth commit (50e387d, merge of origin/main). Diffed the current PR head against my last review point and confirmed:

  • scripts/check_model_defaults.py, codeframe/cli/auth_commands.py, and codeframe/ui/routers/settings_v2.py are byte-identical to what I verified fixed in 5654be5 — both previously-flagged issues (the mid-loop early-return that discarded already-collected violations, and the dead EXCLUDED_FILES entry) remain fixed exactly as described.
  • No merge-conflict artifacts in the diff (checked for <<<<<<</=======/>>>>>>>) and no new file changes beyond what was already reviewed — the merge commit brought in unrelated main history without touching this PR's files.
  • Re-verified the regex (DATED_MODEL_RE) against both alias and dated forms once more directly: it correctly matches claude-3-5-haiku-20241022 / claude-haiku-4-5-20251001 and does not match claude-haiku-4-5 / claude-sonnet-4-5, so the offline rule has no false positive/negative on the current defaults.

Nothing new to flag. This remains a well-scoped, correctly-tested fix:

  • Root cause correctly identified as "broken artifact, not broken code" — the guard prevents recurrence rather than just patching the immediate symptom.
  • Both live call-site bugs it caught (auth_commands.py's already-retired validation model, settings_v2.py's same-trajectory one) are real, independent fixes worth having regardless of the release guard.
  • Test coverage is solid, including the non-tautological partial-live-failure tests added in the fix commit.
  • The scope boundary is honest: tagging v0.9.2, cleanroom-verifying the published wheel, and yanking 0.9.1 are correctly left as operator actions outside this PR (irreversible PyPI operations), not silently assumed done.

No blocking issues. Approving from a code-review standpoint.

@frankbria
frankbria merged commit 9966769 into main Aug 10, 2026
13 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[P0.31] Published codeframe-ai 0.9.1 pins retired Anthropic model IDs — every LLM command 404s on a fresh install

1 participant