diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 34a5619..5cfae3a 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -1,29 +1,43 @@ -## Description +## Summary +## Related Issue + + + ## Type of Change - [ ] Bug fix (non-breaking change that fixes an issue) - [ ] New feature (non-breaking change that adds functionality) -- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected) +- [ ] Breaking change (fix or feature that would cause existing functionality to break) - [ ] Documentation update - [ ] Refactoring (no functional changes) +- [ ] Test improvement -## Checklist +## Changes Made -- [ ] I have read the [CONTRIBUTING.md](../CONTRIBUTING.md) guidelines -- [ ] 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) -- [ ] My code follows the project's code style (PEP 8, type hints) -- [ ] No secrets, API keys, or credentials are included in this PR + + +- +- +- -## Related Issues +## Testing - +- [ ] All unit tests pass (`python test_pipe.py` — 252/252 ✓) +- [ ] New tests added for the changes +- [ ] Integration tests pass (`python integration_test.py`) — if applicable +- [ ] `CHANGELOG.md` updated under `[Unreleased]` -## Additional Notes +## Screenshots / Demo - + + +## Checklist + +- [ ] My code follows the project's [coding standards](../CONTRIBUTING.md#coding-standards) +- [ ] I have added docstrings to new public methods +- [ ] My changes do not introduce debug `print` statements beyond `[OpenRouter Pipe]` logging +- [ ] No secrets, API keys, or credentials are included in this PR +- [ ] My commit messages follow [Conventional Commits](https://www.conventionalcommits.org/) diff --git a/CHANGELOG.md b/CHANGELOG.md index 839325a..7532fa9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ # Changelog -All notable changes to this project will be documented in this file. +All notable changes to **OpenRouter Pipe** will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). @@ -18,7 +18,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - **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 +## [1.2.0] — 2026-02-17 ### Added @@ -57,7 +57,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - `_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.1.1] - 2026-02-17 +## [1.1.1] — 2026-02-17 ### Changed @@ -73,7 +73,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Updated SECURITY.md to list v1.1.x as supported - Updated `function.json` metadata date -## [1.1.0] - 2026-02-15 +## [1.1.0] — 2026-02-15 ### Added @@ -82,7 +82,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Optional `__event_emitter__` support — shows "Querying OpenRouter..." status in chat UI - Additional defensive keys (`metadata`, `files`, `tool_ids`, `session_id`, `message_id`) stripped from payload -## [1.0.0] - 2026-02-14 +## [1.0.0] — 2026-02-14 ### Added @@ -114,7 +114,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Stream parser no longer crashes on malformed JSON chunks - HTTP error handler no longer crashes when response body is not JSON -## [0.1.0] - 2026-01-21 +## [0.1.0] — 2026-01-21 ### Added diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index e7e5350..2553d1e 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,97 +1,169 @@ # Contributing to OpenRouter Pipe -> **Maintained by [Sena Labs](https://github.com/sena-labs)** · [GitHub](https://github.com/sena-labs/OpenRouter-Pipe) +Thanks for wanting to contribute! This document is intentionally opinionated: it codifies the +**deliverable-PR playbook** the maintainers use day-to-day so new contributors can ship changes +with the same cadence and quality. -Thanks for your interest in contributing. +## Table of Contents -## Quick Start +- [Code of Conduct](#code-of-conduct) +- [Getting started](#getting-started) +- [Development setup](#development-setup) +- [Deliverable-PR playbook](#deliverable-pr-playbook) +- [Commit messages](#commit-messages) +- [Coding standards](#coding-standards) +- [Testing](#testing) +- [Reporting bugs](#reporting-bugs) +- [Suggesting features](#suggesting-features) -```bash -git clone https://github.com/sena-labs/OpenRouter-Pipe.git -cd OpenRouter-Pipe -python test_pipe.py -``` +## Code of Conduct + +This project follows the [Contributor Covenant Code of Conduct](.github/CODE_OF_CONDUCT.md). +By participating, you agree to uphold this code. + +## Getting started + +1. **Fork** the repository on GitHub. +2. **Clone** your fork locally: + + ```bash + git clone https://github.com//OpenRouter-Pipe.git + cd OpenRouter-Pipe + ``` + +3. **Install** dependencies: + + ```bash + pip install -r requirements.txt + ``` -## Development +4. **Verify** the baseline is green before changing anything: + + ```bash + python test_pipe.py + ``` + +## Development setup ### Prerequisites -- Python 3.10+ -- `requests` >= 2.20 -- `pydantic` >= 2.0 +- **Python** ≥ 3.10 (matches the CI matrix). +- **`requests`** ≥ 2.20. +- **`pydantic`** ≥ 2.0. -Install dependencies: +### Useful commands -```bash -pip install -r requirements.txt -``` +| Command | Description | +| --- | --- | +| `python test_pipe.py` | Run the full unit test suite (252 tests) | +| `python integration_test.py` | Run live API tests (requires `OPENROUTER_API_KEY`) | -### Running Tests +## Deliverable-PR playbook -```bash -python test_pipe.py # Unit tests (252 tests) -python integration_test.py # Live API tests (requires OPENROUTER_API_KEY) -``` +Every change is shipped as a **single-deliverable pull request**. One PR = one reviewable unit +of value. The playbook: + +1. **Sync `main`** and create a branch with a descriptive slug: + + ```bash + git checkout main && git pull + git checkout -b feat/ + ``` + +2. **Implement** the smallest complete slice of the change. Prefer surgical edits over + incidental refactors. -All unit tests must pass. If adding new functionality, add corresponding tests. +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. -### Code Style +4. **Validate locally** using the same commands CI runs: -- Follow PEP 8 conventions -- Use type hints for all function signatures -- Keep methods focused — one responsibility per method -- Add docstrings for public methods -- Use `print(f"[OpenRouter Pipe] ...")` for debug logging + ```bash + python test_pipe.py + python integration_test.py # optional, requires a valid API key + ``` -## Pull Request Process +5. **Update `CHANGELOG.md`.** Prepend a bullet under `## [Unreleased]` describing the + user-visible impact. -1. **Fork** the repository -2. Create a **feature branch** (`git checkout -b feature/my-feature`) -3. Make your changes -4. **Run the test suite** and ensure all tests pass -5. Update `CHANGELOG.md` with your changes under `[Unreleased]` -6. Commit with a clear message (`git commit -m "feat: add XYZ support"`) -7. Push to your fork and **open a Pull Request** +6. **Commit with Conventional Commits** (see [Commit messages](#commit-messages)) and push: + + ```bash + git push -u origin feat/ + ``` + +7. **Open the pull request** using the [PR template](.github/PULL_REQUEST_TEMPLATE.md) and + fill every section. + +8. **Verify CI** goes green. A failing test leg blocks the merge — fix forward rather than + disabling the check. + +Keep the branch focused: if the diff grows beyond ~300 lines of non-generated code, split it. + +## Commit messages + +We follow [Conventional Commits](https://www.conventionalcommits.org/): + +```text +(): + +[optional body] + +[optional footer] +``` + +**Types:** `feat`, `fix`, `docs`, `refactor`, `test`, `chore`, `ci`. + +**Examples:** + +```text +feat(routing): add PROVIDER_IGNORE valve +fix(stream): close think tag on mid-stream error +docs: update README installation steps +test: add retry exhaustion coverage +``` -### Commit Convention +## Coding standards -We use [Conventional Commits](https://www.conventionalcommits.org/): +- **PEP 8** — enforced by convention; keep line length ≤ 100 chars. +- **Type hints** on every function signature. +- **One responsibility per method** — keep functions under ~50 lines; extract helpers when they grow. +- **Docstrings** on every public method using the one-line summary style. +- **Logging** via `print(f"[OpenRouter Pipe] …")` — never log API keys or user content. +- No `any`-equivalent type erasure; use proper `dict[str, Any]` annotations. -| Prefix | Usage | -|--------|-------| -| `feat:` | New feature | -| `fix:` | Bug fix | -| `docs:` | Documentation only | -| `refactor:` | Code restructuring | -| `test:` | Adding or updating tests | -| `chore:` | Maintenance tasks | +## Testing -### PR Checklist +- **Framework:** Python `unittest` (stdlib). +- **Mock strategy:** `unittest.mock.patch` for HTTP calls and Open WebUI internals. +- **Conventions:** + - Test path mirrors source: `openrouter_pipe.py` ↔ `test_pipe.py`. + - Each test class covers one unit (`TestPreparePayload`, `TestStreamResponse`, etc.). + - Use descriptive method names: `test_fallback_deduplication_removes_duplicates`. +- **Run the suite the same way CI does:** `python test_pipe.py`. Integration tests require a + live API key and are optional for most contributions. -- [ ] Tests pass (`python test_pipe.py` and `python integration_test.py` → 0 failures) -- [ ] New features have corresponding tests -- [ ] `CHANGELOG.md` updated -- [ ] Code follows existing style conventions -- [ ] No OpenRouter API keys or secrets committed +## Reporting bugs -## Reporting Issues +When reporting a bug, open a [GitHub issue](https://github.com/sena-labs/OpenRouter-Pipe/issues) +using the **Bug report** template and include: -When reporting a bug, please include: +1. **Open WebUI version** (`Admin Panel → About`). +2. **Python version** (`python --version`). +3. **Steps to reproduce** the issue. +4. **Expected vs. actual behavior.** +5. **Error logs** — check the Open WebUI server logs and filter for `[OpenRouter Pipe]` messages. -1. **Open WebUI version** you're running -2. **Python version** (`python --version`) -3. **Steps to reproduce** the issue -4. **Expected vs actual behavior** -5. **Error logs** (check Open WebUI server logs for `[OpenRouter Pipe]` messages) +**Do not** include your API key in the issue. Redact it before pasting logs. -## Feature Requests +## Suggesting features -Before requesting a feature: +Before opening a feature request: -1. Check [OpenRouter API docs](https://openrouter.ai/docs) to confirm the feature exists upstream -2. Check [Open WebUI Pipe docs](https://docs.openwebui.com/features/plugin/functions/pipe) for compatibility -3. Open an issue describing the feature and its use case +1. Check the [OpenRouter API docs](https://openrouter.ai/docs) to confirm the feature exists upstream. +2. Check the [Open WebUI Pipe docs](https://docs.openwebui.com/features/plugin/functions/pipe) for compatibility constraints. +3. Open an issue using the **Feature request** template and describe the use case and expected behavior. -## License +--- -By contributing, you agree that your contributions will be licensed under the [MIT License](LICENSE). +Thanks for helping improve OpenRouter Pipe! 🚀 diff --git a/README.md b/README.md index 24b0a72..687f831 100644 --- a/README.md +++ b/README.md @@ -1,192 +1,227 @@ -
+# OpenRouter Pipe - +[![Build](https://github.com/sena-labs/OpenRouter-Pipe/actions/workflows/tests.yml/badge.svg)](https://github.com/sena-labs/OpenRouter-Pipe/actions/workflows/tests.yml) +[![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) -version  -python  -license  -openwebui +Access **300+ AI models** through OpenRouter directly inside Open WebUI — with provider routing, +reasoning tokens, streaming, fallbacks, and cache control out of the box. -tests  -stars  -issues + + -
+## Feature gallery -# OpenRouter Pipe +### Model selector + + +*Models from OpenAI, Anthropic, Google, Meta, Mistral, DeepSeek and more — each with its provider icon.* + +### Reasoning tokens + + +*`` blocks streamed in real time with configurable effort levels.* -**Access 300+ AI models through OpenRouter directly inside Open WebUI.** +### Provider routing in action -Provider routing · Reasoning tokens · Streaming · Fallbacks · Cache control + +*Sort, prefer, exclude and require parameters across providers per request.* --- ## Table of Contents -- [Why OpenRouter Pipe?](#why-openrouter-pipe) -- [Screenshots](#screenshots) -- [Quick Start](#quick-start) - [Features](#features) +- [Requirements](#requirements) +- [Installation](#installation) + - [From Open WebUI Community](#from-open-webui-community) + - [Manual install](#manual-install) + - [From source](#from-source) +- [Usage](#usage) - [Configuration](#configuration) -- [API Reference](#api-reference) -- [Compatibility](#compatibility) -- [Project Structure](#project-structure) -- [Testing](#testing) -- [Troubleshooting](#troubleshooting) + - [Core](#core) + - [Reasoning](#reasoning) + - [Display & Filtering](#display--filtering) + - [Provider Routing](#provider-routing) + - [Advanced](#advanced) + - [Network](#network) +- [Architecture](#architecture) +- [Development](#development) - [Contributing](#contributing) +- [Troubleshooting](#troubleshooting) +- [FAQ](#faq) - [License](#license) --- -## Why OpenRouter Pipe? +## Features -OpenRouter Pipe is the most feature-complete integration between [Open WebUI](https://docs.openwebui.com) and [OpenRouter](https://openrouter.ai). It gives you access to **300+ AI models** — including GPT-5, Claude 4, Gemini 2.5, Llama 4, DeepSeek R1, and more — directly in your Open WebUI interface, with zero configuration beyond an API key. +- **Manifold pipe** — exposes all OpenRouter models as native Open WebUI models in the model selector. +- **Provider routing** — sort by `price`, `throughput`, or `latency`; prefer or exclude specific providers; enforce `require_parameters`. +- **Reasoning tokens** — `` blocks streamed in real time with configurable effort (`low`, `medium`, `high`). +- **Streaming** — full SSE streaming with mid-stream error handling and automatic `` closure on error. +- **Model fallbacks** — automatic failover to one or more backup models via `FALLBACK_MODELS`. +- **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. +- **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. -**Key differentiators:** -- **Pre-flight API key validation** — invalid keys are caught at model-fetch time, not after you send a message -- **Full provider routing** — sort, prefer, exclude, and require parameters across providers -- **Native reasoning tokens** — `` blocks with configurable effort levels -- **Production-grade reliability** — retry logic, fallback models, mid-stream error recovery -- **22 provider icons** — visual model identification in the selector +## Requirements ---- +- **[Open WebUI](https://docs.openwebui.com/)** ≥ 0.4.0 running locally or in Docker. +- **[OpenRouter API key](https://openrouter.ai/keys)** — free account, key starts with `sk-or-`. +- **Python** ≥ 3.10 (managed by Open WebUI; no separate install needed for the pipe). -## Screenshots +## Installation - - +### From Open WebUI Community -*Screenshots coming soon — contributions welcome!* +Search for **"OpenRouter Pipe"** on [openwebui.com](https://openwebui.com) and install it directly +from the community hub — no copy-paste required. ---- +### Manual install -## Quick Start +1. Copy the full content of [`openrouter_pipe.py`](openrouter_pipe.py). +2. In Open WebUI, navigate to **Admin Panel → Functions**. +3. Click **+ Add Function** (or **Import**). +4. Paste the code and save. +5. **Enable** the function using the toggle. +6. Click the **⚙️ Valves** icon and enter your `OPENROUTER_API_KEY`. -### Prerequisites +All OpenRouter models will appear in the model selector immediately. -- [Open WebUI](https://docs.openwebui.com) v0.4+ running -- [OpenRouter](https://openrouter.ai) API key +> **Note:** You can also set `OPENROUTER_API_KEY` as a server environment variable instead of +> entering it in Valves. -### Installation +### From source -1. **Open your Open WebUI instance** -2. Navigate to **Admin Panel → Functions** -3. Click **"+ Add Function"** (or **Import**) -4. Paste the entire contents of [`openrouter_pipe.py`](openrouter_pipe.py) -5. Save and **enable** the function -6. Go to **Valves** (⚙️ icon) and enter your `OPENROUTER_API_KEY` -7. All OpenRouter models will appear in your model selector +```bash +git clone https://github.com/sena-labs/OpenRouter-Pipe.git +cd OpenRouter-Pipe +pip install -r requirements.txt +python test_pipe.py # 252 tests — verify everything is green +``` -> **Tip:** You can also set the API key via environment variable `OPENROUTER_API_KEY` on the server. +## Usage -Alternatively, search for **"OpenRouter Pipe"** on [openwebui.com](https://openwebui.com) and install it directly from the community hub. +All behavior is controlled through **Valves** in the Open WebUI admin panel. Every valve accepts +an environment variable fallback (see [Configuration](#configuration)). ---- +### Common valve combinations -## Features +| Goal | Valves to set | +| --- | --- | +| Show only OpenAI and Anthropic models | `MODEL_PROVIDERS = openai,anthropic` | +| Show only free models | `FREE_ONLY = true` | +| Use DeepSeek for reasoning | select `deepseek/deepseek-r1`, `INCLUDE_REASONING = true` | +| Route cheapest provider first | `PROVIDER_SORT = price` | +| Add a fallback model | `FALLBACK_MODELS = anthropic/claude-3.5-sonnet` | -| Feature | Description | -|---------|-------------| -| **Manifold Pipe** | Exposes all OpenRouter models as native Open WebUI models | -| **Provider Routing** | Sort by price/throughput/latency, prefer or exclude providers | -| **Reasoning Tokens** | `` tags with configurable effort (low/medium/high) | -| **Streaming** | Full SSE streaming with mid-stream error handling | -| **Model Fallbacks** | Automatic failover to backup models | -| **Middle-Out Compression** | Fit long prompts within context windows | -| **Cache Control** | Anthropic-style prompt caching for cost savings | -| **Citations** | Auto-inject citation links from web-search enabled models | -| **Provider Icons** | 22 provider logos displayed in the model selector | -| **Retry Logic** | Configurable auto-retry on timeout/connection errors | -| **FREE_ONLY Mode** | Filter to show only free-tier models | +### Reasoning tokens ---- +When `INCLUDE_REASONING` is enabled (default), the pipe requests reasoning tokens from models that +support them. The internal reasoning appears inside `` blocks before the main +response. + +Set `REASONING_EFFORT` to `low`, `medium`, or `high` to control how much compute the model +allocates to reasoning. Leave it empty to let the model decide. + +### Citations + +Models with web-search capabilities return citation annotations. The pipe automatically converts +`[1]`, `[2]` references to `[[1]](url)` markdown links and appends a numbered **Citations:** +section at the end of the response. ## Configuration -All settings are configurable via **Valves** in the Open WebUI admin panel. Every valve also accepts an environment variable fallback. +Every valve accepts an environment variable fallback. The table below lists both. ### Core | Valve | Env Var | Default | Description | -|-------|---------|---------|-------------| +| --- | --- | --- | --- | | `OPENROUTER_API_KEY` | `OPENROUTER_API_KEY` | `""` | Your OpenRouter API key | | `OPENROUTER_BASE_URL` | `OPENROUTER_BASE_URL` | `https://openrouter.ai/api/v1` | API endpoint | ### Reasoning | Valve | Env Var | Default | Description | -|-------|---------|---------|-------------| -| `INCLUDE_REASONING` | `OPENROUTER_INCLUDE_REASONING` | `true` | Request reasoning tokens (shows `` blocks) | -| `REASONING_EFFORT` | `OPENROUTER_REASONING_EFFORT` | `""` | Effort level: `low`, `medium`, `high`, or empty to disable | +| --- | --- | --- | --- | +| `INCLUDE_REASONING` | `OPENROUTER_INCLUDE_REASONING` | `true` | Request reasoning tokens (`` blocks) | +| `REASONING_EFFORT` | `OPENROUTER_REASONING_EFFORT` | `""` | Effort level: `low`, `medium`, `high`, or empty | ### Display & Filtering | Valve | Env Var | Default | Description | -|-------|---------|---------|-------------| +| --- | --- | --- | --- | | `MODEL_PREFIX` | — | `None` | Custom prefix for model names (e.g. `🔥 `) | -| `MODEL_PROVIDERS` | `OPENROUTER_MODEL_PROVIDERS` | `ALL` | Provider filter (e.g. `openai,anthropic`). Use `ALL` for all models | -| `INVERT_PROVIDER_LIST` | `OPENROUTER_INVERT_PROVIDER_LIST` | `false` | Invert filter → exclusion list | -| `FREE_ONLY` | `OPENROUTER_FREE_ONLY` | `false` | Show only free-tier models (by suffix or pricing) | +| `MODEL_PROVIDERS` | `OPENROUTER_MODEL_PROVIDERS` | `ALL` | Provider filter (e.g. `openai,anthropic`). `ALL` means no filter | +| `INVERT_PROVIDER_LIST` | `OPENROUTER_INVERT_PROVIDER_LIST` | `false` | Treat `MODEL_PROVIDERS` as an exclusion list | +| `FREE_ONLY` | `OPENROUTER_FREE_ONLY` | `false` | Show only free-tier models | ### Provider Routing | Valve | Env Var | Default | Description | -|-------|---------|---------|-------------| +| --- | --- | --- | --- | | `PROVIDER_SORT` | `OPENROUTER_PROVIDER_SORT` | `""` | Sort: `price`, `throughput`, `latency` | | `PROVIDER_ORDER` | `OPENROUTER_PROVIDER_ORDER` | `""` | Preferred providers (comma-separated) | | `PROVIDER_IGNORE` | `OPENROUTER_PROVIDER_IGNORE` | `""` | Excluded providers (comma-separated) | -| `REQUIRE_PARAMETERS` | `OPENROUTER_REQUIRE_PARAMETERS` | `false` | Only use providers supporting all request params | +| `REQUIRE_PARAMETERS` | `OPENROUTER_REQUIRE_PARAMETERS` | `false` | Only use providers that support all request parameters | | `DATA_COLLECTION` | `OPENROUTER_DATA_COLLECTION` | `allow` | Data policy: `allow` or `deny` | ### Advanced | Valve | Env Var | Default | Description | -|-------|---------|---------|-------------| +| --- | --- | --- | --- | | `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 | +| `ENABLE_CACHE_CONTROL` | `OPENROUTER_ENABLE_CACHE_CONTROL` | `false` | Inject Anthropic `cache_control` on the longest message | | `SYNC_PROVIDER_ICONS` | `OPENROUTER_SYNC_ICONS` | `true` | Sync provider icons into Open WebUI's model database | ### Network | Valve | Env Var | Default | Description | -|-------|---------|---------|-------------| +| --- | --- | --- | --- | | `REQUEST_TIMEOUT` | `OPENROUTER_REQUEST_TIMEOUT` | `90` | HTTP timeout in seconds | -| `MAX_RETRIES` | — | `2` | Auto-retry on transient errors | - ---- +| `MAX_RETRIES` | — | `2` | Auto-retry count on transient errors | -## API Reference +## Architecture -### Architecture +The pipe implements the **Manifold** pattern: one pipe entry point that surfaces multiple models. -This pipe implements the **Manifold** pattern: +| Layer | Files | Responsibility | +| --- | --- | --- | +| Entry points | `Pipe.pipes()`, `Pipe.pipe()` | Model listing and chat routing | +| Payload | `_prepare_payload()` | Sanitize OWUI internals, inject routing and reasoning | +| Transport | `_retryable_request()` | Retry wrapper with exponential backoff | +| Streaming | `_stream_response()` | SSE parser, `` management, mid-stream errors | +| Non-streaming | `_non_stream_response()` | JSON response, body-level error detection | +| Enrichment | `_inject_cache_control()`, `_insert_citations()` | Post-processing | +```text +OpenRouter-Pipe/ +├── 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) +├── TESTING.md # Manual pre-release checklist +├── SECURITY.md # Security policy +├── CONTRIBUTING.md # Contribution guidelines +├── CHANGELOG.md # Version history +├── LICENSE # MIT License +├── requirements.txt # Python dependencies +└── .github/ + ├── workflows/ + │ └── tests.yml # CI pipeline (Python 3.10–3.13) + └── ISSUE_TEMPLATE/ + ├── bug_report.yml + └── feature_request.yml ``` -Open WebUI ↔️ Pipe.pipes() → model list from OpenRouter /models -Open WebUI ↔️ Pipe.pipe() → chat completions via OpenRouter /chat/completions -``` - -### Key Methods - -| Method | Description | -|--------|-------------| -| `pipes()` | Fetches and filters the model catalog from OpenRouter | -| `pipe(body, __user__, __event_emitter__)` | Routes chat completion to stream or non-stream handler, emits status events | -| `_prepare_payload(body)` | Sanitizes OWUI internals, injects provider routing, reasoning, fallbacks | -| `_stream_response(headers, payload)` | SSE parser with `` management and mid-stream error recovery | -| `_non_stream_response(headers, payload)` | JSON response handler with body-level error detection | -| `_retryable_request(headers, payload, stream)` | Retry wrapper for timeout/connection errors | -| `_inject_cache_control(payload)` | Applies Anthropic `cache_control` to longest message chunk | - -### Payload Sanitization -The pipe strips these Open WebUI internal keys before forwarding to OpenRouter: +The pipe strips these Open WebUI-internal keys before forwarding to OpenRouter: ```python _OWUI_INTERNAL_KEYS = { @@ -195,138 +230,99 @@ _OWUI_INTERNAL_KEYS = { } ``` -It also removes `user` when sent as a dict (OWUI format) since OpenRouter expects a string. +It also removes `user` when sent as a dict (Open WebUI format) since OpenRouter expects a string. ---- +## Development -## Compatibility +```bash +python test_pipe.py # Unit tests (252 tests) +python integration_test.py # Live API tests (requires OPENROUTER_API_KEY) +``` -| Component | Version | -|-----------|----------| -| Open WebUI | v0.4.0+ | -| Python | 3.10, 3.11, 3.12, 3.13 | -| Pydantic | v2.x (v2.0+) | -| OpenRouter API | v1 | +The unit test suite covers: valve defaults, payload preparation, streaming and non-streaming +responses, retry logic, citation injection, model listing, and `pipe()` routing. ---- +## Contributing -## Project Structure +Contributions are welcome. See [CONTRIBUTING.md](CONTRIBUTING.md) for the full playbook. -``` -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 (252 tests) -├── integration_test.py # Live API integration tests (47 tests) -├── TESTING.md # Pre-release testing checklist -├── SECURITY.md # Security policy and vulnerability reporting -├── README.md # This file -├── CHANGELOG.md # Version history -├── CONTRIBUTING.md # Contribution guidelines -├── LICENSE # MIT License -├── .gitignore # Git ignore rules -├── requirements.txt # Python dependencies -└── .github/ - ├── FUNDING.yml # GitHub Sponsors configuration - ├── CODE_OF_CONDUCT.md # Contributor Covenant v2.1 - ├── PULL_REQUEST_TEMPLATE.md # PR checklist template - ├── workflows/ - │ └── tests.yml # CI pipeline (Python 3.10–3.13) - └── ISSUE_TEMPLATE/ - ├── bug_report.yml # Bug report template - └── feature_request.yml # Feature request template -``` +## Troubleshooting ---- +### "OpenRouter API key not configured" -## Testing +#### Solution -```bash -python test_pipe.py -``` +Set your API key in **Admin Panel → Functions → OpenRouter Pipe → Valves** (⚙️), or set the +`OPENROUTER_API_KEY` environment variable on the server and restart Open WebUI. -Tests cover: -- Helper functions (`_insert_citations`, `_format_citation_list`, `_parse_csv`) -- Valve defaults and validation -- Payload preparation (key stripping, model ID fix, provider routing, fallbacks) -- Stream response (reasoning tags, mid-stream errors, auto-close, citations) -- Non-stream response (API errors, empty choices, timeout handling) -- Retry logic (success, retry on timeout, exhaustion, HTTPError passthrough) -- Async `pipe()` entry point (stream/non-stream routing, event emitter) -- Model listing (`pipes()`) with filters, prefix, error handling -- Valve `json_schema_extra` validation (password, dropdown menus) +### "Invalid API key (HTTP 401 / 502)" ---- +#### Solution -## Troubleshooting +Your key is incorrect or malformed. Retrieve a valid key from +[openrouter.ai/keys](https://openrouter.ai/keys) — it should start with `sk-or-`. -
-"OpenRouter API key not configured" +### "Rate limit exceeded (HTTP 429)" -Set your API key in **Admin Panel → Functions → OpenRouter Pipe → Valves** (⚙️ icon), or set the `OPENROUTER_API_KEY` environment variable on the server. -
+#### Solution -
-"Invalid API key (HTTP 401/502)" +Wait a moment and retry. `MAX_RETRIES` only retries on network timeouts and connection failures — +HTTP 429 errors are returned immediately. Consider upgrading your OpenRouter plan for higher limits. -Your API key is incorrect or malformed. Get a valid key from [openrouter.ai/keys](https://openrouter.ai/keys) and make sure it starts with `sk-or-`. -
+### "Insufficient credits (HTTP 402)" -
-"Rate limit exceeded (HTTP 429)" +#### Solution -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. -
+Add credits at [openrouter.ai/credits](https://openrouter.ai/credits). -
-"Insufficient credits (HTTP 402)" +### "Request timed out" -Your OpenRouter account balance is too low. Add credits at [openrouter.ai/credits](https://openrouter.ai/credits). -
+#### Solution -
-"Request timed out" +Increase `REQUEST_TIMEOUT` in Valves (default: 90 seconds), or try a faster model. Some large +reasoning models can take over a minute for complex prompts. -The model took too long to respond. Increase `REQUEST_TIMEOUT` in the valve settings (default: 90 seconds), or try a different model. -
+### No models appear in the selector -
-No models showing in the selector +#### Solution -1. Check that your API key is valid -2. If using `MODEL_PROVIDERS`, verify the provider names are correct (e.g., `openai`, `anthropic`, `google`) -3. If `FREE_ONLY` is enabled, some providers may not have free models available -4. Try setting `MODEL_PROVIDERS` to `ALL` to see all models -
+1. Verify your API key is valid (a single "error" model appears if it is not). +2. If `MODEL_PROVIDERS` is set, confirm the provider names are lowercase: `openai`, `anthropic`, `google`. +3. If `FREE_ONLY` is enabled, some providers may have no free models — try disabling it. +4. Set `MODEL_PROVIDERS = ALL` to show the full catalog. -
-Models load but chat returns errors +### Models load but chat returns errors -Some models may be temporarily unavailable on OpenRouter. Try a different model or check [OpenRouter Status](https://status.openrouter.ai). -
+#### Solution ---- +Some models may be temporarily unavailable. Try a different model or check +[status.openrouter.ai](https://status.openrouter.ai). -## Contributing +## FAQ -Contributions are welcome! See [CONTRIBUTING.md](CONTRIBUTING.md) for details. +**Q: Does this work with Open WebUI's native tool calling?** ---- +A: Tool calling is handled by Open WebUI before the pipe receives the request — the pipe forwards +the composed messages as-is. Whether a given OpenRouter model supports tool use depends on +OpenRouter's provider support for that model. -## License +**Q: Why does `FREE_ONLY` include models without a `:free` suffix?** -This project is licensed under the **MIT License** — see the [LICENSE](LICENSE) file for details. +A: Some models (e.g. `google/gemma-*`, `qwen/qwen3-*`) are genuinely free but are not suffixed +with `:free`. The pipe checks both the suffix and the actual pricing (`0/0` prompt and completion +cost) to catch these cases. -``` -Copyright (c) 2026 Sena Labs -``` +**Q: Can I use multiple provider filters at once?** ---- +A: `MODEL_PROVIDERS` accepts a comma-separated list (e.g. `openai,anthropic`). Enable +`INVERT_PROVIDER_LIST` to turn it into an exclusion list instead. -
+**Q: How do fallback models work?** - +A: `FALLBACK_MODELS` adds extra model IDs to the `models` array in the OpenRouter request. If the +primary model fails, OpenRouter automatically tries the next one. Non-streaming responses include +a "Responded by: model-id" attribution when a fallback handled the request. -Powered by **[Sena Labs](https://github.com/sena-labs)** +## License -
+This project is licensed under the **MIT License** — see the [LICENSE](LICENSE) file for details. diff --git a/SECURITY.md b/SECURITY.md index 33b3fa9..d56292c 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -1,27 +1,79 @@ # Security Policy +OpenRouter Pipe follows a **best-effort patch policy** on the latest minor release and +critical-only fixes on the previous one. + ## Supported Versions -| Version | Supported | -|---------|-----------| -| 1.2.x | Yes | -| 1.1.x | Yes | -| 1.0.x | Yes | -| < 1.0 | No | +| 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 | ## Reporting a Vulnerability -If you discover a security vulnerability, please report it responsibly: +We take the security of OpenRouter Pipe seriously. If you discover a security vulnerability, +please report it responsibly. + +### How to Report + +**Do not open a public GitHub issue for security vulnerabilities.** + +Instead, please send an email to **** with: + +1. **Description** of the vulnerability. +2. **Steps to reproduce** the issue. +3. **Impact assessment** — what an attacker could achieve. +4. **Affected versions** — which version(s) are impacted. +5. **Suggested fix** (if you have one). + +Alternatively, use +[GitHub's private vulnerability reporting](https://github.com/sena-labs/OpenRouter-Pipe/security/advisories/new). + +### What to Expect + +- **Acknowledgment** within 48 hours of your report. +- **Initial assessment** within 5 business days. +- **Fix timeline** communicated within 10 business days. +- **Credit** in the release notes (unless you prefer to remain anonymous). + +### Scope + +The following are in scope for security reports: + +- API key exposure through logs, error messages, or HTTP responses. +- Injection of arbitrary HTTP headers or request parameters via user-supplied valves. +- Unintended forwarding of sensitive Open WebUI internal data to OpenRouter. +- Dependency vulnerabilities with a known CVE affecting the production dependency closure. + +### Out of Scope + +- Vulnerabilities in the OpenRouter API itself (report to [OpenRouter](https://openrouter.ai)). +- Vulnerabilities in Open WebUI (report to the [Open WebUI project](https://github.com/open-webui/open-webui)). +- Denial of service via excessive `MAX_RETRIES` or `REQUEST_TIMEOUT` configuration. +- Social engineering attacks. + +### Security Measures + +The pipe implements the following security practices: + +- **No key logging** — `OPENROUTER_API_KEY` is never written to logs or included in error messages. +- **Pre-flight validation** — invalid keys are caught at model-fetch time via the `/models` response, before any user message is sent. +- **TLS enforced by default** — `OPENROUTER_BASE_URL` defaults to `https://openrouter.ai/api/v1`; the Pydantic validator requires the value to start with `https://` or `http://` and rejects any other scheme. +- **Internal key stripping** — Open WebUI internal fields (`chat_id`, `title`, `task`, `metadata`, `files`, `tool_ids`, `session_id`, `message_id`) are removed from the payload before forwarding. +- **No data persistence** — the pipe does not store user messages, model responses, or API keys beyond the scope of a single request. +- **Deep-copy payload** — `copy.deepcopy` is used on the request body to prevent mutation of Open WebUI's internal state. + +### Automated Security Gates + +Every push to `main` and every pull request runs: -1. **Do NOT** open a public issue -2. Use [GitHub's private vulnerability reporting](https://github.com/sena-labs/OpenRouter-Pipe/security/advisories/new) -3. Include: description, steps to reproduce, and potential impact -4. You will receive a response within 48 hours +- **Unit tests** (`.github/workflows/tests.yml`) — 252 tests across Python 3.10–3.13. Failures block merge. -## Security Design +## Disclosure Policy -- API keys are never logged or included in error messages -- API key validation at model-fetch time prevents requests with invalid credentials -- All HTTP requests use TLS (HTTPS) -- Open WebUI internal keys are stripped before forwarding to OpenRouter -- No user data is stored by the pipe +- We follow [coordinated vulnerability disclosure](https://en.wikipedia.org/wiki/Coordinated_vulnerability_disclosure). +- We aim to release patches within 14 days of confirming a vulnerability. +- Security advisories are published via [GitHub Security Advisories](https://github.com/sena-labs/OpenRouter-Pipe/security/advisories). diff --git a/TESTING.md b/TESTING.md index b82ba40..88c7a05 100644 --- a/TESTING.md +++ b/TESTING.md @@ -1,8 +1,12 @@ # Pre-Release Testing Guide -Manual checklist to verify every Pipe feature before release. +Manual checklist to verify every Pipe feature before release. Run the automated suite first, +then work through each section in order against a live Open WebUI instance. -> **Prerequisites**: Open WebUI >= 0.4.0 running, valid OpenRouter API key. +## Prerequisites + +- **Open WebUI** ≥ 0.4.0 running locally or in Docker. +- A valid **OpenRouter API key** (starts with `sk-or-`). --- @@ -190,22 +194,20 @@ 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 ✓ -[ ] Empty API key → clear error -[ ] Valid API key → 340+ models -[ ] Non-streaming chat works -[ ] Streaming chat works (token by token) -[ ] Reasoning tokens shown with -[ ] FREE_ONLY filters correctly (suffix + pricing) -[ ] Provider filter + inversion works -[ ] Prefix applied and removable -[ ] Fallbacks in payload -[ ] Middle-out in payload -[ ] Cache control on list content -[ ] Retry on timeout, no retry on 4xx -[ ] Errors formatted correctly -[ ] No secrets in logs/messages -[ ] OWUI internal fields removed from payload -``` +- [ ] `python test_pipe.py` → 252 passed, 0 failed +- [ ] `python integration_test.py` → 47/47 +- [ ] Empty API key → clear error message in model selector +- [ ] Valid API key → 340+ models with provider icons +- [ ] Non-streaming chat works +- [ ] Streaming chat works (token by token) +- [ ] Reasoning tokens shown with `` +- [ ] `FREE_ONLY` filters correctly (`:free` suffix + 0/0 pricing) +- [ ] Provider filter + inversion works +- [ ] Model prefix applied and removable +- [ ] Fallback models present in payload +- [ ] Middle-out present in payload +- [ ] Cache control applied on list-type message content +- [ ] Retry on timeout, no retry on 4xx errors +- [ ] Errors formatted correctly (no raw tracebacks) +- [ ] No secrets in logs or error messages +- [ ] Open WebUI internal fields removed from payload