Skip to content

Commit 1a1f80e

Browse files
sena-labsclaude
andcommitted
fix: pre-release blockers (redirect guards, FREE_ONLY shim, version, docs)
Final pre-release hardening for v1.6.1, found by a multi-agent audit: Security: - Add allow_redirects=False to all four OpenRouter HTTP calls (/models, /chat/completions, provider-registry fetch, ZDR /endpoints/zdr fetch). v1.3-1.6 added two new GETs that lacked the guard; with requests>=2.32.4 this closes the Authorization-header redirect-leak class (CVE-2024-35195). Changed: - FREE_MODEL_FILTER default now honours the legacy OPENROUTER_FREE_ONLY=true env var (maps to "only") so upgrades from the FREE_ONLY era don't silently return paid models. Fixed: - function.json version 1.6.0 -> 1.6.1 (matched the code) + updated_at bump. - Docs: replace all FREE_ONLY references in README/TESTING with FREE_MODEL_FILTER, add a migration note, add the missing SHOW_COST_INFO / COST_CURRENCY valves to the config table, expand the Features list (ZDR, tool-calling filter, provider preferences), and correct test counts (557 unit / 44 integration). Test suite: 557 passing; mypy + pyflakes clean. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
1 parent 760da56 commit 1a1f80e

5 files changed

Lines changed: 57 additions & 18 deletions

File tree

CHANGELOG.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,19 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
### Security
11+
12+
- **No redirect following on any OpenRouter call**`allow_redirects=False` is now passed to all four HTTP requests (`/models`, `/chat/completions`, the provider-registry fetch, and the ZDR `/endpoints/zdr` fetch). Combined with the `requests>=2.32.4` floor, this prevents the `Authorization: Bearer` header from being forwarded to a redirect target (CVE-2024-35195 family) if the base URL is misconfigured
13+
14+
### Changed
15+
16+
- **`FREE_MODEL_FILTER` honours the legacy `OPENROUTER_FREE_ONLY` env var** — when `OPENROUTER_FREE_MODEL_FILTER` is unset, `OPENROUTER_FREE_ONLY=true` now maps to `only`, so installs upgrading from the pre-1.5 `FREE_ONLY` era don't silently start returning paid models
17+
18+
### Fixed
19+
20+
- **Version metadata**`function.json` reported `1.6.0` while the code was `1.6.1`; bumped the manifest version (and `updated_at`) to match
21+
- **Docs: `FREE_ONLY``FREE_MODEL_FILTER`** — README and TESTING.md still referenced the removed `FREE_ONLY` valve; updated all references, added a migration note, documented the missing `SHOW_COST_INFO` / `COST_CURRENCY` valves, and corrected the test counts
22+
1023
## [1.6.1] — 2026-05-08
1124

1225
### Fixed

README.md

Lines changed: 22 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -66,8 +66,12 @@ cache control out of the box.
6666
- **Cache control** — Anthropic-style `cache_control` injection on the longest message chunk.
6767
- **Citations**`[n]` references from web-search-enabled models are converted to markdown links.
6868
- **Provider icons** — 13 hardcoded fast-path logos plus auto-discovered icons for ~20 more providers (xAI, Inflection, NVIDIA, Arcee, Morph, Cerebras, …) lazy-loaded from OpenRouter's provider registry, all synced directly into Open WebUI's model database.
69-
- **Retry logic** — exponential backoff with jitter on timeout and connection errors.
70-
- **FREE_ONLY mode** — filter to show only free-tier models (`:free` suffix or `0/0` pricing).
69+
- **ZDR (Zero Data Retention)** — filter the catalog to ZDR-capable models (`ZDR_MODELS_ONLY`) and/or enforce ZDR per request (`ZDR_ENFORCE`).
70+
- **Tool-calling filter** — show all / only / exclude tool-capable models (`TOOL_CALLING_FILTER`).
71+
- **Provider preferences**`PROVIDER_ONLY` allowlist, `PROVIDER_QUANTIZATIONS`, `PROVIDER_ALLOW_FALLBACKS`, and `PROVIDER_MAX_PRICE_PROMPT/COMPLETION` price caps.
72+
- **Free-tier filter**`FREE_MODEL_FILTER` shows all / only / excludes free-tier models (`:free` suffix or `0/0` pricing).
73+
- **Retry logic** — exponential backoff with proportional jitter on timeout/connection errors and on HTTP 429/502/503/504 (honours `Retry-After`).
74+
- **Cost transparency**`SHOW_COST_INFO` appends token usage + cost (currency configurable via `COST_CURRENCY`).
7175
- **Pre-flight validation** — invalid API keys are caught at model-fetch time, not after sending a message.
7276

7377
## Requirements
@@ -103,7 +107,7 @@ All OpenRouter models will appear in the model selector immediately.
103107
git clone https://github.com/sena-labs/Open-WebUI-Pipe-OpenRouter.git
104108
cd Open-WebUI-Pipe-OpenRouter
105109
pip install -r requirements.txt
106-
python test_pipe.py # 431 tests — verify everything is green
110+
python test_pipe.py # 557 tests — verify everything is green
107111
```
108112

109113
## Usage
@@ -116,7 +120,7 @@ an environment variable fallback (see [Configuration](#configuration)).
116120
| Goal | Valves to set |
117121
| --- | --- |
118122
| Show only OpenAI and Anthropic models | `MODEL_PROVIDERS = openai,anthropic` |
119-
| Show only free models | `FREE_ONLY = true` |
123+
| Show only free models | `FREE_MODEL_FILTER = only` |
120124
| Use DeepSeek for reasoning | select `deepseek/deepseek-r1`, `INCLUDE_REASONING = true` |
121125
| Route cheapest provider first | `PROVIDER_SORT = price` |
122126
| Add a fallback model | `FALLBACK_MODELS = anthropic/claude-3.5-sonnet` |
@@ -213,6 +217,15 @@ Every valve accepts an environment variable fallback. The table below lists both
213217
| `MAX_RETRIES` || `2` | Auto-retry count on transient errors |
214218
| `HTTP_REFERER_OVERRIDE` | `OPENROUTER_HTTP_REFERER` | `""` | Override the `HTTP-Referer` header sent to OpenRouter (must include scheme). Empty falls back to `WEBUI_URL` |
215219

220+
### Cost Display
221+
222+
| Valve | Env Var | Default | Description |
223+
| --- | --- | --- | --- |
224+
| `SHOW_COST_INFO` || `false` | Append token usage and cost to each response (also requests `usage` so streaming responses include cost) |
225+
| `COST_CURRENCY` | `OPENROUTER_COST_CURRENCY` | `USD` | Currency label for the cost display (display only; OpenRouter bills in USD) |
226+
227+
> **Migration (v1.5.0):** the old boolean `FREE_ONLY` valve was replaced by `FREE_MODEL_FILTER` (`all` / `only` / `exclude`). Set `FREE_MODEL_FILTER = only` to preserve the old `FREE_ONLY = true` behaviour. For backward compatibility, the legacy `OPENROUTER_FREE_ONLY=true` environment variable is still honoured when `FREE_MODEL_FILTER` is unset.
228+
216229
## Architecture
217230

218231
The pipe implements the **Manifold** pattern: one pipe entry point that surfaces multiple models.
@@ -230,8 +243,8 @@ The pipe implements the **Manifold** pattern: one pipe entry point that surfaces
230243
Open-WebUI-Pipe-OpenRouter/
231244
├── openrouter_pipe.py # Main pipe source — install this in Open WebUI
232245
├── function.json # Open WebUI community manifest
233-
├── test_pipe.py # Unit test suite (431 tests)
234-
├── integration_test.py # Live API integration tests (43 assertions)
246+
├── test_pipe.py # Unit test suite (557 tests)
247+
├── integration_test.py # Live API integration tests (44 assertions)
235248
├── TESTING.md # Manual pre-release checklist
236249
├── SECURITY.md # Security policy
237250
├── CONTRIBUTING.md # Contribution guidelines
@@ -260,7 +273,7 @@ It also removes `user` when sent as a dict (Open WebUI format) since OpenRouter
260273
## Development
261274

262275
```bash
263-
python test_pipe.py # Unit tests (431 tests)
276+
python test_pipe.py # Unit tests (557 tests)
264277
python integration_test.py # Live API tests (requires OPENROUTER_API_KEY)
265278
```
266279

@@ -313,7 +326,7 @@ reasoning models can take over a minute for complex prompts.
313326

314327
1. Verify your API key is valid (a single "error" model appears if it is not).
315328
2. If `MODEL_PROVIDERS` is set, confirm the provider names are lowercase: `openai`, `anthropic`, `google`.
316-
3. If `FREE_ONLY` is enabled, some providers may have no free models — try disabling it.
329+
3. If `FREE_MODEL_FILTER = only` is set, some providers may have no free models — set it back to `all`.
317330
4. Set `MODEL_PROVIDERS = ALL` to show the full catalog.
318331

319332
### Models load but chat returns errors
@@ -333,7 +346,7 @@ re-invokes the pipe with the updated thread. The pipe forwards the full message
333346
OpenRouter on each invocation. Whether a model can generate tool calls depends on
334347
OpenRouter's provider support for that model.
335348

336-
**Q: Why does `FREE_ONLY` include models without a `:free` suffix?**
349+
**Q: Why does `FREE_MODEL_FILTER = only` include models without a `:free` suffix?**
337350

338351
A: Some models are listed as free on OpenRouter without carrying a `:free` suffix in their
339352
ID. The pipe uses a two-pass check: first it looks for the `:free` suffix, then it falls

TESTING.md

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -91,8 +91,10 @@ Must exit with `All tests passed! ✓` and `✗ Failed: 0`. If any test fails, *
9191
| 7.1 | Set `MODEL_PROVIDERS = openai` | Only OpenAI models visible in selector |
9292
| 7.2 | Set `MODEL_PROVIDERS = openai` + `INVERT_PROVIDER_LIST = true` | All models **except** OpenAI visible |
9393
| 7.3 | Set `MODEL_PROVIDERS = ALL` or clear the field | **All** models visible again. Default `ALL` means no filter |
94-
| 7.4 | Set `FREE_ONLY = true` | Only free models (including those with pricing 0/0 without `:free` suffix) |
95-
| 7.5 | `FREE_ONLY = true` > verify that free `google/gemma-*` or `qwen/qwen3-*` appear | Models without `:free` but with pricing 0/0 are included |
94+
| 7.4 | Set `FREE_MODEL_FILTER = only` | Only free models (including those with pricing 0/0 without `:free` suffix) |
95+
| 7.5 | `FREE_MODEL_FILTER = only` > verify that free `google/gemma-*` or `qwen/qwen3-*` appear | Models without `:free` but with pricing 0/0 are included |
96+
| 7.6 | Set `FREE_MODEL_FILTER = exclude` | Free models hidden; only paid models remain |
97+
| 7.7 | Set env `OPENROUTER_FREE_ONLY=true` (legacy), leave `FREE_MODEL_FILTER` unset | Back-compat: behaves like `only` |
9698

9799
---
98100

@@ -194,14 +196,14 @@ Must exit with `All tests passed! ✓` and `✗ Failed: 0`. If any test fails, *
194196

195197
## Quick pre-release checklist
196198

197-
- [ ] `python test_pipe.py`431 passed, 0 failed
198-
- [ ] `python integration_test.py`43/43
199+
- [ ] `python test_pipe.py`557 passed, 0 failed
200+
- [ ] `python integration_test.py`44/44
199201
- [ ] Empty API key → clear error message in model selector
200202
- [ ] Valid API key → 340+ models with provider icons
201203
- [ ] Non-streaming chat works
202204
- [ ] Streaming chat works (token by token)
203205
- [ ] Reasoning tokens shown with `<think>`
204-
- [ ] `FREE_ONLY` filters correctly (`:free` suffix + 0/0 pricing)
206+
- [ ] `FREE_MODEL_FILTER` filters correctly (`only`/`exclude`/`all`; `:free` suffix + 0/0 pricing)
205207
- [ ] Provider filter + inversion works
206208
- [ ] Model prefix applied and removable
207209
- [ ] Fallback models present in payload

function.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
"author": "Sena Labs",
1010
"author_url": "https://github.com/sena-labs",
1111
"funding_url": "https://github.com/sponsors/sena-labs",
12-
"version": "1.6.0",
12+
"version": "1.6.1",
1313
"license": "MIT",
1414
"required_open_webui_version": "0.4.0",
1515
"requirements": ["requests>=2.32.4", "pydantic>=2.0"]
@@ -32,6 +32,6 @@
3232
"content": "openrouter_pipe.py",
3333
"is_active": true,
3434
"is_global": true,
35-
"updated_at": "2026-05-07T00:00:00.000Z",
35+
"updated_at": "2026-05-28T00:00:00.000Z",
3636
"created_at": "2026-01-21T00:00:00.000Z"
3737
}

openrouter_pipe.py

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -329,11 +329,18 @@ class Valves(BaseModel):
329329
description="When true the provider list becomes an exclusion list",
330330
)
331331
FREE_MODEL_FILTER: str = Field(
332-
default=os.getenv("OPENROUTER_FREE_MODEL_FILTER", "all"),
332+
# Back-compat: honour the legacy OPENROUTER_FREE_ONLY env var
333+
# (boolean) when the new var is unset, so installs upgrading from
334+
# the FREE_ONLY era don't silently start returning paid models.
335+
default=os.getenv(
336+
"OPENROUTER_FREE_MODEL_FILTER",
337+
"only" if os.getenv("OPENROUTER_FREE_ONLY", "").lower() == "true" else "all",
338+
),
333339
description=(
334340
"Filter the catalog by free-tier status (':free' suffix or zero "
335341
"prompt+completion pricing). 'all' = no filter (default), "
336-
"'only' = keep just free models, 'exclude' = hide free models."
342+
"'only' = keep just free models, 'exclude' = hide free models. "
343+
"Replaces the legacy FREE_ONLY valve (FREE_ONLY=true → 'only')."
337344
),
338345
json_schema_extra={
339346
"input": {
@@ -746,6 +753,7 @@ def pipes(self) -> List[dict]:
746753
headers=headers,
747754
params=params,
748755
timeout=self.valves.REQUEST_TIMEOUT,
756+
allow_redirects=False,
749757
)
750758
# Detect auth errors from the models endpoint itself
751759
# 502 from Clerk usually means the key format is invalid
@@ -1165,6 +1173,7 @@ def _load_provider_registry(self) -> dict:
11651173
resp = self._session.get(
11661174
_PROVIDER_REGISTRY_URL,
11671175
timeout=min(self.valves.REQUEST_TIMEOUT, 15),
1176+
allow_redirects=False,
11681177
)
11691178
try:
11701179
if resp.status_code == 200:
@@ -1260,6 +1269,7 @@ def _load_zdr_model_ids(self) -> frozenset:
12601269
f"{self._base}{_API_PATH_ZDR_ENDPOINTS}",
12611270
headers=self._build_headers(include_content_type=False),
12621271
timeout=min(self.valves.REQUEST_TIMEOUT, 30),
1272+
allow_redirects=False,
12631273
)
12641274
try:
12651275
if resp.status_code == 200:
@@ -1815,6 +1825,7 @@ def _retryable_request(
18151825
json=payload,
18161826
timeout=self.valves.REQUEST_TIMEOUT,
18171827
stream=stream,
1828+
allow_redirects=False,
18181829
)
18191830
response.raise_for_status()
18201831
return response

0 commit comments

Comments
 (0)