Skip to content
Open
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
6 changes: 6 additions & 0 deletions changelog.d/track-a-foundation.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
category: Chores & Docs
pr: 757
---

**Track A foundation**: Plugin header contract documentation and `httpx-sse` dependency for upcoming multi-provider passthrough routes.
112 changes: 112 additions & 0 deletions docs/plugin-header-contract.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
# Plugin Header Contract

> **Version**: v1.0 (contract version, independent of plugin npm version)
> **Status**: Contract published ahead of implementation — gateway ingestion code lands in [PR #758](https://github.com/LuthienResearch/luthien-proxy/pull/758). Forward references to `passthrough_routes.py` and `019_add_agent_to_request_logs.sql` will be accurate once PR #758 merges.
> **Related**: [opencode-luthien plugin](https://github.com/LuthienResearch/opencode-luthien), [PR #758](https://github.com/LuthienResearch/luthien-proxy/pull/758) (gateway implementation)

This document defines the canonical set of HTTP headers injected by the `opencode-luthien` plugin into every proxied request. The gateway reads these headers to populate observability columns in `request_logs`.

---

## Trust Boundary

The gateway trusts `x-luthien-*` headers as received — it does not authenticate their origin beyond the client credential check already applied to every request. Operators who expose the gateway to untrusted clients should configure a reverse proxy to strip the specific headers documented here (`x-luthien-session-id`, `x-luthien-agent`, `x-luthien-provider`, `x-luthien-model`, `x-luthien-plugin-version`) before they reach the gateway, preventing clients from spoofing session IDs or agent names in logs.

> **Note**: `x-luthien-user-id` is a separate header controlled by the `TRUST_USER_ID_HEADER` gateway config. A blanket strip of all `x-luthien-*` headers would silently disable user attribution for operators who have intentionally enabled that setting. Strip only the headers listed above.

---

## Headers

### `x-luthien-session-id`

| Field | Value |
|---|---|
| **Source** | OpenCode session ID (UUIDv4, provided by OpenCode runtime) |
| **Type** | String (UUIDv4 format: `[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}`) |
| **Required** | No — absent when plugin is not loaded or proxy is unreachable |
| **Semantics** | Identifies the OpenCode session that originated the request. Unique per OpenCode process invocation. Shared across all requests within a single session. |
| **Example** | `x-luthien-session-id: 550e8400-e29b-41d4-a716-446655440000` |
| **Persisted to** | `request_logs.session_id` (dedicated column) |

> **Validation**: The gateway stores the value as-is with no length enforcement or UUID validation. The plugin MUST send a valid UUIDv4. Behavior for malformed or oversized values is undefined until PR-B adds explicit validation.

### `x-luthien-agent`

| Field | Value |
|---|---|
| **Source** | OpenCode agent name (e.g., `build`, `test`, `review`) |
| **Type** | String |
| **Required** | No — **omit this header when the agent name is unavailable**. Do not send a placeholder like `"unknown"`. |
| **Semantics** | Identifies which OpenCode agent mode was active when the request was made. Useful for filtering logs by agent type. When the header is absent, `request_logs.agent` is NULL — this is the canonical signal for "agent name unavailable or non-plugin traffic". Sending a placeholder string instead of omitting the header would conflate two distinct cases (plugin loaded but unresolved vs. plugin not loaded) and make them indistinguishable at query time. |
| **Example** | `x-luthien-agent: build` |
| **Persisted to** | `request_logs.agent` (dedicated column, introduced in PR-B / migration 019) |

### `x-luthien-provider`

| Field | Value |
|---|---|
| **Source** | Plugin — derived from the AI SDK provider ID |
| **Type** | String — known values: `anthropic`, `openai`, `google` |
| **Required** | No |
| **Semantics** | Identifies which AI provider the request targets. Redundant with the URL prefix (`/openai/`, `/gemini/`, `/anthropic/`) but included for convenience. Unknown values are logged via `request_headers` JSONB and passed through — the gateway does not reject unrecognised provider strings, supporting forward-compatibility as new providers are added. |
| **Example** | `x-luthien-provider: openai` |
| **Persisted to** | `request_logs.request_headers` JSONB (not a dedicated column — derivable from `endpoint` URL prefix at query time) |

### `x-luthien-model`

| Field | Value |
|---|---|
| **Source** | Plugin — derived from the AI SDK model ID |
| **Type** | String |
| **Required** | No |
| **Semantics** | Identifies the specific model requested (e.g., `gpt-4o`, `claude-3-5-sonnet-20241022`, `gemini-1.5-flash`). |
| **Example** | `x-luthien-model: gpt-4o` |
| **Persisted to** | `request_logs.model` (existing column) |

### `x-luthien-plugin-version`

| Field | Value |
|---|---|
| **Source** | Plugin — hardcoded to the plugin's npm package version |
| **Type** | String (semver) |
| **Required** | No |
| **Semantics** | Identifies the version of the `opencode-luthien` plugin that injected these headers. Useful for debugging version-specific behavior. |
| **Example** | `x-luthien-plugin-version: 0.1.0` |
| **Persisted to** | `request_logs.request_headers` JSONB (not a dedicated column) |

---

## Gateway Behavior

- **Inbound**: The gateway will read `x-luthien-session-id`, `x-luthien-agent`, and `x-luthien-model` from inbound requests and persist them to dedicated `request_logs` columns (agent column introduced in PR-B).
- **Outbound**: All `x-luthien-*` headers will be **stripped** before forwarding to upstream providers (Anthropic, OpenAI, Gemini) — by prefix match, not by enumerating the documented header names. This ensures headers added in future plugin versions are also stripped. They are internal observability headers and must not leak to external APIs.
- **Unknown headers**: Any `x-luthien-*` header not listed above will be logged (via `request_headers` JSONB) and ignored. This supports additive evolution.
- **Missing headers**: When `x-luthien-*` headers are absent (plugin not loaded, proxy unreachable), the corresponding `request_logs` columns will be NULL. Requests still succeed.

> **Note on `request_headers` JSONB**: `request_logs.request_headers` stores the raw inbound header set. Headers marked "Persisted to `request_headers` JSONB" are not routed to dedicated columns — they are accessible via JSON queries on the raw header blob.

---

## Versioning Policy

This contract is at **v1.0**.

| Change type | Policy |
|---|---|
| **Add a new `x-luthien-*` header** | Allowed. Bump minor version (v1.0 -> v1.1). Gateway ignores unknown headers. |
| **Remove an existing header** | **FORBIDDEN**. Removing a header is a breaking change. If removal is required, deprecate first (one release cycle), then remove in a new major version. |
| **Change the semantics of an existing header** | **FORBIDDEN**. Create a new header with a new name instead. |
| **Change the format of an existing header** | Treat as a semantic change — **FORBIDDEN** without a new header name. |

The plugin sends `x-luthien-plugin-version` on every request so the gateway can detect version mismatches in logs.

---

## Related

- Plugin source: [LuthienResearch/opencode-luthien](https://github.com/LuthienResearch/opencode-luthien)
- Plugin README: see plugin repo for installation and configuration
- Gateway passthrough routes: `src/luthien_proxy/passthrough_routes.py` (introduced in PR-B)
- Database schema: `migrations/postgres/008_add_request_logs_table.sql` (session_id), `migrations/postgres/019_add_agent_to_request_logs.sql` (agent, introduced in PR-B)
- Track B: Native provider pipelines will replace the passthrough routes and may extend this contract
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ dependencies = [
"anthropic>=0.84.0",
"aiohttp>=3.9.0",
"sentry-sdk[fastapi]>=2.54.0",
"httpx-sse>=0.4",
]

[tool.hatch.version]
Expand Down
2 changes: 2 additions & 0 deletions uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading