Support Anthropic fast mode (speed: fast) (#127)

This PR adds support for Anthropic's fast mode beta feature (`speed:
fast`), which enables up to 2.5x faster output for Claude Opus 4.6 at a
6x pricing premium. The implementation follows the same patterns
established by `reasoning_effort`, threading a new `speed:
Option<String>` field through the request/response pipeline from graph
stylesheet properties down through agent configuration, session
management, and the Anthropic provider adapter.

On the provider side, when `speed: "fast"` is set, the `ApiRequest`
struct now includes the `speed` field in the serialized JSON body, and
the `build_beta_header` function injects the required `anthropic-beta:
fast-mode-2026-02-01` header alongside any existing beta headers (cache,
interleaved thinking, etc.) without duplication. The response's
`usage.speed` field is parsed and propagated back through both streaming
and non-streaming paths into `StageUsage` and `Usage` types for
tracking.

Cost accounting applies a 6x multiplier in `compute_stage_cost` when
`speed == "fast"`, reflecting Anthropic's actual pricing differential.
The feature is configurable via stylesheet (`* { speed: fast; }`), which
gets wired through `SessionConfig` and prompt-mode `Request`
construction in the backend layer. New tests cover the API request
serialization, beta header injection, combined cache+fast-mode headers,
and the cost multiplier, while all existing test fixtures have been
updated with `speed: None` to maintain struct exhaustiveness.

### Fabro Details

<details>
<summary>Ran 9 stages in 58m 27s for $6.56</summary>

| Stage | Duration | Cost | Retries |
|---|---|---|---|
| start | 0s | – | 0 |
| toolchain | 0s | – | 0 |
| preflight_compile | 1m 20s | – | 0 |
| preflight_lint | 14s | – | 0 |
| implement | 47m 31s | $5.36 | 0 |
| simplify_opus | 7m 59s | $1.20 | 0 |
| simplify_gpt | 0s | – | 0 |
| verify | 25s | – | 0 |
| fmt | 1s | – | 0 |
| **Total** | **58m 27s** | **$6.56** | **0** |

</details>

<details>
<summary>Ran <code>ImplementAndSimplify.fabro</code> (12 nodes and 15
edges)</summary>

```dot
digraph ImplementAndSimplify {
    graph [
        goal="Implement and simplify",
        model_stylesheet="
            * { backend: api; model: claude-opus-4-6;}
        "
    ]
    rankdir=LR

    start [shape=Mdiamond, label="Start"]
    exit  [shape=Msquare, label="Exit"]

    toolchain         [label="Toolchain", shape=parallelogram, script="command -v cargo >/dev/null || { curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y && sudo ln -sf $HOME/.cargo/bin/* /usr/local/bin/; }; cargo --version 2>&1", max_retries=0]
    preflight_compile [label="Preflight Compile", shape=parallelogram, script="cargo check -q --workspace 2>&1", max_retries=0]
    preflight_lint    [label="Preflight Lint", shape=parallelogram, script="cargo clippy -q --workspace -- -D warnings 2>&1", max_retries=0]
    fix_lints         [label="Fix Lints", prompt="The preflight lint step failed. Read the build output from context and fix all clippy lint warnings.", max_visits=3]
    implement         [label="Implement", prompt="Read the plan file referenced in the goal and implement every step. Make all the code changes described in the plan. Use red/green TDD."]
    simplify_opus     [label="Simplify (Opus)", prompt="@prompts/simplify.md"]
    simplify_gpt      [label="Simplify (GPT-54)", prompt="@prompts/simplify.md", model="gpt-54"]
    verify            [label="Verify", shape=parallelogram, script="cargo clippy -q --workspace -- -D warnings 2>&1 && cargo nextest run --cargo-quiet --workspace --status-level fail 2>&1", goal_gate=true, retry_target="fixup"]
    fixup             [label="Fixup", prompt="The verify step failed. Read the build output from context and fix all clippy lint warnings and test failures.", max_visits=3]
    fmt               [label="Format", shape=parallelogram, script="cargo fmt --all 2>&1", max_retries=0]

    start -> toolchain
    toolchain -> preflight_compile [condition="outcome=success"]
    toolchain -> exit
    preflight_compile -> preflight_lint [condition="outcome=success"]
    preflight_compile -> exit
    preflight_lint -> implement [condition="outcome=success"]
    preflight_lint -> fix_lints
    fix_lints -> preflight_lint
    implement -> simplify_opus -> simplify_gpt -> verify
    verify -> fmt   [condition="outcome=success"]
    verify -> fixup
    fixup -> verify
    fmt -> exit
}

```

</details>

⚒️ Generated with [Fabro](https://fabro.sh)

---------

Co-authored-by: Fabro <noreply@fabro.sh>
This commit is contained in:
brynary-fabro[bot] 2026-03-21 14:37:42 -04:00 committed by GitHub
parent 844e00b72c
commit cd7f0b1415
26 changed files with 192 additions and 11 deletions

View file

@ -112,6 +112,7 @@ function names, error messages, and exact values. Omit pleasantries and conversa
max_tokens: Some(4096),
stop_sequences: None,
reasoning_effort: None,
speed: None,
metadata: None,
provider_options: None,
};

View file

@ -64,6 +64,7 @@ pub struct SessionConfig {
pub default_command_timeout_ms: u64,
pub max_command_timeout_ms: u64,
pub reasoning_effort: Option<String>,
pub speed: Option<String>,
pub tool_output_limits: HashMap<String, usize>,
pub tool_line_limits: HashMap<String, usize>,
/// Override the provider's default max_tokens when set.
@ -133,6 +134,7 @@ impl Default for SessionConfig {
max_command_timeout_ms: 600_000,
max_tokens: None,
reasoning_effort: None,
speed: None,
tool_output_limits: HashMap::new(),
tool_line_limits: HashMap::new(),
enable_loop_detection: true,

View file

@ -459,6 +459,10 @@ impl Session {
self.config.reasoning_effort = effort;
}
pub fn set_speed(&mut self, speed: Option<String>) {
self.config.speed = speed;
}
pub const fn set_max_turns(&mut self, max_turns: usize) {
self.config.max_turns = max_turns;
}
@ -906,6 +910,7 @@ impl Session {
}),
stop_sequences: None,
reasoning_effort: self.config.reasoning_effort.clone(),
speed: self.config.speed.clone(),
metadata: None,
provider_options: self.provider_profile.provider_options(),
}

View file

@ -588,6 +588,7 @@ pub(crate) fn make_web_fetch_tool(summarizer: Option<WebFetchSummarizer>) -> Reg
max_tokens: None,
stop_sequences: None,
reasoning_effort: None,
speed: None,
metadata: None,
provider_options: None,
};

View file

@ -624,6 +624,7 @@ mod tests {
cache_read_tokens: Some(80),
cache_write_tokens: Some(10),
reasoning_tokens: Some(20),
speed: None,
raw: None,
};
let event = AgentEvent::AssistantMessage {

View file

@ -1232,6 +1232,7 @@ async fn create_completion(
Some(req.stop_sequences)
},
reasoning_effort: req.reasoning_effort,
speed: None,
metadata: None,
provider_options: req.provider_options,
};

View file

@ -877,6 +877,7 @@ async fn probe_llm_provider(
max_tokens: Some(16),
stop_sequences: None,
reasoning_effort: None,
speed: None,
metadata: None,
provider_options: None,
};

View file

@ -210,6 +210,11 @@ impl Node {
self.str_attr("reasoning_effort").unwrap_or("high")
}
#[must_use]
pub fn speed(&self) -> Option<&str> {
self.str_attr("speed")
}
#[must_use]
pub fn auto_status(&self) -> bool {
self.bool_attr("auto_status").unwrap_or(false)
@ -534,6 +539,7 @@ mod tests {
assert_eq!(node.model(), None);
assert_eq!(node.provider(), None);
assert_eq!(node.reasoning_effort(), "high");
assert_eq!(node.speed(), None);
assert!(!node.auto_status());
assert!(!node.allow_partial());
assert_eq!(node.retry_policy(), None);

View file

@ -343,6 +343,7 @@ impl HookExecutorImpl {
max_tokens: None,
stop_sequences: None,
reasoning_effort: None,
speed: None,
metadata: None,
provider_options: None,
};

View file

@ -339,6 +339,7 @@ mod tests {
max_tokens: None,
stop_sequences: None,
reasoning_effort: None,
speed: None,
metadata: None,
provider_options: None,
}

View file

@ -69,6 +69,7 @@ fn build_request(
max_tokens: params.max_tokens,
stop_sequences: params.stop_sequences.clone(),
reasoning_effort: params.reasoning_effort.clone(),
speed: params.speed.clone(),
metadata: params.metadata.clone(),
provider_options: params.provider_options.clone(),
}
@ -288,6 +289,7 @@ pub struct GenerateParams {
pub max_tokens: Option<i64>,
pub stop_sequences: Option<Vec<String>>,
pub reasoning_effort: Option<String>,
pub speed: Option<String>,
pub provider: Option<String>,
pub provider_options: Option<serde_json::Value>,
pub metadata: Option<std::collections::HashMap<String, String>>,
@ -318,6 +320,7 @@ impl GenerateParams {
max_tokens: None,
stop_sequences: None,
reasoning_effort: None,
speed: None,
provider: None,
provider_options: None,
metadata: None,

View file

@ -109,6 +109,8 @@ struct ApiRequest {
#[serde(skip_serializing_if = "Option::is_none")]
output_config: Option<serde_json::Value>,
#[serde(skip_serializing_if = "Option::is_none")]
speed: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
metadata: Option<std::collections::HashMap<String, String>>,
#[serde(skip_serializing_if = "std::ops::Not::not")]
stream: bool,
@ -165,6 +167,8 @@ struct ApiUsage {
cache_read_input_tokens: Option<i64>,
#[serde(default)]
cache_creation_input_tokens: Option<i64>,
#[serde(default)]
speed: Option<String>,
}
/// Estimate reasoning tokens from thinking content blocks.
@ -508,6 +512,7 @@ fn convert_stream_event_for_json_schema(event: StreamEvent) -> StreamEvent {
// --- Prompt caching helpers ---
const CACHE_BETA_HEADER: &str = "prompt-caching-2024-07-31";
const FAST_MODE_BETA_HEADER: &str = "fast-mode-2026-02-01";
/// Check whether auto-caching is disabled via `provider_options`.
///
@ -594,6 +599,7 @@ fn apply_cache_control_to_conversation_prefix(messages: &mut [ApiMessage]) {
fn build_beta_header(
provider_options: Option<&serde_json::Value>,
include_cache_header: bool,
include_fast_mode_header: bool,
) -> Option<String> {
let mut headers: Vec<String> = Vec::new();
@ -616,6 +622,11 @@ fn build_beta_header(
headers.push(CACHE_BETA_HEADER.to_string());
}
// Add fast-mode header if speed=fast and not already present
if include_fast_mode_header && !headers.iter().any(|h| h == FAST_MODE_BETA_HEADER) {
headers.push(FAST_MODE_BETA_HEADER.to_string());
}
if headers.is_empty() {
None
} else {
@ -710,6 +721,10 @@ impl StreamAccumulator {
self.usage.cache_write_tokens = usage
.get("cache_creation_input_tokens")
.and_then(serde_json::Value::as_i64);
self.usage.speed = usage
.get("speed")
.and_then(serde_json::Value::as_str)
.map(String::from);
}
}
vec![StreamEvent::StreamStart]
@ -1122,6 +1137,8 @@ fn build_api_request(
(explicit_thinking, None)
};
let is_fast = request.speed.as_deref() == Some("fast");
let api_request = ApiRequest {
model: request.model.clone(),
messages: api_messages,
@ -1134,6 +1151,7 @@ fn build_api_request(
tool_choice: tool_choice_json,
thinking,
output_config,
speed: request.speed.clone(),
metadata: request.metadata.clone(),
stream,
};
@ -1150,7 +1168,9 @@ fn build_api_request(
.header("x-api-key", &adapter.http.api_key)
.header("anthropic-version", "2023-06-01");
if let Some(beta_str) = build_beta_header(request.provider_options.as_ref(), auto_cache) {
if let Some(beta_str) =
build_beta_header(request.provider_options.as_ref(), auto_cache, is_fast)
{
req_builder = req_builder.header("anthropic-beta", beta_str);
}
} else {
@ -1234,6 +1254,7 @@ impl ProviderAdapter for Adapter {
reasoning_tokens,
cache_read_tokens: api_resp.usage.cache_read_input_tokens,
cache_write_tokens: api_resp.usage.cache_creation_input_tokens,
speed: api_resp.usage.speed,
..Usage::default()
},
raw: serde_json::from_str(&body).ok(),
@ -1527,13 +1548,13 @@ mod tests {
#[test]
fn beta_header_includes_cache_header() {
let result = build_beta_header(None, true);
let result = build_beta_header(None, true, false);
assert_eq!(result, Some(CACHE_BETA_HEADER.to_string()));
}
#[test]
fn beta_header_no_cache_no_user_headers() {
let result = build_beta_header(None, false);
let result = build_beta_header(None, false, false);
assert_eq!(result, None);
}
@ -1544,7 +1565,7 @@ mod tests {
"beta_headers": ["interleaved-thinking-2025-05-14"]
}
});
let result = build_beta_header(Some(&opts), true);
let result = build_beta_header(Some(&opts), true, false);
assert_eq!(
result,
Some(format!(
@ -1560,7 +1581,7 @@ mod tests {
"beta_headers": [CACHE_BETA_HEADER]
}
});
let result = build_beta_header(Some(&opts), true);
let result = build_beta_header(Some(&opts), true, false);
// Should not duplicate the header
assert_eq!(result, Some(CACHE_BETA_HEADER.to_string()));
}
@ -1572,7 +1593,7 @@ mod tests {
"beta_headers": ["interleaved-thinking-2025-05-14"]
}
});
let result = build_beta_header(Some(&opts), false);
let result = build_beta_header(Some(&opts), false, false);
assert_eq!(result, Some("interleaved-thinking-2025-05-14".to_string()));
}
@ -1624,6 +1645,7 @@ mod tests {
tool_choice: None,
thinking: None,
output_config: None,
speed: None,
metadata: None,
stream: false,
};
@ -1663,6 +1685,7 @@ mod tests {
max_tokens: Some(128),
stop_sequences: None,
reasoning_effort: None,
speed: None,
metadata: None,
provider_options: None,
}
@ -1979,7 +2002,7 @@ mod tests {
];
// No user headers — only cache header should appear
let header = build_beta_header(None, true).unwrap_or_default();
let header = build_beta_header(None, true, false).unwrap_or_default();
for dep in &deprecated {
assert!(
!header.contains(dep),
@ -1993,7 +2016,7 @@ mod tests {
"beta_headers": ["interleaved-thinking-2025-05-14"]
}
});
let header = build_beta_header(Some(&opts), true).unwrap_or_default();
let header = build_beta_header(Some(&opts), true, false).unwrap_or_default();
for dep in &deprecated {
assert!(
!header.contains(dep),
@ -2019,6 +2042,7 @@ mod tests {
tool_choice: None,
thinking: None,
output_config: None,
speed: None,
metadata: None,
stream: false,
};
@ -2051,6 +2075,7 @@ mod tests {
tool_choice: None,
thinking: None,
output_config: None,
speed: None,
metadata: None,
stream: false,
};
@ -2111,4 +2136,52 @@ mod tests {
let (api_request, _req_builder) = build_api_request(&adapter, &request, false);
assert!(api_request.output_config.is_none());
}
#[test]
fn build_api_request_sets_speed() {
let adapter = Adapter::new("test-key");
let request = Request {
speed: Some("fast".to_string()),
..make_base_request()
};
let (api_request, _req_builder) = build_api_request(&adapter, &request, false);
assert_eq!(api_request.speed, Some("fast".to_string()));
}
#[test]
fn build_api_request_injects_fast_mode_beta_header() {
let adapter = Adapter::new("test-key");
let request = Request {
speed: Some("fast".to_string()),
..make_base_request()
};
let (_api_request, req_builder) = build_api_request(&adapter, &request, false);
let built = req_builder.build().expect("should build request");
let beta = built
.headers()
.get("anthropic-beta")
.expect("anthropic-beta header should be present")
.to_str()
.unwrap();
assert!(
beta.contains(FAST_MODE_BETA_HEADER),
"beta header should contain fast-mode header, got: {beta}"
);
}
#[test]
fn beta_header_includes_both_cache_and_fast_mode() {
let result = build_beta_header(None, true, true);
let header = result.expect("should produce a header");
assert!(
header.contains(CACHE_BETA_HEADER),
"should contain cache header"
);
assert!(
header.contains(FAST_MODE_BETA_HEADER),
"should contain fast-mode header"
);
}
}

View file

@ -245,6 +245,7 @@ mod tests {
max_tokens: None,
stop_sequences: None,
reasoning_effort: None,
speed: None,
metadata: None,
provider_options: None,
}

View file

@ -976,6 +976,7 @@ mod tests {
max_tokens: None,
stop_sequences: None,
reasoning_effort: None,
speed: None,
metadata: None,
provider_options: None,
}

View file

@ -1085,6 +1085,7 @@ mod tests {
max_tokens: None,
stop_sequences: None,
reasoning_effort: None,
speed: None,
metadata: None,
provider_options: None,
}

View file

@ -1296,6 +1296,7 @@ mod tests {
max_tokens: None,
stop_sequences: None,
reasoning_effort: None,
speed: None,
metadata: None,
provider_options: None,
}

View file

@ -370,6 +370,8 @@ pub struct Usage {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub cache_write_tokens: Option<i64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub speed: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub raw: Option<serde_json::Value>,
}
@ -393,6 +395,7 @@ impl std::ops::Add for Usage {
reasoning_tokens: add_optional(self.reasoning_tokens, rhs.reasoning_tokens),
cache_read_tokens: add_optional(self.cache_read_tokens, rhs.cache_read_tokens),
cache_write_tokens: add_optional(self.cache_write_tokens, rhs.cache_write_tokens),
speed: self.speed,
raw: None,
}
}
@ -452,6 +455,7 @@ pub struct Request {
pub max_tokens: Option<i64>,
pub stop_sequences: Option<Vec<String>>,
pub reasoning_effort: Option<String>,
pub speed: Option<String>,
pub metadata: Option<HashMap<String, String>>,
pub provider_options: Option<serde_json::Value>,
}
@ -888,6 +892,7 @@ mod tests {
reasoning_tokens: None,
cache_read_tokens: None,
cache_write_tokens: None,
speed: None,
raw: None,
};
insta::assert_snapshot!(serde_json::to_string_pretty(&usage).unwrap(), @r#"
@ -908,6 +913,7 @@ mod tests {
reasoning_tokens: Some(20),
cache_read_tokens: Some(80),
cache_write_tokens: Some(10),
speed: None,
raw: None,
};
insta::assert_snapshot!(serde_json::to_string_pretty(&usage).unwrap(), @r#"
@ -940,6 +946,7 @@ mod tests {
reasoning_tokens: Some(5),
cache_read_tokens: Some(3),
cache_write_tokens: Some(1),
speed: None,
raw: None,
};
let b = Usage {
@ -949,6 +956,7 @@ mod tests {
reasoning_tokens: Some(10),
cache_read_tokens: Some(7),
cache_write_tokens: Some(2),
speed: None,
raw: None,
};
let sum = a + b;
@ -969,6 +977,7 @@ mod tests {
reasoning_tokens: Some(5),
cache_read_tokens: None,
cache_write_tokens: None,
speed: None,
raw: None,
};
let b = Usage {
@ -978,6 +987,7 @@ mod tests {
reasoning_tokens: None,
cache_read_tokens: Some(7),
cache_write_tokens: None,
speed: None,
raw: None,
};
let sum = a + b;

View file

@ -15,6 +15,7 @@ fn make_request(model: &str) -> Request {
max_tokens: Some(50),
stop_sequences: None,
reasoning_effort: None,
speed: None,
metadata: None,
provider_options: None,
}
@ -138,6 +139,7 @@ async fn run_multi_turn_cache_test(
max_tokens: Some(100),
stop_sequences: None,
reasoning_effort: None,
speed: None,
metadata: None,
provider_options: None,
};

View file

@ -195,6 +195,7 @@ impl AgentApiBackend {
let config = SessionConfig {
max_tokens: node.max_tokens(),
reasoning_effort: Some(node.reasoning_effort().to_string()),
speed: node.speed().map(String::from),
tool_hooks,
mcp_servers,
..SessionConfig::default()
@ -283,6 +284,7 @@ impl CodergenBackend for AgentApiBackend {
messages,
provider,
reasoning_effort: Some(node.reasoning_effort().to_string()),
speed: node.speed().map(String::from),
tools: None,
tool_choice: None,
response_format: None,
@ -393,6 +395,7 @@ impl CodergenBackend for AgentApiBackend {
cache_read_tokens: response.usage.cache_read_tokens,
cache_write_tokens: response.usage.cache_write_tokens,
reasoning_tokens: response.usage.reasoning_tokens,
speed: response.usage.speed.clone(),
cost: None,
};
stage_usage.cost = compute_stage_cost(&stage_usage);
@ -593,6 +596,7 @@ impl CodergenBackend for AgentApiBackend {
cache_read_tokens: total_usage.cache_read_tokens,
cache_write_tokens: total_usage.cache_write_tokens,
reasoning_tokens: total_usage.reasoning_tokens,
speed: total_usage.speed.clone(),
cost: None,
};
stage_usage.cost = compute_stage_cost(&stage_usage);

View file

@ -706,6 +706,7 @@ impl CodergenBackend for AgentCliBackend {
cache_read_tokens: None,
cache_write_tokens: None,
reasoning_tokens: None,
speed: None,
cost: None,
};
stage_usage.cost = compute_stage_cost(&stage_usage);

View file

@ -6,9 +6,15 @@ pub fn compute_stage_cost(usage: &StageUsage) -> Option<f64> {
let info = fabro_model::get_model_info(&usage.model)?;
let input_rate = info.costs.input_cost_per_mtok?;
let output_rate = info.costs.output_cost_per_mtok?;
let multiplier = if usage.speed.as_deref() == Some("fast") {
6.0
} else {
1.0
};
Some(
usage.input_tokens as f64 * input_rate / 1_000_000.0
+ usage.output_tokens as f64 * output_rate / 1_000_000.0,
(usage.input_tokens as f64 * input_rate / 1_000_000.0
+ usage.output_tokens as f64 * output_rate / 1_000_000.0)
* multiplier,
)
}
@ -47,6 +53,7 @@ mod tests {
cache_read_tokens: None,
cache_write_tokens: None,
reasoning_tokens: None,
speed: None,
cost: None,
};
let cost = compute_stage_cost(&usage);
@ -63,8 +70,39 @@ mod tests {
cache_read_tokens: None,
cache_write_tokens: None,
reasoning_tokens: None,
speed: None,
cost: None,
};
assert_eq!(compute_stage_cost(&usage), None);
}
#[test]
fn compute_stage_cost_fast_mode_6x_multiplier() {
let standard_usage = StageUsage {
model: "claude-sonnet-4-5".into(),
input_tokens: 1000,
output_tokens: 500,
cache_read_tokens: None,
cache_write_tokens: None,
reasoning_tokens: None,
speed: None,
cost: None,
};
let fast_usage = StageUsage {
model: "claude-sonnet-4-5".into(),
input_tokens: 1000,
output_tokens: 500,
cache_read_tokens: None,
cache_write_tokens: None,
reasoning_tokens: None,
speed: Some("fast".into()),
cost: None,
};
let standard_cost = compute_stage_cost(&standard_usage).unwrap();
let fast_cost = compute_stage_cost(&fast_usage).unwrap();
assert!(
(fast_cost - standard_cost * 6.0).abs() < 1e-10,
"fast mode should be 6x standard cost: standard={standard_cost}, fast={fast_cost}"
);
}
}

View file

@ -1199,6 +1199,7 @@ mod tests {
cache_read_tokens: Some(800),
cache_write_tokens: Some(50),
reasoning_tokens: Some(100),
speed: None,
raw: None,
},
tool_call_count: 3,
@ -2286,6 +2287,7 @@ mod tests {
cache_read_tokens: Some(3000),
cache_write_tokens: Some(500),
reasoning_tokens: Some(800),
speed: None,
raw: None,
}),
};

View file

@ -58,6 +58,8 @@ pub struct StageUsage {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub reasoning_tokens: Option<i64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub speed: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub cost: Option<f64>,
}
@ -70,6 +72,7 @@ impl From<&StageUsage> for fabro_llm::types::Usage {
cache_read_tokens: u.cache_read_tokens,
cache_write_tokens: u.cache_write_tokens,
reasoning_tokens: u.reasoning_tokens,
speed: u.speed.clone(),
raw: None,
}
}
@ -378,6 +381,7 @@ mod tests {
cache_read_tokens: Some(800),
cache_write_tokens: Some(50),
reasoning_tokens: Some(100),
speed: None,
cost: None,
};
let json = serde_json::to_string(&usage).unwrap();
@ -398,6 +402,7 @@ mod tests {
cache_read_tokens: None,
cache_write_tokens: None,
reasoning_tokens: None,
speed: None,
cost: None,
};
let json = serde_json::to_string(&usage).unwrap();

View file

@ -924,6 +924,7 @@ mod tests {
cache_read_tokens: None,
cache_write_tokens: None,
reasoning_tokens: None,
speed: None,
cost: None,
});
outcome.files_touched = vec!["src/lib.rs".to_string(), "src/main.rs".to_string()];
@ -1190,6 +1191,7 @@ mod tests {
cache_read_tokens: None,
cache_write_tokens: None,
reasoning_tokens: None,
speed: None,
cost: None,
});
node_outcomes.insert("report".to_string(), outcome);
@ -1369,6 +1371,7 @@ mod tests {
cache_read_tokens: None,
cache_write_tokens: None,
reasoning_tokens: None,
speed: None,
cost: None,
});
outcome.files_touched = vec!["src/lib.rs".to_string()];
@ -1588,6 +1591,7 @@ mod tests {
cache_read_tokens: None,
cache_write_tokens: None,
reasoning_tokens: None,
speed: None,
cost: None,
});
outcome.files_touched = vec!["src/lib.rs".to_string()];

View file

@ -2,7 +2,8 @@ use fabro_graphviz::graph::{AttrValue, Graph};
pub use fabro_graphviz::stylesheet::{parse_stylesheet, Declaration, Rule, Selector, Stylesheet};
/// Recognized stylesheet properties.
const STYLESHEET_PROPERTIES: &[&str] = &["model", "provider", "reasoning_effort", "backend"];
const STYLESHEET_PROPERTIES: &[&str] =
&["model", "provider", "reasoning_effort", "speed", "backend"];
/// Apply a stylesheet to a graph. Rules are applied by specificity order;
/// higher specificity wins. Explicit node attributes are never overridden.
@ -240,4 +241,17 @@ mod tests {
Some(&AttrValue::String("api".into()))
);
}
#[test]
fn apply_speed_property() {
let ss = parse_stylesheet("* { speed: fast; }").unwrap();
let mut graph = Graph::new("test");
graph.nodes.insert("a".into(), Node::new("a"));
apply_stylesheet(&ss, &mut graph);
assert_eq!(
graph.nodes["a"].attrs.get("speed"),
Some(&AttrValue::String("fast".into()))
);
}
}

View file

@ -6071,6 +6071,7 @@ mod real_llm {
max_tokens: Some(200),
stop_sequences: None,
reasoning_effort: None,
speed: None,
metadata: None,
provider_options: None,
};