From e92ecbbb7c80540322b5896087ddfaca9ae41e8a Mon Sep 17 00:00:00 2001 From: majdyz Date: Sun, 12 Apr 2026 10:08:29 +0000 Subject: [PATCH] fix(backend): address review comments on SDK upgrade PR - Make strip_forbidden_betas_from_body non-mutating (returns shallow copy instead of modifying caller's dict in-place) - Add os.access(X_OK) validation for claude_agent_cli_path to reject non-executable paths at config load time - Replace hardcoded /v1 path dedup with generic urlparse-based logic that handles any API version prefix in the target URL --- .../backend/backend/copilot/config.py | 5 +++ .../copilot/sdk/openrouter_compat_proxy.py | 35 +++++++++++-------- 2 files changed, 26 insertions(+), 14 deletions(-) diff --git a/autogpt_platform/backend/backend/copilot/config.py b/autogpt_platform/backend/backend/copilot/config.py index 7132c22dc4..0206d7930b 100644 --- a/autogpt_platform/backend/backend/copilot/config.py +++ b/autogpt_platform/backend/backend/copilot/config.py @@ -348,6 +348,11 @@ class ChatConfig(BaseSettings): v = os.getenv("CHAT_CLAUDE_AGENT_CLI_PATH") if not v: v = os.getenv("CLAUDE_AGENT_CLI_PATH") + if v and not os.access(v, os.X_OK): + raise ValueError( + f"claude_agent_cli_path '{v}' is not an executable file. " + "Check the path and file permissions." + ) return v @model_validator(mode="before") diff --git a/autogpt_platform/backend/backend/copilot/sdk/openrouter_compat_proxy.py b/autogpt_platform/backend/backend/copilot/sdk/openrouter_compat_proxy.py index d940a83f73..103046942e 100644 --- a/autogpt_platform/backend/backend/copilot/sdk/openrouter_compat_proxy.py +++ b/autogpt_platform/backend/backend/copilot/sdk/openrouter_compat_proxy.py @@ -162,6 +162,9 @@ def strip_forbidden_betas_from_body(payload: Any) -> Any: """Remove forbidden tokens from the ``betas`` array of an Anthropic Messages API request body, if present. + Returns a shallow copy with the ``betas`` key cleaned — the input + dict is never mutated. + The Messages API accepts a top-level ``betas: list[str]`` parameter used to opt into beta features. We drop tokens in :data:`_FORBIDDEN_BETA_TOKENS` so OpenRouter's check passes. @@ -169,15 +172,13 @@ def strip_forbidden_betas_from_body(payload: Any) -> Any: if not isinstance(payload, dict): return payload betas = payload.get("betas") - if isinstance(betas, list): - cleaned_betas = [b for b in betas if b not in _FORBIDDEN_BETA_TOKENS] - if cleaned_betas: - payload["betas"] = cleaned_betas - else: - # Drop the empty array entirely so OpenRouter doesn't even - # see an empty `betas` field. - payload.pop("betas", None) - return payload + if not isinstance(betas, list): + return payload + cleaned_betas = [b for b in betas if b not in _FORBIDDEN_BETA_TOKENS] + result = {k: v for k, v in payload.items() if k != "betas"} + if cleaned_betas: + result["betas"] = cleaned_betas + return result def strip_forbidden_anthropic_beta_header(value: str | None) -> str | None: @@ -444,12 +445,18 @@ class OpenRouterCompatProxy: # ``/api/v1/v1/messages``. Strip a leading ``/v1`` from the # incoming path if the target already ends with ``/v1`` (or # similar API-version segment). + # Deduplicate API version prefix: if the target URL already + # contains a versioned path segment (e.g. ``/api/v1``) and the + # incoming request path starts with the same segment, strip it + # to avoid ``/api/v1/v1/messages``. + from urllib.parse import urlparse + target_base = self._target_base_url - target_lower = target_base.lower() - for prefix in ("/v1",): - if target_lower.endswith(prefix) and upstream_path.startswith(prefix + "/"): - upstream_path = upstream_path[len(prefix) :] - break + target_path = urlparse(target_base).path.rstrip("/") + if target_path and upstream_path.startswith(target_path + "/"): + upstream_path = upstream_path[len(target_path) :] + elif target_path and upstream_path == target_path: + upstream_path = "/" upstream_url = f"{target_base}{upstream_path}" body_bytes = await request.read()