Skip to content

Add OpenRouter app attribution headers#708

Open
andyylin wants to merge 3 commits into
plastic-labs:mainfrom
andyylin:openrouter-app-attribution
Open

Add OpenRouter app attribution headers#708
andyylin wants to merge 3 commits into
plastic-labs:mainfrom
andyylin:openrouter-app-attribution

Conversation

@andyylin
Copy link
Copy Markdown

@andyylin andyylin commented May 21, 2026

Summary

  • add configurable OpenRouter attribution headers for OpenAI-compatible LLM clients
  • cover embedding client OpenRouter calls too
  • document LLM_OPENROUTER_APP_URL and LLM_OPENROUTER_APP_TITLE
  • add unit coverage for attribution behavior

Validation

  • rebuilt Honcho api/deriver Docker images locally
  • force-recreated live api/deriver containers
  • verified /health returns ok
  • verified live container resolves OpenRouter headers:
  • ran targeted ruff/compile/runtime smoke checks locally

Note: full pytest was not run locally because the production image excludes dev dependencies and the Pi host lacks the full Honcho test environment; CI should run the full suite.

Summary by CodeRabbit

  • New Features

    • Auto-applies OpenRouter attribution headers when configured; added configurable app URL/title.
    • Added CLI utilities for re-embedding and semantic benchmarking.
  • Configuration

    • Example config now includes commented OpenRouter app attribution keys.
    • New full config.toml for self-hosted deployment example.
  • Docker

    • Added docker-compose for self-hosted deployment and a local-compose profile for embedding tests.
  • Tests

    • Added tests for OpenRouter attribution and updated embedding client test fakes.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 21, 2026

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: bb5e8603-2a1e-4950-8dc9-914e48f1e5ac

📥 Commits

Reviewing files that changed from the base of the PR and between 0f5d71e and a00d66c.

📒 Files selected for processing (2)
  • config.toml
  • docker-compose.yml

Walkthrough

Adds optional OpenRouter app attribution settings and utilities, wires conditional attribution headers into OpenAI client construction for registry and embedding clients, updates tests, and introduces local-embedding tooling: a docker-compose for local testing, a re-embed pgvector script, and a semantic benchmark CLI.

Changes

OpenRouter Attribution

Layer / File(s) Summary
Configuration & Settings Schema
config.toml.example, src/config.py
Example config and LLMSettings schema now document and define optional OPENROUTER_APP_URL and OPENROUTER_APP_TITLE fields for OpenRouter app attribution.
OpenRouter Attribution Utilities
src/llm/openrouter.py
New module defines OPENROUTER_BASE_URL constant, is_openrouter_base_url() detector, and attribution_headers() builder that returns None for non-OpenRouter base URLs or attribution headers (HTTP-Referer, X-OpenRouter-Title, X-Title) when base URL targets OpenRouter and app metadata is provided.
LLM Registry Client Integration
src/llm/registry.py
Imports attribution_headers and applies it as default_headers to all OpenAI client instances: get_openai_client(), get_openai_override_client() cache entries, and module-level CLIENTS["openai"] prebuilt client.
Embedding Client Attribution
src/embedding_client.py
Imports attribution_headers and passes it (built from embedding client config and OpenRouter app metadata) as default_headers when constructing the AsyncOpenAI embeddings client.
Test Fakes & Attribution Coverage
tests/llm/test_embedding_client.py, tests/llm/test_openrouter_attribution.py
Test doubles updated to accept default_headers parameter; new test module verifies attribution_headers() returns None for non-OpenRouter URLs and expected headers for OpenRouter, omits empty values, and confirms get_openai_override_client() receives OpenRouter headers when configured with OpenRouter base URL.

Local Embeddings & Tooling

Layer / File(s) Summary
Local Docker Compose for Embeddings
docker-compose.local-embeddings.yml
Adds a docker-compose file with a shared embedding environment anchor applied to api and deriver, host mappings, and localhost-bound database/redis port exposure for local testing.
Re-embed pgvector Script
scripts/reembed_pgvector.py
New resumable async CLI that selects pending NULL-embedding rows, batch-embeds content, validates vector dimensions, updates pgvector embeddings and sync metadata, and commits per batch with progress logging and flags for table, batch size, and limit.
Semantic Benchmark & Search CLI
scripts/semantic_benchmark.py
New CLI that embeds provided or default queries, searches documents and message_embeddings via pgvector distance, and prints JSON or compact text results. Includes helpers for vector literal building and snippet truncation.

🎯 4 (Complex) | ⏱️ ~45 minutes

Suggested reviewers:

  • ajspig
  • VVoruganti

"I hopped through code with a twitchy nose,
Headers and docker where the wild grass grows,
Tests give a thump, scripts hum a tune,
Re-embed the vectors beneath the moon,
A rabbit's cheer for builds that bloom!" 🐇✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 17.39% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'Add OpenRouter app attribution headers' directly and concisely summarizes the main change across multiple files (config, LLMSettings, openrouter utilities, registry, embedding client, and tests) which all center on implementing OpenRouter attribution headers.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@tests/llm/test_embedding_client.py`:
- Around line 39-45: The FakeOpenAIClient used in
test_openai_embedding_client_rejects_dimension_mismatch must accept the
default_headers kwarg because _EmbeddingClient forwards default_headers into
AsyncOpenAI; update FakeOpenAIClient's __init__ signature to include
default_headers: dict[str,str] | None = None (and store or ignore it) so
instantiation from AsyncOpenAI won't raise TypeError, keeping the rest of the
fake behavior intact to allow the test to reach the dimension-mismatch
assertion.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 84710182-0de4-4e1b-b82e-ee2901b76a8f

📥 Commits

Reviewing files that changed from the base of the PR and between b5f24a6 and 63aae19.

📒 Files selected for processing (7)
  • config.toml.example
  • src/config.py
  • src/embedding_client.py
  • src/llm/openrouter.py
  • src/llm/registry.py
  • tests/llm/test_embedding_client.py
  • tests/llm/test_openrouter_attribution.py

Comment on lines +39 to +45
def __init__(
self,
*,
api_key: str | None,
base_url: str | None,
default_headers: dict[str, str] | None = None,
) -> None:
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify _EmbeddingClient passes default_headers and all FakeOpenAIClient
# test doubles in this file accept the kwarg.

rg -n -C2 'AsyncOpenAI\(' src/embedding_client.py
rg -n -C3 'class FakeOpenAIClient|def __init__\(' tests/llm/test_embedding_client.py

Repository: plastic-labs/honcho

Length of output: 1658


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Show how _EmbeddingClient constructs AsyncOpenAI (and whether default_headers is passed)
echo "== src/embedding_client.py: AsyncOpenAI construction =="
sed -n '150,230p' src/embedding_client.py

# Show the FakeOpenAIClient definitions with their full init signatures
echo
echo "== tests/llm/test_embedding_client.py: FakeOpenAIClient init signatures =="
sed -n '1,220p' tests/llm/test_embedding_client.py | nl -ba | sed -n '20,120p'
sed -n '1,220p' tests/llm/test_embedding_client.py | nl -ba | sed -n '140,220p'

Repository: plastic-labs/honcho

Length of output: 3768


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== tests/llm/test_embedding_client.py: FakeOpenAIClient init signatures (excerpts) =="

# Full file might be small; otherwise just focus on likely ranges
wc -l tests/llm/test_embedding_client.py

cat -n tests/llm/test_embedding_client.py | sed -n '1,130p'
echo
cat -n tests/llm/test_embedding_client.py | sed -n '130,230p'

Repository: plastic-labs/honcho

Length of output: 8720


🏁 Script executed:

#!/bin/bash
set -euo pipefail

rg -n "class FakeOpenAIClient" tests/llm/test_embedding_client.py
echo "----"
rg -n "def __init__" tests/llm/test_embedding_client.py | head -n 50
echo "----"
# Print the two FakeOpenAIClient blocks fully (small and targeted)
sed -n '30,110p' tests/llm/test_embedding_client.py
echo "----"
sed -n '150,210p' tests/llm/test_embedding_client.py

Repository: plastic-labs/honcho

Length of output: 4676


Update FakeOpenAIClient in test_openai_embedding_client_rejects_dimension_mismatch to accept default_headers.

_EmbeddingClient passes default_headers into AsyncOpenAI, but the fake in that test doesn’t accept the kwarg, so the test will fail with TypeError before reaching the dimension-mismatch assertion.

Proposed fix
 class FakeOpenAIClient:
-    def __init__(self, *, api_key: str | None, base_url: str | None) -> None:
+    def __init__(
+        self,
+        *,
+        api_key: str | None,
+        base_url: str | None,
+        default_headers: dict[str, str] | None = None,
+    ) -> None:
         self.embeddings: FakeOpenAIEmbeddingsAPI = fake_embeddings
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
def __init__(
self,
*,
api_key: str | None,
base_url: str | None,
default_headers: dict[str, str] | None = None,
) -> None:
class FakeOpenAIClient:
def __init__(
self,
*,
api_key: str | None,
base_url: str | None,
default_headers: dict[str, str] | None = None,
) -> None:
self.embeddings: FakeOpenAIEmbeddingsAPI = fake_embeddings
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/llm/test_embedding_client.py` around lines 39 - 45, The
FakeOpenAIClient used in test_openai_embedding_client_rejects_dimension_mismatch
must accept the default_headers kwarg because _EmbeddingClient forwards
default_headers into AsyncOpenAI; update FakeOpenAIClient's __init__ signature
to include default_headers: dict[str,str] | None = None (and store or ignore it)
so instantiation from AsyncOpenAI won't raise TypeError, keeping the rest of the
fake behavior intact to allow the test to reach the dimension-mismatch
assertion.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
scripts/reembed_pgvector.py (1)

35-46: 💤 Low value

SQL table interpolation is safe here (S608 false positive).

Ruff flags the f-string SQL, but table originates only from the TABLES constant / argparse choices, so it is not externally influenced. No action needed; optionally add an assert table in TABLES at the top of these helpers as defense against future refactors that call them with unchecked input.

Also applies to: 49-65

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/reembed_pgvector.py` around lines 35 - 46, The SQL f-string in async
def _count_remaining(table: str) is flagged but table is only allowed from
TABLES/argparse choices; add a defensive check at the top of _count_remaining
(and the other helper functions around lines 49-65 that also interpolate table)
such as an assert or explicit if raising ValueError to ensure table in TABLES
before executing the query so the linter/maintainers know the input is
constrained and future refactors won’t introduce unsafe interpolation.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@scripts/reembed_pgvector.py`:
- Around line 35-46: The SQL f-string in async def _count_remaining(table: str)
is flagged but table is only allowed from TABLES/argparse choices; add a
defensive check at the top of _count_remaining (and the other helper functions
around lines 49-65 that also interpolate table) such as an assert or explicit if
raising ValueError to ensure table in TABLES before executing the query so the
linter/maintainers know the input is constrained and future refactors won’t
introduce unsafe interpolation.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: f1f2cc93-fea9-45da-9507-45d439581872

📥 Commits

Reviewing files that changed from the base of the PR and between 63aae19 and 0f5d71e.

📒 Files selected for processing (3)
  • docker-compose.local-embeddings.yml
  • scripts/reembed_pgvector.py
  • scripts/semantic_benchmark.py
✅ Files skipped from review due to trivial changes (1)
  • docker-compose.local-embeddings.yml

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant