Skip to content

Commit 760da56

Browse files
sena-labsclaude
andcommitted
fix: pre-publication hardening (BOMs, redirect-CVE pin, None guard, lint)
Publication-readiness fixes on v1.6.0: Security / correctness (HIGH): - Strip UTF-8 BOM from function.json so strict JSON parsers (including OWUI's manifest loader, which uses json.load) can read it. LICENSE BOM removed too for encoding consistency. - Guard exc.response is None in pipes() HTTPError handler — manually-raised HTTPError without a response object would crash with AttributeError on .status_code / .json(); now returns "HTTP error fetching models (no response)". - Bump minimum requests dependency to >=2.32.4 in requirements.txt, function.json, and the module docstring. Pre-2.32 versions leak the Authorization header to redirect targets when the header is set manually (CVE-2024-35195 family). Lint / hygiene (LOW): - Remove unused prefix_str local in _expand_variant_models. - test_pipe.py: drop unused traceback / time-as-_time_mod imports; drop the unnecessary f-prefix on a placeholder-free string. - integration_test.py: drop the unnecessary f-prefix on a placeholder-free assertion message. CI: - Install pinned deps via `pip install -r requirements.txt` so the tested environment enforces the security-critical requests>=2.32.4 floor instead of installing unpinned `requests pydantic`. Test suite: 622 passing; mypy + pyflakes clean. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
1 parent 8988643 commit 760da56

7 files changed

Lines changed: 19 additions & 18 deletions

File tree

.github/workflows/tests.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ jobs:
2727
- name: Install dependencies
2828
run: |
2929
python -m pip install --upgrade pip
30-
pip install requests pydantic
30+
pip install -r requirements.txt
3131
3232
- name: Run tests
3333
run: python test_pipe.py

LICENSE

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
MIT License
1+
MIT License
22

33
Copyright (c) 2026 Sena Labs
44

function.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
{
1+
{
22
"id": "openrouter_pipe",
33
"name": "OpenRouter Pipe",
44
"type": "manifold",
@@ -12,7 +12,7 @@
1212
"version": "1.6.0",
1313
"license": "MIT",
1414
"required_open_webui_version": "0.4.0",
15-
"requirements": ["requests>=2.20", "pydantic>=2.0"]
15+
"requirements": ["requests>=2.32.4", "pydantic>=2.0"]
1616
},
1717
"tags": [
1818
"openrouter",

integration_test.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -214,7 +214,7 @@ async def _test_non_stream() -> str:
214214

215215
_assert(isinstance(ns_result, str), "non-stream returns string")
216216
_assert(len(ns_result) > 0, f"non-stream has content ({len(ns_result)} chars)")
217-
_assert("Error" not in ns_result or "INTEGRATION" in ns_result, f"no error in response")
217+
_assert("Error" not in ns_result or "INTEGRATION" in ns_result, "no error in response")
218218
print(f" ⏱ Response in {elapsed:.2f}s")
219219
print(f" ℹ Response: {ns_result[:150]}{'...' if len(ns_result) > 150 else ''}")
220220

openrouter_pipe.py

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
license: MIT
88
icon_url: data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxMDAgMTAwIj48ZGVmcz48bGluZWFyR3JhZGllbnQgaWQ9ImJnIiB4MT0iMCUiIHkxPSIwJSIgeDI9IjEwMCUiIHkyPSIxMDAlIj48c3RvcCBvZmZzZXQ9IjAlIiBzdG9wLWNvbG9yPSIjNmQyOGQ5Ii8+PHN0b3Agb2Zmc2V0PSIxMDAlIiBzdG9wLWNvbG9yPSIjYTc4YmZhIi8+PC9saW5lYXJHcmFkaWVudD48L2RlZnM+PHJlY3Qgd2lkdGg9IjEwMCIgaGVpZ2h0PSIxMDAiIHJ4PSIyMCIgZmlsbD0idXJsKCNiZykiLz48cGF0aCBkPSJNMjAgNTAgQzIwIDMwLCA0MCAzMCwgNTAgMzAgTDUwIDIyIEw2OCA0MCBMNTAgNTggTDUwIDUwIEM0MCA1MCwgMzUgNDUsIDMwIDUwIEMyNSA1NSwgMjAgNzAsIDIwIDUwIFoiIGZpbGw9IndoaXRlIiBvcGFjaXR5PSIwLjk1Ii8+PGNpcmNsZSBjeD0iNzgiIGN5PSIzMCIgcj0iNyIgZmlsbD0id2hpdGUiIG9wYWNpdHk9IjAuOCIvPjxjaXJjbGUgY3g9IjgyIiBjeT0iNTAiIHI9IjciIGZpbGw9IndoaXRlIiBvcGFjaXR5PSIwLjk1Ii8+PGNpcmNsZSBjeD0iNzgiIGN5PSI3MCIgcj0iNyIgZmlsbD0id2hpdGUiIG9wYWNpdHk9IjAuOCIvPjxsaW5lIHgxPSI2OCIgeTE9IjQwIiB4Mj0iNzYiIHkyPSIzMiIgc3Ryb2tlPSJ3aGl0ZSIgc3Ryb2tlLXdpZHRoPSIyIiBvcGFjaXR5PSIwLjUiLz48bGluZSB4MT0iNjgiIHkxPSI0MCIgeDI9Ijc2IiB5Mj0iNTAiIHN0cm9rZT0id2hpdGUiIHN0cm9rZS13aWR0aD0iMiIgb3BhY2l0eT0iMC41Ii8+PGxpbmUgeDE9IjY4IiB5MT0iNDAiIHgyPSI3NiIgeTI9IjY4IiBzdHJva2U9IndoaXRlIiBzdHJva2Utd2lkdGg9IjIiIG9wYWNpdHk9IjAuNSIvPjwvc3ZnPg==
99
required_open_webui_version: 0.4.0
10-
requirements: requests>=2.20, pydantic>=2.0
10+
requirements: requests>=2.32.4, pydantic>=2.0
1111
description: The definitive OpenRouter integration for Open WebUI. Full catalog (chat/TTS/audio/image/embeddings), variant routing (:nitro/:exacto/:thinking/:online/:free/:extended), web search plugin with domain filters, server-side category filter, deprecation warnings, extended reasoning (minimal→xhigh + max_tokens + summary), Anthropic interleaved thinking + cache TTL, ZDR enforcement, tool/free-tier filters, provider preferences (only/quantizations/max_price/allow_fallbacks), service tier routing (auto/flex/priority/scale), generation-ID auditability, cached-input cost breakdown, model fallbacks, middle-out compression, citations, auto-discovered provider icons.
1212
"""
1313

@@ -765,14 +765,17 @@ def pipes(self) -> List[dict]:
765765
except requests.exceptions.Timeout:
766766
return [{"id": "error", "name": "Timeout fetching models. Try again or increase REQUEST_TIMEOUT."}]
767767
except requests.exceptions.HTTPError as exc:
768-
msg = f"HTTP {exc.response.status_code} fetching models"
769-
try:
770-
err = exc.response.json().get("error", {})
771-
detail = err.get("message", str(err)) if isinstance(err, dict) else str(err)
772-
if detail:
773-
msg += f": {detail}"
774-
except Exception:
775-
pass
768+
if exc.response is not None:
769+
msg = f"HTTP {exc.response.status_code} fetching models"
770+
try:
771+
err = exc.response.json().get("error", {})
772+
detail = err.get("message", str(err)) if isinstance(err, dict) else str(err)
773+
if detail:
774+
msg += f": {detail}"
775+
except Exception:
776+
pass
777+
else:
778+
msg = "HTTP error fetching models (no response)"
776779
print(f"[OpenRouter Pipe] {msg}")
777780
return [{"id": "error", "name": msg}]
778781
except Exception as exc:
@@ -1341,7 +1344,6 @@ def _expand_variant_models(self, models: List[dict], prefix: str) -> List[dict]:
13411344
if not specs:
13421345
return models
13431346

1344-
prefix_str = prefix or ""
13451347
# Strip the user-set prefix so we can reuse base names verbatim.
13461348
by_id: dict = {}
13471349
for entry in models:

requirements.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
1-
requests>=2.20
1+
requests>=2.32.4
22
pydantic>=2.0

test_pipe.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@
1616
import json
1717
import os
1818
import sys
19-
import traceback
2019
from types import ModuleType
2120
from typing import List
2221
from unittest.mock import MagicMock, patch
@@ -131,7 +130,7 @@ def _section(title: str):
131130
# The default is os.getenv() evaluated at class-definition time (module load);
132131
# if the env var was set at that point, the default is non-empty — by design.
133132
frozen_default = Pipe.Valves.model_fields["OPENROUTER_API_KEY"].default
134-
_assert(v.OPENROUTER_API_KEY == frozen_default, f"API key default matches frozen class default")
133+
_assert(v.OPENROUTER_API_KEY == frozen_default, "API key default matches frozen class default")
135134
_assert(v.OPENROUTER_BASE_URL == "https://openrouter.ai/api/v1", "base URL default")
136135
_assert(v.REASONING_EFFORT == "", "reasoning effort empty")
137136
_assert(v.INCLUDE_REASONING is True, "include_reasoning True by default")

0 commit comments

Comments
 (0)