Compare commits

..

10 Commits

12 changed files with 923 additions and 21 deletions

9
Cargo.lock generated
View File

@@ -1496,9 +1496,9 @@ checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
[[package]] [[package]]
name = "jiff" name = "jiff"
version = "0.2.37" version = "0.2.38"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0ab1baf72f08796de0260609515130699b890ac25f30e610ad894bc5856cafdb" checksum = "b2b005715dcbeb0089a3c0dab99f2ff1cc3b2525323552703d648585d342a383"
dependencies = [ dependencies = [
"defmt", "defmt",
"jiff-core", "jiff-core",
@@ -1523,9 +1523,9 @@ dependencies = [
[[package]] [[package]]
name = "jiff-static" name = "jiff-static"
version = "0.2.37" version = "0.2.38"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "378268a1116ad67ae6228701118ac9f491d78fda38a40a1f1a9e1348de6f7212" checksum = "2cc9817253cf7c7ee4684451bd327e88d6f3658014e54a29198625590650695c"
dependencies = [ dependencies = [
"jiff-core", "jiff-core",
"proc-macro2", "proc-macro2",
@@ -1633,6 +1633,7 @@ dependencies = [
"headers", "headers",
"indexmap 2.14.2", "indexmap 2.14.2",
"itertools 0.15.0", "itertools 0.15.0",
"jiff",
"mitsein", "mitsein",
"rand 0.10.3", "rand 0.10.3",
"reqwest", "reqwest",

View File

@@ -16,6 +16,7 @@ futures = "0.3.34"
headers = "0.4.2" headers = "0.4.2"
indexmap = "2.14.2" indexmap = "2.14.2"
itertools = "0.15.0" itertools = "0.15.0"
jiff = { version = "0.2.38", features = ["serde"] }
mitsein = { version = "0.9.0", features = ["indexmap"] } mitsein = { version = "0.9.0", features = ["indexmap"] }
rand = "0.10.3" rand = "0.10.3"
reqwest = { version = "0.13.5", features = ["brotli", "cookies", "deflate", "form", "gzip", "hickory-dns", "json", "multipart", "query", "rustls-no-provider", "socks", "stream", "zstd"], default-features = false } reqwest = { version = "0.13.5", features = ["brotli", "cookies", "deflate", "form", "gzip", "hickory-dns", "json", "multipart", "query", "rustls-no-provider", "socks", "stream", "zstd"], default-features = false }

View File

@@ -39,6 +39,9 @@ pub struct AppArgs {
#[arg(long)] #[arg(long)]
pub cloudflare_workers_ai_credentials: Vec<CloudflareCredentials>, pub cloudflare_workers_ai_credentials: Vec<CloudflareCredentials>,
#[arg(long)]
pub cohere_api_key: Vec<SecretString>,
#[arg(long)] #[arg(long)]
pub google_ai_studio_api_key: Vec<SecretString>, pub google_ai_studio_api_key: Vec<SecretString>,
@@ -72,6 +75,9 @@ pub struct AppArgs {
#[arg(long)] #[arg(long)]
pub openrouter_api_key: Vec<SecretString>, pub openrouter_api_key: Vec<SecretString>,
#[arg(long)]
pub poolside_api_key: Vec<SecretString>,
#[arg(long)] #[arg(long)]
pub vercel_ai_gateway_api_key: Vec<SecretString>, pub vercel_ai_gateway_api_key: Vec<SecretString>,

524
src/openai/extraction.rs Normal file
View File

@@ -0,0 +1,524 @@
//! System One-style models that can extract, compute, estimate,
//! and forecast values from inputs.
//! They are strongly typed and output confidence scores and probabilities against their outputs.
//!
//! Request: [`ExtractionRequest`] · Response: [`ExtractionResponse`]
use jiff::civil::{Date, DateTime, Time};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
/// The item type required for `list` fields.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
pub enum ListItems {
/// List of text items.
Text,
/// List of numbers.
Number,
/// List of ISO dates.
Date,
}
/// Scale for estimate/forecast grids.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
#[serde(rename_all = "lowercase")]
pub enum Scale {
/// Linear scale (default).
#[default]
Linear,
/// Logarithmic scale.
Log,
}
/// Interval for forecast fields.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
pub enum ForecastInterval {
/// One step of the grid.
Step,
/// Per minute.
Minute,
/// Per hour.
Hour,
/// Per day.
Day,
/// Per week.
Week,
/// Per month.
Month,
/// Per quarter.
Quarter,
/// Per year.
Year,
}
/// A value that may be a number or an ISO date string. Used for the
/// `min`/`max` bounds of estimate/forecast fields (ISO date for date
/// estimates) and for the `step` grid.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(untagged)]
pub enum NumberOrDate {
/// A numeric bound or step size.
Number(f64),
/// An ISO date string (for date estimates).
Date(Date),
}
/// A step size for estimate grids: a positive number (days for date
/// estimates), or the strings `"month"` / `"year"`.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(untagged)]
pub enum EstimateStep {
/// A positive numeric step (days for date estimates).
Number(f64),
/// Calendar-aware step: `"month"`.
Month,
/// Calendar-aware step: `"year"`.
Year,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum DurationUnit {
Seconds,
Minutes,
Hours,
Days,
Weeks,
Months,
Years,
}
/// A typed field specification
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(tag = "type", rename_all = "lowercase")]
pub enum Field {
/// A string extracted from the input.
#[serde(rename = "text")]
Text {
/// What the field should contain.
#[serde(skip_serializing_if = "Option::is_none")]
instructions: Option<String>,
/// Probability of null above which the value is null (default 0.5).
/// Must be in `(0, 1]`.
#[serde(skip_serializing_if = "Option::is_none")]
null_threshold: Option<f64>,
},
/// A numeric value.
#[serde(rename = "number")]
Number {
/// What the field should contain.
#[serde(skip_serializing_if = "Option::is_none")]
instructions: Option<String>,
/// Unit label for the number (e.g. `USD`).
#[serde(skip_serializing_if = "Option::is_none")]
unit: Option<String>,
/// Whether the number must be an integer. Numbers and durations only.
#[serde(skip_serializing_if = "Option::is_none")]
integer: Option<bool>,
/// Number of decimal places (0–6). Numbers, durations and number
/// lists only.
#[serde(skip_serializing_if = "Option::is_none")]
decimals: Option<u8>,
/// Probability of null above which the value is null (default 0.5).
#[serde(skip_serializing_if = "Option::is_none")]
null_threshold: Option<f64>,
},
/// An ISO date value.
#[serde(rename = "date")]
Date {
/// What the field should contain.
#[serde(skip_serializing_if = "Option::is_none")]
instructions: Option<String>,
/// Probability of null above which the value is null (default 0.5).
#[serde(skip_serializing_if = "Option::is_none")]
null_threshold: Option<f64>,
},
/// An ISO time value.
#[serde(rename = "time")]
Time {
/// What the field should contain.
#[serde(skip_serializing_if = "Option::is_none")]
instructions: Option<String>,
/// Probability of null above which the value is null (default 0.5).
#[serde(skip_serializing_if = "Option::is_none")]
null_threshold: Option<f64>,
},
/// An ISO date-time value.
#[serde(rename = "datetime")]
Datetime {
/// What the field should contain.
#[serde(skip_serializing_if = "Option::is_none")]
instructions: Option<String>,
/// Probability of null above which the value is null (default 0.5).
#[serde(skip_serializing_if = "Option::is_none")]
null_threshold: Option<f64>,
},
/// A duration value.
#[serde(rename = "duration")]
Duration {
/// What the field should contain.
#[serde(skip_serializing_if = "Option::is_none")]
instructions: Option<String>,
/// Unit label
#[serde(skip_serializing_if = "Option::is_none")]
unit: Option<DurationUnit>,
/// Whether the duration must be an integer. Numbers and durations only.
#[serde(skip_serializing_if = "Option::is_none")]
integer: Option<bool>,
/// Number of decimal places (0–6). Numbers, durations and number
/// lists only.
#[serde(skip_serializing_if = "Option::is_none")]
decimals: Option<u8>,
/// Probability of null above which the value is null (default 0.5).
#[serde(skip_serializing_if = "Option::is_none")]
null_threshold: Option<f64>,
},
/// A list of typed items.
#[serde(rename = "list")]
List {
/// What the field should contain.
#[serde(skip_serializing_if = "Option::is_none")]
instructions: Option<String>,
/// Required for list fields: the type of each item.
items: ListItems,
/// Number of decimal places (0–6) for number lists.
#[serde(skip_serializing_if = "Option::is_none")]
decimals: Option<u8>,
/// Probability of null above which the value is null (default 0.5).
#[serde(skip_serializing_if = "Option::is_none")]
null_threshold: Option<f64>,
},
/// A probabilistic estimate over a bounded grid.
#[serde(rename = "estimate")]
Estimate {
/// What the field should contain.
#[serde(skip_serializing_if = "Option::is_none")]
instructions: Option<String>,
/// Required for estimate/forecast: lower bound (a number, or an ISO
/// date for date estimates).
min: NumberOrDate,
/// Required for estimate/forecast: upper bound (a number, or an ISO
/// date for date estimates).
max: NumberOrDate,
/// Estimate grid: a positive number (days for date estimates), or
/// `"month"` / `"year"`.
#[serde(skip_serializing_if = "Option::is_none")]
step: Option<EstimateStep>,
/// Grid scale: `linear` (default) or `log`.
#[serde(skip_serializing_if = "Option::is_none")]
scale: Option<Scale>,
/// Probability of null above which the value is null (default 0.5).
#[serde(skip_serializing_if = "Option::is_none")]
null_threshold: Option<f64>,
},
/// A forecast: a sequence of estimates over a horizon.
#[serde(rename = "forecast")]
Forecast {
/// What the field should contain.
#[serde(skip_serializing_if = "Option::is_none")]
instructions: Option<String>,
/// Required for estimate/forecast: lower bound (a number, or an ISO
/// date for date estimates).
min: NumberOrDate,
/// Required for estimate/forecast: upper bound (a number, or an ISO
/// date for date estimates).
max: NumberOrDate,
/// Estimate grid: a positive number (days for date estimates), or
/// `"month"` / `"year"`.
#[serde(skip_serializing_if = "Option::is_none")]
step: Option<EstimateStep>,
/// Grid scale: `linear` (default) or `log`.
#[serde(skip_serializing_if = "Option::is_none")]
scale: Option<Scale>,
/// Forecast only: number of steps (1–64).
#[serde(skip_serializing_if = "Option::is_none")]
horizon: Option<u32>,
/// Forecast only: the interval between steps.
#[serde(skip_serializing_if = "Option::is_none")]
interval: Option<ForecastInterval>,
/// Probability of null above which the value is null (default 0.5).
#[serde(skip_serializing_if = "Option::is_none")]
null_threshold: Option<f64>,
},
}
/// An image input. Either a data URL string or an explicit base64 object.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(untagged)]
pub enum Image {
/// A data URL string (must start with `data:`, case-insensitive).
DataUrl(
/// The data URL, e.g. `data:image/png;base64,...`.
String,
),
/// An explicitly split base64 image.
Base64 {
/// MIME content type of the image (PNG, JPEG or WebP).
content_type: String,
/// Base64-encoded image bytes.
base64: String,
},
}
/// Optional request settings.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
pub struct RequestOptions {
/// Add 99 percentiles to estimates (default `false`).
#[serde(default, skip_serializing_if = "Option::is_none")]
pub include_quantiles: Option<bool>,
/// Return the text that spans index into (default `false`).
#[serde(default, skip_serializing_if = "Option::is_none")]
pub return_state_text: Option<bool>,
}
/// The extraction request body.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct ExtractionRequest<Model> {
/// The model selector
pub model: Model,
/// The text to read and predict from. Must be non-empty unless images
/// are sent. Text answers are spans of this string.
#[serde(skip_serializing_if = "Option::is_none")]
pub state: Option<String>,
/// Required. Map of field name to a typed field spec. At least one
/// field; no upper limit. Fields that do not fit next to the state in
/// one 8,192-token pass are split across parallel passes. Field names
/// must match `^[A-Za-z_][A-Za-z0-9_.\-]{0,63}$`.
pub fields: HashMap<String, Field>,
/// Optional embedded PNG, JPEG or WebP images placed before the state
/// (max 4; 4 MiB each, 8 MiB total decoded; request body max 16 MiB).
/// Remote URLs are not accepted.
#[serde(skip_serializing_if = "Option::is_none")]
pub images: Option<Vec<Image>>,
/// ISO date that anchors dates written without a year.
#[serde(skip_serializing_if = "Option::is_none")]
pub reference_date: Option<Date>,
/// Optional request settings.
#[serde(skip_serializing_if = "Option::is_none")]
pub options: Option<RequestOptions>,
}
/// The value of an answer. Shape depends on the field type; `null` when the
/// input does not determine it.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(untagged)]
pub enum List {
// A list of strings
Strings(Vec<String>),
// A list of numbers
Numbers(Vec<f64>),
// A list of dates
Dates(Vec<Date>),
}
/// A forecast step: one estimate per step, with median and interval bounds.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct ForecastStep {
/// The point (median) value for this step.
pub value: f64,
/// Mean of the distribution for this step.
#[serde(skip_serializing_if = "Option::is_none")]
pub mean: Option<f64>,
/// Lower bound of the interval for this step.
#[serde(skip_serializing_if = "Option::is_none")]
pub p10: Option<f64>,
/// Upper bound of the interval for this step.
#[serde(skip_serializing_if = "Option::is_none")]
pub p90: Option<f64>,
}
/// One answer per requested field, keyed by field name. Tagged by `type`,
/// mirroring the field spec that produced it.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(tag = "type", rename_all = "lowercase")]
pub enum Answer {
/// Answer for a text field: an exact span of the state, with character
/// offsets.
#[serde(rename = "text")]
Text {
/// The extracted value, or null when the input does not determine it.
value: Option<String>,
/// The model's best guess before applying the null threshold.
#[serde(skip_serializing_if = "Option::is_none")]
guess: Option<String>,
/// Probability that the field is null.
#[serde(skip_serializing_if = "Option::is_none")]
p_null: Option<f64>,
/// Confidence score in `[0, 1]`.
#[serde(skip_serializing_if = "Option::is_none")]
confidence: Option<f64>,
/// Character offsets `[start, end]` of the span into the state.
#[serde(skip_serializing_if = "Option::is_none")]
span: Option<[usize; 2]>,
},
/// Answer for a number field.
#[serde(rename = "number")]
Number {
/// The extracted value, or null when the input does not determine it.
value: Option<f64>,
/// The model's best guess before applying the null threshold.
#[serde(skip_serializing_if = "Option::is_none")]
guess: Option<f64>,
/// Probability that the field is null.
#[serde(skip_serializing_if = "Option::is_none")]
p_null: Option<f64>,
/// Confidence score in `[0, 1]`.
#[serde(skip_serializing_if = "Option::is_none")]
confidence: Option<f64>,
},
/// Answer for a date field.
#[serde(rename = "date")]
Date {
/// The computed/extracted ISO date, or null.
value: Option<Date>,
/// The model's best guess before applying the null threshold.
#[serde(skip_serializing_if = "Option::is_none")]
guess: Option<Date>,
/// Probability that the field is null.
#[serde(skip_serializing_if = "Option::is_none")]
p_null: Option<f64>,
/// Confidence score in `[0, 1]`.
#[serde(skip_serializing_if = "Option::is_none")]
confidence: Option<f64>,
},
/// Answer for a time field.
#[serde(rename = "time")]
Time {
/// The extracted ISO time, or null.
value: Option<Time>,
/// The model's best guess before applying the null threshold.
#[serde(skip_serializing_if = "Option::is_none")]
guess: Option<Time>,
/// Probability that the field is null.
#[serde(skip_serializing_if = "Option::is_none")]
p_null: Option<f64>,
/// Confidence score in `[0, 1]`.
#[serde(skip_serializing_if = "Option::is_none")]
confidence: Option<f64>,
},
/// Answer for a datetime field.
#[serde(rename = "datetime")]
Datetime {
/// The extracted ISO date-time, or null.
value: Option<DateTime>,
/// The model's best guess before applying the null threshold.
#[serde(skip_serializing_if = "Option::is_none")]
guess: Option<DateTime>,
/// Probability that the field is null.
#[serde(skip_serializing_if = "Option::is_none")]
p_null: Option<f64>,
/// Confidence score in `[0, 1]`.
#[serde(skip_serializing_if = "Option::is_none")]
confidence: Option<f64>,
},
/// Answer for a duration field.
#[serde(rename = "duration")]
Duration {
/// The extracted duration, or null.
value: Option<f64>,
/// The model's best guess before applying the null threshold.
#[serde(skip_serializing_if = "Option::is_none")]
guess: Option<f64>,
/// Probability that the field is null.
#[serde(skip_serializing_if = "Option::is_none")]
p_null: Option<f64>,
/// Confidence score in `[0, 1]`.
#[serde(skip_serializing_if = "Option::is_none")]
confidence: Option<f64>,
},
/// Answer for a list field: values with per-item spans.
#[serde(rename = "list")]
List {
/// The extracted items, or null.
value: Option<List>,
/// Per-item character offsets `[start, end]` into the state.
#[serde(skip_serializing_if = "Option::is_none")]
spans: Option<Vec<[usize; 2]>>,
/// Probability that the field is null.
#[serde(skip_serializing_if = "Option::is_none")]
p_null: Option<f64>,
/// Confidence score in `[0, 1]`.
#[serde(skip_serializing_if = "Option::is_none")]
confidence: Option<f64>,
},
/// Answer for an estimate field: a distribution summary.
#[serde(rename = "estimate")]
Estimate {
/// The point estimate (median).
value: NumberOrDate,
/// Mean of the estimated distribution.
#[serde(skip_serializing_if = "Option::is_none")]
mean: Option<f64>,
/// Median of the estimated distribution.
#[serde(skip_serializing_if = "Option::is_none")]
median: Option<f64>,
/// 10th percentile of the estimated distribution.
#[serde(skip_serializing_if = "Option::is_none")]
p10: Option<f64>,
/// 90th percentile of the estimated distribution.
#[serde(skip_serializing_if = "Option::is_none")]
p90: Option<f64>,
/// Probability the true value is below the grid minimum.
#[serde(skip_serializing_if = "Option::is_none")]
p_below: Option<f64>,
/// Probability the true value is above the grid maximum.
#[serde(skip_serializing_if = "Option::is_none")]
p_above: Option<f64>,
/// 99 percentiles, when `options.include_quantiles` is set.
#[serde(skip_serializing_if = "Option::is_none")]
quantiles: Option<Vec<f64>>,
},
/// Answer for a forecast field: one estimate per step.
#[serde(rename = "forecast")]
Forecast {
/// One estimate per step.
#[serde(default)]
steps: Vec<ForecastStep>,
/// Probability that the field is null.
#[serde(skip_serializing_if = "Option::is_none")]
p_null: Option<f64>,
/// Confidence score in `[0, 1]`.
#[serde(skip_serializing_if = "Option::is_none")]
confidence: Option<f64>,
},
}
/// Token and pass accounting for a request.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct Usage {
/// Tokens consumed by the state text.
#[serde(skip_serializing_if = "Option::is_none")]
pub state_tokens: Option<u64>,
/// Tokens consumed by images.
#[serde(skip_serializing_if = "Option::is_none")]
pub image_tokens: Option<u64>,
/// Tokens consumed by the field schema.
#[serde(skip_serializing_if = "Option::is_none")]
pub schema_tokens: Option<u64>,
/// Total input tokens.
pub input_tokens: u64,
/// Total output tokens.
pub output_tokens: u64,
/// Number of parallel passes used.
#[serde(skip_serializing_if = "Option::is_none")]
pub passes: Option<u64>,
}
/// The extraction response body.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct ExtractionResponse<Model> {
/// The model that produced this response
pub model: Model,
/// One answer per field, keyed by field name. `value` is null when the
/// input does not determine it.
pub answers: HashMap<String, Answer>,
/// Whether the input was truncated to fit the context window.
pub truncated: bool,
/// The text that spans index into, when `options.return_state_text` is set.
#[serde(skip_serializing_if = "Option::is_none")]
pub state_text: Option<String>,
/// Token and pass accounting for the request.
pub usage: Usage,
}

View File

@@ -1,5 +1,6 @@
pub mod chat; pub mod chat;
pub mod decisions; pub mod decisions;
pub mod embeddings; pub mod embeddings;
pub mod extraction;
pub mod models; pub mod models;
pub mod system_one; pub mod system_one;

View File

@@ -25,9 +25,12 @@ use std::collections::VecDeque;
use std::convert::Infallible; use std::convert::Infallible;
use std::fmt::Display; use std::fmt::Display;
use std::str::FromStr; use std::str::FromStr;
use std::time::Duration;
use strum::EnumString; use strum::EnumString;
use strum::VariantArray; use strum::VariantArray;
use tokio::runtime::Handle; use tokio::runtime::Handle;
use tokio::select;
use tokio::time::sleep;
#[derive(Deserialize, TypedPath)] #[derive(Deserialize, TypedPath)]
#[typed_path("/v1/chat/completions")] #[typed_path("/v1/chat/completions")]
@@ -88,6 +91,7 @@ impl Display for ShortcutAndMaybeReasoning {
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
enum Provider { enum Provider {
CloudflareWorkersAi, CloudflareWorkersAi,
Cohere,
GoogleAiStudio, GoogleAiStudio,
Groq, Groq,
HuggingFace, HuggingFace,
@@ -98,6 +102,7 @@ enum Provider {
Ollama, Ollama,
OpenCode, OpenCode,
OpenRouter, OpenRouter,
Poolside,
VercelAiGateway, VercelAiGateway,
ZAi, ZAi,
} }
@@ -110,19 +115,22 @@ impl Shortcut {
match self { match self {
S::XS => vec![ S::XS => vec![
(P::GoogleAiStudio, "gemini-3.5-flash-lite".into()), (P::GoogleAiStudio, "gemini-3.5-flash-lite".into()),
(P::NovitaAi, "apodex/apodex-1.1-mini".into()),
(P::OpenRouter, "apodex/apodex-1.1-mini:free".into()),
(P::VercelAiGateway, "openai/gpt-5.4-nano".into()), (P::VercelAiGateway, "openai/gpt-5.4-nano".into()),
(P::HuggingFace, "Qwen/Qwen3.6-35B-A3B".into()), (P::HuggingFace, "Qwen/Qwen3.6-35B-A3B".into()),
(P::OpenRouter, "xingchen-agi/xing4.0-29b-a4b:free".into()),
(P::NovitaAi, "apodex/apodex-1.1-mini".into()),
(P::OpenRouter, "apodex/apodex-1.1-mini:free".into()),
(P::VercelAiGateway, "liquid/lfm2.5-24b-a2b".into()), (P::VercelAiGateway, "liquid/lfm2.5-24b-a2b".into()),
(P::GoogleAiStudio, "gemma-4-26b-a4b-it".into()), (P::GoogleAiStudio, "gemma-4-26b-a4b-it".into()),
(P::OpenRouter, "google/gemma-4-26b-a4b-it:free".into()), (P::OpenRouter, "google/gemma-4-26b-a4b-it:free".into()),
(P::Kilo, "google-gemma-4-26b-a4b-it-free".into()), (P::Kilo, "google-gemma-4-26b-a4b-it-free".into()),
(P::HuggingFace, "google/gemma-4-26B-A4B-it".into()), (P::HuggingFace, "google/gemma-4-26B-A4B-it".into()),
(P::Poolside, "poolside/laguna-xs-2.1".into()),
(P::NvidiaNim, "poolside/laguna-xs-2.1".into()), (P::NvidiaNim, "poolside/laguna-xs-2.1".into()),
(P::OpenRouter, "poolside/laguna-xs-2.1:free".into()), (P::OpenRouter, "poolside/laguna-xs-2.1:free".into()),
(P::ZAi, "glm-4.7-flash".into()), (P::ZAi, "glm-4.7-flash".into()),
(P::NvidiaNim, "google/diffusiongemma-26b-a4b-it".into()), (P::NvidiaNim, "google/diffusiongemma-26b-a4b-it".into()),
(P::Cohere, "north-mini-code-1-0".into()),
(P::OpenRouter, "cohere/north-mini-code:free".into()), (P::OpenRouter, "cohere/north-mini-code:free".into()),
(P::NvidiaNim, "nvidia/nemotron-3.5-lightning-30b-a3b".into()), (P::NvidiaNim, "nvidia/nemotron-3.5-lightning-30b-a3b".into()),
( (
@@ -185,28 +193,21 @@ impl Shortcut {
(P::OpenCode, "nemotron-3.5-super-vl-free".into()), (P::OpenCode, "nemotron-3.5-super-vl-free".into()),
(P::Mistral, "mistral-medium-3.5".into()), (P::Mistral, "mistral-medium-3.5".into()),
(P::Kilo, "inclusionai-ling-3-0-flash-free".into()), (P::Kilo, "inclusionai-ling-3-0-flash-free".into()),
(P::NovitaAi, "inclusionai/ling-3.0-flash-vl".into()),
(P::Kilo, "inclusionai-ling-3-0-flash-vl-free".into()), (P::Kilo, "inclusionai-ling-3-0-flash-vl-free".into()),
(P::OpenRouter, "inclusionai/ling-3.0-flash-vl:free".into()),
(P::VercelAiGateway, "inclusionai/ling-3.0-flash-vl".into()), (P::VercelAiGateway, "inclusionai/ling-3.0-flash-vl".into()),
(P::VercelAiGateway, "poolside/laguna-s-2.1-free".into()), (P::VercelAiGateway, "poolside/laguna-s-2.1-free".into()),
(P::Poolside, "poolside/laguna-s-2.1".into()),
(P::Kilo, "poolside-laguna-s-2-1-free".into()), (P::Kilo, "poolside-laguna-s-2-1-free".into()),
(P::OpenRouter, "poolside/laguna-s-2.1:free".into()), (P::OpenRouter, "poolside/laguna-s-2.1:free".into()),
(P::VercelAiGateway, "stepfun/step-3.7-flash".into()), (P::VercelAiGateway, "stepfun/step-3.7-flash".into()),
(P::NovitaAi, "inclusionai/ling-3.0-flash-fin".into()),
(P::OpenRouter, "inclusionai/ling-3.0-flash-fin:free".into()),
(P::VercelAiGateway, "inclusionai/ling-3.0-flash-fin".into()), (P::VercelAiGateway, "inclusionai/ling-3.0-flash-fin".into()),
(P::NovitaAi, "inclusionai/ling-3.0-flash-sante".into()),
(
P::OpenRouter,
"inclusionai/ling-3.0-flash-sante:free".into(),
),
( (
P::VercelAiGateway, P::VercelAiGateway,
"inclusionai/ling-3.0-flash-sante".into(), "inclusionai/ling-3.0-flash-sante".into(),
), ),
(P::VercelAiGateway, "mistral/mistral-large-3".into()), (P::VercelAiGateway, "mistral/mistral-large-3".into()),
(P::Ollama, "nemotron-3-super".into()), (P::Ollama, "nemotron-3-super".into()),
(P::Cohere, "command-a-plus-05-2026".into()),
(P::NvidiaNim, "nvidia/nemotron-3-super-120b-a12b".into()), (P::NvidiaNim, "nvidia/nemotron-3-super-120b-a12b".into()),
(P::Kilo, "nvidia-nemotron-3-super-120b-a12b-free".into()), (P::Kilo, "nvidia-nemotron-3-super-120b-a12b-free".into()),
( (
@@ -233,8 +234,11 @@ impl Shortcut {
(P::NvidiaNim, "deepseek-ai/deepseek-v4.1-flash".into()), (P::NvidiaNim, "deepseek-ai/deepseek-v4.1-flash".into()),
(P::HuggingFace, "deepseek-ai/DeepSeek-V4.1-Flash".into()), (P::HuggingFace, "deepseek-ai/DeepSeek-V4.1-Flash".into()),
(P::OpenCode, "deepseek-v4.1-flash-free".into()), (P::OpenCode, "deepseek-v4.1-flash-free".into()),
(P::VercelAiGateway, "minimax/minimax-m3.1-flash".into()),
(P::OpenRouter, "minimax/minimax-m3.1-flash:free".into()),
(P::VercelAiGateway, "inclusionai/ling-3.1-flash".into()), (P::VercelAiGateway, "inclusionai/ling-3.1-flash".into()),
(P::HuggingFace, "inclusionAI/Ling-3.1-flash".into()), (P::HuggingFace, "inclusionAI/Ling-3.1-flash".into()),
(P::VercelAiGateway, "reflection/beam-free".into()),
(P::Ollama, "beam".into()), (P::Ollama, "beam".into()),
(P::OpenCode, "beam-free".into()), (P::OpenCode, "beam-free".into()),
(P::VercelAiGateway, "xiaomi/mimo-v2.6-flash".into()), (P::VercelAiGateway, "xiaomi/mimo-v2.6-flash".into()),
@@ -254,6 +258,10 @@ impl Shortcut {
(P::Ollama, "nemotron-3-ultra".into()), (P::Ollama, "nemotron-3-ultra".into()),
(P::NvidiaNim, "nvidia/nemotron-3-ultra-550b-a55b".into()), (P::NvidiaNim, "nvidia/nemotron-3-ultra-550b-a55b".into()),
(P::Kilo, "nvidia-nemotron-3-ultra-550b-a55b-free".into()), (P::Kilo, "nvidia-nemotron-3-ultra-550b-a55b-free".into()),
(
P::OpenRouter,
"nvidia/nemotron-3-ultra-550b-a55b:free".into(),
),
(P::OpenCode, "nemotron-3-ultra-free".into()), (P::OpenCode, "nemotron-3-ultra-free".into()),
], ],
@@ -262,6 +270,14 @@ impl Shortcut {
(P::HuggingFace, "zai-org/GLM-5.3".into()), (P::HuggingFace, "zai-org/GLM-5.3".into()),
(P::CloudflareWorkersAi, "@cf/zai-org/glm-5.3".into()), (P::CloudflareWorkersAi, "@cf/zai-org/glm-5.3".into()),
(P::Mistral, "zai-glm-5-3".into()), (P::Mistral, "zai-glm-5-3".into()),
(
P::VercelAiGateway,
"deepseek/deepseek-v4.1-flash-1017".into(),
),
(
P::CloudflareWorkersAi,
"@cf/deepseek-ai/deepseek-v4.1-flash-1017".into(),
),
(P::OpenCode, "muse-spark-1.3-contributor-free".into()), (P::OpenCode, "muse-spark-1.3-contributor-free".into()),
(P::NvidiaNim, "moonshotai/kimi-k3".into()), (P::NvidiaNim, "moonshotai/kimi-k3".into()),
(P::HuggingFace, "moonshotai/Kimi-K3".into()), (P::HuggingFace, "moonshotai/Kimi-K3".into()),
@@ -395,6 +411,22 @@ pub async fn create_chat_completion(
.build() .build()
}) })
.collect(), .collect(),
Provider::Cohere => state
.app_args
.cohere_api_key
.iter()
.cloned()
.map(|api_key| {
Upstream::builder()
.url(
"https://api.cohere.com/compatibility/v1/chat/completions"
.parse()
.expect("TODO"),
)
.api_key(api_key)
.build()
})
.collect(),
Provider::Groq => state Provider::Groq => state
.app_args .app_args
.groq_api_key .groq_api_key
@@ -503,6 +535,18 @@ pub async fn create_chat_completion(
.build() .build()
}) })
.collect(), .collect(),
Provider::Poolside => state
.app_args
.poolside_api_key
.iter()
.cloned()
.map(|api_key| {
Upstream::builder()
.url("https://inference.poolside.ai/v1/chat/completions".parse().expect("TODO"))
.api_key(api_key)
.build()
})
.collect(),
Provider::VercelAiGateway => state Provider::VercelAiGateway => state
.app_args .app_args
.vercel_ai_gateway_api_key .vercel_ai_gateway_api_key
@@ -649,12 +693,21 @@ pub async fn create_chat_completion(
}); });
} }
let upstream_response = match upstream_request.send().await { let upstream_response = select! {
Ok(upstream_response) => upstream_response, _ = sleep(Duration::from_secs(20)) => {
Err(upstream_error) => { tracing::error!("timed out");
tracing::error!(?upstream_error);
continue; continue;
} }
upstream_response_result = upstream_request.send() => {
match upstream_response_result {
Ok(upstream_response) => upstream_response,
Err(upstream_error) => {
tracing::error!(?upstream_error);
continue;
}
}
}
}; };
if let Err(upstream_error) = upstream_response.error_for_status_ref() { if let Err(upstream_error) = upstream_response.error_for_status_ref() {

View File

@@ -1,4 +1,3 @@
use crate::CloudflareCredentials;
use crate::openai; use crate::openai;
use crate::server::AppState; use crate::server::AppState;
use crate::server::Upstream; use crate::server::Upstream;
@@ -34,6 +33,18 @@ pub enum Shortcut {
#[strum(serialize = "codestral-embed-2505")] #[strum(serialize = "codestral-embed-2505")]
CodestralEmbed2505, CodestralEmbed2505,
#[strum(serialize = "cohere-embed-v4.0")]
CohereEmbedV4_0,
#[strum(serialize = "cohere-embed-v5.0-fast")]
CohereEmbedV5_0Fast,
#[strum(serialize = "cohere-embed-v5.0-pro")]
CohereEmbedV5_0Pro,
#[strum(serialize = "gemini-embedding-2")]
GeminiEmbedding2,
#[strum(serialize = "lfm2.5-embedding-350m")] #[strum(serialize = "lfm2.5-embedding-350m")]
Lfm2_5Embedding350M, Lfm2_5Embedding350M,
@@ -46,9 +57,12 @@ pub enum Shortcut {
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
enum Provider { enum Provider {
Cohere,
GoogleAiStudio,
Mistral, Mistral,
NvidiaNim, NvidiaNim,
OpenRouter, OpenRouter,
VercelAiGateway,
} }
impl Shortcut { impl Shortcut {
@@ -58,6 +72,19 @@ impl Shortcut {
match self { match self {
S::CodestralEmbed2505 => vec![(P::Mistral, "codestral-embed-2505".into())], S::CodestralEmbed2505 => vec![(P::Mistral, "codestral-embed-2505".into())],
S::CohereEmbedV4_0 => vec![
(P::Cohere, "embed-v4.0".into()),
(P::VercelAiGateway, "cohere/embed-v4.0".into()),
],
S::CohereEmbedV5_0Fast => vec![
(P::Cohere, "embed-v5.0-fast".into()),
(P::VercelAiGateway, "cohere/embed-v5.0-fast".into()),
],
S::CohereEmbedV5_0Pro => vec![
(P::Cohere, "embed-v5.0-pro".into()),
(P::VercelAiGateway, "cohere/embed-v5.0-pro".into()),
],
S::GeminiEmbedding2 => vec![(P::GoogleAiStudio, "gemini-embedding-2".into())],
S::Lfm2_5Embedding350M => { S::Lfm2_5Embedding350M => {
vec![(P::OpenRouter, "liquid/lfm-2.5-embedding-350m:free".into())] vec![(P::OpenRouter, "liquid/lfm-2.5-embedding-350m:free".into())]
} }
@@ -103,6 +130,22 @@ pub async fn create_embeddings(
for (provider, model) in shortcut.suggestions() { for (provider, model) in shortcut.suggestions() {
let upstreams: Vec<Upstream> = match provider { let upstreams: Vec<Upstream> = match provider {
Provider::Cohere => state
.app_args
.cohere_api_key
.iter()
.cloned()
.map(|api_key| {
Upstream::builder()
.url(
"https://api.cohere.com/compatibility/v1/embeddings"
.parse()
.expect("TODO"),
)
.api_key(api_key)
.build()
})
.collect(),
Provider::Mistral => state Provider::Mistral => state
.app_args .app_args
.mistral_api_key .mistral_api_key
@@ -119,6 +162,22 @@ pub async fn create_embeddings(
.build() .build()
}) })
.collect(), .collect(),
Provider::GoogleAiStudio => state
.app_args
.google_ai_studio_api_key
.iter()
.cloned()
.map(|api_key| {
Upstream::builder()
.url(
"https://generativelanguage.googleapis.com/v1beta/openai/embeddings"
.parse()
.expect("TODO"),
)
.api_key(api_key)
.build()
})
.collect(),
Provider::NvidiaNim => state Provider::NvidiaNim => state
.app_args .app_args
.nvidia_nim_api_key .nvidia_nim_api_key
@@ -151,6 +210,22 @@ pub async fn create_embeddings(
.build() .build()
}) })
.collect(), .collect(),
Provider::VercelAiGateway => state
.app_args
.vercel_ai_gateway_api_key
.iter()
.cloned()
.map(|api_key| {
Upstream::builder()
.url(
"https://ai-gateway.vercel.sh/v1/embeddings"
.parse()
.expect("TODO"),
)
.api_key(api_key)
.build()
})
.collect(),
}; };
if let Ok(upstreams) = Vec1::try_from(upstreams) { if let Ok(upstreams) = Vec1::try_from(upstreams) {

217
src/server/extraction.rs Normal file
View File

@@ -0,0 +1,217 @@
use crate::CloudflareCredentials;
use crate::openai;
use crate::server::AppState;
use crate::server::Upstream;
use crate::server::take_random_by_swapping;
use axum::Json;
use axum::extract::State;
use axum::http::StatusCode;
use axum::response::IntoResponse;
use axum::response::Response;
use axum_extra::routing::TypedPath;
use ecow::EcoString;
use headers::Header;
use headers::authorization::Bearer;
use mitsein::vec1::Vec1;
use reqwest::Certificate;
use secrecy::ExposeSecret;
use serde::Deserialize;
use serde_with::DeserializeFromStr;
use serde_with::SerializeDisplay;
use std::collections::VecDeque;
use strum::EnumString;
use strum::VariantArray;
#[derive(Deserialize, TypedPath)]
#[typed_path("/v1/extraction")]
pub struct V1ExtractionPath;
#[derive(
Clone, Debug, EnumString, DeserializeFromStr, strum::Display, SerializeDisplay, VariantArray,
)]
#[strum(serialize_all = "lowercase")]
pub enum Shortcut {
M,
}
#[derive(Debug, Clone)]
enum Provider {
Cloudflare,
}
impl Shortcut {
fn suggestions(&self) -> Vec<(Provider, EcoString)> {
use Provider as P;
use Shortcut as S;
match self {
S::M => {
vec![(P::Cloudflare, "@cf/cloudflare/clairic".into())]
}
}
}
}
pub struct ExtractionResponse(pub openai::extraction::ExtractionResponse<String>);
impl IntoResponse for ExtractionResponse {
fn into_response(self) -> Response {
let Self(extraction) = self;
Json(extraction).into_response()
}
}
pub enum ExtractionError {
NoneSucceeded,
}
impl IntoResponse for ExtractionError {
fn into_response(self) -> Response {
match self {
ExtractionError::NoneSucceeded => StatusCode::SERVICE_UNAVAILABLE.into_response(),
}
}
}
#[tracing::instrument(skip(state, request))]
pub async fn create_extraction(
_: V1ExtractionPath,
State(state): State<AppState>,
Json(request): Json<openai::extraction::ExtractionRequest<Shortcut>>,
) -> Result<ExtractionResponse, ExtractionError> {
let shortcut = request.model.clone();
let mut model_upstreams = VecDeque::new();
for (provider, model) in shortcut.suggestions() {
let upstreams: Vec<Upstream> =
match provider {
Provider::Cloudflare => state
.app_args
.cloudflare_workers_ai_credentials
.iter()
.cloned()
.map(
|CloudflareCredentials {
account_id,
api_key,
}| {
Upstream::builder()
.url(
format!("https://api.cloudflare.com/client/v4/accounts/{account_id}/ai/run/{model}")
.parse()
.expect("TODO"),
)
.api_key(api_key)
.build()
},
)
.collect(),
};
if let Ok(upstreams) = Vec1::try_from(upstreams) {
model_upstreams.push_back((model, upstreams));
}
}
while let Some((model, upstreams)) = model_upstreams.pop_front() {
let (upstream, upstreams) = take_random_by_swapping(upstreams, &mut rand::rng());
if let Some(upstreams) = upstreams {
model_upstreams.push_back((model.clone(), upstreams));
}
tracing::info!(?model, ?upstream);
let openai::extraction::ExtractionRequest {
model: _shortcut,
state,
fields,
images,
reference_date,
options,
} = request.clone();
let request = openai::extraction::ExtractionRequest {
model,
state,
fields,
images,
reference_date,
options,
};
let client = reqwest::Client::builder()
.tls_certs_only(
webpki_root_certs::TLS_SERVER_ROOT_CERTS
.iter()
.filter_map(|cert| Certificate::from_der(cert).ok()),
)
.build()
.expect("TODO");
let mut upstream_request = client.post(upstream.url).json(&request);
if let Some(api_key) = upstream.api_key {
let authorization =
headers::Authorization::bearer(api_key.expose_secret()).expect("TODO");
upstream_request = upstream_request.header(headers::Authorization::<Bearer>::name(), {
let mut vec = vec![];
authorization.encode(&mut vec);
let [one] = vec.try_into().expect("TODO");
one
});
}
let upstream_response = match upstream_request.send().await {
Ok(upstream_response) => upstream_response,
Err(upstream_error) => {
tracing::error!(?upstream_error);
continue;
}
};
if let Err(upstream_error) = upstream_response.error_for_status_ref() {
tracing::error!(?upstream_error);
match upstream_response.text().await {
Ok(body) => tracing::error!(?body),
Err(body_error) => tracing::error!(?body_error),
};
continue;
};
let response: serde_json::Value = match upstream_response.json().await {
Ok(response) => response,
Err(json_error) => {
tracing::error!(?json_error);
continue;
}
};
match openai::extraction::ExtractionResponse::deserialize(
response.clone(), // TODO: not need to clone
) {
Ok(response) => return Ok(ExtractionResponse(response)),
Err(structure_error) => {
#[derive(Deserialize)]
struct CloudflareRunResponse {
result: openai::extraction::ExtractionResponse<String>,
success: bool,
errors: Vec<serde_json::Value>, // TODO
messages: Vec<serde_json::Value>, // TODO
}
match CloudflareRunResponse::deserialize(response) {
Ok(response) => return Ok(ExtractionResponse(response.result)),
Err(cloudflare_structure_error) => {
tracing::error!(?structure_error);
tracing::error!(?cloudflare_structure_error);
}
}
}
}
}
Err(ExtractionError::NoneSucceeded)
}

View File

@@ -0,0 +1,12 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Basic Page</title>
</head>
<body>
<h1>Hello, world!</h1>
<p>This is a basic HTML page.</p>
</body>
</html>

View File

@@ -12,8 +12,9 @@ use url::Url;
mod chat; mod chat;
mod decisions; mod decisions;
mod frontend;
mod embeddings; mod embeddings;
mod extraction;
mod frontend;
mod models; mod models;
mod system_one; mod system_one;
@@ -46,6 +47,7 @@ pub fn create_router() -> Router<AppState> {
.typed_post(chat::completions::create_chat_completion) .typed_post(chat::completions::create_chat_completion)
.typed_post(decisions::create_decision) .typed_post(decisions::create_decision)
.typed_post(embeddings::create_embeddings) .typed_post(embeddings::create_embeddings)
.typed_post(extraction::create_extraction)
.typed_post(system_one::create_system_one) .typed_post(system_one::create_system_one)
.typed_get(models::list_models) .typed_get(models::list_models)
.typed_get(frontend::spa) .typed_get(frontend::spa)

View File

@@ -33,6 +33,7 @@ pub struct V1SystemOnePath;
pub enum Shortcut { pub enum Shortcut {
S, S,
M, M,
L,
} }
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
@@ -52,6 +53,10 @@ impl Shortcut {
match self { match self {
S::S => { S::S => {
vec![ vec![
(
P::VercelAiGateway,
"perplexity/pplx-decider-flash-v1.2".into(),
),
(P::LiquidAi, "d1:free".into()), (P::LiquidAi, "d1:free".into()),
(P::CloudflareWorkersAi, "clef-omni".into()), (P::CloudflareWorkersAi, "clef-omni".into()),
(P::CloudflareWorkersAi, "clef-flash".into()), (P::CloudflareWorkersAi, "clef-flash".into()),
@@ -65,10 +70,15 @@ impl Shortcut {
S::M => { S::M => {
vec![ vec![
(P::OpenCode, "jev-1.13-free".into()), (P::OpenCode, "jev-1.13-free".into()),
(P::VercelAiGateway, "perplexity/pplx-decider-v1.2".into()),
(P::CloudflareWorkersAi, "clef".into()), (P::CloudflareWorkersAi, "clef".into()),
(P::OpenRouter, "inception/mercury-decide:free".into()), (P::OpenRouter, "inception/mercury-decide:free".into()),
] ]
} }
S::L => {
vec![(P::VercelAiGateway, "liquid/d1-ultra".into())]
}
} }
} }
} }