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
26 changes: 26 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -157,8 +157,34 @@ for deployment examples.
| Moonshot KimiCode | [`configs/moonshot-kimicode.toml`](configs/moonshot-kimicode.toml) | Ready profile for Moonshot KimiCode subscription keys with a local Kimi model catalog fallback. | No |
| OpenCode Go | [`configs/opencode-go.toml`](configs/opencode-go.toml) | Ready profile for OpenCode Go subscription keys, limited to its OpenAI-compatible chat-completions models. | No |
| Xiaomi Token Plan | [`configs/xiaomi-token-plan.toml`](configs/xiaomi-token-plan.toml) | Ready profile for `https://token-plan-sgp.xiaomimimo.com/v1`. | No |
| OpenRouter | [`configs/openrouter.toml`](configs/openrouter.toml) | Ready profile for OpenRouter; app attribution headers are attached on all upstream requests. | No |
| Destination override | `--destination https://provider.example/v1` | Quick one-off target without editing provider config. | Only when passed |

## OpenRouter App Attribution

Codex Warp automatically attaches [OpenRouter app attribution](https://openrouter.ai/docs/app-attribution)
headers on **every upstream request** — for all configured gateways, models, and
API paths (`/chat/completions`, native `/responses`, `/models`, and any other
outbound call) — not only when the [`configs/openrouter.toml`](configs/openrouter.toml)
profile is the default gateway. OpenRouter documents attribution across all of
its API routes and models; Warp always sends the headers so no gateway/model
combination can skip them.

- `HTTP-Referer`: `https://github.com/jatmn/Codex-warp`
- `X-OpenRouter-Title`: `Codex Warp`
- `X-Title`: `Codex Warp` (backwards-compatible alias)
- `X-OpenRouter-Categories`: `cli-agent,programming-app`

These are Codex Warp's own identity values. To override any of them for a
specific provider, set the header under that provider's `[provider.headers]` or
`[providers.<id>.headers]` section — user-supplied headers always take
precedence over the automatic ones.

Note: `HTTP-Referer` is Codex Warp's public GitHub URL, so traffic sent through
OpenRouter is attributed under that identity in OpenRouter's public rankings.
To attribute traffic to your own project instead, override `HTTP-Referer` (and
the other headers) under `[provider.headers]` or `[providers.<id>.headers]`.

## Supported Model Families

| Parent brand | Catalog | Examples covered |
Expand Down
1 change: 1 addition & 0 deletions codex-warp.toml
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ hide_codex_builtin_models = true
# "configs/opencode-go.toml",
# "configs/xiaomi-token-plan.toml",
# "configs/openai-compatible.toml",
# "configs/openrouter.toml",
# ]
model_family_include = [
"configs/model-families/deepseek.toml",
Expand Down
18 changes: 18 additions & 0 deletions configs/openrouter.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# OpenRouter provider profile.
# Docs: https://openrouter.ai/docs/app-attribution
#
# OpenRouter exposes a large live /models catalog, so no local model_catalog is
# needed here. Codex Warp automatically attaches the OpenRouter app attribution
# headers (HTTP-Referer, X-OpenRouter-Title, X-Title, X-OpenRouter-Categories)
# on every upstream request across all gateways and models. To override any of
# them, set the header under [provider.headers] or [providers.<id>.headers].

[providers.openrouter]
name = "OpenRouter"
base_url = "https://openrouter.ai/api/v1"
api_key_env = "OPENROUTER_API_KEY"
auth_header = "authorization"
auth_scheme = "Bearer"
responses_path = "/responses"
chat_completions_path = "/chat/completions"
models_path = "/models"
5 changes: 5 additions & 0 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,11 @@ Some providers require extra headers:
"X-Title" = "Codex Warp"
```

Codex Warp also attaches [OpenRouter app attribution](../README.md#openrouter-app-attribution)
headers on every upstream request (`HTTP-Referer`, `X-OpenRouter-Title`, `X-Title`,
and `X-OpenRouter-Categories`). Set any of those names under `[provider.headers]`
or `[providers.<id>.headers]` to override the automatic values for that gateway.

Codex Warp always sends its own `User-Agent` as `codex-warp/<version>` to
upstream providers. Configured `User-Agent` values are ignored so provider logs
can identify the proxy consistently.
Expand Down
8 changes: 7 additions & 1 deletion docs/provider-catalogs.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,11 @@ model_catalog_only = false
"X-Title" = "Codex Warp"
```

Codex Warp also auto-attaches [OpenRouter app attribution](../README.md#openrouter-app-attribution)
headers on every upstream request. Set any of those header names under
`[provider.headers]` or `[providers.<id>.headers]` to override the defaults for
that gateway.

Use named providers when you want Codex Warp to merge more than one upstream
model catalog. Codex Warp groups the merged `/v1/models` response by gateway
and prefixes display names with `[name]`, for example `[Provider A] Model`.
Expand All @@ -83,7 +88,7 @@ and prefixes display names with `[name]`, for example `[Provider A] Model`.
| `api_key` | Inline upstream key. Useful for local experiments, but avoid committing it. |
| `auth_header` | Header used for auth. Defaults to `authorization`. |
| `auth_scheme` | Prefix for the key. Defaults to `Bearer`; set to `""` for raw keys. |
| `headers` | Static extra headers required by the gateway. `User-Agent` is ignored here because Codex Warp always reports itself as `codex-warp/<version>`. |
| `headers` | Static extra headers required by the gateway. `User-Agent` is ignored here because Codex Warp always reports itself as `codex-warp/<version>`. OpenRouter attribution headers are also auto-attached on every upstream request; set them here to override. See [OpenRouter App Attribution](../README.md#openrouter-app-attribution). |
| `responses_path` | Upstream Responses endpoint path. |
| `chat_completions_path` | Upstream chat completions endpoint path. |
| `models_path` | Upstream model catalog endpoint path. |
Expand Down Expand Up @@ -124,6 +129,7 @@ models_path = "/models"
model_catalog_only = true

[providers.acme_ai.headers]
# Optional: override auto OpenRouter attribution headers for this gateway.
"X-Title" = "Codex Warp"

[[providers.acme_ai.model_catalog]]
Expand Down
21 changes: 21 additions & 0 deletions src/config_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ fn example_configs_parse_request_morphs() {
.expect("opencode go layered config parses");
let generic_config = load_config_layers(&[PathBuf::from("configs/openai-compatible.toml")])
.expect("generic openai-compatible profile parses");
let openrouter_config = load_config_layers(&[PathBuf::from("configs/openrouter.toml")])
.expect("openrouter layered config parses");

assert!(
default_config
Expand Down Expand Up @@ -130,6 +132,24 @@ fn example_configs_parse_request_morphs() {
.iter()
.any(|entry| entry.id == "kimi-k2.7-code-highspeed")
);
assert_eq!(
provider_id_for_config_model(&kimicode_config, "kimi-k2.7-code-highspeed").as_deref(),
Some("moonshot_kimicode")
);
let openrouter = openrouter_config
.providers
.get("openrouter")
.expect("openrouter provider exists");
assert_eq!(openrouter.name.as_deref(), Some("OpenRouter"));
assert_eq!(openrouter.base_url, "https://openrouter.ai/api/v1");
assert_eq!(
openrouter.api_key_env.as_deref(),
Some("OPENROUTER_API_KEY")
);
assert_eq!(
provider_id_for_config_model(&openrouter_config, "openrouter").as_deref(),
None
);
assert_eq!(
xiaomi_config.provider.base_url,
"https://token-plan-sgp.xiaomimimo.com/v1"
Expand Down Expand Up @@ -159,6 +179,7 @@ fn reusable_provider_profiles_leave_auto_review_to_model_families() {
"configs/clinepass.toml",
"configs/moonshot-kimicode.toml",
"configs/opencode-go.toml",
"configs/openrouter.toml",
"configs/xiaomi-token-plan.toml",
] {
let config = load_config_layers(&[PathBuf::from(config_path)])
Expand Down
41 changes: 41 additions & 0 deletions src/http.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,45 @@ use serde_json::json;
use crate::config::ProviderConfig;
use crate::version::user_agent;

// OpenRouter app attribution (https://openrouter.ai/docs/app-attribution).
// Codex Warp identifies itself on every upstream request so OpenRouter can
// attribute usage across all of its API routes and models (chat completions,
// native /responses, /models, and any other outbound call) regardless of which
// gateway profile or model is selected. These are the project's own identity
// values; they can be overridden per provider via [provider.headers] or
// [providers.<id>.headers].
//
// The values are hardcoded in Rust (rather than in configs/openrouter.toml) on
// purpose: attribution must not depend on loading the shipped `openrouter`
// profile or on which gateway happens to be the default in a multi-provider
// setup.
const OPENROUTER_REFERER: &str = "https://github.com/jatmn/Codex-warp";
const OPENROUTER_TITLE: &str = "Codex Warp";
const OPENROUTER_CATEGORIES: &str = "cli-agent,programming-app";

fn apply_openrouter_attribution(
mut request: reqwest::RequestBuilder,
provider: &ProviderConfig,
) -> reqwest::RequestBuilder {
let has_header = |name: &str| {
provider
.headers
.keys()
.any(|key| key.eq_ignore_ascii_case(name))
};
if !has_header("HTTP-Referer") && !has_header("Referer") {
request = request.header("HTTP-Referer", OPENROUTER_REFERER);
}
if !has_header("X-OpenRouter-Title") && !has_header("X-Title") {
request = request.header("X-OpenRouter-Title", OPENROUTER_TITLE);
request = request.header("X-Title", OPENROUTER_TITLE);
}
if !has_header("X-OpenRouter-Categories") {
request = request.header("X-OpenRouter-Categories", OPENROUTER_CATEGORIES);
}
request
}

pub(crate) fn endpoint_url(provider: &ProviderConfig, path: &str) -> String {
format!(
"{}/{}",
Expand Down Expand Up @@ -48,6 +87,8 @@ pub(crate) fn apply_headers_with_accept(
request = request.header(name, value);
}

let request = apply_openrouter_attribution(request, provider);

request
.header(axum::http::header::USER_AGENT, user_agent())
.header(axum::http::header::ACCEPT, accept)
Expand Down
Loading