mirror of
https://github.com/Sea-Haven-Industries/open-swe.git
synced 2026-09-30 10:23:14 +00:00
* fix(webhooks): fall back to vision model for Slack/Linear image threads Re-land upstream #1626 onto the modular webhook structure. When a Slack mention or Linear issue carries images but the resolved model is text-only, fall back to a vision-capable model instead of dropping the images. Re-points default_vision_model_pair at the fork's image-capable models (Opus 4.8 default, else any supports_images model) rather than upstream's openai:/anthropic: provider filter. Refs #80, upstream #1626 * fix(slack): persist trace_message_ts so web-handoff updates the trace reply Re-land upstream #1630 onto the modular structure. The first-mention store_slack_run_mapping call did not pass trace_message_ts, so it was never persisted (nothing to preserve from on first mention) and _notify_slack_web_handoff always skipped the trace-reply update on web handoff. Pass it through and cover it with a test. Refs #80, upstream #1630 * feat(slack): include channel context in Slack prompts Re-land upstream #1633 onto the modular structure. Fetch cached Slack channel metadata once per event (_get_slack_channel_context) and thread it through the docs-plz gate, repo resolution, and process_slack_mention so prompts carry the channel name and a clearly-marked untrusted channel description. Avoids duplicate conversations.info calls. Refs #80, upstream #1633 * feat(tools): add slack_start_new_thread breakout tool Re-land upstream #1638 onto the modular structure. Adds the slack_start_new_thread tool (posts a top-level Slack message and dispatches a fresh agent run for a broken-out task via the durable dispatch_agent_run contract), wires it into the agent tool list and tools/__init__, adds prompt guidance, and excludes it from plan mode so it can't bypass the approval flow. Tool imports only live modules. Refs #80, upstream #1638 * feat(plan): notify Slack on plan approval Re-land upstream #1632 onto the modular structure. When a plan is approved via the dashboard approve endpoint, post a thread reply to the originating Slack thread noting the comment count and approver, after the follow-up run is dispatched. Slack post failures never break approval. Adapted to the fork's approve_plan (no plan_markdown read). Refs #80, upstream #1632 * feat(plan): publish plans from sandbox files Re-land upstream #1635 onto the modular structure, completing the partially-ported change so dev is internally consistent. save_plan now takes a plan_file_path, reads the agent-authored Markdown file from /workspace/plans/ (validating extension/location/UTF-8/size) and publishes it, instead of taking a plan_markdown string. Removes write_file/edit_file from PLAN_MODE_EXCLUDED_TOOLS so the agent can author the plan file, updates enter_plan_mode/reject_plan guidance and the e2e fake LLM. Skips the #1610-only update_plan hunk (not on dev). Refs #80, upstream #1635 * fix(security): SSRF-harden server-side image fetch + stop logging raw image URLs INJ-01 (high): fetch_image_block used follow_redirects=True with no per-hop revalidation and discarded the resolved-IP pin, so an attacker-authored Slack/ Linear image URL could 302-redirect the fetch to an internal host / cloud metadata endpoint (blind SSRF), and DNS-rebinding could bypass the one-shot is_url_safe check. Route image fetches through the same per-hop resolve+pin+ revalidate loop the http_request tool uses, lifted into url_safety as the shared request_with_safe_redirects. Also strip the per-host Slack/Linear bearer token on redirect so it can't be replayed to a redirect target. SC-1 (low): linear.py logged full image URLs (which can carry signed tokens) at DEBUG; multimodal logged them at INFO on every fetch. Log host-only. Sink lived in multimodal.py (unchanged by the feature work) but PR #128 widened its reach by no longer dropping images for text-only models. Fixing on the base branch so #130/#129 inherit it on rebase. Adds fetch_image_block SSRF regression tests (redirect-to-internal blocked; auth stripped on redirect).
186 lines
6.2 KiB
Python
186 lines
6.2 KiB
Python
"""Supported models and reasoning efforts surfaced in the profile editor."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from typing import TypedDict
|
|
|
|
|
|
class ModelOption(TypedDict):
|
|
id: str
|
|
label: str
|
|
efforts: list[str]
|
|
default_effort: str
|
|
supports_images: bool
|
|
|
|
|
|
SUPPORTED_MODELS: list[ModelOption] = [
|
|
{
|
|
"id": "bedrock_converse:us.anthropic.claude-opus-4-8",
|
|
"label": "Opus 4.8 (Bedrock)",
|
|
"efforts": ["low", "medium", "high", "xhigh", "max"],
|
|
"default_effort": "high",
|
|
"supports_images": True,
|
|
},
|
|
{
|
|
"id": "bedrock_converse:us.anthropic.claude-sonnet-5",
|
|
"label": "Sonnet 5 (Bedrock)",
|
|
"efforts": ["low", "medium", "high", "xhigh", "max"],
|
|
"default_effort": "high",
|
|
"supports_images": True,
|
|
},
|
|
{
|
|
"id": "fireworks:accounts/fireworks/models/kimi-k2p7-code",
|
|
"label": "Kimi K2.7",
|
|
"efforts": ["low", "medium", "high"],
|
|
"default_effort": "high",
|
|
"supports_images": False,
|
|
},
|
|
{
|
|
"id": "fireworks:accounts/fireworks/models/deepseek-v4-pro",
|
|
"label": "DeepSeek V4 Pro",
|
|
"efforts": ["none", "low", "medium", "high", "xhigh", "max"],
|
|
"default_effort": "high",
|
|
"supports_images": False,
|
|
},
|
|
{
|
|
"id": "fireworks:accounts/fireworks/models/glm-5p2",
|
|
"label": "GLM 5.2",
|
|
"efforts": ["none", "high", "max"],
|
|
"default_effort": "high",
|
|
"supports_images": False,
|
|
},
|
|
{
|
|
"id": "fireworks:accounts/fireworks/models/minimax-m3",
|
|
"label": "MiniMax M3",
|
|
"efforts": ["medium", "high"],
|
|
"default_effort": "high",
|
|
"supports_images": True,
|
|
},
|
|
{
|
|
"id": "fireworks:accounts/fireworks/models/gpt-oss-120b",
|
|
"label": "gpt-oss-120b",
|
|
"efforts": ["low", "medium", "high"],
|
|
"default_effort": "medium",
|
|
"supports_images": False,
|
|
},
|
|
{
|
|
"id": "fireworks:accounts/fireworks/models/deepseek-v4-flash",
|
|
"label": "DeepSeek V4 Flash",
|
|
"efforts": ["none", "medium", "high"],
|
|
"default_effort": "high",
|
|
"supports_images": False,
|
|
},
|
|
]
|
|
|
|
SUPPORTED_MODEL_IDS: frozenset[str] = frozenset(m["id"] for m in SUPPORTED_MODELS)
|
|
|
|
DEFAULT_MODEL_ID: str = "bedrock_converse:us.anthropic.claude-opus-4-8"
|
|
DEFAULT_MODEL_EFFORT: str = "medium"
|
|
|
|
|
|
def model_supports_effort(model_id: str, effort: str) -> bool:
|
|
for m in SUPPORTED_MODELS:
|
|
if m["id"] == model_id:
|
|
return effort in m["efforts"]
|
|
return False
|
|
|
|
|
|
def model_supports_images(model_id: str) -> bool:
|
|
for m in SUPPORTED_MODELS:
|
|
if m["id"] == model_id:
|
|
return m["supports_images"]
|
|
return False
|
|
|
|
|
|
def _provider_of(model_id: str) -> str | None:
|
|
provider, _, rest = model_id.partition(":")
|
|
return provider if rest else None
|
|
|
|
|
|
def _claude_family_of(model_id: str) -> str | None:
|
|
provider, _, name = model_id.partition(":")
|
|
if provider == "anthropic":
|
|
claude = name
|
|
elif provider == "bedrock_converse":
|
|
# Bedrock ids embed the model as a region-prefixed path, e.g.
|
|
# "us.anthropic.claude-sonnet-5" — take the trailing "claude-*" segment so
|
|
# our Bedrock Claude models get the same family-aware fallback.
|
|
claude = name.rpartition(".")[2]
|
|
else:
|
|
return None
|
|
if not claude.startswith("claude-"):
|
|
return None
|
|
parts = claude.split("-")
|
|
if len(parts) < 2:
|
|
return None
|
|
return "-".join(parts[:2])
|
|
|
|
|
|
def _fallback_effort_for(model: ModelOption, effort: object) -> str | None:
|
|
if not isinstance(effort, str):
|
|
return None
|
|
if effort in model["efforts"]:
|
|
return effort
|
|
if (
|
|
model["id"].startswith("google_genai:")
|
|
and effort == "none"
|
|
and "minimal" in model["efforts"]
|
|
):
|
|
return "minimal"
|
|
return None
|
|
|
|
|
|
def provider_fallback_pair(model_id: object, effort: object = None) -> tuple[str, str] | None:
|
|
"""Newest supported ``(model_id, effort)`` for the same provider/family.
|
|
|
|
Keeps a stored selection on its original provider when its exact id has
|
|
dropped out of the supported set (e.g. an Opus minor-version bump), preferring
|
|
the same Claude family when available instead of falling through to the
|
|
cross-provider global default. Preserves ``effort`` when the fallback model
|
|
supports it, otherwise uses that model's default effort. Returns ``None`` when
|
|
no supported model shares the provider.
|
|
"""
|
|
if not isinstance(model_id, str):
|
|
return None
|
|
provider = _provider_of(model_id)
|
|
if provider is None:
|
|
return None
|
|
family = _claude_family_of(model_id)
|
|
if family is not None:
|
|
for m in SUPPORTED_MODELS:
|
|
if _provider_of(m["id"]) == provider and _claude_family_of(m["id"]) == family:
|
|
return m["id"], _fallback_effort_for(m, effort) or m["default_effort"]
|
|
for m in SUPPORTED_MODELS:
|
|
if _provider_of(m["id"]) == provider:
|
|
return m["id"], _fallback_effort_for(m, effort) or m["default_effort"]
|
|
return None
|
|
|
|
|
|
def default_model_pair() -> tuple[str, str]:
|
|
"""Hardcoded fallback (model_id, reasoning_effort) used when no team default is set."""
|
|
if DEFAULT_MODEL_ID in SUPPORTED_MODEL_IDS and model_supports_effort(
|
|
DEFAULT_MODEL_ID, DEFAULT_MODEL_EFFORT
|
|
):
|
|
return DEFAULT_MODEL_ID, DEFAULT_MODEL_EFFORT
|
|
first = SUPPORTED_MODELS[0]
|
|
return first["id"], first["default_effort"]
|
|
|
|
|
|
def default_vision_model_pair() -> tuple[str, str]:
|
|
"""Default (model_id, reasoning_effort) to use when image input is required.
|
|
|
|
Prefers the configured default model when it is vision-capable (Opus 4.8),
|
|
otherwise the first vision-capable supported model. The fork ships only
|
|
Bedrock/Fireworks models, so this selects on ``supports_images`` rather than
|
|
upstream's ``openai:``/``anthropic:`` provider filter.
|
|
"""
|
|
if (
|
|
DEFAULT_MODEL_ID in SUPPORTED_MODEL_IDS
|
|
and model_supports_images(DEFAULT_MODEL_ID)
|
|
and model_supports_effort(DEFAULT_MODEL_ID, DEFAULT_MODEL_EFFORT)
|
|
):
|
|
return DEFAULT_MODEL_ID, DEFAULT_MODEL_EFFORT
|
|
for model in SUPPORTED_MODELS:
|
|
if model["supports_images"]:
|
|
return model["id"], model["default_effort"]
|
|
return default_model_pair()
|