From 17a9a251604c6326f7ec4dca8169650924aaeb1b Mon Sep 17 00:00:00 2001 From: Adam Moussa Date: Fri, 10 Jul 2026 13:09:19 -0400 Subject: [PATCH] refactor(models): convert re-added Fable 5 to Bedrock model IDs --- agent/dashboard/options.py | 10 ++++++---- docs/upstream-sync/triage.jsonl | 2 +- docs/upstream-sync/triage.md | 2 +- tests/test_agent_subagent_models.py | 19 ++++++++++++------ tests/test_anthropic_model.py | 26 +++++++++++++------------ tests/test_dashboard_thread_api.py | 4 ++-- tests/test_model_fallback_resolution.py | 22 +++++++++++++-------- tests/test_team_settings_fable.py | 6 +++--- 8 files changed, 54 insertions(+), 37 deletions(-) diff --git a/agent/dashboard/options.py b/agent/dashboard/options.py index 8ba13838..736f61e3 100644 --- a/agent/dashboard/options.py +++ b/agent/dashboard/options.py @@ -29,7 +29,7 @@ SUPPORTED_MODELS: list[ModelOption] = [ "supports_images": True, }, { - "id": "anthropic:claude-fable-5", + "id": "bedrock_converse:us.anthropic.claude-fable-5", "label": "Fable 5", "efforts": ["low", "medium", "high", "xhigh", "max"], "default_effort": "high", @@ -81,17 +81,19 @@ SUPPORTED_MODELS: list[ModelOption] = [ SUPPORTED_MODEL_IDS: frozenset[str] = frozenset(m["id"] for m in SUPPORTED_MODELS) +_FABLE_ID_PREFIX = "bedrock_converse:us.anthropic.claude-fable" + FABLE_MODEL_IDS: frozenset[str] = frozenset( - m["id"] for m in SUPPORTED_MODELS if m["id"].startswith("anthropic:claude-fable") + m["id"] for m in SUPPORTED_MODELS if m["id"].startswith(_FABLE_ID_PREFIX) ) def fable_disabled_fallback(effort: object = None) -> tuple[str, str]: - """Newest supported non-Fable Anthropic model (keeps the Claude family), + """Newest supported non-Fable Claude model (keeps the Claude family), else the global default. Substitutes a Fable selection when Fable is disabled workspace-wide, preserving ``effort`` when the fallback supports it.""" for m in SUPPORTED_MODELS: - if m["id"].startswith("anthropic:") and m["id"] not in FABLE_MODEL_IDS: + if m["id"].startswith("bedrock_converse:us.anthropic.") and m["id"] not in FABLE_MODEL_IDS: return m["id"], _fallback_effort_for(m, effort) or m["default_effort"] return default_model_pair() diff --git a/docs/upstream-sync/triage.jsonl b/docs/upstream-sync/triage.jsonl index e35241d2..71b30459 100644 --- a/docs/upstream-sync/triage.jsonl +++ b/docs/upstream-sync/triage.jsonl @@ -70,7 +70,7 @@ {"sha": "216cf181", "pr": 1699, "subject": "fix: keep workflow HITL without token downscoping (#1699)", "disposition": "landed", "reason": "DIVERGES-FROM-UPSTREAM: fork deliberately does NOT adopt #1699's standing-token workflows:write broadening. Security review (#159) BLOCKed it — the standing ALWAYS-ON proxy token carrying workflows:write turns the HITL guard's git-push-parser gaps (obfuscated-expansion push, `gh api` REST contents PUT, cross-branch refspecs) into live unapproved-workflow-push exploits. Fork keeps BASE without workflows:write and restores the transient per-approval elevation (_run_with_workflow_token mints WORKFLOW_RUNTIME_PROXY_TOKEN_PERMISSIONS around the approved, guard-normalized fixed_command, then downscopes to RUNTIME then BASE): the token scope is the backstop the parser relies on, so a bypass hits GitHub 403. HITL diff-preview/approval-URL/Slack-card additions from #159 retained; token-model divergence only.", "branch": "plan-approval", "local_sha": null, "updated": "2026-07-09T20:00:00Z"} {"sha": "67abf5b0", "pr": 1659, "subject": "fix: surface attributed PR creation failures (#1659)", "disposition": "deferred", "reason": "PR-attribution-failure guard (new mw, safe imports); heavy conflict on diverged open_pull_request.py", "branch": "pr-attribution", "local_sha": null, "updated": "2026-07-08T20:14:42Z"} {"sha": "3dbc0282", "pr": 1676, "subject": "fix: preserve plan redirects after login (#1676)", "disposition": "landed", "reason": "FLAG-HUMAN: follow-on to landed #1668 refining sanitize_redirect_to (open-redirect auth surface); not a dup", "branch": "plan-approval", "local_sha": null, "updated": "2026-07-09T17:10:22Z"} -{"sha": "c75cbb1f", "pr": 1677, "subject": "feat: re-add Fable 5 with an admin toggle to disable it (#1677)", "disposition": "deferred", "reason": "FLAG-HUMAN: re-adds Fable 5 via anthropic: — contradicts dev's deliberate hide (#1483) + Bedrock migration (#62); wont-merge candidate", "branch": "fable-admin-toggle", "local_sha": null, "updated": "2026-07-08T20:14:42Z"} +{"sha": "c75cbb1f", "pr": 1677, "subject": "feat: re-add Fable 5 with an admin toggle to disable it (#1677)", "disposition": "landed", "reason": "Ported + Bedrock-converted onto dev via feat/readd-fable5-bedrock; anthropic: Fable ID mapped to bedrock_converse:us.anthropic.claude-fable-5.", "branch": "fable-admin-toggle", "local_sha": null, "updated": "2026-07-10T17:07:19Z"} {"sha": "bb104d93", "pr": 1679, "subject": "fix: submit plan comments with cmd enter (#1679)", "disposition": "landed", "reason": "applies clean but edits fork-diverged PlanReview.tsx (#130); needs UI/e2e validation — separate PR", "branch": "plan-approval", "local_sha": null, "updated": "2026-07-09T17:10:22Z"} {"sha": "304032fa", "pr": 1680, "subject": "chore: clarify question answering prompt (#1680)", "disposition": "deferred", "reason": "reword Slack info-only answer guidance; conflicts w/ fork's customized Slack prompt", "branch": "prompt-tweaks", "local_sha": null, "updated": "2026-07-08T20:14:42Z"} {"sha": "5003c953", "pr": 1683, "subject": "feat: open Linear-triggered PRs as the triggering user (#1683)", "disposition": "deferred", "reason": "FLAG-HUMAN: adds linear to resolve_github_token per-user OAuth branch (auth surface); depends on #1626 linear.py", "branch": "linear-pr-as-user", "local_sha": null, "updated": "2026-07-08T20:14:42Z"} diff --git a/docs/upstream-sync/triage.md b/docs/upstream-sync/triage.md index 143e0a28..f7b054fe 100644 --- a/docs/upstream-sync/triage.md +++ b/docs/upstream-sync/triage.md @@ -53,6 +53,7 @@ Rows key on the **upstream SHA** (stable across local cherry-picks). Deferred ro | `5f7f5fbd` | #1697 | fix: Reduce graph import and loader startup latency (#1697) | Landed | import-hygiene refactor; cross-cutting, references many deferred upstream-only modules | durable-dispatch | | `216cf181` | #1699 | fix: keep workflow HITL without token downscoping (#1699) | Landed | DIVERGES-FROM-UPSTREAM: fork deliberately does NOT adopt #1699's standing-token workflows:write broadening. Security review (#159) BLOCKed it — the standing ALWAYS-ON proxy token carrying workflows:write turns the HITL guard's git-push-parser gaps (obfuscated-expansion push, `gh api` REST contents PUT, cross-branch refspecs) into live unapproved-workflow-push exploits. Fork keeps BASE without workflows:write and restores the transient per-approval elevation (_run_with_workflow_token mints WORKFLOW_RUNTIME_PROXY_TOKEN_PERMISSIONS around the approved, guard-normalized fixed_command, then downscopes to RUNTIME then BASE): the token scope is the backstop the parser relies on, so a bypass hits GitHub 403. HITL diff-preview/approval-URL/Slack-card additions from #159 retained; token-model divergence only. | plan-approval | | `3dbc0282` | #1676 | fix: preserve plan redirects after login (#1676) | Landed | FLAG-HUMAN: follow-on to landed #1668 refining sanitize_redirect_to (open-redirect auth surface); not a dup | plan-approval | +| `c75cbb1f` | #1677 | feat: re-add Fable 5 with an admin toggle to disable it (#1677) | Landed | Ported + Bedrock-converted onto dev via feat/readd-fable5-bedrock; anthropic: Fable ID mapped to bedrock_converse:us.anthropic.claude-fable-5. | fable-admin-toggle | | `bb104d93` | #1679 | fix: submit plan comments with cmd enter (#1679) | Landed | applies clean but edits fork-diverged PlanReview.tsx (#130); needs UI/e2e validation — separate PR | plan-approval | | `7f7af715` | #1684 | feat: auto-load scoped AGENTS on reads (#1684) | Landed | ported in #129 (SubdirAgentsReadMiddleware) | subdir-agents | | `88b62322` | #1685 | feat: add platform issue reporting tool (#1685) | Landed | ported in #129 (report_platform_issue tool) | small-tools | @@ -88,7 +89,6 @@ Rows key on the **upstream SHA** (stable across local cherry-picks). Deferred ro | `48217b68` | #1489 | feat(open-swe): add E2B sandbox provider (#1489) | Deferred | additive E2B provider; separable but ships on the async sandbox.py base | sandbox-refactor | | `fbc6de85` | #1667 | chore(deps): bump fireworks-ai from 1.2.0a75 to 1.2.0a86 (#1667) | Deferred | conflicts on pick — dev diverged to fireworks-ai a85; a86 still wanted, needs manual bump + uv lock | deps | | `67abf5b0` | #1659 | fix: surface attributed PR creation failures (#1659) | Deferred | PR-attribution-failure guard (new mw, safe imports); heavy conflict on diverged open_pull_request.py | pr-attribution | -| `c75cbb1f` | #1677 | feat: re-add Fable 5 with an admin toggle to disable it (#1677) | Deferred | FLAG-HUMAN: re-adds Fable 5 via anthropic: — contradicts dev's deliberate hide (#1483) + Bedrock migration (#62); wont-merge candidate | fable-admin-toggle | | `304032fa` | #1680 | chore: clarify question answering prompt (#1680) | Deferred | reword Slack info-only answer guidance; conflicts w/ fork's customized Slack prompt | prompt-tweaks | | `5003c953` | #1683 | feat: open Linear-triggered PRs as the triggering user (#1683) | Deferred | FLAG-HUMAN: adds linear to resolve_github_token per-user OAuth branch (auth surface); depends on #1626 linear.py | linear-pr-as-user | | `feb7ac98` | #1689 | feat(web): surface thread sandbox ID with touch-friendly menu (#1689) | Deferred | applies clean but frontend<->backend contract (thread_api->queries->types->sidebar); needs UI build + e2e validation — separate PR | dashboard-ui | diff --git a/tests/test_agent_subagent_models.py b/tests/test_agent_subagent_models.py index 2989874f..c3f1ab8d 100644 --- a/tests/test_agent_subagent_models.py +++ b/tests/test_agent_subagent_models.py @@ -199,26 +199,33 @@ async def test_agent_gate_swaps_disabled_fable_profile_to_opus() -> None: patch( "agent.server.get_team_default_model_pair", new_callable=AsyncMock, - return_value=(("openai:gpt-5.5", "medium"), ("openai:gpt-5.5", "low")), + return_value=( + ("bedrock_converse:us.anthropic.claude-opus-4-8", "medium"), + ("bedrock_converse:us.anthropic.claude-opus-4-8", "low"), + ), ), # Profile selected Fable back when it was allowed; it's now disabled. patch( "agent.server.load_profile", new_callable=AsyncMock, return_value={ - "default_model": "anthropic:claude-fable-5", + "default_model": "bedrock_converse:us.anthropic.claude-fable-5", "reasoning_effort": "high", }, ), patch("agent.server.get_team_fable_enabled", new_callable=AsyncMock, return_value=False), patch("agent.server.fallback_model_id_for", return_value=None), - patch("agent.server.make_model", side_effect=[main_model, subagent_model]) as make_model, + patch( + "agent.utils.deferred_model.make_model", side_effect=[main_model, subagent_model] + ) as make_model, patch("agent.server.construct_system_prompt", return_value="prompt"), patch("agent.server.create_deep_agent", side_effect=fake_create_deep_agent), ): await get_agent(config) # Fable was scrubbed to Opus for both main and subagent; effort preserved. - assert make_model.call_args_list[0].args == ("anthropic:claude-opus-4-8",) - assert make_model.call_args_list[0].kwargs["effort"] == "high" - assert make_model.call_args_list[1].args == ("anthropic:claude-opus-4-8",) + assert make_model.call_args_list[0].args == ("bedrock_converse:us.anthropic.claude-opus-4-8",) + assert make_model.call_args_list[0].kwargs["additional_model_request_fields"][ + "output_config" + ] == {"effort": "high"} + assert make_model.call_args_list[1].args == ("bedrock_converse:us.anthropic.claude-opus-4-8",) diff --git a/tests/test_anthropic_model.py b/tests/test_anthropic_model.py index bec6b3ca..aec1777e 100644 --- a/tests/test_anthropic_model.py +++ b/tests/test_anthropic_model.py @@ -3,36 +3,37 @@ import pytest from agent.dashboard.options import SUPPORTED_MODELS, provider_fallback_pair from agent.utils.model import provider_model_kwargs -SONNET_5_ID = "anthropic:claude-sonnet-5" -FABLE_5_ID = "anthropic:claude-fable-5" +SONNET_5_ID = "bedrock_converse:us.anthropic.claude-sonnet-5" +FABLE_5_ID = "bedrock_converse:us.anthropic.claude-fable-5" def test_sonnet_5_is_supported_with_documented_efforts() -> None: sonnet = next(m for m in SUPPORTED_MODELS if m["id"] == SONNET_5_ID) - assert sonnet["label"] == "Sonnet 5" + assert sonnet["label"] == "Sonnet 5 (Bedrock)" assert sonnet["efforts"] == ["low", "medium", "high", "xhigh", "max"] assert sonnet["default_effort"] == "high" assert sonnet["supports_images"] is True @pytest.mark.parametrize("effort", ["low", "medium", "high", "xhigh", "max"]) -def test_sonnet_5_efforts_map_to_anthropic_kwargs(effort: str) -> None: +def test_sonnet_5_efforts_map_to_bedrock_kwargs(effort: str) -> None: kwargs = provider_model_kwargs(SONNET_5_ID, effort, max_tokens=16_000) assert kwargs["max_tokens"] == 16_000 - assert kwargs["effort"] == effort - assert kwargs["thinking"] == {"type": "adaptive", "display": "summarized"} + fields = kwargs["additional_model_request_fields"] + assert fields["output_config"] == {"effort": effort} + assert fields["thinking"] == {"type": "adaptive", "display": "summarized"} def test_sonnet_46_fallback_uses_sonnet_5() -> None: - assert provider_fallback_pair("anthropic:claude-sonnet-4-6", "xhigh") == ( + assert provider_fallback_pair("bedrock_converse:us.anthropic.claude-sonnet-4-6", "xhigh") == ( SONNET_5_ID, "xhigh", ) def test_opus_fallback_stays_on_opus_family() -> None: - assert provider_fallback_pair("anthropic:claude-opus-4-7", "xhigh") == ( - "anthropic:claude-opus-4-8", + assert provider_fallback_pair("bedrock_converse:us.anthropic.claude-opus-4-7", "xhigh") == ( + "bedrock_converse:us.anthropic.claude-opus-4-8", "xhigh", ) @@ -46,8 +47,9 @@ def test_fable_5_is_supported_with_documented_efforts() -> None: @pytest.mark.parametrize("effort", ["low", "medium", "high", "xhigh", "max"]) -def test_fable_5_efforts_map_to_anthropic_kwargs(effort: str) -> None: +def test_fable_5_efforts_map_to_bedrock_kwargs(effort: str) -> None: kwargs = provider_model_kwargs(FABLE_5_ID, effort, max_tokens=16_000) assert kwargs["max_tokens"] == 16_000 - assert kwargs["effort"] == effort - assert kwargs["thinking"] == {"type": "adaptive", "display": "summarized"} + fields = kwargs["additional_model_request_fields"] + assert fields["output_config"] == {"effort": effort} + assert fields["thinking"] == {"type": "adaptive", "display": "summarized"} diff --git a/tests/test_dashboard_thread_api.py b/tests/test_dashboard_thread_api.py index b8fc52e7..388a2564 100644 --- a/tests/test_dashboard_thread_api.py +++ b/tests/test_dashboard_thread_api.py @@ -12,8 +12,8 @@ from agent.dashboard.options import model_supports_images _TEXT_ONLY_MODEL = "fireworks:accounts/fireworks/models/deepseek-v4-pro" _VISION_MODEL = "bedrock_converse:us.anthropic.claude-opus-4-8" -_FABLE = "anthropic:claude-fable-5" -_PAIR = ("openai:gpt-5.5", "medium") +_FABLE = "bedrock_converse:us.anthropic.claude-fable-5" +_PAIR = ("bedrock_converse:us.anthropic.claude-opus-4-8", "medium") def _image() -> thread_api.DashboardImageBody: diff --git a/tests/test_model_fallback_resolution.py b/tests/test_model_fallback_resolution.py index 9562451c..8f0305f3 100644 --- a/tests/test_model_fallback_resolution.py +++ b/tests/test_model_fallback_resolution.py @@ -94,28 +94,34 @@ def test_global_default_is_supported() -> None: def test_gate_fable_passthrough_when_enabled() -> None: - assert gate_fable_model("anthropic:claude-fable-5", "high", fable_enabled=True) == ( - "anthropic:claude-fable-5", + assert gate_fable_model( + "bedrock_converse:us.anthropic.claude-fable-5", "high", fable_enabled=True + ) == ( + "bedrock_converse:us.anthropic.claude-fable-5", "high", ) def test_gate_fable_swaps_to_opus_when_disabled() -> None: - assert gate_fable_model("anthropic:claude-fable-5", "high", fable_enabled=False) == ( - "anthropic:claude-opus-4-8", + assert gate_fable_model( + "bedrock_converse:us.anthropic.claude-fable-5", "high", fable_enabled=False + ) == ( + "bedrock_converse:us.anthropic.claude-opus-4-8", "high", ) def test_gate_fable_leaves_non_fable_ids_alone() -> None: - assert gate_fable_model("openai:gpt-5.5", "high", fable_enabled=False) == ( - "openai:gpt-5.5", + assert gate_fable_model( + "fireworks:accounts/fireworks/models/deepseek-v4-pro", "high", fable_enabled=False + ) == ( + "fireworks:accounts/fireworks/models/deepseek-v4-pro", "high", ) -def test_fable_disabled_fallback_is_non_fable_anthropic() -> None: +def test_fable_disabled_fallback_is_non_fable_claude() -> None: model, effort = fable_disabled_fallback("high") - assert model == "anthropic:claude-opus-4-8" + assert model == "bedrock_converse:us.anthropic.claude-opus-4-8" assert model not in FABLE_MODEL_IDS assert effort == "high" diff --git a/tests/test_team_settings_fable.py b/tests/test_team_settings_fable.py index 336d2054..6e4ccefa 100644 --- a/tests/test_team_settings_fable.py +++ b/tests/test_team_settings_fable.py @@ -7,7 +7,7 @@ import pytest from agent.dashboard.options import FABLE_MODEL_IDS from agent.dashboard.team_settings import TeamSettingsUpdate, get_team_fable_enabled -_FABLE = "anthropic:claude-fable-5" +_FABLE = "bedrock_converse:us.anthropic.claude-fable-5" # --- accessor: get_team_fable_enabled (async, patched store) --- @@ -105,10 +105,10 @@ def test_disable_transition_coerces_all_fable_defaults() -> None: def test_update_leaves_non_fable_defaults_untouched_when_disabled() -> None: # Coercion only rewrites Fable ids; other selections pass through unchanged. update = TeamSettingsUpdate( - default_agent_model="openai:gpt-5.5", + default_agent_model="bedrock_converse:us.anthropic.claude-sonnet-5", default_agent_reasoning_effort="medium", ) - assert update.default_agent_model == "openai:gpt-5.5" + assert update.default_agent_model == "bedrock_converse:us.anthropic.claude-sonnet-5" assert update.default_agent_reasoning_effort == "medium"