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
1 change: 1 addition & 0 deletions Cargo.lock

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

247 changes: 247 additions & 0 deletions crates/switchyard-costs/prices.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,247 @@
# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0

# Rates are USD per million text tokens. Quote IDs so dots remain part of the ID.
# These defaults stay fixed until Switchyard is updated; an override replaces this table.
# Estimates exclude service tiers, long-context premiums, tools, and time-based cache storage.
# OpenAI cache-write rates cover 30-minute retention, not 12-hour retention.

# OpenAI's standard short-context rates were checked on 2026-10-21.
# https://developers.openai.com/api/docs/pricing
# GPT-5.6 and newer use the default 30-minute cache-write rate.
# Older models have no extra write fee, so separately counted writes use the input rate.

["gpt-6-astra"]
input = 10
output = 50
cache_read = 1
cache_write = 12.5

["gpt-6.1-sol"]
input = 2
output = 10
cache_read = 0.1
cache_write = 2.5

["gpt-6-sol"]
input = 2
output = 10
cache_read = 0.2
cache_write = 2.5

["gpt-6-luna"]
input = 0.1
output = 0.5
cache_read = 0.01
cache_write = 0.125

["gpt-5.6-sol"]
input = 4
output = 20
cache_read = 0.4
cache_write = 5

["gpt-5.6-luna"]
input = 0.2
output = 1.2
cache_read = 0.02
cache_write = 0.25

["gpt-5.6-terra"]
input = 2
output = 12
cache_read = 0.2
cache_write = 2.5

["gpt-5.5"]
input = 5
output = 30
cache_read = 0.5
cache_write = 5

["gpt-5.4-mini"]
input = 0.75
output = 4.5
cache_read = 0.075
cache_write = 0.75

["gpt-5.4-nano"]
input = 0.2
output = 1.25
cache_read = 0.02
cache_write = 0.2

["gpt-5.2"]
input = 1.75
output = 14
cache_read = 0.175
cache_write = 1.75

["gpt-4.1"]
input = 2
output = 8
cache_read = 0.5
cache_write = 2

["gpt-4.1-mini"]
input = 0.4
output = 1.6
cache_read = 0.1
cache_write = 0.4

["o3"]
input = 2
output = 8
cache_read = 0.5
cache_write = 2

["o4-mini"]
input = 1.1
output = 4.4
cache_read = 0.275
cache_write = 1.1

# codex-auto-review uses the gpt-6-sol rates for cost and baseline estimates.

["codex-auto-review"]
input = 2
output = 10
cache_read = 0.2
cache_write = 2.5

# These exact IDs occur in the shipped configs. They use OpenAI list-rate estimates;
# marketplace and cloud charges may differ. A reported charge takes precedence.

["openai/gpt-5.5"]
input = 5
output = 30
cache_read = 0.5
cache_write = 5

["openai/gpt-5.2"]
input = 1.75
output = 14
cache_read = 0.175
cache_write = 1.75

["openai/gpt-5.6-sol"]
input = 4
output = 20
cache_read = 0.4
cache_write = 5

["openai/gpt-5.6-luna"]
input = 0.2
output = 1.2
cache_read = 0.02
cache_write = 0.25

["openai/gpt-5.6-terra"]
input = 2
output = 12
cache_read = 0.2
cache_write = 2.5

["azure/openai/gpt-5.5"]
input = 5
output = 30
cache_read = 0.5
cache_write = 5

["openrouter/openai/gpt-5.6-sol"]
input = 4
output = 20
cache_read = 0.4
cache_write = 5

["openrouter/openai/gpt-5.6-terra"]
input = 2
output = 12
cache_read = 0.2
cache_write = 2.5

# Anthropic's standard token rates were checked on 2026-10-21.
# https://platform.claude.com/docs/en/about-claude/pricing
# The cache_write field uses the five-minute rate.
# The cache_write_1h field uses the one-hour rate.

["claude-sonnet-5"]
input = 2
output = 10
cache_read = 0.2
cache_write = 2.5
cache_write_1h = 4

["claude-sonnet-5-5"]
input = 2
output = 10
cache_read = 0.1
cache_write = 2.5
cache_write_1h = 4

["claude-opus-5-5"]
input = 4
output = 20
cache_read = 0.2
cache_write = 5
cache_write_1h = 8

["claude-opus-4-7"]
input = 5
output = 25
cache_read = 0.5
cache_write = 6.25
cache_write_1h = 10

["claude-opus-4-8"]
input = 5
output = 25
cache_read = 0.5
cache_write = 6.25
cache_write_1h = 10

["claude-haiku-4-5"]
input = 1
output = 5
cache_read = 0.1
cache_write = 1.25
cache_write_1h = 2

["claude-haiku-4-5-20251001"]
input = 1
output = 5
cache_read = 0.1
cache_write = 1.25
cache_write_1h = 2

# These exact IDs and token rates were checked against OpenRouter on 2026-10-21.
# https://openrouter.ai/api/v1/models

["anthropic/claude-sonnet-5"]
input = 2
output = 10
cache_read = 0.2
cache_write = 2.5
cache_write_1h = 4

["anthropic/claude-opus-4.7"]
input = 5
output = 25
cache_read = 0.5
cache_write = 6.25
cache_write_1h = 10

["anthropic/claude-opus-4.8"]
input = 5
output = 25
cache_read = 0.5
cache_write = 6.25
cache_write_1h = 10

# This gateway ID uses Anthropic list rates as an estimate. Gateway charges may differ.
["azure/anthropic/claude-haiku-4-5"]
input = 1
output = 5
cache_read = 0.1
cache_write = 1.25
cache_write_1h = 2
36 changes: 31 additions & 5 deletions crates/switchyard-costs/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -73,18 +73,44 @@ pub struct RecordedCost {
}

impl Pricing {
/// This method loads and validates a JSON pricing file.
/// This method loads the rates shipped with this version of Switchyard.
pub fn bundled() -> Result<Self, String> {
Self::parse_toml(include_str!("../prices.toml"))
}

/// This method loads TOML from `.toml` files and JSON from other filenames.
/// The extension check ignores ASCII case.
pub fn load(path: &Path) -> Result<Self, String> {
let bytes = std::fs::read(path)
.map_err(|error| format!("read pricing file {}: {error}", path.display()))?;
Self::parse(&bytes)
.map_err(|error| format!("invalid pricing file {}: {error}", path.display()))
let pricing = if path
.extension()
.is_some_and(|ext| ext.eq_ignore_ascii_case("toml"))
{
std::str::from_utf8(&bytes)
.map_err(|error| error.to_string())
.and_then(Self::parse_toml)
} else {
Self::parse(&bytes)
};
pricing.map_err(|error| format!("invalid pricing file {}: {error}", path.display()))
}

/// This function reads a pricing file's JSON and checks every model ID and rate.
pub fn parse(json: &[u8]) -> Result<Self, String> {
let pricing: Self = serde_json::from_slice(json).map_err(|error| error.to_string())?;
for (model, rate) in &pricing.0 {
pricing.validate()
}

/// TOML rates use the same typed fields and validation as JSON rates.
fn parse_toml(text: &str) -> Result<Self, String> {
let pricing: Self = toml::from_str(text).map_err(|error| error.to_string())?;
pricing.validate()
}

/// Both parsers use this check so model IDs and rate limits agree across formats.
fn validate(self) -> Result<Self, String> {
for (model, rate) in &self.0 {
if model.trim().is_empty()
|| model.trim() != model
|| [
Expand All @@ -103,7 +129,7 @@ impl Pricing {
));
}
}
Ok(pricing)
Ok(self)
}

/// Provider charges take precedence because token rates may omit provider surcharges.
Expand Down
1 change: 1 addition & 0 deletions crates/switchyard-desktop/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ publish = false

[dependencies]
switchyard-runner.workspace = true
switchyard-costs.workspace = true
serde.workspace = true
serde_json.workspace = true
tempfile = "3"
Expand Down
48 changes: 0 additions & 48 deletions crates/switchyard-desktop/prices.toml

This file was deleted.

15 changes: 2 additions & 13 deletions crates/switchyard-desktop/src/bin/switchyard-desktop-install.rs
Original file line number Diff line number Diff line change
Expand Up @@ -150,22 +150,11 @@ fn serve(file: &Path) -> Result<(), String> {
use std::os::unix::process::CommandExt;
let s = Settings::load(file)?;
let config = s.sy_home.join("composite.toml");
let mut server = std::process::Command::new(s.binary("switchyard-server"));
let mut server = s.server_command();
#[cfg(target_os = "linux")]
credentials::wait_for_keyring()?;
credentials::configure(&mut server, &config)?;
let error = server
.arg("--config")
.arg(config)
.args([
"--host",
"127.0.0.1",
"--port",
&s.port.to_string(),
"--routing-log-file",
])
.arg(s.sy_home.join("routing.jsonl"))
.exec();
let error = server.exec();
Err(format!("Could not start the server: {error}"))
}

Expand Down
Loading
Loading