Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/ISSUE_TEMPLATE/bug_report.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
2 changes: 1 addition & 1 deletion .github/PULL_REQUEST_TEMPLATE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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` — 252/252 ✓)
- [ ] I have added tests for new functionality (if applicable)
- [ ] I have updated `CHANGELOG.md` under `[Unreleased]`
- [ ] I have updated documentation (if applicable)
Expand Down
11 changes: 11 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -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/
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
2 changes: 1 addition & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 (252 tests)
python integration_test.py # Live API tests (requires OPENROUTER_API_KEY)
```

Expand Down
5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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 (252 tests)
├── integration_test.py # Live API integration tests (47 tests)
├── TESTING.md # Pre-release testing checklist
├── SECURITY.md # Security policy and vulnerability reporting
Expand Down Expand Up @@ -274,7 +275,7 @@ Your API key is incorrect or malformed. Get a valid key from [openrouter.ai/keys
<details>
<summary><strong>"Rate limit exceeded (HTTP 429)"</strong></summary>

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.
</details>

<details>
Expand Down
4 changes: 3 additions & 1 deletion SECURITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |

Expand Down
8 changes: 4 additions & 4 deletions TESTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 exit with `All tests passed! ✓` and `✗ Failed: 0`. If any test fails, **do not release**.

---

Expand Down Expand Up @@ -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 `<think>...</think>` blocks followed by content |
| 3.2 | Select a reasoning model (e.g. `deepseek/deepseek-r1`) | The response contains `<think>...</think>` blocks followed by content |

---

Expand Down Expand Up @@ -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 |

---
Expand Down Expand Up @@ -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 → 252 passed, 0 failed
[ ] python integration_test.py → 47/47 ✓
[ ] Empty API key → clear error
[ ] Valid API key → 340+ models
Expand Down
4 changes: 4 additions & 0 deletions openrouter_pipe.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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()
Comment on lines +317 to +319

provider_filter = self._parse_provider_filter()
prefix = self.valves.MODEL_PREFIX or ""
Expand Down
32 changes: 32 additions & 0 deletions test_pipe.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
Loading