From ffc8bc572f9bb7094c4f716cee09dcc13d11a58a Mon Sep 17 00:00:00 2001 From: sena-labs <218400180+sena-labs@users.noreply.github.com> Date: Tue, 5 May 2026 03:52:04 +0200 Subject: [PATCH 1/3] chore: add auto-claude entries to .gitignore --- .gitignore | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/.gitignore b/.gitignore index 8526ea6..b2b998f 100644 --- a/.gitignore +++ b/.gitignore @@ -41,3 +41,14 @@ htmlcov/ tmpclaude-* nul + +# Auto Claude data directory +.auto-claude/ + +# Auto Claude generated files +.auto-claude-security.json +.auto-claude-status +.claude_settings.json +.worktrees/ +.security-key +logs/security/ From a2a96fbce416d3d074a62bb401958688775c319c Mon Sep 17 00:00:00 2001 From: sena-labs <218400180+sena-labs@users.noreply.github.com> Date: Wed, 6 May 2026 23:48:23 +0200 Subject: [PATCH 2/3] fix: close HTTP response in pipes() and update docs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Code fix: - pipes(): initialize response=None before try-block and add finally: response.close() to guarantee the connection is returned to the session pool in every code path (auth errors, JSON decode failures, unexpected exceptions) Documentation fixes: - Update test count from 234 to 249 across README, TESTING, CONTRIBUTING, and PULL_REQUEST_TEMPLATE (all missed or newly identified references) - Fix SECURITY.md Supported Versions table (malformed Markdown — rows were fused onto one line, breaking GitHub rendering) - Add SYNC_PROVIDER_ICONS valve to README Advanced configuration table - Correct HTTP 429 troubleshooting tip: MAX_RETRIES does not retry HTTP errors — it only retries on network timeouts and connection failures - Update bug_report.yml Pipe Version placeholder from 1.0.0 to 1.2.0 - Clarify TESTING.md §9.1 FALLBACK_MODELS test: make explicit that the primary model is the first element in the models array - Replace non-existent reasoning model example (claude-3.7-sonnet:thinking) with deepseek/deepseek-r1 in TESTING.md §3.2 - Add pipes() response-close fix entry to CHANGELOG [Unreleased] Co-Authored-By: Claude Sonnet 4.6 --- .github/ISSUE_TEMPLATE/bug_report.yml | 2 +- .github/PULL_REQUEST_TEMPLATE.md | 2 +- CHANGELOG.md | 1 + CONTRIBUTING.md | 2 +- README.md | 5 +++-- SECURITY.md | 4 +++- TESTING.md | 8 ++++---- openrouter_pipe.py | 4 ++++ 8 files changed, 18 insertions(+), 10 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index f2cc8e6..065ef31 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -28,7 +28,7 @@ body: id: pipe-version attributes: label: Pipe Version - placeholder: "e.g. 1.0.0" + placeholder: "e.g. 1.2.0" validations: required: true diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 26c31c8..1ec8e69 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -13,7 +13,7 @@ ## Checklist - [ ] I have read the [CONTRIBUTING.md](../CONTRIBUTING.md) guidelines -- [ ] All tests pass (`python test_pipe.py` — 234/234 ✓) +- [ ] All tests pass (`python test_pipe.py` — 249/249 ✓) - [ ] I have added tests for new functionality (if applicable) - [ ] I have updated `CHANGELOG.md` under `[Unreleased]` - [ ] I have updated documentation (if applicable) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7de1eac..839325a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - **Icon sync: correct prefixed model IDs** — `_sync_model_icons()` now discovers the pipe's `function_id` via `type(self).__module__` and writes DB records with the full prefixed ID (e.g. `openrouter_pipe.openai/gpt-4o`) matching what Open WebUI's frontend requests at `/models/model/profile/image` - **Streaming status event** — the "done" status event is now correctly emitted at the end of streaming responses (async generator wrapper replaces sync generator that could not `await`) - **Dead provider-icon code removed** — `info.meta.profile_image_url` was included in model dicts returned by `pipes()` but Open WebUI ignores all fields except `id` and `name`; the field has been removed in favour of the new DB-sync approach +- **`pipes()` response always closed** — added `finally: response.close()` to guarantee HTTP connections are returned to the session pool in all code paths (auth errors, JSON decode failures, unexpected exceptions) ## [1.2.0] - 2026-02-17 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 0dfdee9..4f8e416 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -29,7 +29,7 @@ pip install -r requirements.txt ### Running Tests ```bash -python test_pipe.py # Unit tests (234 tests) +python test_pipe.py # Unit tests (249 tests) python integration_test.py # Live API tests (requires OPENROUTER_API_KEY) ``` diff --git a/README.md b/README.md index d269d07..109b5a8 100644 --- a/README.md +++ b/README.md @@ -150,6 +150,7 @@ All settings are configurable via **Valves** in the Open WebUI admin panel. Ever | `FALLBACK_MODELS` | `OPENROUTER_FALLBACK_MODELS` | `""` | Fallback model IDs (comma-separated) | | `ENABLE_MIDDLE_OUT` | `OPENROUTER_ENABLE_MIDDLE_OUT` | `false` | Middle-out compression for long prompts | | `ENABLE_CACHE_CONTROL` | `OPENROUTER_ENABLE_CACHE_CONTROL` | `false` | Anthropic cache_control injection | +| `SYNC_PROVIDER_ICONS` | `OPENROUTER_SYNC_ICONS` | `true` | Sync provider icons into Open WebUI's model database | ### Network @@ -215,7 +216,7 @@ It also removes `user` when sent as a dict (OWUI format) since OpenRouter expect OpenRouter-Pipe/ ├── openrouter_pipe.py # Main pipe source (install this in Open WebUI) ├── function.json # Open WebUI community manifest (metadata, tags, categories) -├── test_pipe.py # Unit test suite (234 tests) +├── test_pipe.py # Unit test suite (249 tests) ├── integration_test.py # Live API integration tests (47 tests) ├── TESTING.md # Pre-release testing checklist ├── SECURITY.md # Security policy and vulnerability reporting @@ -274,7 +275,7 @@ Your API key is incorrect or malformed. Get a valid key from [openrouter.ai/keys
"Rate limit exceeded (HTTP 429)" -You're sending too many requests. Wait a moment and try again. Consider setting `MAX_RETRIES` to `2` or higher for automatic backoff. +You're sending too many requests. Wait a moment and try again, or consider upgrading your OpenRouter plan. Note: `MAX_RETRIES` only retries on network timeouts and connection failures — HTTP rate limit errors are returned immediately without retry.
diff --git a/SECURITY.md b/SECURITY.md index 406dfcc..33b3fa9 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -3,7 +3,9 @@ ## Supported Versions | Version | Supported | -|---------|-----------|| 1.2.x | Yes || 1.1.x | Yes | +|---------|-----------| +| 1.2.x | Yes | +| 1.1.x | Yes | | 1.0.x | Yes | | < 1.0 | No | diff --git a/TESTING.md b/TESTING.md index 582f827..f1cb9d3 100644 --- a/TESTING.md +++ b/TESTING.md @@ -12,7 +12,7 @@ Manual checklist to verify every Pipe feature before release. python test_pipe.py ``` -Must print **234/234 passed**. If any test fails, **do not release**. +Must print **249/249 passed**. If any test fails, **do not release**. --- @@ -41,7 +41,7 @@ Must print **234/234 passed**. If any test fails, **do not release**. | # | Action | Expected result | |---|--------|-----------------| | 3.1 | Select a model (e.g. `openai/gpt-4o`), type "Hello" with `stream: false` | The response appears all at once, correct text | -| 3.2 | Select a reasoning model (e.g. `anthropic/claude-3.7-sonnet:thinking`) | The response contains `...` blocks followed by content | +| 3.2 | Select a reasoning model (e.g. `deepseek/deepseek-r1`) | The response contains `...` blocks followed by content | --- @@ -105,7 +105,7 @@ Must print **234/234 passed**. If any test fails, **do not release**. | # | Action | Expected result | |---|--------|-----------------| -| 9.1 | Set `FALLBACK_MODELS = openai/gpt-4o, anthropic/claude-3.5-sonnet` | payload > `"models": ["openai/gpt-4o", "anthropic/claude-3.5-sonnet"]` | +| 9.1 | While using `openai/gpt-4o` as primary, set `FALLBACK_MODELS = anthropic/claude-3.5-sonnet` | payload contains `"models": ["openai/gpt-4o", "anthropic/claude-3.5-sonnet"]` (primary model first, then fallbacks) | | 9.2 | Leave `FALLBACK_MODELS` empty | No `models` field in payload | --- @@ -191,7 +191,7 @@ Must print **234/234 passed**. If any test fails, **do not release**. ## Quick pre-release checklist ``` -[ ] python test_pipe.py → 234/234 ✓ +[ ] python test_pipe.py → 249/249 ✓ [ ] python integration_test.py → 47/47 ✓ [ ] Empty API key → clear error [ ] Valid API key → 340+ models diff --git a/openrouter_pipe.py b/openrouter_pipe.py index 67117a0..b3ba401 100644 --- a/openrouter_pipe.py +++ b/openrouter_pipe.py @@ -277,6 +277,7 @@ def pipes(self) -> List[dict]: return self._models_cache headers = self._build_headers(include_content_type=False) + response = None try: response = self._session.get( self.models_url, headers=headers, timeout=self.valves.REQUEST_TIMEOUT @@ -313,6 +314,9 @@ def pipes(self) -> List[dict]: print(f"[OpenRouter Pipe] Model fetch error: {exc}") traceback.print_exc() return [{"id": "error", "name": f"Unexpected error: {exc}"}] + finally: + if response is not None: + response.close() provider_filter = self._parse_provider_filter() prefix = self.valves.MODEL_PREFIX or "" From 72fedbc1bbc46d5a976ff5279d34ec519240d603 Mon Sep 17 00:00:00 2001 From: sena-labs <218400180+sena-labs@users.noreply.github.com> Date: Wed, 6 May 2026 23:55:46 +0200 Subject: [PATCH 3/3] test: add response.close() assertions and fix test output wording MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add 3 unit tests (15o/15p/15q) to verify pipes() always calls response.close() via the finally block: · 15o: auth-error path (HTTP 401) — close() called once · 15p: JSON decode error path — close() called once · 15q: success path — close() called once This prevents future regressions of the connection-pool fix. - Fix TESTING.md §0: replace "Must print 249/249 passed" with the actual output format the runner emits ("All tests passed! ✓" and "✗ Failed: 0"), addressing Copilot review comment. - Update test count from 249 → 252 across README, CONTRIBUTING, TESTING, and PULL_REQUEST_TEMPLATE to reflect the 3 new tests. Co-Authored-By: Claude Sonnet 4.6 --- .github/PULL_REQUEST_TEMPLATE.md | 2 +- CONTRIBUTING.md | 2 +- README.md | 2 +- TESTING.md | 4 ++-- test_pipe.py | 32 ++++++++++++++++++++++++++++++++ 5 files changed, 37 insertions(+), 5 deletions(-) diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 1ec8e69..34a5619 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -13,7 +13,7 @@ ## Checklist - [ ] I have read the [CONTRIBUTING.md](../CONTRIBUTING.md) guidelines -- [ ] All tests pass (`python test_pipe.py` — 249/249 ✓) +- [ ] All tests pass (`python test_pipe.py` — 252/252 ✓) - [ ] I have added tests for new functionality (if applicable) - [ ] I have updated `CHANGELOG.md` under `[Unreleased]` - [ ] I have updated documentation (if applicable) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 4f8e416..e7e5350 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -29,7 +29,7 @@ pip install -r requirements.txt ### Running Tests ```bash -python test_pipe.py # Unit tests (249 tests) +python test_pipe.py # Unit tests (252 tests) python integration_test.py # Live API tests (requires OPENROUTER_API_KEY) ``` diff --git a/README.md b/README.md index 109b5a8..24b0a72 100644 --- a/README.md +++ b/README.md @@ -216,7 +216,7 @@ It also removes `user` when sent as a dict (OWUI format) since OpenRouter expect OpenRouter-Pipe/ ├── openrouter_pipe.py # Main pipe source (install this in Open WebUI) ├── function.json # Open WebUI community manifest (metadata, tags, categories) -├── test_pipe.py # Unit test suite (249 tests) +├── test_pipe.py # Unit test suite (252 tests) ├── integration_test.py # Live API integration tests (47 tests) ├── TESTING.md # Pre-release testing checklist ├── SECURITY.md # Security policy and vulnerability reporting diff --git a/TESTING.md b/TESTING.md index f1cb9d3..b82ba40 100644 --- a/TESTING.md +++ b/TESTING.md @@ -12,7 +12,7 @@ Manual checklist to verify every Pipe feature before release. python test_pipe.py ``` -Must print **249/249 passed**. If any test fails, **do not release**. +Must exit with `All tests passed! ✓` and `✗ Failed: 0`. If any test fails, **do not release**. --- @@ -191,7 +191,7 @@ Must print **249/249 passed**. If any test fails, **do not release**. ## Quick pre-release checklist ``` -[ ] python test_pipe.py → 249/249 ✓ +[ ] python test_pipe.py → 252 passed, 0 failed ✓ [ ] python integration_test.py → 47/47 ✓ [ ] Empty API key → clear error [ ] Valid API key → 340+ models diff --git a/test_pipe.py b/test_pipe.py index f7d91a2..c546831 100644 --- a/test_pipe.py +++ b/test_pipe.py @@ -1026,6 +1026,38 @@ async def _test_pipe_stream() -> str: _assert(models[0]["id"] == "error", "pipes provider no match: error id") _assert("No models match" in models[0]["name"], "pipes provider no match: correct message") +# 15o. response.close() called via finally on auth-error branch (401) +_close_auth_resp = MagicMock() +_close_auth_resp.status_code = 401 +_close_auth_resp.json.return_value = {"error": {"message": "Unauthorized"}} +pipe.valves = Pipe.Valves(OPENROUTER_API_KEY="bad-key") +pipe._models_cache = None +with patch.object(pipe._session, "get", return_value=_close_auth_resp): + pipe.pipes() +_assert(_close_auth_resp.close.call_count == 1, "pipes response.close(): called after 401 auth error") + +# 15p. response.close() called via finally when response.json() raises (JSONDecodeError) +_close_json_err_resp = MagicMock() +_close_json_err_resp.status_code = 200 +_close_json_err_resp.raise_for_status = MagicMock() +_close_json_err_resp.json.side_effect = ValueError("No JSON object could be decoded") +pipe.valves = Pipe.Valves(OPENROUTER_API_KEY="test-key") +pipe._models_cache = None +with patch.object(pipe._session, "get", return_value=_close_json_err_resp): + pipe.pipes() +_assert(_close_json_err_resp.close.call_count == 1, "pipes response.close(): called after JSON decode error") + +# 15q. response.close() called via finally on success path +_close_ok_resp = MagicMock() +_close_ok_resp.status_code = 200 +_close_ok_resp.raise_for_status = MagicMock() +_close_ok_resp.json.return_value = {"data": [{"id": "openai/gpt-4o", "name": "GPT-4o"}]} +pipe.valves = Pipe.Valves(OPENROUTER_API_KEY="test-key") +pipe._models_cache = None +with patch.object(pipe._session, "get", return_value=_close_ok_resp): + pipe.pipes() +_assert(_close_ok_resp.close.call_count == 1, "pipes response.close(): called after successful listing") + # ── 16. Valve json_schema_extra ────────────────────────────────────────────── _section("16. Valve json_schema_extra")