mirror of
https://github.com/fabro-sh/fabro.git
synced 2026-08-28 05:27:41 +00:00
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:
parent
844e00b72c
commit
cd7f0b1415
26 changed files with 192 additions and 11 deletions
|
|
@ -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,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -343,6 +343,7 @@ impl HookExecutorImpl {
|
|||
max_tokens: None,
|
||||
stop_sequences: None,
|
||||
reasoning_effort: None,
|
||||
speed: None,
|
||||
metadata: None,
|
||||
provider_options: None,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -339,6 +339,7 @@ mod tests {
|
|||
max_tokens: None,
|
||||
stop_sequences: None,
|
||||
reasoning_effort: None,
|
||||
speed: None,
|
||||
metadata: None,
|
||||
provider_options: None,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -245,6 +245,7 @@ mod tests {
|
|||
max_tokens: None,
|
||||
stop_sequences: None,
|
||||
reasoning_effort: None,
|
||||
speed: None,
|
||||
metadata: None,
|
||||
provider_options: None,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -976,6 +976,7 @@ mod tests {
|
|||
max_tokens: None,
|
||||
stop_sequences: None,
|
||||
reasoning_effort: None,
|
||||
speed: None,
|
||||
metadata: None,
|
||||
provider_options: None,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1085,6 +1085,7 @@ mod tests {
|
|||
max_tokens: None,
|
||||
stop_sequences: None,
|
||||
reasoning_effort: None,
|
||||
speed: None,
|
||||
metadata: None,
|
||||
provider_options: None,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1296,6 +1296,7 @@ mod tests {
|
|||
max_tokens: None,
|
||||
stop_sequences: None,
|
||||
reasoning_effort: None,
|
||||
speed: None,
|
||||
metadata: None,
|
||||
provider_options: None,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}),
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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()];
|
||||
|
|
|
|||
|
|
@ -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()))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6071,6 +6071,7 @@ mod real_llm {
|
|||
max_tokens: Some(200),
|
||||
stop_sequences: None,
|
||||
reasoning_effort: None,
|
||||
speed: None,
|
||||
metadata: None,
|
||||
provider_options: None,
|
||||
};
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue