open-swe/agent/middleware/model_fallback.py

285 lines
12 KiB
Python
Raw Permalink Normal View History

feat: port model-fallback resilience from upstream (#1694, #1695) (#161) * feat: port plan-review & workflow-approval UX (#135) Port six upstream commits onto dev: - c03a6be7 (already ported): keep plan guidance high-level - 546042a4: add workflow approval UI with diff preview, approval URLs, web review links, and polling for approval status during active runs - 216cf181: remove workflow token elevation; approved pushes pass through directly without proxy token rewriting - 3dbc0282: preserve plan redirects after login by accepting relative same-origin redirect_to values and rejecting blocked paths - bb104d93: submit plan comments with cmd+enter - 90cb6caa: terse Slack replies, shared content via save_plan outside plan mode (PLAN_STATUS_SHARED), reject shared-content mutations Refs: #135 * feat: port durable dispatch hardening and startup latency improvements Port five upstream PRs onto dev: - #1621 / #1658: durable dispatch with loopback webhook defense, create_durable_run helper, _config_with_prepare_run_id, degradation to None for relative/loopback completion webhook URLs - #1696: run-level completion webhook deduplication (replace claim-then-post with post-then-flag per run_id), DeferredErrorModel for graph-factory resilience, ToolRetryMiddleware for task subagents, TimeoutWrapupMiddleware for all three graphs - #1697: lazy-load __init__.py for agent.middleware, agent.tools, agent.dashboard (PEP 562); defer heavy imports (exa_py in web_search, agent.webapp in request_pr_review, deepagents in sandbox.py); add ttl_cache.py with stale-while-revalidate for tool loaders Refs: #137 * fix: restore login page render and clear CI lint/format The plan-review port removed the authRedirectUrl import from login.tsx but left its call site, crashing the login page at runtime (blank page, no 'Sign in to open-swe'). Pass the relative path straight to loginUrl, matching the plan route and the backend relative-redirect handling. Also drop an unused os import in the guard test and reformat workflow_push_guard.py to satisfy ruff. * feat: port model-fallback resilience from upstream (#1694, #1695) - Add httpx.TransportError to the transient-exception set so incomplete chunked reads on streamed responses trigger a fallback instead of cancelling the run (#1694 / c9f6dd86). - Rewrite fallback to alternate primary/fallback with exponential backoff instead of a single failover, so the agent survives multi-minute gateway outages spanning both providers (#1695 / c9a9a7cd). - Default backoff schedule (0, 5, 15, 30, 45) reaches past the gateway's ~30s recovery window; jittered ±25%. - On exhaustion, surface a terminal AIMessage explaining the outage instead of crashing — progress is checkpointed so the user can retrigger to continue. - Preserve fork conventions: Bedrock ClientError retryability check, sync wrap_model_call (using time.sleep instead of asyncio.sleep), and existing access-error surfacing for Anthropic, OpenAI, and Bedrock (botocore) provider errors. - Update triage ledger (c9f6dd86, c9a9a7cd → landed) and re-render triage.md. Refs: #139 * fix: align workflow-push-guard tests with dev's transient-elevation impl The dev merge auto-combined dev's elevation tests with the stale passthrough tests inherited from the durable-dispatch branch; the passthrough tests contradict dev's restored _run_with_workflow_token impl. Take dev's test file. * fix: drop dead ttl_cache module; make fallback backoff jitter two-sided ttl_cache.py was re-introduced via the dev merge but dev/#160 deliberately removed it as dead code (no agent importer). Remove it to match dev. Also make _jittered_delay symmetric (±25%) to match its docstring. * chore(upstream-sync): triage 4 new upstream commits (#1708-#1713) Synced ledger to upstream/main (71e3b818). New rows all deferred: - #1708 add GPT-5.6 OpenAI models (FLAG-HUMAN: fork picker is Bedrock/Fireworks-only) - #1709 stale admin model defaults after upgrades - #1710 bump langchain-fireworks 1.4.4 - #1713 align reviewer eval with published findings --------- Co-authored-by: amoussa1229 <166072409+amoussa1229@users.noreply.github.com> Co-authored-by: Adam Moussa <adam@seahavenind.com>
2026-07-09 18:28:21 -04:00
"""Middleware that retries model calls across a primary and fallback provider.
feat: port model-fallback resilience from upstream (#1694, #1695) (#161) * feat: port plan-review & workflow-approval UX (#135) Port six upstream commits onto dev: - c03a6be7 (already ported): keep plan guidance high-level - 546042a4: add workflow approval UI with diff preview, approval URLs, web review links, and polling for approval status during active runs - 216cf181: remove workflow token elevation; approved pushes pass through directly without proxy token rewriting - 3dbc0282: preserve plan redirects after login by accepting relative same-origin redirect_to values and rejecting blocked paths - bb104d93: submit plan comments with cmd+enter - 90cb6caa: terse Slack replies, shared content via save_plan outside plan mode (PLAN_STATUS_SHARED), reject shared-content mutations Refs: #135 * feat: port durable dispatch hardening and startup latency improvements Port five upstream PRs onto dev: - #1621 / #1658: durable dispatch with loopback webhook defense, create_durable_run helper, _config_with_prepare_run_id, degradation to None for relative/loopback completion webhook URLs - #1696: run-level completion webhook deduplication (replace claim-then-post with post-then-flag per run_id), DeferredErrorModel for graph-factory resilience, ToolRetryMiddleware for task subagents, TimeoutWrapupMiddleware for all three graphs - #1697: lazy-load __init__.py for agent.middleware, agent.tools, agent.dashboard (PEP 562); defer heavy imports (exa_py in web_search, agent.webapp in request_pr_review, deepagents in sandbox.py); add ttl_cache.py with stale-while-revalidate for tool loaders Refs: #137 * fix: restore login page render and clear CI lint/format The plan-review port removed the authRedirectUrl import from login.tsx but left its call site, crashing the login page at runtime (blank page, no 'Sign in to open-swe'). Pass the relative path straight to loginUrl, matching the plan route and the backend relative-redirect handling. Also drop an unused os import in the guard test and reformat workflow_push_guard.py to satisfy ruff. * feat: port model-fallback resilience from upstream (#1694, #1695) - Add httpx.TransportError to the transient-exception set so incomplete chunked reads on streamed responses trigger a fallback instead of cancelling the run (#1694 / c9f6dd86). - Rewrite fallback to alternate primary/fallback with exponential backoff instead of a single failover, so the agent survives multi-minute gateway outages spanning both providers (#1695 / c9a9a7cd). - Default backoff schedule (0, 5, 15, 30, 45) reaches past the gateway's ~30s recovery window; jittered ±25%. - On exhaustion, surface a terminal AIMessage explaining the outage instead of crashing — progress is checkpointed so the user can retrigger to continue. - Preserve fork conventions: Bedrock ClientError retryability check, sync wrap_model_call (using time.sleep instead of asyncio.sleep), and existing access-error surfacing for Anthropic, OpenAI, and Bedrock (botocore) provider errors. - Update triage ledger (c9f6dd86, c9a9a7cd → landed) and re-render triage.md. Refs: #139 * fix: align workflow-push-guard tests with dev's transient-elevation impl The dev merge auto-combined dev's elevation tests with the stale passthrough tests inherited from the durable-dispatch branch; the passthrough tests contradict dev's restored _run_with_workflow_token impl. Take dev's test file. * fix: drop dead ttl_cache module; make fallback backoff jitter two-sided ttl_cache.py was re-introduced via the dev merge but dev/#160 deliberately removed it as dead code (no agent importer). Remove it to match dev. Also make _jittered_delay symmetric (±25%) to match its docstring. * chore(upstream-sync): triage 4 new upstream commits (#1708-#1713) Synced ledger to upstream/main (71e3b818). New rows all deferred: - #1708 add GPT-5.6 OpenAI models (FLAG-HUMAN: fork picker is Bedrock/Fireworks-only) - #1709 stale admin model defaults after upgrades - #1710 bump langchain-fireworks 1.4.4 - #1713 align reviewer eval with published findings --------- Co-authored-by: amoussa1229 <166072409+amoussa1229@users.noreply.github.com> Co-authored-by: Adam Moussa <adam@seahavenind.com>
2026-07-09 18:28:21 -04:00
Wraps the model call. When a model raises a transient provider error (5xx,
429, connection/timeout), the request is retried, alternating between the
primary and the configured fallback model with exponential backoff between
attempts. The fallback is bound to tools by the agent factory on each call,
so swapping ``request.model`` is sufficient.
Why alternate with backoff instead of failing over once: both providers can
be routed through the same LLM Gateway, so a gateway outage takes out the
"cross-provider" fallback too. A single immediate failover cannot ride out
even a short shared outage (the gateway's 502 page literally says "try again
in 30 seconds"), and an unprotected fallback call crashes the whole run.
Alternating with a backoff schedule that reaches past 30s lets a long-running
agent run survive multi-minute provider or gateway blips.
Bidirectional: if the primary is Anthropic the fallback is typically OpenAI,
and vice versa. The middleware itself is provider-agnostic — it inspects the
feat: port model-fallback resilience from upstream (#1694, #1695) (#161) * feat: port plan-review & workflow-approval UX (#135) Port six upstream commits onto dev: - c03a6be7 (already ported): keep plan guidance high-level - 546042a4: add workflow approval UI with diff preview, approval URLs, web review links, and polling for approval status during active runs - 216cf181: remove workflow token elevation; approved pushes pass through directly without proxy token rewriting - 3dbc0282: preserve plan redirects after login by accepting relative same-origin redirect_to values and rejecting blocked paths - bb104d93: submit plan comments with cmd+enter - 90cb6caa: terse Slack replies, shared content via save_plan outside plan mode (PLAN_STATUS_SHARED), reject shared-content mutations Refs: #135 * feat: port durable dispatch hardening and startup latency improvements Port five upstream PRs onto dev: - #1621 / #1658: durable dispatch with loopback webhook defense, create_durable_run helper, _config_with_prepare_run_id, degradation to None for relative/loopback completion webhook URLs - #1696: run-level completion webhook deduplication (replace claim-then-post with post-then-flag per run_id), DeferredErrorModel for graph-factory resilience, ToolRetryMiddleware for task subagents, TimeoutWrapupMiddleware for all three graphs - #1697: lazy-load __init__.py for agent.middleware, agent.tools, agent.dashboard (PEP 562); defer heavy imports (exa_py in web_search, agent.webapp in request_pr_review, deepagents in sandbox.py); add ttl_cache.py with stale-while-revalidate for tool loaders Refs: #137 * fix: restore login page render and clear CI lint/format The plan-review port removed the authRedirectUrl import from login.tsx but left its call site, crashing the login page at runtime (blank page, no 'Sign in to open-swe'). Pass the relative path straight to loginUrl, matching the plan route and the backend relative-redirect handling. Also drop an unused os import in the guard test and reformat workflow_push_guard.py to satisfy ruff. * feat: port model-fallback resilience from upstream (#1694, #1695) - Add httpx.TransportError to the transient-exception set so incomplete chunked reads on streamed responses trigger a fallback instead of cancelling the run (#1694 / c9f6dd86). - Rewrite fallback to alternate primary/fallback with exponential backoff instead of a single failover, so the agent survives multi-minute gateway outages spanning both providers (#1695 / c9a9a7cd). - Default backoff schedule (0, 5, 15, 30, 45) reaches past the gateway's ~30s recovery window; jittered ±25%. - On exhaustion, surface a terminal AIMessage explaining the outage instead of crashing — progress is checkpointed so the user can retrigger to continue. - Preserve fork conventions: Bedrock ClientError retryability check, sync wrap_model_call (using time.sleep instead of asyncio.sleep), and existing access-error surfacing for Anthropic, OpenAI, and Bedrock (botocore) provider errors. - Update triage ledger (c9f6dd86, c9a9a7cd → landed) and re-render triage.md. Refs: #139 * fix: align workflow-push-guard tests with dev's transient-elevation impl The dev merge auto-combined dev's elevation tests with the stale passthrough tests inherited from the durable-dispatch branch; the passthrough tests contradict dev's restored _run_with_workflow_token impl. Take dev's test file. * fix: drop dead ttl_cache module; make fallback backoff jitter two-sided ttl_cache.py was re-introduced via the dev merge but dev/#160 deliberately removed it as dead code (no agent importer). Remove it to match dev. Also make _jittered_delay symmetric (±25%) to match its docstring. * chore(upstream-sync): triage 4 new upstream commits (#1708-#1713) Synced ledger to upstream/main (71e3b818). New rows all deferred: - #1708 add GPT-5.6 OpenAI models (FLAG-HUMAN: fork picker is Bedrock/Fireworks-only) - #1709 stale admin model defaults after upgrades - #1710 bump langchain-fireworks 1.4.4 - #1713 align reviewer eval with published findings --------- Co-authored-by: amoussa1229 <166072409+amoussa1229@users.noreply.github.com> Co-authored-by: Adam Moussa <adam@seahavenind.com>
2026-07-09 18:28:21 -04:00
exception type/status code to decide whether an attempt is retryable.
If every attempt fails, the middleware either raises the last error or (by
default) returns a terminal ``AIMessage`` explaining the outage, so the run
ends with a visible message in Slack/GitHub instead of an abrupt crash. The
turn's progress is checkpointed, so the user can retrigger to continue.
"""
from __future__ import annotations
feat: port model-fallback resilience from upstream (#1694, #1695) (#161) * feat: port plan-review & workflow-approval UX (#135) Port six upstream commits onto dev: - c03a6be7 (already ported): keep plan guidance high-level - 546042a4: add workflow approval UI with diff preview, approval URLs, web review links, and polling for approval status during active runs - 216cf181: remove workflow token elevation; approved pushes pass through directly without proxy token rewriting - 3dbc0282: preserve plan redirects after login by accepting relative same-origin redirect_to values and rejecting blocked paths - bb104d93: submit plan comments with cmd+enter - 90cb6caa: terse Slack replies, shared content via save_plan outside plan mode (PLAN_STATUS_SHARED), reject shared-content mutations Refs: #135 * feat: port durable dispatch hardening and startup latency improvements Port five upstream PRs onto dev: - #1621 / #1658: durable dispatch with loopback webhook defense, create_durable_run helper, _config_with_prepare_run_id, degradation to None for relative/loopback completion webhook URLs - #1696: run-level completion webhook deduplication (replace claim-then-post with post-then-flag per run_id), DeferredErrorModel for graph-factory resilience, ToolRetryMiddleware for task subagents, TimeoutWrapupMiddleware for all three graphs - #1697: lazy-load __init__.py for agent.middleware, agent.tools, agent.dashboard (PEP 562); defer heavy imports (exa_py in web_search, agent.webapp in request_pr_review, deepagents in sandbox.py); add ttl_cache.py with stale-while-revalidate for tool loaders Refs: #137 * fix: restore login page render and clear CI lint/format The plan-review port removed the authRedirectUrl import from login.tsx but left its call site, crashing the login page at runtime (blank page, no 'Sign in to open-swe'). Pass the relative path straight to loginUrl, matching the plan route and the backend relative-redirect handling. Also drop an unused os import in the guard test and reformat workflow_push_guard.py to satisfy ruff. * feat: port model-fallback resilience from upstream (#1694, #1695) - Add httpx.TransportError to the transient-exception set so incomplete chunked reads on streamed responses trigger a fallback instead of cancelling the run (#1694 / c9f6dd86). - Rewrite fallback to alternate primary/fallback with exponential backoff instead of a single failover, so the agent survives multi-minute gateway outages spanning both providers (#1695 / c9a9a7cd). - Default backoff schedule (0, 5, 15, 30, 45) reaches past the gateway's ~30s recovery window; jittered ±25%. - On exhaustion, surface a terminal AIMessage explaining the outage instead of crashing — progress is checkpointed so the user can retrigger to continue. - Preserve fork conventions: Bedrock ClientError retryability check, sync wrap_model_call (using time.sleep instead of asyncio.sleep), and existing access-error surfacing for Anthropic, OpenAI, and Bedrock (botocore) provider errors. - Update triage ledger (c9f6dd86, c9a9a7cd → landed) and re-render triage.md. Refs: #139 * fix: align workflow-push-guard tests with dev's transient-elevation impl The dev merge auto-combined dev's elevation tests with the stale passthrough tests inherited from the durable-dispatch branch; the passthrough tests contradict dev's restored _run_with_workflow_token impl. Take dev's test file. * fix: drop dead ttl_cache module; make fallback backoff jitter two-sided ttl_cache.py was re-introduced via the dev merge but dev/#160 deliberately removed it as dead code (no agent importer). Remove it to match dev. Also make _jittered_delay symmetric (±25%) to match its docstring. * chore(upstream-sync): triage 4 new upstream commits (#1708-#1713) Synced ledger to upstream/main (71e3b818). New rows all deferred: - #1708 add GPT-5.6 OpenAI models (FLAG-HUMAN: fork picker is Bedrock/Fireworks-only) - #1709 stale admin model defaults after upgrades - #1710 bump langchain-fireworks 1.4.4 - #1713 align reviewer eval with published findings --------- Co-authored-by: amoussa1229 <166072409+amoussa1229@users.noreply.github.com> Co-authored-by: Adam Moussa <adam@seahavenind.com>
2026-07-09 18:28:21 -04:00
import asyncio
import logging
feat: port model-fallback resilience from upstream (#1694, #1695) (#161) * feat: port plan-review & workflow-approval UX (#135) Port six upstream commits onto dev: - c03a6be7 (already ported): keep plan guidance high-level - 546042a4: add workflow approval UI with diff preview, approval URLs, web review links, and polling for approval status during active runs - 216cf181: remove workflow token elevation; approved pushes pass through directly without proxy token rewriting - 3dbc0282: preserve plan redirects after login by accepting relative same-origin redirect_to values and rejecting blocked paths - bb104d93: submit plan comments with cmd+enter - 90cb6caa: terse Slack replies, shared content via save_plan outside plan mode (PLAN_STATUS_SHARED), reject shared-content mutations Refs: #135 * feat: port durable dispatch hardening and startup latency improvements Port five upstream PRs onto dev: - #1621 / #1658: durable dispatch with loopback webhook defense, create_durable_run helper, _config_with_prepare_run_id, degradation to None for relative/loopback completion webhook URLs - #1696: run-level completion webhook deduplication (replace claim-then-post with post-then-flag per run_id), DeferredErrorModel for graph-factory resilience, ToolRetryMiddleware for task subagents, TimeoutWrapupMiddleware for all three graphs - #1697: lazy-load __init__.py for agent.middleware, agent.tools, agent.dashboard (PEP 562); defer heavy imports (exa_py in web_search, agent.webapp in request_pr_review, deepagents in sandbox.py); add ttl_cache.py with stale-while-revalidate for tool loaders Refs: #137 * fix: restore login page render and clear CI lint/format The plan-review port removed the authRedirectUrl import from login.tsx but left its call site, crashing the login page at runtime (blank page, no 'Sign in to open-swe'). Pass the relative path straight to loginUrl, matching the plan route and the backend relative-redirect handling. Also drop an unused os import in the guard test and reformat workflow_push_guard.py to satisfy ruff. * feat: port model-fallback resilience from upstream (#1694, #1695) - Add httpx.TransportError to the transient-exception set so incomplete chunked reads on streamed responses trigger a fallback instead of cancelling the run (#1694 / c9f6dd86). - Rewrite fallback to alternate primary/fallback with exponential backoff instead of a single failover, so the agent survives multi-minute gateway outages spanning both providers (#1695 / c9a9a7cd). - Default backoff schedule (0, 5, 15, 30, 45) reaches past the gateway's ~30s recovery window; jittered ±25%. - On exhaustion, surface a terminal AIMessage explaining the outage instead of crashing — progress is checkpointed so the user can retrigger to continue. - Preserve fork conventions: Bedrock ClientError retryability check, sync wrap_model_call (using time.sleep instead of asyncio.sleep), and existing access-error surfacing for Anthropic, OpenAI, and Bedrock (botocore) provider errors. - Update triage ledger (c9f6dd86, c9a9a7cd → landed) and re-render triage.md. Refs: #139 * fix: align workflow-push-guard tests with dev's transient-elevation impl The dev merge auto-combined dev's elevation tests with the stale passthrough tests inherited from the durable-dispatch branch; the passthrough tests contradict dev's restored _run_with_workflow_token impl. Take dev's test file. * fix: drop dead ttl_cache module; make fallback backoff jitter two-sided ttl_cache.py was re-introduced via the dev merge but dev/#160 deliberately removed it as dead code (no agent importer). Remove it to match dev. Also make _jittered_delay symmetric (±25%) to match its docstring. * chore(upstream-sync): triage 4 new upstream commits (#1708-#1713) Synced ledger to upstream/main (71e3b818). New rows all deferred: - #1708 add GPT-5.6 OpenAI models (FLAG-HUMAN: fork picker is Bedrock/Fireworks-only) - #1709 stale admin model defaults after upgrades - #1710 bump langchain-fireworks 1.4.4 - #1713 align reviewer eval with published findings --------- Co-authored-by: amoussa1229 <166072409+amoussa1229@users.noreply.github.com> Co-authored-by: Adam Moussa <adam@seahavenind.com>
2026-07-09 18:28:21 -04:00
import random
import time
from collections.abc import Awaitable, Callable, Sequence
from typing import Any
import anthropic
feat: port model-fallback resilience from upstream (#1694, #1695) (#161) * feat: port plan-review & workflow-approval UX (#135) Port six upstream commits onto dev: - c03a6be7 (already ported): keep plan guidance high-level - 546042a4: add workflow approval UI with diff preview, approval URLs, web review links, and polling for approval status during active runs - 216cf181: remove workflow token elevation; approved pushes pass through directly without proxy token rewriting - 3dbc0282: preserve plan redirects after login by accepting relative same-origin redirect_to values and rejecting blocked paths - bb104d93: submit plan comments with cmd+enter - 90cb6caa: terse Slack replies, shared content via save_plan outside plan mode (PLAN_STATUS_SHARED), reject shared-content mutations Refs: #135 * feat: port durable dispatch hardening and startup latency improvements Port five upstream PRs onto dev: - #1621 / #1658: durable dispatch with loopback webhook defense, create_durable_run helper, _config_with_prepare_run_id, degradation to None for relative/loopback completion webhook URLs - #1696: run-level completion webhook deduplication (replace claim-then-post with post-then-flag per run_id), DeferredErrorModel for graph-factory resilience, ToolRetryMiddleware for task subagents, TimeoutWrapupMiddleware for all three graphs - #1697: lazy-load __init__.py for agent.middleware, agent.tools, agent.dashboard (PEP 562); defer heavy imports (exa_py in web_search, agent.webapp in request_pr_review, deepagents in sandbox.py); add ttl_cache.py with stale-while-revalidate for tool loaders Refs: #137 * fix: restore login page render and clear CI lint/format The plan-review port removed the authRedirectUrl import from login.tsx but left its call site, crashing the login page at runtime (blank page, no 'Sign in to open-swe'). Pass the relative path straight to loginUrl, matching the plan route and the backend relative-redirect handling. Also drop an unused os import in the guard test and reformat workflow_push_guard.py to satisfy ruff. * feat: port model-fallback resilience from upstream (#1694, #1695) - Add httpx.TransportError to the transient-exception set so incomplete chunked reads on streamed responses trigger a fallback instead of cancelling the run (#1694 / c9f6dd86). - Rewrite fallback to alternate primary/fallback with exponential backoff instead of a single failover, so the agent survives multi-minute gateway outages spanning both providers (#1695 / c9a9a7cd). - Default backoff schedule (0, 5, 15, 30, 45) reaches past the gateway's ~30s recovery window; jittered ±25%. - On exhaustion, surface a terminal AIMessage explaining the outage instead of crashing — progress is checkpointed so the user can retrigger to continue. - Preserve fork conventions: Bedrock ClientError retryability check, sync wrap_model_call (using time.sleep instead of asyncio.sleep), and existing access-error surfacing for Anthropic, OpenAI, and Bedrock (botocore) provider errors. - Update triage ledger (c9f6dd86, c9a9a7cd → landed) and re-render triage.md. Refs: #139 * fix: align workflow-push-guard tests with dev's transient-elevation impl The dev merge auto-combined dev's elevation tests with the stale passthrough tests inherited from the durable-dispatch branch; the passthrough tests contradict dev's restored _run_with_workflow_token impl. Take dev's test file. * fix: drop dead ttl_cache module; make fallback backoff jitter two-sided ttl_cache.py was re-introduced via the dev merge but dev/#160 deliberately removed it as dead code (no agent importer). Remove it to match dev. Also make _jittered_delay symmetric (±25%) to match its docstring. * chore(upstream-sync): triage 4 new upstream commits (#1708-#1713) Synced ledger to upstream/main (71e3b818). New rows all deferred: - #1708 add GPT-5.6 OpenAI models (FLAG-HUMAN: fork picker is Bedrock/Fireworks-only) - #1709 stale admin model defaults after upgrades - #1710 bump langchain-fireworks 1.4.4 - #1713 align reviewer eval with published findings --------- Co-authored-by: amoussa1229 <166072409+amoussa1229@users.noreply.github.com> Co-authored-by: Adam Moussa <adam@seahavenind.com>
2026-07-09 18:28:21 -04:00
import httpx
import openai
feat: migrate model providers to Bedrock (Claude) + Fireworks (everything else) (#62) * feat: switch model providers to AWS Bedrock (Claude) and Fireworks (non-Claude) Migrate off direct provider APIs: AWS Bedrock for Anthropic/Claude via the cross-region inference profile us.anthropic.claude-opus-4-8, Fireworks AI for all non-Claude models. Drop OpenAI (gpt-5.5) and Google (gemini-3.5-flash) entirely. DEFAULT_MODEL_ID is now Bedrock Claude; all Fireworks models stay freely selectable for the agent and reviewer graphs and via team/profile defaults. - pyproject: add langchain-aws (ChatBedrockConverse + boto3) - options.py: Bedrock Claude entry + default; remove openai/google entries - model.py: bedrock_converse provider_model_kwargs (effort -> thinking budget), region pin in make_model, bedrock<->fireworks fallback pairing, AWS_REGION/ FIREWORKS_API_KEY local-dev validation - server.py: provider-aware fallback kwargs build - sanitize_thinking_blocks: also sanitize ChatBedrockConverse thinking blocks - model_fallback: treat transient botocore ClientError codes as fallback-worthy - eval_jobs: repoint hardcoded eval model id to Bedrock Claude - tests: repoint dropped model ids; drop obsolete google test module * fix(bedrock): use adaptive thinking + output_config.effort for Opus 4.8 The handoff spec wired Bedrock Converse thinking as {type: enabled, budget_tokens: N}, but Opus 4.7+ rejects that with a ValidationException: thinking.type "enabled" is not supported; it requires thinking.type "adaptive" plus output_config.effort. Verified by live invoke against us.anthropic.claude-opus-4-8 (account 328440206208, us-east-1): the enabled+budget shape 400s, adaptive+effort returns normally. Map profile effort to additional_model_request_fields: {thinking: {type: adaptive, display: summarized}, output_config: {effort: <low|medium|high|xhigh|max>}} reusing anthropic_thinking_for/anthropic_effort_for. Update the two subagent-model tests asserting the old shape. * fix(deploy): seed Bedrock/Fireworks models, not the dropped anthropic:/openai: ids Model selection is store-driven, so seed_store.sh's team_settings/default seed is what runs in prod. It still seeded the removed providers, which would fail at runtime after the migration: - agent/builder: anthropic:claude-opus-4-8 -> bedrock_converse:us.anthropic.claude-opus-4-8 - reviewer: openai:gpt-5.5 (dropped) -> bedrock_converse:us.anthropic.claude-opus-4-8 (set SEED_REVIEWER_MODEL to a Fireworks model for a cross-family reviewer) - fetch-config REQUIRED_PROVIDER_KEYS default ANTHROPIC_API_KEY,OPENAI_API_KEY -> FIREWORKS_API_KEY (Bedrock auths via host IAM role; dropping the old keys would otherwise fail-fast at boot) - docs (DEPLOYMENT/ROTATION/put-config) updated to match. Surfaced by the cross-family review + verified against deploy/. * fix(bedrock): security-review NITs — region resolution, error sanitization, reasoning-block strip From /sh-security-review (all confirmed-low): - model.py: resolve region from AWS_REGION OR AWS_DEFAULT_REGION (matches validate_local_dev_llm_config) so the validated region is the one actually used. - model_fallback.py: sanitize Bedrock AccessDenied/ResourceNotFound errors to the error code only, so the role ARN + account id in the raw botocore message never reach logs or the user channel (CWE-209). - sanitize_thinking_blocks.py: also strip empty Bedrock reasoning_content blocks (Converse emits reasoning_content, not thinking) so the middleware is not a no-op on Bedrock; + unit tests. (Empty blocks replay fine today; defensive.) * deploy(bedrock): grant instance-role Bedrock invoke + repoint LLM_MODEL_ID / eval model ids Deployment-readiness for the Bedrock migration (PR #62): - instance-role.ts: least-privilege bedrock:InvokeModel[WithResponseStream] on the us.anthropic.claude-opus-4-8 inference-profile ARN + the foundation-model ARN in each routed region (us-east-1/2, us-west-2). The model runs in the server process on the box, so the EC2 instance role is the principal. Simulator-verified (allowed for opus-4-8, implicitDeny for other models) and synth-verified. Passed the mandatory GPT-4.1 IAM cross-review (no blockers, least-privilege confirmed). - config-store.ts: IaC SSM LLM_MODEL_ID anthropic:claude-opus-4-8 -> bedrock_converse:us.anthropic.claude-opus-4-8. This SSM value overrides seed_store.sh's default via pick precedence, so the seed-script fix alone was insufficient — both sources now point at the supported Bedrock id. - infra/README.md + evals/reviewer/config.toml: repoint stale anthropic:/google_genai: ids to the Bedrock id (config.toml's model_id was an active, now-broken value). AWS_REGION is already wired via user-data.sh (IMDS -> boot.env), so no change needed there. * chore(secrets): drop OPENAI/GOOGLE/GROQ key shells (revoked, providers removed) Those three providers were dropped in the Bedrock/Fireworks migration and their keys revoked; the live Secrets Manager objects (open-swe-{dev,prod}/{OPENAI,GOOGLE,GROQ}_API_KEY) were deleted (7-day recovery). Remove them from the IaC so a future cdk deploy does not recreate the shells, and from fetch-config's mirror array so boot stops requesting them: - config-store.ts SECRET_VARS + descriptions (28 -> 25 shells) - fetch-config.sh SECRET_VARS array (kept in lockstep) - put-config.sh: drop the put_secret lines; ANTHROPIC_API_KEY re-labelled optional (eval judge only — Bedrock builder/reviewer auth via the host IAM role). REQUIRED_PROVIDER_KEYS is not set in SSM, so it uses the FIREWORKS_API_KEY default.
2026-06-29 15:57:19 -04:00
from botocore.exceptions import ClientError
from langchain.agents.middleware import AgentMiddleware
from langchain.agents.middleware.types import ModelCallResult, ModelRequest, ModelResponse
from langchain_core.language_models import BaseChatModel
from langchain_core.messages import AIMessage
logger = logging.getLogger(__name__)
_RETRYABLE_STATUS_CODES = {408, 409, 425, 429, 500, 502, 503, 504, 529}
_TRANSIENT_EXCEPTIONS: tuple[type[BaseException], ...] = (
anthropic.APIConnectionError,
anthropic.APITimeoutError,
anthropic.RateLimitError,
anthropic.InternalServerError,
openai.APIConnectionError,
openai.APITimeoutError,
openai.RateLimitError,
openai.InternalServerError,
feat: port model-fallback resilience from upstream (#1694, #1695) (#161) * feat: port plan-review & workflow-approval UX (#135) Port six upstream commits onto dev: - c03a6be7 (already ported): keep plan guidance high-level - 546042a4: add workflow approval UI with diff preview, approval URLs, web review links, and polling for approval status during active runs - 216cf181: remove workflow token elevation; approved pushes pass through directly without proxy token rewriting - 3dbc0282: preserve plan redirects after login by accepting relative same-origin redirect_to values and rejecting blocked paths - bb104d93: submit plan comments with cmd+enter - 90cb6caa: terse Slack replies, shared content via save_plan outside plan mode (PLAN_STATUS_SHARED), reject shared-content mutations Refs: #135 * feat: port durable dispatch hardening and startup latency improvements Port five upstream PRs onto dev: - #1621 / #1658: durable dispatch with loopback webhook defense, create_durable_run helper, _config_with_prepare_run_id, degradation to None for relative/loopback completion webhook URLs - #1696: run-level completion webhook deduplication (replace claim-then-post with post-then-flag per run_id), DeferredErrorModel for graph-factory resilience, ToolRetryMiddleware for task subagents, TimeoutWrapupMiddleware for all three graphs - #1697: lazy-load __init__.py for agent.middleware, agent.tools, agent.dashboard (PEP 562); defer heavy imports (exa_py in web_search, agent.webapp in request_pr_review, deepagents in sandbox.py); add ttl_cache.py with stale-while-revalidate for tool loaders Refs: #137 * fix: restore login page render and clear CI lint/format The plan-review port removed the authRedirectUrl import from login.tsx but left its call site, crashing the login page at runtime (blank page, no 'Sign in to open-swe'). Pass the relative path straight to loginUrl, matching the plan route and the backend relative-redirect handling. Also drop an unused os import in the guard test and reformat workflow_push_guard.py to satisfy ruff. * feat: port model-fallback resilience from upstream (#1694, #1695) - Add httpx.TransportError to the transient-exception set so incomplete chunked reads on streamed responses trigger a fallback instead of cancelling the run (#1694 / c9f6dd86). - Rewrite fallback to alternate primary/fallback with exponential backoff instead of a single failover, so the agent survives multi-minute gateway outages spanning both providers (#1695 / c9a9a7cd). - Default backoff schedule (0, 5, 15, 30, 45) reaches past the gateway's ~30s recovery window; jittered ±25%. - On exhaustion, surface a terminal AIMessage explaining the outage instead of crashing — progress is checkpointed so the user can retrigger to continue. - Preserve fork conventions: Bedrock ClientError retryability check, sync wrap_model_call (using time.sleep instead of asyncio.sleep), and existing access-error surfacing for Anthropic, OpenAI, and Bedrock (botocore) provider errors. - Update triage ledger (c9f6dd86, c9a9a7cd → landed) and re-render triage.md. Refs: #139 * fix: align workflow-push-guard tests with dev's transient-elevation impl The dev merge auto-combined dev's elevation tests with the stale passthrough tests inherited from the durable-dispatch branch; the passthrough tests contradict dev's restored _run_with_workflow_token impl. Take dev's test file. * fix: drop dead ttl_cache module; make fallback backoff jitter two-sided ttl_cache.py was re-introduced via the dev merge but dev/#160 deliberately removed it as dead code (no agent importer). Remove it to match dev. Also make _jittered_delay symmetric (±25%) to match its docstring. * chore(upstream-sync): triage 4 new upstream commits (#1708-#1713) Synced ledger to upstream/main (71e3b818). New rows all deferred: - #1708 add GPT-5.6 OpenAI models (FLAG-HUMAN: fork picker is Bedrock/Fireworks-only) - #1709 stale admin model defaults after upgrades - #1710 bump langchain-fireworks 1.4.4 - #1713 align reviewer eval with published findings --------- Co-authored-by: amoussa1229 <166072409+amoussa1229@users.noreply.github.com> Co-authored-by: Adam Moussa <adam@seahavenind.com>
2026-07-09 18:28:21 -04:00
httpx.TransportError,
)
feat: migrate model providers to Bedrock (Claude) + Fireworks (everything else) (#62) * feat: switch model providers to AWS Bedrock (Claude) and Fireworks (non-Claude) Migrate off direct provider APIs: AWS Bedrock for Anthropic/Claude via the cross-region inference profile us.anthropic.claude-opus-4-8, Fireworks AI for all non-Claude models. Drop OpenAI (gpt-5.5) and Google (gemini-3.5-flash) entirely. DEFAULT_MODEL_ID is now Bedrock Claude; all Fireworks models stay freely selectable for the agent and reviewer graphs and via team/profile defaults. - pyproject: add langchain-aws (ChatBedrockConverse + boto3) - options.py: Bedrock Claude entry + default; remove openai/google entries - model.py: bedrock_converse provider_model_kwargs (effort -> thinking budget), region pin in make_model, bedrock<->fireworks fallback pairing, AWS_REGION/ FIREWORKS_API_KEY local-dev validation - server.py: provider-aware fallback kwargs build - sanitize_thinking_blocks: also sanitize ChatBedrockConverse thinking blocks - model_fallback: treat transient botocore ClientError codes as fallback-worthy - eval_jobs: repoint hardcoded eval model id to Bedrock Claude - tests: repoint dropped model ids; drop obsolete google test module * fix(bedrock): use adaptive thinking + output_config.effort for Opus 4.8 The handoff spec wired Bedrock Converse thinking as {type: enabled, budget_tokens: N}, but Opus 4.7+ rejects that with a ValidationException: thinking.type "enabled" is not supported; it requires thinking.type "adaptive" plus output_config.effort. Verified by live invoke against us.anthropic.claude-opus-4-8 (account 328440206208, us-east-1): the enabled+budget shape 400s, adaptive+effort returns normally. Map profile effort to additional_model_request_fields: {thinking: {type: adaptive, display: summarized}, output_config: {effort: <low|medium|high|xhigh|max>}} reusing anthropic_thinking_for/anthropic_effort_for. Update the two subagent-model tests asserting the old shape. * fix(deploy): seed Bedrock/Fireworks models, not the dropped anthropic:/openai: ids Model selection is store-driven, so seed_store.sh's team_settings/default seed is what runs in prod. It still seeded the removed providers, which would fail at runtime after the migration: - agent/builder: anthropic:claude-opus-4-8 -> bedrock_converse:us.anthropic.claude-opus-4-8 - reviewer: openai:gpt-5.5 (dropped) -> bedrock_converse:us.anthropic.claude-opus-4-8 (set SEED_REVIEWER_MODEL to a Fireworks model for a cross-family reviewer) - fetch-config REQUIRED_PROVIDER_KEYS default ANTHROPIC_API_KEY,OPENAI_API_KEY -> FIREWORKS_API_KEY (Bedrock auths via host IAM role; dropping the old keys would otherwise fail-fast at boot) - docs (DEPLOYMENT/ROTATION/put-config) updated to match. Surfaced by the cross-family review + verified against deploy/. * fix(bedrock): security-review NITs — region resolution, error sanitization, reasoning-block strip From /sh-security-review (all confirmed-low): - model.py: resolve region from AWS_REGION OR AWS_DEFAULT_REGION (matches validate_local_dev_llm_config) so the validated region is the one actually used. - model_fallback.py: sanitize Bedrock AccessDenied/ResourceNotFound errors to the error code only, so the role ARN + account id in the raw botocore message never reach logs or the user channel (CWE-209). - sanitize_thinking_blocks.py: also strip empty Bedrock reasoning_content blocks (Converse emits reasoning_content, not thinking) so the middleware is not a no-op on Bedrock; + unit tests. (Empty blocks replay fine today; defensive.) * deploy(bedrock): grant instance-role Bedrock invoke + repoint LLM_MODEL_ID / eval model ids Deployment-readiness for the Bedrock migration (PR #62): - instance-role.ts: least-privilege bedrock:InvokeModel[WithResponseStream] on the us.anthropic.claude-opus-4-8 inference-profile ARN + the foundation-model ARN in each routed region (us-east-1/2, us-west-2). The model runs in the server process on the box, so the EC2 instance role is the principal. Simulator-verified (allowed for opus-4-8, implicitDeny for other models) and synth-verified. Passed the mandatory GPT-4.1 IAM cross-review (no blockers, least-privilege confirmed). - config-store.ts: IaC SSM LLM_MODEL_ID anthropic:claude-opus-4-8 -> bedrock_converse:us.anthropic.claude-opus-4-8. This SSM value overrides seed_store.sh's default via pick precedence, so the seed-script fix alone was insufficient — both sources now point at the supported Bedrock id. - infra/README.md + evals/reviewer/config.toml: repoint stale anthropic:/google_genai: ids to the Bedrock id (config.toml's model_id was an active, now-broken value). AWS_REGION is already wired via user-data.sh (IMDS -> boot.env), so no change needed there. * chore(secrets): drop OPENAI/GOOGLE/GROQ key shells (revoked, providers removed) Those three providers were dropped in the Bedrock/Fireworks migration and their keys revoked; the live Secrets Manager objects (open-swe-{dev,prod}/{OPENAI,GOOGLE,GROQ}_API_KEY) were deleted (7-day recovery). Remove them from the IaC so a future cdk deploy does not recreate the shells, and from fetch-config's mirror array so boot stops requesting them: - config-store.ts SECRET_VARS + descriptions (28 -> 25 shells) - fetch-config.sh SECRET_VARS array (kept in lockstep) - put-config.sh: drop the put_secret lines; ANTHROPIC_API_KEY re-labelled optional (eval judge only — Bedrock builder/reviewer auth via the host IAM role). REQUIRED_PROVIDER_KEYS is not set in SSM, so it uses the FIREWORKS_API_KEY default.
2026-06-29 15:57:19 -04:00
_RETRYABLE_BEDROCK_ERROR_CODES = {
"ThrottlingException",
"ServiceUnavailableException",
"ModelTimeoutException",
"InternalServerException",
}
feat: port model-fallback resilience from upstream (#1694, #1695) (#161) * feat: port plan-review & workflow-approval UX (#135) Port six upstream commits onto dev: - c03a6be7 (already ported): keep plan guidance high-level - 546042a4: add workflow approval UI with diff preview, approval URLs, web review links, and polling for approval status during active runs - 216cf181: remove workflow token elevation; approved pushes pass through directly without proxy token rewriting - 3dbc0282: preserve plan redirects after login by accepting relative same-origin redirect_to values and rejecting blocked paths - bb104d93: submit plan comments with cmd+enter - 90cb6caa: terse Slack replies, shared content via save_plan outside plan mode (PLAN_STATUS_SHARED), reject shared-content mutations Refs: #135 * feat: port durable dispatch hardening and startup latency improvements Port five upstream PRs onto dev: - #1621 / #1658: durable dispatch with loopback webhook defense, create_durable_run helper, _config_with_prepare_run_id, degradation to None for relative/loopback completion webhook URLs - #1696: run-level completion webhook deduplication (replace claim-then-post with post-then-flag per run_id), DeferredErrorModel for graph-factory resilience, ToolRetryMiddleware for task subagents, TimeoutWrapupMiddleware for all three graphs - #1697: lazy-load __init__.py for agent.middleware, agent.tools, agent.dashboard (PEP 562); defer heavy imports (exa_py in web_search, agent.webapp in request_pr_review, deepagents in sandbox.py); add ttl_cache.py with stale-while-revalidate for tool loaders Refs: #137 * fix: restore login page render and clear CI lint/format The plan-review port removed the authRedirectUrl import from login.tsx but left its call site, crashing the login page at runtime (blank page, no 'Sign in to open-swe'). Pass the relative path straight to loginUrl, matching the plan route and the backend relative-redirect handling. Also drop an unused os import in the guard test and reformat workflow_push_guard.py to satisfy ruff. * feat: port model-fallback resilience from upstream (#1694, #1695) - Add httpx.TransportError to the transient-exception set so incomplete chunked reads on streamed responses trigger a fallback instead of cancelling the run (#1694 / c9f6dd86). - Rewrite fallback to alternate primary/fallback with exponential backoff instead of a single failover, so the agent survives multi-minute gateway outages spanning both providers (#1695 / c9a9a7cd). - Default backoff schedule (0, 5, 15, 30, 45) reaches past the gateway's ~30s recovery window; jittered ±25%. - On exhaustion, surface a terminal AIMessage explaining the outage instead of crashing — progress is checkpointed so the user can retrigger to continue. - Preserve fork conventions: Bedrock ClientError retryability check, sync wrap_model_call (using time.sleep instead of asyncio.sleep), and existing access-error surfacing for Anthropic, OpenAI, and Bedrock (botocore) provider errors. - Update triage ledger (c9f6dd86, c9a9a7cd → landed) and re-render triage.md. Refs: #139 * fix: align workflow-push-guard tests with dev's transient-elevation impl The dev merge auto-combined dev's elevation tests with the stale passthrough tests inherited from the durable-dispatch branch; the passthrough tests contradict dev's restored _run_with_workflow_token impl. Take dev's test file. * fix: drop dead ttl_cache module; make fallback backoff jitter two-sided ttl_cache.py was re-introduced via the dev merge but dev/#160 deliberately removed it as dead code (no agent importer). Remove it to match dev. Also make _jittered_delay symmetric (±25%) to match its docstring. * chore(upstream-sync): triage 4 new upstream commits (#1708-#1713) Synced ledger to upstream/main (71e3b818). New rows all deferred: - #1708 add GPT-5.6 OpenAI models (FLAG-HUMAN: fork picker is Bedrock/Fireworks-only) - #1709 stale admin model defaults after upgrades - #1710 bump langchain-fireworks 1.4.4 - #1713 align reviewer eval with published findings --------- Co-authored-by: amoussa1229 <166072409+amoussa1229@users.noreply.github.com> Co-authored-by: Adam Moussa <adam@seahavenind.com>
2026-07-09 18:28:21 -04:00
# Seconds slept before each retry attempt (attempt 0 is the initial call).
# The first failover is immediate: a provider-specific outage should not delay
# the cross-provider retry. Later delays grow past the ~30s the gateway's 502
# page asks for. Each attempt additionally benefits from the SDK's own
# ``max_retries`` backoff, so worst-case wall time before giving up is a few
# minutes — acceptable for a long-running agent, far better than crashing.
DEFAULT_BACKOFF_SCHEDULE: tuple[float, ...] = (0.0, 5.0, 15.0, 30.0, 45.0)
MODEL_OUTAGE_MESSAGE = (
"I wasn't able to reach the language model providers after several retries "
"(both the primary and fallback models returned transient errors, e.g. "
"502/503/overloaded). This is a temporary provider or gateway outage, not a "
"problem with your task. My progress so far has been saved — please retrigger "
"the run in a few minutes to continue."
)
def _should_fallback(exc: BaseException) -> bool:
if isinstance(exc, _TRANSIENT_EXCEPTIONS):
return True
# Catches OverloadedError (529) and other 5xx/429 surfaced as APIStatusError.
if isinstance(exc, (anthropic.APIStatusError, openai.APIStatusError)):
status = getattr(exc, "status_code", None)
if isinstance(status, int) and status in _RETRYABLE_STATUS_CODES:
return True
feat: migrate model providers to Bedrock (Claude) + Fireworks (everything else) (#62) * feat: switch model providers to AWS Bedrock (Claude) and Fireworks (non-Claude) Migrate off direct provider APIs: AWS Bedrock for Anthropic/Claude via the cross-region inference profile us.anthropic.claude-opus-4-8, Fireworks AI for all non-Claude models. Drop OpenAI (gpt-5.5) and Google (gemini-3.5-flash) entirely. DEFAULT_MODEL_ID is now Bedrock Claude; all Fireworks models stay freely selectable for the agent and reviewer graphs and via team/profile defaults. - pyproject: add langchain-aws (ChatBedrockConverse + boto3) - options.py: Bedrock Claude entry + default; remove openai/google entries - model.py: bedrock_converse provider_model_kwargs (effort -> thinking budget), region pin in make_model, bedrock<->fireworks fallback pairing, AWS_REGION/ FIREWORKS_API_KEY local-dev validation - server.py: provider-aware fallback kwargs build - sanitize_thinking_blocks: also sanitize ChatBedrockConverse thinking blocks - model_fallback: treat transient botocore ClientError codes as fallback-worthy - eval_jobs: repoint hardcoded eval model id to Bedrock Claude - tests: repoint dropped model ids; drop obsolete google test module * fix(bedrock): use adaptive thinking + output_config.effort for Opus 4.8 The handoff spec wired Bedrock Converse thinking as {type: enabled, budget_tokens: N}, but Opus 4.7+ rejects that with a ValidationException: thinking.type "enabled" is not supported; it requires thinking.type "adaptive" plus output_config.effort. Verified by live invoke against us.anthropic.claude-opus-4-8 (account 328440206208, us-east-1): the enabled+budget shape 400s, adaptive+effort returns normally. Map profile effort to additional_model_request_fields: {thinking: {type: adaptive, display: summarized}, output_config: {effort: <low|medium|high|xhigh|max>}} reusing anthropic_thinking_for/anthropic_effort_for. Update the two subagent-model tests asserting the old shape. * fix(deploy): seed Bedrock/Fireworks models, not the dropped anthropic:/openai: ids Model selection is store-driven, so seed_store.sh's team_settings/default seed is what runs in prod. It still seeded the removed providers, which would fail at runtime after the migration: - agent/builder: anthropic:claude-opus-4-8 -> bedrock_converse:us.anthropic.claude-opus-4-8 - reviewer: openai:gpt-5.5 (dropped) -> bedrock_converse:us.anthropic.claude-opus-4-8 (set SEED_REVIEWER_MODEL to a Fireworks model for a cross-family reviewer) - fetch-config REQUIRED_PROVIDER_KEYS default ANTHROPIC_API_KEY,OPENAI_API_KEY -> FIREWORKS_API_KEY (Bedrock auths via host IAM role; dropping the old keys would otherwise fail-fast at boot) - docs (DEPLOYMENT/ROTATION/put-config) updated to match. Surfaced by the cross-family review + verified against deploy/. * fix(bedrock): security-review NITs — region resolution, error sanitization, reasoning-block strip From /sh-security-review (all confirmed-low): - model.py: resolve region from AWS_REGION OR AWS_DEFAULT_REGION (matches validate_local_dev_llm_config) so the validated region is the one actually used. - model_fallback.py: sanitize Bedrock AccessDenied/ResourceNotFound errors to the error code only, so the role ARN + account id in the raw botocore message never reach logs or the user channel (CWE-209). - sanitize_thinking_blocks.py: also strip empty Bedrock reasoning_content blocks (Converse emits reasoning_content, not thinking) so the middleware is not a no-op on Bedrock; + unit tests. (Empty blocks replay fine today; defensive.) * deploy(bedrock): grant instance-role Bedrock invoke + repoint LLM_MODEL_ID / eval model ids Deployment-readiness for the Bedrock migration (PR #62): - instance-role.ts: least-privilege bedrock:InvokeModel[WithResponseStream] on the us.anthropic.claude-opus-4-8 inference-profile ARN + the foundation-model ARN in each routed region (us-east-1/2, us-west-2). The model runs in the server process on the box, so the EC2 instance role is the principal. Simulator-verified (allowed for opus-4-8, implicitDeny for other models) and synth-verified. Passed the mandatory GPT-4.1 IAM cross-review (no blockers, least-privilege confirmed). - config-store.ts: IaC SSM LLM_MODEL_ID anthropic:claude-opus-4-8 -> bedrock_converse:us.anthropic.claude-opus-4-8. This SSM value overrides seed_store.sh's default via pick precedence, so the seed-script fix alone was insufficient — both sources now point at the supported Bedrock id. - infra/README.md + evals/reviewer/config.toml: repoint stale anthropic:/google_genai: ids to the Bedrock id (config.toml's model_id was an active, now-broken value). AWS_REGION is already wired via user-data.sh (IMDS -> boot.env), so no change needed there. * chore(secrets): drop OPENAI/GOOGLE/GROQ key shells (revoked, providers removed) Those three providers were dropped in the Bedrock/Fireworks migration and their keys revoked; the live Secrets Manager objects (open-swe-{dev,prod}/{OPENAI,GOOGLE,GROQ}_API_KEY) were deleted (7-day recovery). Remove them from the IaC so a future cdk deploy does not recreate the shells, and from fetch-config's mirror array so boot stops requesting them: - config-store.ts SECRET_VARS + descriptions (28 -> 25 shells) - fetch-config.sh SECRET_VARS array (kept in lockstep) - put-config.sh: drop the put_secret lines; ANTHROPIC_API_KEY re-labelled optional (eval judge only — Bedrock builder/reviewer auth via the host IAM role). REQUIRED_PROVIDER_KEYS is not set in SSM, so it uses the FIREWORKS_API_KEY default.
2026-06-29 15:57:19 -04:00
# Bedrock (Claude) raises botocore ClientError for transient throttling/5xx.
if isinstance(exc, ClientError):
code = exc.response.get("Error", {}).get("Code", "")
if code in _RETRYABLE_BEDROCK_ERROR_CODES:
return True
return False
def _error_body(exc: BaseException) -> dict[str, Any]:
body = getattr(exc, "body", None)
return body if isinstance(body, dict) else {}
def _nested_str(data: dict[str, Any], *keys: str) -> str | None:
current: Any = data
for key in keys:
if not isinstance(current, dict):
return None
current = current.get(key)
return current if isinstance(current, str) and current else None
def _provider_access_error_message(exc: BaseException) -> str | None:
if isinstance(exc, anthropic.BadRequestError):
body = _error_body(exc)
error_code = _nested_str(body, "error", "details", "error_code")
if error_code == "model_not_available":
provider_message = _nested_str(body, "error", "message") or str(exc)
return (
"The selected Anthropic model is not available to this workspace. "
f"Anthropic returned: {provider_message} "
"Choose a different model or update the workspace's Anthropic access and retry."
)
if isinstance(exc, (openai.BadRequestError, openai.NotFoundError)):
body = _error_body(exc)
error_code = _nested_str(body, "error", "code")
if error_code in {"model_not_found", "model_not_available"}:
provider_message = _nested_str(body, "error", "message") or str(exc)
return (
"The selected OpenAI model is not available to this workspace. "
f"OpenAI returned: {provider_message} "
"Choose a different model or update the workspace's OpenAI access and retry."
)
feat: migrate model providers to Bedrock (Claude) + Fireworks (everything else) (#62) * feat: switch model providers to AWS Bedrock (Claude) and Fireworks (non-Claude) Migrate off direct provider APIs: AWS Bedrock for Anthropic/Claude via the cross-region inference profile us.anthropic.claude-opus-4-8, Fireworks AI for all non-Claude models. Drop OpenAI (gpt-5.5) and Google (gemini-3.5-flash) entirely. DEFAULT_MODEL_ID is now Bedrock Claude; all Fireworks models stay freely selectable for the agent and reviewer graphs and via team/profile defaults. - pyproject: add langchain-aws (ChatBedrockConverse + boto3) - options.py: Bedrock Claude entry + default; remove openai/google entries - model.py: bedrock_converse provider_model_kwargs (effort -> thinking budget), region pin in make_model, bedrock<->fireworks fallback pairing, AWS_REGION/ FIREWORKS_API_KEY local-dev validation - server.py: provider-aware fallback kwargs build - sanitize_thinking_blocks: also sanitize ChatBedrockConverse thinking blocks - model_fallback: treat transient botocore ClientError codes as fallback-worthy - eval_jobs: repoint hardcoded eval model id to Bedrock Claude - tests: repoint dropped model ids; drop obsolete google test module * fix(bedrock): use adaptive thinking + output_config.effort for Opus 4.8 The handoff spec wired Bedrock Converse thinking as {type: enabled, budget_tokens: N}, but Opus 4.7+ rejects that with a ValidationException: thinking.type "enabled" is not supported; it requires thinking.type "adaptive" plus output_config.effort. Verified by live invoke against us.anthropic.claude-opus-4-8 (account 328440206208, us-east-1): the enabled+budget shape 400s, adaptive+effort returns normally. Map profile effort to additional_model_request_fields: {thinking: {type: adaptive, display: summarized}, output_config: {effort: <low|medium|high|xhigh|max>}} reusing anthropic_thinking_for/anthropic_effort_for. Update the two subagent-model tests asserting the old shape. * fix(deploy): seed Bedrock/Fireworks models, not the dropped anthropic:/openai: ids Model selection is store-driven, so seed_store.sh's team_settings/default seed is what runs in prod. It still seeded the removed providers, which would fail at runtime after the migration: - agent/builder: anthropic:claude-opus-4-8 -> bedrock_converse:us.anthropic.claude-opus-4-8 - reviewer: openai:gpt-5.5 (dropped) -> bedrock_converse:us.anthropic.claude-opus-4-8 (set SEED_REVIEWER_MODEL to a Fireworks model for a cross-family reviewer) - fetch-config REQUIRED_PROVIDER_KEYS default ANTHROPIC_API_KEY,OPENAI_API_KEY -> FIREWORKS_API_KEY (Bedrock auths via host IAM role; dropping the old keys would otherwise fail-fast at boot) - docs (DEPLOYMENT/ROTATION/put-config) updated to match. Surfaced by the cross-family review + verified against deploy/. * fix(bedrock): security-review NITs — region resolution, error sanitization, reasoning-block strip From /sh-security-review (all confirmed-low): - model.py: resolve region from AWS_REGION OR AWS_DEFAULT_REGION (matches validate_local_dev_llm_config) so the validated region is the one actually used. - model_fallback.py: sanitize Bedrock AccessDenied/ResourceNotFound errors to the error code only, so the role ARN + account id in the raw botocore message never reach logs or the user channel (CWE-209). - sanitize_thinking_blocks.py: also strip empty Bedrock reasoning_content blocks (Converse emits reasoning_content, not thinking) so the middleware is not a no-op on Bedrock; + unit tests. (Empty blocks replay fine today; defensive.) * deploy(bedrock): grant instance-role Bedrock invoke + repoint LLM_MODEL_ID / eval model ids Deployment-readiness for the Bedrock migration (PR #62): - instance-role.ts: least-privilege bedrock:InvokeModel[WithResponseStream] on the us.anthropic.claude-opus-4-8 inference-profile ARN + the foundation-model ARN in each routed region (us-east-1/2, us-west-2). The model runs in the server process on the box, so the EC2 instance role is the principal. Simulator-verified (allowed for opus-4-8, implicitDeny for other models) and synth-verified. Passed the mandatory GPT-4.1 IAM cross-review (no blockers, least-privilege confirmed). - config-store.ts: IaC SSM LLM_MODEL_ID anthropic:claude-opus-4-8 -> bedrock_converse:us.anthropic.claude-opus-4-8. This SSM value overrides seed_store.sh's default via pick precedence, so the seed-script fix alone was insufficient — both sources now point at the supported Bedrock id. - infra/README.md + evals/reviewer/config.toml: repoint stale anthropic:/google_genai: ids to the Bedrock id (config.toml's model_id was an active, now-broken value). AWS_REGION is already wired via user-data.sh (IMDS -> boot.env), so no change needed there. * chore(secrets): drop OPENAI/GOOGLE/GROQ key shells (revoked, providers removed) Those three providers were dropped in the Bedrock/Fireworks migration and their keys revoked; the live Secrets Manager objects (open-swe-{dev,prod}/{OPENAI,GOOGLE,GROQ}_API_KEY) were deleted (7-day recovery). Remove them from the IaC so a future cdk deploy does not recreate the shells, and from fetch-config's mirror array so boot stops requesting them: - config-store.ts SECRET_VARS + descriptions (28 -> 25 shells) - fetch-config.sh SECRET_VARS array (kept in lockstep) - put-config.sh: drop the put_secret lines; ANTHROPIC_API_KEY re-labelled optional (eval judge only — Bedrock builder/reviewer auth via the host IAM role). REQUIRED_PROVIDER_KEYS is not set in SSM, so it uses the FIREWORKS_API_KEY default.
2026-06-29 15:57:19 -04:00
# Bedrock access/lookup failures embed the caller's role ARN and account id in the
# raw botocore message; surface only the error code so identifiers never reach logs
# or the user-facing channel.
if isinstance(exc, ClientError):
code = exc.response.get("Error", {}).get("Code", "")
if code in {"AccessDeniedException", "ResourceNotFoundException"}:
return (
"The selected Bedrock model is not available to this deployment "
f"(Bedrock error: {code}). Verify the model's inference-profile access and "
"IAM permissions, choose a different model, and retry."
)
return None
class ModelFallbackMiddleware(AgentMiddleware):
feat: port model-fallback resilience from upstream (#1694, #1695) (#161) * feat: port plan-review & workflow-approval UX (#135) Port six upstream commits onto dev: - c03a6be7 (already ported): keep plan guidance high-level - 546042a4: add workflow approval UI with diff preview, approval URLs, web review links, and polling for approval status during active runs - 216cf181: remove workflow token elevation; approved pushes pass through directly without proxy token rewriting - 3dbc0282: preserve plan redirects after login by accepting relative same-origin redirect_to values and rejecting blocked paths - bb104d93: submit plan comments with cmd+enter - 90cb6caa: terse Slack replies, shared content via save_plan outside plan mode (PLAN_STATUS_SHARED), reject shared-content mutations Refs: #135 * feat: port durable dispatch hardening and startup latency improvements Port five upstream PRs onto dev: - #1621 / #1658: durable dispatch with loopback webhook defense, create_durable_run helper, _config_with_prepare_run_id, degradation to None for relative/loopback completion webhook URLs - #1696: run-level completion webhook deduplication (replace claim-then-post with post-then-flag per run_id), DeferredErrorModel for graph-factory resilience, ToolRetryMiddleware for task subagents, TimeoutWrapupMiddleware for all three graphs - #1697: lazy-load __init__.py for agent.middleware, agent.tools, agent.dashboard (PEP 562); defer heavy imports (exa_py in web_search, agent.webapp in request_pr_review, deepagents in sandbox.py); add ttl_cache.py with stale-while-revalidate for tool loaders Refs: #137 * fix: restore login page render and clear CI lint/format The plan-review port removed the authRedirectUrl import from login.tsx but left its call site, crashing the login page at runtime (blank page, no 'Sign in to open-swe'). Pass the relative path straight to loginUrl, matching the plan route and the backend relative-redirect handling. Also drop an unused os import in the guard test and reformat workflow_push_guard.py to satisfy ruff. * feat: port model-fallback resilience from upstream (#1694, #1695) - Add httpx.TransportError to the transient-exception set so incomplete chunked reads on streamed responses trigger a fallback instead of cancelling the run (#1694 / c9f6dd86). - Rewrite fallback to alternate primary/fallback with exponential backoff instead of a single failover, so the agent survives multi-minute gateway outages spanning both providers (#1695 / c9a9a7cd). - Default backoff schedule (0, 5, 15, 30, 45) reaches past the gateway's ~30s recovery window; jittered ±25%. - On exhaustion, surface a terminal AIMessage explaining the outage instead of crashing — progress is checkpointed so the user can retrigger to continue. - Preserve fork conventions: Bedrock ClientError retryability check, sync wrap_model_call (using time.sleep instead of asyncio.sleep), and existing access-error surfacing for Anthropic, OpenAI, and Bedrock (botocore) provider errors. - Update triage ledger (c9f6dd86, c9a9a7cd → landed) and re-render triage.md. Refs: #139 * fix: align workflow-push-guard tests with dev's transient-elevation impl The dev merge auto-combined dev's elevation tests with the stale passthrough tests inherited from the durable-dispatch branch; the passthrough tests contradict dev's restored _run_with_workflow_token impl. Take dev's test file. * fix: drop dead ttl_cache module; make fallback backoff jitter two-sided ttl_cache.py was re-introduced via the dev merge but dev/#160 deliberately removed it as dead code (no agent importer). Remove it to match dev. Also make _jittered_delay symmetric (±25%) to match its docstring. * chore(upstream-sync): triage 4 new upstream commits (#1708-#1713) Synced ledger to upstream/main (71e3b818). New rows all deferred: - #1708 add GPT-5.6 OpenAI models (FLAG-HUMAN: fork picker is Bedrock/Fireworks-only) - #1709 stale admin model defaults after upgrades - #1710 bump langchain-fireworks 1.4.4 - #1713 align reviewer eval with published findings --------- Co-authored-by: amoussa1229 <166072409+amoussa1229@users.noreply.github.com> Co-authored-by: Adam Moussa <adam@seahavenind.com>
2026-07-09 18:28:21 -04:00
"""Retry the model call across primary and fallback providers on transient errors.
Args:
fallback_model: Cross-provider model used on odd-numbered attempts.
backoff_schedule: Seconds slept before each retry. ``len(schedule) + 1``
is the total number of attempts. Delays get ±25% jitter.
surface_outage_message: When all attempts fail, return a terminal
``AIMessage`` describing the outage instead of raising, so the run
ends gracefully with a user-visible message rather than a crash.
Set to ``False`` to re-raise the last error (e.g. if platform-level
alerting keys off failed runs).
"""
feat: port model-fallback resilience from upstream (#1694, #1695) (#161) * feat: port plan-review & workflow-approval UX (#135) Port six upstream commits onto dev: - c03a6be7 (already ported): keep plan guidance high-level - 546042a4: add workflow approval UI with diff preview, approval URLs, web review links, and polling for approval status during active runs - 216cf181: remove workflow token elevation; approved pushes pass through directly without proxy token rewriting - 3dbc0282: preserve plan redirects after login by accepting relative same-origin redirect_to values and rejecting blocked paths - bb104d93: submit plan comments with cmd+enter - 90cb6caa: terse Slack replies, shared content via save_plan outside plan mode (PLAN_STATUS_SHARED), reject shared-content mutations Refs: #135 * feat: port durable dispatch hardening and startup latency improvements Port five upstream PRs onto dev: - #1621 / #1658: durable dispatch with loopback webhook defense, create_durable_run helper, _config_with_prepare_run_id, degradation to None for relative/loopback completion webhook URLs - #1696: run-level completion webhook deduplication (replace claim-then-post with post-then-flag per run_id), DeferredErrorModel for graph-factory resilience, ToolRetryMiddleware for task subagents, TimeoutWrapupMiddleware for all three graphs - #1697: lazy-load __init__.py for agent.middleware, agent.tools, agent.dashboard (PEP 562); defer heavy imports (exa_py in web_search, agent.webapp in request_pr_review, deepagents in sandbox.py); add ttl_cache.py with stale-while-revalidate for tool loaders Refs: #137 * fix: restore login page render and clear CI lint/format The plan-review port removed the authRedirectUrl import from login.tsx but left its call site, crashing the login page at runtime (blank page, no 'Sign in to open-swe'). Pass the relative path straight to loginUrl, matching the plan route and the backend relative-redirect handling. Also drop an unused os import in the guard test and reformat workflow_push_guard.py to satisfy ruff. * feat: port model-fallback resilience from upstream (#1694, #1695) - Add httpx.TransportError to the transient-exception set so incomplete chunked reads on streamed responses trigger a fallback instead of cancelling the run (#1694 / c9f6dd86). - Rewrite fallback to alternate primary/fallback with exponential backoff instead of a single failover, so the agent survives multi-minute gateway outages spanning both providers (#1695 / c9a9a7cd). - Default backoff schedule (0, 5, 15, 30, 45) reaches past the gateway's ~30s recovery window; jittered ±25%. - On exhaustion, surface a terminal AIMessage explaining the outage instead of crashing — progress is checkpointed so the user can retrigger to continue. - Preserve fork conventions: Bedrock ClientError retryability check, sync wrap_model_call (using time.sleep instead of asyncio.sleep), and existing access-error surfacing for Anthropic, OpenAI, and Bedrock (botocore) provider errors. - Update triage ledger (c9f6dd86, c9a9a7cd → landed) and re-render triage.md. Refs: #139 * fix: align workflow-push-guard tests with dev's transient-elevation impl The dev merge auto-combined dev's elevation tests with the stale passthrough tests inherited from the durable-dispatch branch; the passthrough tests contradict dev's restored _run_with_workflow_token impl. Take dev's test file. * fix: drop dead ttl_cache module; make fallback backoff jitter two-sided ttl_cache.py was re-introduced via the dev merge but dev/#160 deliberately removed it as dead code (no agent importer). Remove it to match dev. Also make _jittered_delay symmetric (±25%) to match its docstring. * chore(upstream-sync): triage 4 new upstream commits (#1708-#1713) Synced ledger to upstream/main (71e3b818). New rows all deferred: - #1708 add GPT-5.6 OpenAI models (FLAG-HUMAN: fork picker is Bedrock/Fireworks-only) - #1709 stale admin model defaults after upgrades - #1710 bump langchain-fireworks 1.4.4 - #1713 align reviewer eval with published findings --------- Co-authored-by: amoussa1229 <166072409+amoussa1229@users.noreply.github.com> Co-authored-by: Adam Moussa <adam@seahavenind.com>
2026-07-09 18:28:21 -04:00
def __init__(
self,
fallback_model: BaseChatModel,
*,
backoff_schedule: Sequence[float] = DEFAULT_BACKOFF_SCHEDULE,
surface_outage_message: bool = True,
) -> None:
super().__init__()
self._fallback_model = fallback_model
feat: port model-fallback resilience from upstream (#1694, #1695) (#161) * feat: port plan-review & workflow-approval UX (#135) Port six upstream commits onto dev: - c03a6be7 (already ported): keep plan guidance high-level - 546042a4: add workflow approval UI with diff preview, approval URLs, web review links, and polling for approval status during active runs - 216cf181: remove workflow token elevation; approved pushes pass through directly without proxy token rewriting - 3dbc0282: preserve plan redirects after login by accepting relative same-origin redirect_to values and rejecting blocked paths - bb104d93: submit plan comments with cmd+enter - 90cb6caa: terse Slack replies, shared content via save_plan outside plan mode (PLAN_STATUS_SHARED), reject shared-content mutations Refs: #135 * feat: port durable dispatch hardening and startup latency improvements Port five upstream PRs onto dev: - #1621 / #1658: durable dispatch with loopback webhook defense, create_durable_run helper, _config_with_prepare_run_id, degradation to None for relative/loopback completion webhook URLs - #1696: run-level completion webhook deduplication (replace claim-then-post with post-then-flag per run_id), DeferredErrorModel for graph-factory resilience, ToolRetryMiddleware for task subagents, TimeoutWrapupMiddleware for all three graphs - #1697: lazy-load __init__.py for agent.middleware, agent.tools, agent.dashboard (PEP 562); defer heavy imports (exa_py in web_search, agent.webapp in request_pr_review, deepagents in sandbox.py); add ttl_cache.py with stale-while-revalidate for tool loaders Refs: #137 * fix: restore login page render and clear CI lint/format The plan-review port removed the authRedirectUrl import from login.tsx but left its call site, crashing the login page at runtime (blank page, no 'Sign in to open-swe'). Pass the relative path straight to loginUrl, matching the plan route and the backend relative-redirect handling. Also drop an unused os import in the guard test and reformat workflow_push_guard.py to satisfy ruff. * feat: port model-fallback resilience from upstream (#1694, #1695) - Add httpx.TransportError to the transient-exception set so incomplete chunked reads on streamed responses trigger a fallback instead of cancelling the run (#1694 / c9f6dd86). - Rewrite fallback to alternate primary/fallback with exponential backoff instead of a single failover, so the agent survives multi-minute gateway outages spanning both providers (#1695 / c9a9a7cd). - Default backoff schedule (0, 5, 15, 30, 45) reaches past the gateway's ~30s recovery window; jittered ±25%. - On exhaustion, surface a terminal AIMessage explaining the outage instead of crashing — progress is checkpointed so the user can retrigger to continue. - Preserve fork conventions: Bedrock ClientError retryability check, sync wrap_model_call (using time.sleep instead of asyncio.sleep), and existing access-error surfacing for Anthropic, OpenAI, and Bedrock (botocore) provider errors. - Update triage ledger (c9f6dd86, c9a9a7cd → landed) and re-render triage.md. Refs: #139 * fix: align workflow-push-guard tests with dev's transient-elevation impl The dev merge auto-combined dev's elevation tests with the stale passthrough tests inherited from the durable-dispatch branch; the passthrough tests contradict dev's restored _run_with_workflow_token impl. Take dev's test file. * fix: drop dead ttl_cache module; make fallback backoff jitter two-sided ttl_cache.py was re-introduced via the dev merge but dev/#160 deliberately removed it as dead code (no agent importer). Remove it to match dev. Also make _jittered_delay symmetric (±25%) to match its docstring. * chore(upstream-sync): triage 4 new upstream commits (#1708-#1713) Synced ledger to upstream/main (71e3b818). New rows all deferred: - #1708 add GPT-5.6 OpenAI models (FLAG-HUMAN: fork picker is Bedrock/Fireworks-only) - #1709 stale admin model defaults after upgrades - #1710 bump langchain-fireworks 1.4.4 - #1713 align reviewer eval with published findings --------- Co-authored-by: amoussa1229 <166072409+amoussa1229@users.noreply.github.com> Co-authored-by: Adam Moussa <adam@seahavenind.com>
2026-07-09 18:28:21 -04:00
self._backoff_schedule = tuple(backoff_schedule)
self._surface_outage_message = surface_outage_message
def _fallback_name(self) -> str:
return (
getattr(self._fallback_model, "model_name", None)
or getattr(self._fallback_model, "model", None)
or "fallback"
)
def _jittered_delay(self, delay: float) -> float:
return delay + random.uniform(-delay * 0.25, delay * 0.25) if delay > 0 else 0.0
def _log_retry(
self, exc_type: str, use_fallback: bool, attempt: int, total: int, delay: float
) -> None:
logger.warning(
"Model call failed transiently (%s) on %s model "
"(attempt %d/%d); retrying %s model in %.1fs",
exc_type,
"fallback" if use_fallback else "primary",
attempt + 1,
total,
"primary" if use_fallback else f"fallback ({self._fallback_name()})",
delay,
)
def _resolve_request(self, request: ModelRequest, use_fallback: bool) -> ModelRequest:
return request.override(model=self._fallback_model) if use_fallback else request
def wrap_model_call(
self,
request: ModelRequest,
handler: Callable[[ModelRequest], ModelResponse],
) -> ModelCallResult:
feat: port model-fallback resilience from upstream (#1694, #1695) (#161) * feat: port plan-review & workflow-approval UX (#135) Port six upstream commits onto dev: - c03a6be7 (already ported): keep plan guidance high-level - 546042a4: add workflow approval UI with diff preview, approval URLs, web review links, and polling for approval status during active runs - 216cf181: remove workflow token elevation; approved pushes pass through directly without proxy token rewriting - 3dbc0282: preserve plan redirects after login by accepting relative same-origin redirect_to values and rejecting blocked paths - bb104d93: submit plan comments with cmd+enter - 90cb6caa: terse Slack replies, shared content via save_plan outside plan mode (PLAN_STATUS_SHARED), reject shared-content mutations Refs: #135 * feat: port durable dispatch hardening and startup latency improvements Port five upstream PRs onto dev: - #1621 / #1658: durable dispatch with loopback webhook defense, create_durable_run helper, _config_with_prepare_run_id, degradation to None for relative/loopback completion webhook URLs - #1696: run-level completion webhook deduplication (replace claim-then-post with post-then-flag per run_id), DeferredErrorModel for graph-factory resilience, ToolRetryMiddleware for task subagents, TimeoutWrapupMiddleware for all three graphs - #1697: lazy-load __init__.py for agent.middleware, agent.tools, agent.dashboard (PEP 562); defer heavy imports (exa_py in web_search, agent.webapp in request_pr_review, deepagents in sandbox.py); add ttl_cache.py with stale-while-revalidate for tool loaders Refs: #137 * fix: restore login page render and clear CI lint/format The plan-review port removed the authRedirectUrl import from login.tsx but left its call site, crashing the login page at runtime (blank page, no 'Sign in to open-swe'). Pass the relative path straight to loginUrl, matching the plan route and the backend relative-redirect handling. Also drop an unused os import in the guard test and reformat workflow_push_guard.py to satisfy ruff. * feat: port model-fallback resilience from upstream (#1694, #1695) - Add httpx.TransportError to the transient-exception set so incomplete chunked reads on streamed responses trigger a fallback instead of cancelling the run (#1694 / c9f6dd86). - Rewrite fallback to alternate primary/fallback with exponential backoff instead of a single failover, so the agent survives multi-minute gateway outages spanning both providers (#1695 / c9a9a7cd). - Default backoff schedule (0, 5, 15, 30, 45) reaches past the gateway's ~30s recovery window; jittered ±25%. - On exhaustion, surface a terminal AIMessage explaining the outage instead of crashing — progress is checkpointed so the user can retrigger to continue. - Preserve fork conventions: Bedrock ClientError retryability check, sync wrap_model_call (using time.sleep instead of asyncio.sleep), and existing access-error surfacing for Anthropic, OpenAI, and Bedrock (botocore) provider errors. - Update triage ledger (c9f6dd86, c9a9a7cd → landed) and re-render triage.md. Refs: #139 * fix: align workflow-push-guard tests with dev's transient-elevation impl The dev merge auto-combined dev's elevation tests with the stale passthrough tests inherited from the durable-dispatch branch; the passthrough tests contradict dev's restored _run_with_workflow_token impl. Take dev's test file. * fix: drop dead ttl_cache module; make fallback backoff jitter two-sided ttl_cache.py was re-introduced via the dev merge but dev/#160 deliberately removed it as dead code (no agent importer). Remove it to match dev. Also make _jittered_delay symmetric (±25%) to match its docstring. * chore(upstream-sync): triage 4 new upstream commits (#1708-#1713) Synced ledger to upstream/main (71e3b818). New rows all deferred: - #1708 add GPT-5.6 OpenAI models (FLAG-HUMAN: fork picker is Bedrock/Fireworks-only) - #1709 stale admin model defaults after upgrades - #1710 bump langchain-fireworks 1.4.4 - #1713 align reviewer eval with published findings --------- Co-authored-by: amoussa1229 <166072409+amoussa1229@users.noreply.github.com> Co-authored-by: Adam Moussa <adam@seahavenind.com>
2026-07-09 18:28:21 -04:00
total_attempts = len(self._backoff_schedule) + 1
last_exc: BaseException | None = None
for attempt in range(total_attempts):
use_fallback = attempt % 2 == 1
attempt_request = self._resolve_request(request, use_fallback)
try:
return handler(attempt_request)
except Exception as exc:
access_error_message = _provider_access_error_message(exc)
if access_error_message is not None:
logger.warning("Model access error surfaced to user: %s", type(exc).__name__)
return AIMessage(content=access_error_message)
if not _should_fallback(exc):
raise
last_exc = exc
if attempt + 1 >= total_attempts:
break
delay = self._jittered_delay(self._backoff_schedule[attempt])
self._log_retry(type(exc).__name__, use_fallback, attempt, total_attempts, delay)
if delay > 0:
time.sleep(delay)
assert last_exc is not None
logger.error(
"Model call failed after %d attempts across primary and fallback (%s): %s",
total_attempts,
self._fallback_name(),
last_exc,
)
if self._surface_outage_message:
return AIMessage(content=MODEL_OUTAGE_MESSAGE)
raise last_exc
async def awrap_model_call(
self,
request: ModelRequest,
handler: Callable[[ModelRequest], Awaitable[ModelResponse]],
) -> Any:
feat: port model-fallback resilience from upstream (#1694, #1695) (#161) * feat: port plan-review & workflow-approval UX (#135) Port six upstream commits onto dev: - c03a6be7 (already ported): keep plan guidance high-level - 546042a4: add workflow approval UI with diff preview, approval URLs, web review links, and polling for approval status during active runs - 216cf181: remove workflow token elevation; approved pushes pass through directly without proxy token rewriting - 3dbc0282: preserve plan redirects after login by accepting relative same-origin redirect_to values and rejecting blocked paths - bb104d93: submit plan comments with cmd+enter - 90cb6caa: terse Slack replies, shared content via save_plan outside plan mode (PLAN_STATUS_SHARED), reject shared-content mutations Refs: #135 * feat: port durable dispatch hardening and startup latency improvements Port five upstream PRs onto dev: - #1621 / #1658: durable dispatch with loopback webhook defense, create_durable_run helper, _config_with_prepare_run_id, degradation to None for relative/loopback completion webhook URLs - #1696: run-level completion webhook deduplication (replace claim-then-post with post-then-flag per run_id), DeferredErrorModel for graph-factory resilience, ToolRetryMiddleware for task subagents, TimeoutWrapupMiddleware for all three graphs - #1697: lazy-load __init__.py for agent.middleware, agent.tools, agent.dashboard (PEP 562); defer heavy imports (exa_py in web_search, agent.webapp in request_pr_review, deepagents in sandbox.py); add ttl_cache.py with stale-while-revalidate for tool loaders Refs: #137 * fix: restore login page render and clear CI lint/format The plan-review port removed the authRedirectUrl import from login.tsx but left its call site, crashing the login page at runtime (blank page, no 'Sign in to open-swe'). Pass the relative path straight to loginUrl, matching the plan route and the backend relative-redirect handling. Also drop an unused os import in the guard test and reformat workflow_push_guard.py to satisfy ruff. * feat: port model-fallback resilience from upstream (#1694, #1695) - Add httpx.TransportError to the transient-exception set so incomplete chunked reads on streamed responses trigger a fallback instead of cancelling the run (#1694 / c9f6dd86). - Rewrite fallback to alternate primary/fallback with exponential backoff instead of a single failover, so the agent survives multi-minute gateway outages spanning both providers (#1695 / c9a9a7cd). - Default backoff schedule (0, 5, 15, 30, 45) reaches past the gateway's ~30s recovery window; jittered ±25%. - On exhaustion, surface a terminal AIMessage explaining the outage instead of crashing — progress is checkpointed so the user can retrigger to continue. - Preserve fork conventions: Bedrock ClientError retryability check, sync wrap_model_call (using time.sleep instead of asyncio.sleep), and existing access-error surfacing for Anthropic, OpenAI, and Bedrock (botocore) provider errors. - Update triage ledger (c9f6dd86, c9a9a7cd → landed) and re-render triage.md. Refs: #139 * fix: align workflow-push-guard tests with dev's transient-elevation impl The dev merge auto-combined dev's elevation tests with the stale passthrough tests inherited from the durable-dispatch branch; the passthrough tests contradict dev's restored _run_with_workflow_token impl. Take dev's test file. * fix: drop dead ttl_cache module; make fallback backoff jitter two-sided ttl_cache.py was re-introduced via the dev merge but dev/#160 deliberately removed it as dead code (no agent importer). Remove it to match dev. Also make _jittered_delay symmetric (±25%) to match its docstring. * chore(upstream-sync): triage 4 new upstream commits (#1708-#1713) Synced ledger to upstream/main (71e3b818). New rows all deferred: - #1708 add GPT-5.6 OpenAI models (FLAG-HUMAN: fork picker is Bedrock/Fireworks-only) - #1709 stale admin model defaults after upgrades - #1710 bump langchain-fireworks 1.4.4 - #1713 align reviewer eval with published findings --------- Co-authored-by: amoussa1229 <166072409+amoussa1229@users.noreply.github.com> Co-authored-by: Adam Moussa <adam@seahavenind.com>
2026-07-09 18:28:21 -04:00
total_attempts = len(self._backoff_schedule) + 1
last_exc: BaseException | None = None
for attempt in range(total_attempts):
use_fallback = attempt % 2 == 1
attempt_request = self._resolve_request(request, use_fallback)
try:
return await handler(attempt_request)
except Exception as exc:
access_error_message = _provider_access_error_message(exc)
if access_error_message is not None:
logger.warning("Model access error surfaced to user: %s", type(exc).__name__)
return AIMessage(content=access_error_message)
if not _should_fallback(exc):
raise
last_exc = exc
if attempt + 1 >= total_attempts:
break
delay = self._jittered_delay(self._backoff_schedule[attempt])
self._log_retry(type(exc).__name__, use_fallback, attempt, total_attempts, delay)
if delay > 0:
await asyncio.sleep(delay)
assert last_exc is not None
logger.error(
"Model call failed after %d attempts across primary and fallback (%s): %s",
total_attempts,
self._fallback_name(),
last_exc,
)
if self._surface_outage_message:
return AIMessage(content=MODEL_OUTAGE_MESSAGE)
raise last_exc