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.2.0"
placeholder: "e.g. 1.3.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 @@ -25,7 +25,7 @@

## Testing

- [ ] All unit tests pass (`python test_pipe.py` — 322/322 ✓)
- [ ] All unit tests pass (`python test_pipe.py` — 431/431 ✓)
- [ ] New tests added for the changes
- [ ] Integration tests pass (`python integration_test.py`) — if applicable
- [ ] `CHANGELOG.md` updated under `[Unreleased]`
Expand Down
53 changes: 30 additions & 23 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,29 +7,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

## [1.3.0] — 2026-05-07

### Added

- **Automatic provider-icon sync** — new `_sync_model_icons()` method writes provider icons directly into Open WebUI's Models database so they appear in the UI; controlled by the `SYNC_PROVIDER_ICONS` valve (default: enabled). Models with a manually-set icon are never overwritten
- **`_is_owui_managed_icon()` helper** — distinguishes OWUI-default icons (`data:` URLs) and our own provider icons from user-set custom icons, enabling safe icon updates without clobbering user customisations

### Fixed

- **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`
- **Icon sync: icons now actually appear in the UI** — five bugs prevented provider icons from ever showing after the first pipe load:
- *Wrong skip condition* — `if existing_icon:` skipped any model with *any* icon (including the generic `data:` SVG that OWUI assigns by default), so provider icons were never applied; fixed to skip only user-set custom URLs
- *Race condition* — `_sync_model_icons()` was called before `pipes()` returned, i.e. before OWUI registered the models; OWUI then overwrote the early insert with its own default icon; fixed by also calling `_sync_model_icons()` on cache-hit paths (until all models are confirmed synced)
- *Exception swallowed retry* — DB errors added the model to `_icons_synced` anyway, permanently preventing retry; removed the erroneous add
- *Insert marked as synced prematurely* — after `insert_new_model` the model was marked synced even though OWUI could overwrite it; the insert path no longer updates `_icons_synced`
- *User params clobbered* — `update_model_by_id` used an empty `ModelParams()`, erasing user-configured temperature/system-prompt/etc.; now preserves `existing.params`
- **Icon sync: `function_id` cached at init** — `type(self).__module__` is evaluated once in `__init__` instead of on every `_sync_model_icons()` call
- **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

### Added

- **Audio output handling** — models that return audio (e.g. `openai/gpt-4o-audio-preview`) now have their transcript surfaced as text in both streaming and non-streaming responses
- **Image output handling** — models that return images (e.g. `google/gemini-2.5-flash-image-preview`) now embed valid HTTP/HTTPS image URLs as markdown, with a leading blank-line separator and URL validation to drop unsafe schemes
- **Token usage and cost display** — non-stream responses append a "Tokens: X in / Y out · Cost: $Z" footer when the OpenRouter response includes `usage` data
- **Connection pooling** via `requests.Session` for better performance across multiple API calls
- **Model list caching** with 5-minute TTL and valve-fingerprint invalidation — avoids redundant API calls when reopening the model selector
- **Exponential backoff with jitter** on transient errors (Timeout, ConnectionError) — `min(2^attempt + random, 30s)`
Expand All @@ -55,16 +41,37 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- `FREE_ONLY` pricing comparison uses `float()` instead of string comparison
- Model cache key includes `MODEL_PREFIX` to prevent stale results after prefix changes
- Removed unused `_API_PATH_AUTH` constant and `auth_url` property
- Provider-icon catalogue trimmed to 13 verified providers (was previously documented as 22 but only 13 were ever defined in `_PROVIDER_ICONS`); tilde model filtering and model-ID stripping corrected at the same time

### Fixed

- **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`
- **Icon sync: icons now actually appear in the UI** — five bugs prevented provider icons from ever showing after the first pipe load:
- *Wrong skip condition* — `if existing_icon:` skipped any model with *any* icon (including the generic `data:` SVG that OWUI assigns by default), so provider icons were never applied; fixed to skip only user-set custom URLs
- *Race condition* — `_sync_model_icons()` was called before `pipes()` returned, i.e. before OWUI registered the models; OWUI then overwrote the early insert with its own default icon; fixed by also calling `_sync_model_icons()` on cache-hit paths (until all models are confirmed synced)
- *Exception swallowed retry* — DB errors added the model to `_icons_synced` anyway, permanently preventing retry; removed the erroneous add
- *Insert marked as synced prematurely* — after `insert_new_model` the model was marked synced even though OWUI could overwrite it; the insert path no longer updates `_icons_synced`
- *User params clobbered* — `update_model_by_id` used an empty `ModelParams()`, erasing user-configured temperature/system-prompt/etc.; now preserves `existing.params`
- **Icon sync: `function_id` cached at init** — `type(self).__module__` is evaluated once in `__init__` instead of on every `_sync_model_icons()` call
- **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)
- **Image markdown safety** — image URLs are validated against `http://`/`https://` schemes before being embedded; invalid URLs are silently dropped instead of producing broken markdown
- Fixed potential payload mutation when `ENABLE_CACHE_CONTROL` is active (deepcopy prevents side effects)
- Fixed potential `IndexError` on stream chunks with empty `choices` array
- Fixed stream error handler not caching response body before closing connection
- Safe `isinstance(err, dict)` checks before calling `.get()` on error objects
- `_close_think_tag()` helper eliminates duplicated think-tag closure logic (was 5x repeated)
- `_stream_response` now closes the response in a `finally` block even on consumer `break`

## [1.2.0] — 2026-02-17

> Documented but never tagged on GitHub. The features below shipped to users via direct paste-install of `openrouter_pipe.py` and were rolled into the `v1.3.0` GitHub release.

### Added

- See `[1.3.0]` for the consolidated entry list. Original 1.2.0 scope: connection pooling, model list caching, exponential backoff with jitter, fallback deduplication, citation URL sanitization, base URL validation, "error" model guard, empty messages guard, fallback model attribution, HTTP 502 auth detection.

## [1.1.1] — 2026-02-17

### Changed
Expand Down Expand Up @@ -134,10 +141,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Basic error handling and timeout configuration
- Model prefix customization

<!-- Compare links -->
[Unreleased]: https://github.com/sena-labs/Open-WebUI-Pipe-OpenRouter/compare/v1.2.0...HEAD
[1.2.0]: https://github.com/sena-labs/Open-WebUI-Pipe-OpenRouter/compare/v1.1.1...v1.2.0
[1.1.1]: https://github.com/sena-labs/Open-WebUI-Pipe-OpenRouter/compare/v1.1.0...v1.1.1
<!-- Compare links — only point to tags that exist on GitHub.
v1.1.1 and v1.2.0 were documented but never tagged; their content is consolidated under v1.3.0. -->
[Unreleased]: https://github.com/sena-labs/Open-WebUI-Pipe-OpenRouter/compare/v1.3.0...HEAD
[1.3.0]: https://github.com/sena-labs/Open-WebUI-Pipe-OpenRouter/compare/v1.1.0...v1.3.0
Comment on lines +145 to +147
[1.1.0]: https://github.com/sena-labs/Open-WebUI-Pipe-OpenRouter/compare/v1.0.0...v1.1.0
[1.0.0]: https://github.com/sena-labs/Open-WebUI-Pipe-OpenRouter/compare/v0.1.0...v1.0.0
[0.1.0]: https://github.com/sena-labs/Open-WebUI-Pipe-OpenRouter/releases/tag/v0.1.0
4 changes: 2 additions & 2 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ By participating, you agree to uphold this code.

| Command | Description |
| --- | --- |
| `python test_pipe.py` | Run the full unit test suite (252 tests) |
| `python test_pipe.py` | Run the full unit test suite (431 tests) |
| `python integration_test.py` | Run live API tests (requires `OPENROUTER_API_KEY`) |

## Deliverable-PR playbook
Expand All @@ -74,7 +74,7 @@ of value. The playbook:
incidental refactors.

3. **Add or update tests.** A change without test coverage needs a written justification
in the PR body. The unit test suite must remain at 252/252.
in the PR body. The unit test suite must remain at 431/431.

4. **Validate locally** using the same commands CI runs:

Expand Down
12 changes: 6 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
[![Python](https://img.shields.io/badge/Python-%E2%89%A53.10-blue)](https://www.python.org/)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)

Access **300+ AI models** through OpenRouter directly inside Open WebUI — with provider routing,
Access **340+ AI models** through OpenRouter directly inside Open WebUI — with provider routing,
reasoning tokens, streaming, fallbacks, and cache control out of the box.

## Feature gallery
Expand Down Expand Up @@ -58,7 +58,7 @@ reasoning tokens, streaming, fallbacks, and cache control out of the box.
- **Middle-out compression** — fits long prompts within context windows (`transforms: ["middle-out"]`).
- **Cache control** — Anthropic-style `cache_control` injection on the longest message chunk.
- **Citations** — `[n]` references from web-search-enabled models are converted to markdown links.
- **Provider icons** — 22 provider logos synced directly into Open WebUI's model database.
- **Provider icons** — 13 provider logos synced directly into Open WebUI's model database.
- **Retry logic** — exponential backoff with jitter on timeout and connection errors.
- **FREE_ONLY mode** — filter to show only free-tier models (`:free` suffix or `0/0` pricing).
- **Pre-flight validation** — invalid API keys are caught at model-fetch time, not after sending a message.
Expand Down Expand Up @@ -96,7 +96,7 @@ All OpenRouter models will appear in the model selector immediately.
git clone https://github.com/sena-labs/Open-WebUI-Pipe-OpenRouter.git
cd Open-WebUI-Pipe-OpenRouter
pip install -r requirements.txt
python test_pipe.py # 252 tests — verify everything is green
python test_pipe.py # 431 tests — verify everything is green
```

## Usage
Expand Down Expand Up @@ -199,8 +199,8 @@ The pipe implements the **Manifold** pattern: one pipe entry point that surfaces
Open-WebUI-Pipe-OpenRouter/
├── openrouter_pipe.py # Main pipe source — install this in Open WebUI
├── function.json # Open WebUI community manifest
├── test_pipe.py # Unit test suite (252 tests)
├── integration_test.py # Live API integration tests (47 tests)
├── test_pipe.py # Unit test suite (431 tests)
├── integration_test.py # Live API integration tests (43 assertions)
├── TESTING.md # Manual pre-release checklist
├── SECURITY.md # Security policy
├── CONTRIBUTING.md # Contribution guidelines
Expand Down Expand Up @@ -229,7 +229,7 @@ It also removes `user` when sent as a dict (Open WebUI format) since OpenRouter
## Development

```bash
python test_pipe.py # Unit tests (252 tests)
python test_pipe.py # Unit tests (431 tests)
python integration_test.py # Live API tests (requires OPENROUTER_API_KEY)
```

Expand Down
10 changes: 5 additions & 5 deletions SECURITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,10 @@ critical-only fixes on the previous one.

| Version | Status | Security fixes |
| ------- | ------------------- | -------------- |
| 1.2.x | :white_check_mark: | active |
| 1.1.x | :white_check_mark: | critical only |
| 1.0.x | :x: | end-of-life |
| < 1.0 | :x: | end-of-life |
| 1.3.x | :white_check_mark: | active |
| 1.2.x | :white_check_mark: | critical only |
| 1.1.x | :x: | end-of-life |
| < 1.1 | :x: | end-of-life |

## Reporting a Vulnerability

Expand Down Expand Up @@ -70,7 +70,7 @@ The pipe implements the following security practices:

Every push to `main` and every pull request runs:

- **Unit tests** (`.github/workflows/tests.yml`) — 252 tests across Python 3.10–3.13. Failures block merge.
- **Unit tests** (`.github/workflows/tests.yml`) — 431 tests across Python 3.10–3.13. Failures block merge.

## Disclosure Policy

Expand Down
4 changes: 2 additions & 2 deletions TESTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -194,8 +194,8 @@ Must exit with `All tests passed! ✓` and `✗ Failed: 0`. If any test fails, *

## Quick pre-release checklist

- [ ] `python test_pipe.py` → 252 passed, 0 failed
- [ ] `python integration_test.py` → 47/47
- [ ] `python test_pipe.py` → 431 passed, 0 failed
- [ ] `python integration_test.py` → 43/43
- [ ] Empty API key → clear error message in model selector
- [ ] Valid API key → 340+ models with provider icons
- [ ] Non-streaming chat works
Expand Down
6 changes: 3 additions & 3 deletions function.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,13 @@
"name": "OpenRouter Pipe",
"type": "manifold",
"meta": {
"description": "Access 300+ AI models through OpenRouter directly inside Open WebUI. Features provider routing, reasoning tokens with <think> tags, full SSE streaming, model fallbacks, middle-out compression, Anthropic cache control, citations, 22 provider icons, and configurable retry logic.",
"description": "Access 340+ AI models through OpenRouter directly inside Open WebUI. Features provider routing, reasoning tokens with <think> tags, full SSE streaming, model fallbacks, middle-out compression, Anthropic cache control, citations, 13 provider icons, and configurable retry logic.",
"manifest": {
"title": "OpenRouter Pipe",
"author": "Sena Labs",
"author_url": "https://github.com/sena-labs",
"funding_url": "https://github.com/sponsors/sena-labs",
"version": "1.2.0",
"version": "1.3.0",
"license": "MIT",
"required_open_webui_version": "0.4.0",
"requirements": ["requests>=2.20", "pydantic>=2.0"]
Expand All @@ -32,6 +32,6 @@
"content": "openrouter_pipe.py",
"is_active": true,
"is_global": true,
"updated_at": "2026-02-17T00:00:00.000Z",
"updated_at": "2026-05-07T00:00:00.000Z",
"created_at": "2026-01-21T00:00:00.000Z"
}
2 changes: 1 addition & 1 deletion integration_test.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
"""
Integration test for OpenRouter Pipe v1.2.0
Integration test for OpenRouter Pipe v1.3.0
Tests the pipe against the LIVE OpenRouter API.

Usage:
Expand Down
4 changes: 2 additions & 2 deletions openrouter_pipe.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,12 @@
author: Sena Labs
author_url: https://github.com/sena-labs
funding_url: https://github.com/sponsors/sena-labs
version: 1.2.0
version: 1.3.0
license: MIT
icon_url: data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxMDAgMTAwIj48ZGVmcz48bGluZWFyR3JhZGllbnQgaWQ9ImJnIiB4MT0iMCUiIHkxPSIwJSIgeDI9IjEwMCUiIHkyPSIxMDAlIj48c3RvcCBvZmZzZXQ9IjAlIiBzdG9wLWNvbG9yPSIjNmQyOGQ5Ii8+PHN0b3Agb2Zmc2V0PSIxMDAlIiBzdG9wLWNvbG9yPSIjYTc4YmZhIi8+PC9saW5lYXJHcmFkaWVudD48L2RlZnM+PHJlY3Qgd2lkdGg9IjEwMCIgaGVpZ2h0PSIxMDAiIHJ4PSIyMCIgZmlsbD0idXJsKCNiZykiLz48cGF0aCBkPSJNMjAgNTAgQzIwIDMwLCA0MCAzMCwgNTAgMzAgTDUwIDIyIEw2OCA0MCBMNTAgNTggTDUwIDUwIEM0MCA1MCwgMzUgNDUsIDMwIDUwIEMyNSA1NSwgMjAgNzAsIDIwIDUwIFoiIGZpbGw9IndoaXRlIiBvcGFjaXR5PSIwLjk1Ii8+PGNpcmNsZSBjeD0iNzgiIGN5PSIzMCIgcj0iNyIgZmlsbD0id2hpdGUiIG9wYWNpdHk9IjAuOCIvPjxjaXJjbGUgY3g9IjgyIiBjeT0iNTAiIHI9IjciIGZpbGw9IndoaXRlIiBvcGFjaXR5PSIwLjk1Ii8+PGNpcmNsZSBjeD0iNzgiIGN5PSI3MCIgcj0iNyIgZmlsbD0id2hpdGUiIG9wYWNpdHk9IjAuOCIvPjxsaW5lIHgxPSI2OCIgeTE9IjQwIiB4Mj0iNzYiIHkyPSIzMiIgc3Ryb2tlPSJ3aGl0ZSIgc3Ryb2tlLXdpZHRoPSIyIiBvcGFjaXR5PSIwLjUiLz48bGluZSB4MT0iNjgiIHkxPSI0MCIgeDI9Ijc2IiB5Mj0iNTAiIHN0cm9rZT0id2hpdGUiIHN0cm9rZS13aWR0aD0iMiIgb3BhY2l0eT0iMC41Ii8+PGxpbmUgeDE9IjY4IiB5MT0iNDAiIHgyPSI3NiIgeTI9IjY4IiBzdHJva2U9IndoaXRlIiBzdHJva2Utd2lkdGg9IjIiIG9wYWNpdHk9IjAuNSIvPjwvc3ZnPg==
required_open_webui_version: 0.4.0
requirements: requests>=2.20, pydantic>=2.0
description: Access 300+ AI models through OpenRouter directly inside Open WebUI. Features provider routing, reasoning tokens with <think> tags, full SSE streaming, model fallbacks, middle-out compression, Anthropic cache control, citations, 22 provider icons, and configurable retry logic.
description: Access 340+ AI models through OpenRouter directly inside Open WebUI. Features provider routing, reasoning tokens with <think> tags, full SSE streaming, model fallbacks, middle-out compression, Anthropic cache control, citations, 13 provider icons, and configurable retry logic.
"""

import copy
Expand Down
2 changes: 1 addition & 1 deletion test_pipe.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
"""
Comprehensive test suite for OpenRouter Pipe v1.2.0
Comprehensive test suite for OpenRouter Pipe v1.3.0
Runs with: python test_pipe.py

Author: Sena Labs (https://github.com/sena-labs)
Expand Down
Loading