refactor(models): convert re-added Fable 5 to Bedrock model IDs

This commit is contained in:
Adam Moussa 2026-07-10 13:09:19 -04:00
parent 396780285d
commit 17a9a25160
No known key found for this signature in database
8 changed files with 54 additions and 37 deletions

View file

@ -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()

View file

@ -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"}

View file

@ -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 |

View file

@ -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",)

View file

@ -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"}

View file

@ -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:

View file

@ -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"

View file

@ -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"