mirror of
https://github.com/fabro-sh/fabro.git
synced 2026-09-28 01:31:36 +00:00
docs(openrouter): document typed workflow attrs, cost telemetry; extend e2e
- Rewrites the API-only Note about provider_options into a new "Provider routing in workflows" section that documents the five openrouter_* stage attrs with a .fabro example and an attr table. - Adds a "Cost telemetry" section noting that Fabro surfaces OpenRouter's authoritative usage.cost as Response.cost_usd with cost_source = "authoritative", distinguishing from catalog-derived "estimated" costs returned by direct providers. - Extends the live openrouter_complete integration test to switch from OpenAiCompatibleAdapter to OpenRouterAdapter and assert cost_usd.is_some() + cost_source == Authoritative. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
f72f3fb972
commit
bb2b8845cd
2 changed files with 44 additions and 8 deletions
|
|
@ -25,7 +25,7 @@ That's the entire configuration — Fabro ships the model catalog, base URL, and
|
|||
|
||||
## Data handling
|
||||
|
||||
Requests sent through OpenRouter pass through their infrastructure before reaching the underlying model. Prompt content, completions, and metadata are subject to OpenRouter's logging, moderation, and data-retention policies — review them at [openrouter.ai/docs/features/privacy-and-logging](https://openrouter.ai/docs/features/privacy-and-logging) before sending sensitive prompts. You can also use OpenRouter's per-request privacy controls (`provider.data_collection` and provider routing filters) via the `provider_options.openrouter` field described below.
|
||||
Requests sent through OpenRouter pass through their infrastructure before reaching the underlying model. Prompt content, completions, and metadata are subject to OpenRouter's logging, moderation, and data-retention policies — review them at [openrouter.ai/docs/features/privacy-and-logging](https://openrouter.ai/docs/features/privacy-and-logging) before sending sensitive prompts. You can also use OpenRouter's per-request privacy controls (`provider.data_collection` and provider routing filters) via the `provider_options.openrouter` field described below, or via the `openrouter_data_collection` stage attribute in workflows.
|
||||
|
||||
## Attribution (optional, opt-in)
|
||||
|
||||
|
|
@ -109,6 +109,39 @@ digraph Example {
|
|||
}
|
||||
```
|
||||
|
||||
## Cost telemetry
|
||||
|
||||
Every OpenRouter response includes an inline `usage.cost` field with authoritative USD billing. Fabro surfaces this as `Response.cost_usd` with `cost_source = "authoritative"`. For non-OpenRouter adapters, `cost_usd` is computed from catalog prices and `cost_source = "estimated"`. Hardcoded OpenRouter catalog prices are best-effort — OpenRouter's inline `usage.cost` is the source of truth, and authoritative cost takes precedence in any aggregation.
|
||||
|
||||
## Provider routing in workflows
|
||||
|
||||
Workflow stages can set OpenRouter routing options via typed `openrouter_*` stage attributes. These are translated into OpenRouter's nested `{provider: {...}, models: [...], transforms: [...]}` request shape under the hood.
|
||||
|
||||
```dot title="workflow.fabro"
|
||||
digraph Example {
|
||||
graph [model_stylesheet="* { model: deepseek/deepseek-v4-flash; }"]
|
||||
|
||||
start [shape=Mdiamond, label="Start"]
|
||||
fast [
|
||||
label = "Fast tier"
|
||||
prompt = "Answer concisely."
|
||||
openrouter_provider_sort = "throughput"
|
||||
openrouter_fallback_models = "qwen/qwen3.6-flash,minimax/minimax-m2.7"
|
||||
]
|
||||
exit [shape=Msquare, label="Exit"]
|
||||
|
||||
start -> fast -> exit
|
||||
}
|
||||
```
|
||||
|
||||
| Attribute | Type | Maps to |
|
||||
| --- | --- | --- |
|
||||
| `openrouter_provider_sort` | `"price"`, `"throughput"`, or `"latency"` | `provider.sort` |
|
||||
| `openrouter_fallback_models` | comma-separated list of model IDs | `models` |
|
||||
| `openrouter_transforms` | comma-separated list (e.g. `"middle-out"`) | `transforms` |
|
||||
| `openrouter_allow_fallbacks` | `"true"` or `"false"` | `provider.allow_fallbacks` |
|
||||
| `openrouter_data_collection` | `"allow"` or `"deny"` | `provider.data_collection` |
|
||||
|
||||
## Provider routing (via API or SDK)
|
||||
|
||||
OpenRouter accepts top-level request fields that aren't part of the OpenAI Chat Completions schema — `provider` (routing preferences), `models` (fallback list), `transforms`, `plugins`. Fabro forwards these through the `provider_options.openrouter` field on the request body when calling `POST /api/v1/completions` directly or constructing a `Request` via the Rust/TypeScript SDK:
|
||||
|
|
@ -130,7 +163,7 @@ OpenRouter accepts top-level request fields that aren't part of the OpenAI Chat
|
|||
See [OpenRouter's provider routing documentation](https://openrouter.ai/docs/features/provider-routing) for the full set of supported fields.
|
||||
|
||||
<Note>
|
||||
Workflow files (`workflow.fabro`, `workflow.toml`) don't currently expose `provider_options` — stage attributes can set `model`, `provider`, and `reasoning_effort`, but not provider-specific request fields. Use the API or SDK directly when you need OpenRouter routing controls per call.
|
||||
For routing controls in `.fabro` workflow files, use the typed `openrouter_*` stage attributes documented above instead of `provider_options.openrouter`. The JSON shape is reserved for direct API/SDK callers.
|
||||
</Note>
|
||||
|
||||
## Troubleshooting
|
||||
|
|
|
|||
|
|
@ -7,10 +7,8 @@ use std::sync::Arc;
|
|||
|
||||
use fabro_llm::error::ProviderErrorKind;
|
||||
use fabro_llm::provider::ProviderAdapter;
|
||||
use fabro_llm::providers::{
|
||||
AnthropicAdapter, GeminiAdapter, OpenAiAdapter, OpenAiCompatibleAdapter,
|
||||
};
|
||||
use fabro_llm::types::{FinishReason, Message, Request};
|
||||
use fabro_llm::providers::{AnthropicAdapter, GeminiAdapter, OpenAiAdapter, OpenRouterAdapter};
|
||||
use fabro_llm::types::{CostSource, FinishReason, Message, Request};
|
||||
use fabro_model::Catalog;
|
||||
use fabro_static::EnvVars;
|
||||
|
||||
|
|
@ -180,8 +178,8 @@ async fn gemini_complete() {
|
|||
async fn openrouter_complete() {
|
||||
let api_key =
|
||||
std::env::var(EnvVars::OPENROUTER_API_KEY).expect("OPENROUTER_API_KEY must be set");
|
||||
let adapter = OpenAiCompatibleAdapter::new(api_key, "https://openrouter.ai/api/v1")
|
||||
.with_name("openrouter");
|
||||
let adapter =
|
||||
OpenRouterAdapter::new(api_key, "https://openrouter.ai/api/v1").with_name("openrouter");
|
||||
let request = make_request("deepseek/deepseek-v4-flash");
|
||||
let response = adapter.complete(&request).await.unwrap();
|
||||
|
||||
|
|
@ -192,6 +190,11 @@ async fn openrouter_complete() {
|
|||
assert!(response.usage.input_tokens > 0);
|
||||
assert!(response.usage.output_tokens > 0);
|
||||
assert_eq!(response.provider, "openrouter");
|
||||
assert!(
|
||||
response.cost_usd.is_some(),
|
||||
"OpenRouter responses should carry an authoritative usage.cost",
|
||||
);
|
||||
assert_eq!(response.cost_source, Some(CostSource::Authoritative));
|
||||
}
|
||||
|
||||
async fn run_multi_turn_cache_test(
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue