Compare commits
16 Commits
12eb6f2aee
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
7f1290a86a
|
|||
| 566cc56896 | |||
| 302496c12c | |||
| 1b0ca23448 | |||
| 94803c699a | |||
| bc0223539c | |||
| 69789fbd87 | |||
| 5dedf5fa07 | |||
| b9daf9d6bf | |||
|
d11c2d2589
|
|||
| 94a9594130 | |||
| 692a1a56a7 | |||
| 3be16d3d58 | |||
|
|
6e3a283d81 | ||
| e74b5f6a4b | |||
|
|
831b6f568b |
27
Cargo.lock
generated
27
Cargo.lock
generated
@@ -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",
|
||||||
@@ -1645,6 +1646,7 @@ dependencies = [
|
|||||||
"serde_with",
|
"serde_with",
|
||||||
"strum",
|
"strum",
|
||||||
"tokio",
|
"tokio",
|
||||||
|
"tower-http 0.7.1",
|
||||||
"tracing",
|
"tracing",
|
||||||
"tracing-subscriber",
|
"tracing-subscriber",
|
||||||
"typed-builder",
|
"typed-builder",
|
||||||
@@ -2127,7 +2129,7 @@ dependencies = [
|
|||||||
"tokio-rustls",
|
"tokio-rustls",
|
||||||
"tokio-util",
|
"tokio-util",
|
||||||
"tower",
|
"tower",
|
||||||
"tower-http",
|
"tower-http 0.6.11",
|
||||||
"tower-service",
|
"tower-service",
|
||||||
"url",
|
"url",
|
||||||
"wasm-bindgen",
|
"wasm-bindgen",
|
||||||
@@ -2860,6 +2862,21 @@ dependencies = [
|
|||||||
"url",
|
"url",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tower-http"
|
||||||
|
version = "0.7.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "08a05a66a4fdd61cbbe0a1d755ffe0ca6aba159dd4820936a0ff8a8278245b9c"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags 2.13.2",
|
||||||
|
"bytes",
|
||||||
|
"http",
|
||||||
|
"percent-encoding",
|
||||||
|
"pin-project-lite",
|
||||||
|
"tower-layer",
|
||||||
|
"tower-service",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tower-layer"
|
name = "tower-layer"
|
||||||
version = "0.3.3"
|
version = "0.3.3"
|
||||||
|
|||||||
@@ -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 }
|
||||||
@@ -28,6 +29,7 @@ serde_json = "1.0.151"
|
|||||||
serde_with = "3.24.0"
|
serde_with = "3.24.0"
|
||||||
strum = { version = "0.28.0", features = ["derive"] }
|
strum = { version = "0.28.0", features = ["derive"] }
|
||||||
tokio = { version = "1.53.1", features = ["macros", "rt-multi-thread"] }
|
tokio = { version = "1.53.1", features = ["macros", "rt-multi-thread"] }
|
||||||
|
tower-http = { version = "0.7.1", features = ["cors"] }
|
||||||
tracing = "0.1.44"
|
tracing = "0.1.44"
|
||||||
tracing-subscriber = { version = "0.3.23", features = ["env-filter"] }
|
tracing-subscriber = { version = "0.3.23", features = ["env-filter"] }
|
||||||
typed-builder = "0.23.2"
|
typed-builder = "0.23.2"
|
||||||
|
|||||||
15
src/lib.rs
15
src/lib.rs
@@ -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>,
|
||||||
|
|
||||||
@@ -48,6 +51,9 @@ pub struct AppArgs {
|
|||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
pub hugging_face_api_key: Vec<SecretString>,
|
pub hugging_face_api_key: Vec<SecretString>,
|
||||||
|
|
||||||
|
#[arg(long)]
|
||||||
|
pub kilo_api_key: Vec<SecretString>,
|
||||||
|
|
||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
pub liquid_ai_api_key: Vec<SecretString>,
|
pub liquid_ai_api_key: Vec<SecretString>,
|
||||||
|
|
||||||
@@ -60,9 +66,18 @@ pub struct AppArgs {
|
|||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
pub nvidia_nim_api_key: Vec<SecretString>,
|
pub nvidia_nim_api_key: Vec<SecretString>,
|
||||||
|
|
||||||
|
#[arg(long)]
|
||||||
|
pub ollama_cloud_api_key: Vec<SecretString>,
|
||||||
|
|
||||||
|
#[arg(long)]
|
||||||
|
pub opencode_console_api_key: Vec<SecretString>,
|
||||||
|
|
||||||
#[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
524
src/openai/extraction.rs
Normal 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,
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
use crate::CloudflareCredentials;
|
use crate::CloudflareCredentials;
|
||||||
use crate::openai;
|
use crate::openai;
|
||||||
use crate::server::AppState;
|
use crate::server::AppState;
|
||||||
use crate::server::Provider;
|
|
||||||
use crate::server::Upstream;
|
use crate::server::Upstream;
|
||||||
use crate::server::take_random_by_swapping;
|
use crate::server::take_random_by_swapping;
|
||||||
use axum::Json;
|
use axum::Json;
|
||||||
@@ -26,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")]
|
||||||
@@ -86,6 +88,25 @@ impl Display for ShortcutAndMaybeReasoning {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
enum Provider {
|
||||||
|
CloudflareWorkersAi,
|
||||||
|
Cohere,
|
||||||
|
GoogleAiStudio,
|
||||||
|
Groq,
|
||||||
|
HuggingFace,
|
||||||
|
Kilo,
|
||||||
|
Mistral,
|
||||||
|
NovitaAi,
|
||||||
|
NvidiaNim,
|
||||||
|
Ollama,
|
||||||
|
OpenCode,
|
||||||
|
OpenRouter,
|
||||||
|
Poolside,
|
||||||
|
VercelAiGateway,
|
||||||
|
ZAi,
|
||||||
|
}
|
||||||
|
|
||||||
impl Shortcut {
|
impl Shortcut {
|
||||||
fn suggestions(&self) -> Vec<(Provider, EcoString)> {
|
fn suggestions(&self) -> Vec<(Provider, EcoString)> {
|
||||||
use Provider as P;
|
use Provider as P;
|
||||||
@@ -94,18 +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::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()),
|
||||||
(
|
(
|
||||||
@@ -113,6 +138,7 @@ impl Shortcut {
|
|||||||
"nvidia/nemotron-3.5-lightning-free".into(),
|
"nvidia/nemotron-3.5-lightning-free".into(),
|
||||||
),
|
),
|
||||||
(P::OpenRouter, "nvidia/nemotron-3.5-lightning:free".into()),
|
(P::OpenRouter, "nvidia/nemotron-3.5-lightning:free".into()),
|
||||||
|
(P::OpenCode, "nemotron-3.5-lightning-free".into()),
|
||||||
(P::ZAi, "glm-4.6v-flash".into()),
|
(P::ZAi, "glm-4.6v-flash".into()),
|
||||||
(P::ZAi, "glm-4.5-flash".into()),
|
(P::ZAi, "glm-4.5-flash".into()),
|
||||||
(P::OpenRouter, "poolside/laguna-xs.2:free".into()),
|
(P::OpenRouter, "poolside/laguna-xs.2:free".into()),
|
||||||
@@ -124,7 +150,9 @@ impl Shortcut {
|
|||||||
P::OpenRouter,
|
P::OpenRouter,
|
||||||
"nvidia/nemotron-3-nano-omni-30b-a3b-reasoning:free".into(),
|
"nvidia/nemotron-3-nano-omni-30b-a3b-reasoning:free".into(),
|
||||||
),
|
),
|
||||||
|
(P::Ollama, "nemotron-3-nano:30b".into()),
|
||||||
(P::Groq, "openai/gpt-oss-20b".into()),
|
(P::Groq, "openai/gpt-oss-20b".into()),
|
||||||
|
(P::Ollama, "gpt-oss:20b".into()),
|
||||||
(P::NvidiaNim, "openai/gpt-oss-20b".into()),
|
(P::NvidiaNim, "openai/gpt-oss-20b".into()),
|
||||||
(P::CloudflareWorkersAi, "@cf/openai/gpt-oss-20b".into()),
|
(P::CloudflareWorkersAi, "@cf/openai/gpt-oss-20b".into()),
|
||||||
(P::OpenRouter, "openai/gpt-oss-20b:free".into()),
|
(P::OpenRouter, "openai/gpt-oss-20b:free".into()),
|
||||||
@@ -132,6 +160,7 @@ impl Shortcut {
|
|||||||
(P::NvidiaNim, "meta/llama-3.2-11b-vision-instruct".into()),
|
(P::NvidiaNim, "meta/llama-3.2-11b-vision-instruct".into()),
|
||||||
(P::OpenRouter, "liquid/lfm-2.5-2.6b:free".into()),
|
(P::OpenRouter, "liquid/lfm-2.5-2.6b:free".into()),
|
||||||
(P::Groq, "groq/compound-mini".into()),
|
(P::Groq, "groq/compound-mini".into()),
|
||||||
|
(P::Kilo, "kilo-auto/free".into()),
|
||||||
(P::OpenRouter, "openrouter/free".into()),
|
(P::OpenRouter, "openrouter/free".into()),
|
||||||
],
|
],
|
||||||
|
|
||||||
@@ -143,6 +172,7 @@ impl Shortcut {
|
|||||||
(P::NvidiaNim, "meta/muse-glimmer-30b".into()),
|
(P::NvidiaNim, "meta/muse-glimmer-30b".into()),
|
||||||
(P::HuggingFace, "meta-models/Muse-Glimmer-30B".into()),
|
(P::HuggingFace, "meta-models/Muse-Glimmer-30B".into()),
|
||||||
(P::GoogleAiStudio, "gemma-4-31b-it".into()),
|
(P::GoogleAiStudio, "gemma-4-31b-it".into()),
|
||||||
|
(P::Ollama, "gemma4:31b".into()),
|
||||||
(P::NvidiaNim, "google/gemma-4-31b-it".into()),
|
(P::NvidiaNim, "google/gemma-4-31b-it".into()),
|
||||||
(P::OpenRouter, "google/gemma-4-31b-it:free".into()),
|
(P::OpenRouter, "google/gemma-4-31b-it:free".into()),
|
||||||
(P::HuggingFace, "google/gemma-4-31B-it".into()),
|
(P::HuggingFace, "google/gemma-4-31B-it".into()),
|
||||||
@@ -160,32 +190,32 @@ impl Shortcut {
|
|||||||
"nvidia/nemotron-3.5-super-vl-120b-a12b".into(),
|
"nvidia/nemotron-3.5-super-vl-120b-a12b".into(),
|
||||||
),
|
),
|
||||||
(P::OpenRouter, "nvidia/nemotron-3.5-super-vl:free".into()),
|
(P::OpenRouter, "nvidia/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::NovitaAi, "inclusionai/ling-3.0-flash-vl".into()),
|
(P::Kilo, "inclusionai-ling-3-0-flash-free".into()),
|
||||||
(P::OpenRouter, "inclusionai/ling-3.0-flash-vl:free".into()),
|
(P::Kilo, "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::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::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::OpenRouter,
|
P::OpenRouter,
|
||||||
"nvidia/nemotron-3-super-120b-a12b:free".into(),
|
"nvidia/nemotron-3-super-120b-a12b:free".into(),
|
||||||
),
|
),
|
||||||
(P::Groq, "openai/gpt-oss-120b".into()),
|
(P::Groq, "openai/gpt-oss-120b".into()),
|
||||||
|
(P::Ollama, "gpt-oss:120b".into()),
|
||||||
(P::CloudflareWorkersAi, "@cf/openai/gpt-oss-120b".into()),
|
(P::CloudflareWorkersAi, "@cf/openai/gpt-oss-120b".into()),
|
||||||
(P::OpenRouter, "openai/gpt-oss-120b:free".into()),
|
(P::OpenRouter, "openai/gpt-oss-120b:free".into()),
|
||||||
(P::HuggingFace, "openai/gpt-oss-120b".into()),
|
(P::HuggingFace, "openai/gpt-oss-120b".into()),
|
||||||
@@ -193,24 +223,46 @@ impl Shortcut {
|
|||||||
],
|
],
|
||||||
|
|
||||||
S::L => vec![
|
S::L => vec![
|
||||||
(P::NvidiaNim, "stepfun-ai/step-5-preview".into()),
|
|
||||||
(P::CloudflareWorkersAi, "@cf/zai-org/glm-5.3-flash".into()),
|
(P::CloudflareWorkersAi, "@cf/zai-org/glm-5.3-flash".into()),
|
||||||
(P::NvidiaNim, "z-ai/glm-5-3-flash".into()),
|
(P::NvidiaNim, "z-ai/glm-5-3-flash".into()),
|
||||||
|
(P::OpenCode, "glm-5.3-flash-free".into()),
|
||||||
(P::HuggingFace, "zai-org/GLM-5.3-Flash".into()),
|
(P::HuggingFace, "zai-org/GLM-5.3-Flash".into()),
|
||||||
(P::HuggingFace, "zai-org/GLM-5.3-Flash-BF16".into()),
|
(P::HuggingFace, "zai-org/GLM-5.3-Flash-BF16".into()),
|
||||||
|
(P::Kilo, "step-5-preview-free".into()),
|
||||||
|
(P::NvidiaNim, "stepfun-ai/step-5-preview".into()),
|
||||||
(P::VercelAiGateway, "openai/gpt-5.2".into()),
|
(P::VercelAiGateway, "openai/gpt-5.2".into()),
|
||||||
(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::VercelAiGateway, "xiaomi/mimo-v2.6-flash".into()),
|
(P::OpenCode, "deepseek-v4.1-flash-free".into()),
|
||||||
(P::HuggingFace, "XiaomiMiMo/MiMo-V2.6-Flash-RL".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::OpenRouter, "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::OpenCode, "beam-free".into()),
|
||||||
|
(P::VercelAiGateway, "xiaomi/mimo-v2.6-flash".into()),
|
||||||
|
(P::OpenCode, "mimo-v2.6-flash-free".into()),
|
||||||
|
(P::HuggingFace, "XiaomiMiMo/MiMo-V2.6-Flash-RL".into()),
|
||||||
|
(P::Kilo, "nex-agi-nex-n2-5-pro-free".into()),
|
||||||
(P::VercelAiGateway, "xiaomi/mimo-v2.5".into()),
|
(P::VercelAiGateway, "xiaomi/mimo-v2.5".into()),
|
||||||
|
(P::Kilo, "dots-studio-dots-3-note-preview-free".into()),
|
||||||
(P::OpenRouter, "dots-studio/dots-3-note-preview:free".into()),
|
(P::OpenRouter, "dots-studio/dots-3-note-preview:free".into()),
|
||||||
|
(P::Kilo, "minimax-minimax-m3-free".into()),
|
||||||
(P::HuggingFace, "MiniMaxAI/MiniMax-M3".into()),
|
(P::HuggingFace, "MiniMaxAI/MiniMax-M3".into()),
|
||||||
|
(P::VercelAiGateway, "tencent/hy3".into()),
|
||||||
|
(P::Kilo, "tencent-hy3-free".into()),
|
||||||
|
(P::Kilo, "tencent-hy3-preview-free".into()),
|
||||||
(P::OpenRouter, "thinkingmachines/inkling-small:free".into()),
|
(P::OpenRouter, "thinkingmachines/inkling-small:free".into()),
|
||||||
(P::HuggingFace, "thinkingmachines/Inkling-Small".into()),
|
(P::HuggingFace, "thinkingmachines/Inkling-Small".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::OpenRouter,
|
||||||
|
"nvidia/nemotron-3-ultra-550b-a55b:free".into(),
|
||||||
|
),
|
||||||
|
(P::OpenCode, "nemotron-3-ultra-free".into()),
|
||||||
],
|
],
|
||||||
|
|
||||||
S::XL => vec![
|
S::XL => vec![
|
||||||
@@ -218,14 +270,23 @@ 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::NvidiaNim, "moonshotai/kimi-k3".into()),
|
||||||
|
(P::HuggingFace, "moonshotai/Kimi-K3".into()),
|
||||||
(P::HuggingFace, "tencent/Hy4-preview".into()),
|
(P::HuggingFace, "tencent/Hy4-preview".into()),
|
||||||
(P::HuggingFace, "XiaomiMiMo/MiMo-V2.6-Pro-RL".into()),
|
(P::HuggingFace, "XiaomiMiMo/MiMo-V2.6-Pro-RL".into()),
|
||||||
(P::Mistral, "mistral-large-4".into()),
|
(P::Mistral, "mistral-large-4".into()),
|
||||||
(P::NvidiaNim, "moonshotai/kimi-k3".into()),
|
|
||||||
(P::HuggingFace, "moonshotai/Kimi-K3".into()),
|
|
||||||
(P::HuggingFace, "Qwen/Qwen3.8-2.4T-A95B".into()),
|
(P::HuggingFace, "Qwen/Qwen3.8-2.4T-A95B".into()),
|
||||||
(P::VercelAiGateway, "xiaomi/mimo-v2.5-pro".into()),
|
(P::VercelAiGateway, "xiaomi/mimo-v2.5-pro".into()),
|
||||||
(P::VercelAiGateway, "tencent/hy3".into()),
|
(P::OpenCode, "longcat-2.5-preview-free".into()),
|
||||||
(P::OpenRouter, "thinkingmachines/inkling:free".into()),
|
(P::OpenRouter, "thinkingmachines/inkling:free".into()),
|
||||||
(P::HuggingFace, "thinkingmachines/Inkling".into()),
|
(P::HuggingFace, "thinkingmachines/Inkling".into()),
|
||||||
(P::VercelAiGateway, "thinkingmachines/inkling".into()),
|
(P::VercelAiGateway, "thinkingmachines/inkling".into()),
|
||||||
@@ -235,6 +296,8 @@ impl Shortcut {
|
|||||||
|
|
||||||
S::Experimental => vec![
|
S::Experimental => vec![
|
||||||
(P::OpenRouter, "space-bunny-alpha".into()),
|
(P::OpenRouter, "space-bunny-alpha".into()),
|
||||||
|
(P::OpenCode, "space-bunny-free".into()),
|
||||||
|
(P::OpenCode, "big-pickle".into()),
|
||||||
(P::VercelAiGateway, "stealth/glyph-cluster".into()),
|
(P::VercelAiGateway, "stealth/glyph-cluster".into()),
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
@@ -348,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
|
||||||
@@ -372,7 +451,18 @@ pub async fn create_chat_completion(
|
|||||||
.build()
|
.build()
|
||||||
})
|
})
|
||||||
.collect(),
|
.collect(),
|
||||||
Provider::LiquidAi => unreachable!(),
|
Provider::Kilo => state
|
||||||
|
.app_args
|
||||||
|
.kilo_api_key
|
||||||
|
.iter()
|
||||||
|
.cloned()
|
||||||
|
.map(|api_key| {
|
||||||
|
Upstream::builder()
|
||||||
|
.url("https://api.kilo.ai/api/gateway/v1/chat/completions".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
|
||||||
@@ -409,6 +499,18 @@ pub async fn create_chat_completion(
|
|||||||
.build()
|
.build()
|
||||||
})
|
})
|
||||||
.collect(),
|
.collect(),
|
||||||
|
Provider::Ollama => state
|
||||||
|
.app_args
|
||||||
|
.ollama_cloud_api_key
|
||||||
|
.iter()
|
||||||
|
.cloned()
|
||||||
|
.map(|api_key| {
|
||||||
|
Upstream::builder()
|
||||||
|
.url("https://ollama.com/v1/chat/completions".parse().expect("TODO"))
|
||||||
|
.api_key(api_key)
|
||||||
|
.build()
|
||||||
|
})
|
||||||
|
.collect(),
|
||||||
Provider::OpenRouter => state
|
Provider::OpenRouter => state
|
||||||
.app_args
|
.app_args
|
||||||
.openrouter_api_key
|
.openrouter_api_key
|
||||||
@@ -421,6 +523,30 @@ pub async fn create_chat_completion(
|
|||||||
.build()
|
.build()
|
||||||
})
|
})
|
||||||
.collect(),
|
.collect(),
|
||||||
|
Provider::OpenCode => state
|
||||||
|
.app_args
|
||||||
|
.opencode_console_api_key
|
||||||
|
.iter()
|
||||||
|
.cloned()
|
||||||
|
.map(|api_key| {
|
||||||
|
Upstream::builder()
|
||||||
|
.url("https://opencode.ai/inference/openai/v1/chat/completions".parse().expect("TODO"))
|
||||||
|
.api_key(api_key)
|
||||||
|
.build()
|
||||||
|
})
|
||||||
|
.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
|
||||||
@@ -567,12 +693,21 @@ pub async fn create_chat_completion(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
let upstream_response = match upstream_request.send().await {
|
let upstream_response = select! {
|
||||||
|
_ = sleep(Duration::from_secs(20)) => {
|
||||||
|
tracing::error!("timed out");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
upstream_response_result = upstream_request.send() => {
|
||||||
|
match upstream_response_result {
|
||||||
Ok(upstream_response) => upstream_response,
|
Ok(upstream_response) => upstream_response,
|
||||||
Err(upstream_error) => {
|
Err(upstream_error) => {
|
||||||
tracing::error!(?upstream_error);
|
tracing::error!(?upstream_error);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
if let Err(upstream_error) = upstream_response.error_for_status_ref() {
|
if let Err(upstream_error) = upstream_response.error_for_status_ref() {
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
use crate::CloudflareCredentials;
|
|
||||||
use crate::openai;
|
use crate::openai;
|
||||||
use crate::server::AppState;
|
use crate::server::AppState;
|
||||||
use crate::server::Provider;
|
|
||||||
use crate::server::Upstream;
|
use crate::server::Upstream;
|
||||||
use crate::server::take_random_by_swapping;
|
use crate::server::take_random_by_swapping;
|
||||||
use axum::Json;
|
use axum::Json;
|
||||||
@@ -35,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,
|
||||||
|
|
||||||
@@ -45,6 +55,16 @@ pub enum Shortcut {
|
|||||||
Nemotron3Embed1B,
|
Nemotron3Embed1B,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
enum Provider {
|
||||||
|
Cohere,
|
||||||
|
GoogleAiStudio,
|
||||||
|
Mistral,
|
||||||
|
NvidiaNim,
|
||||||
|
OpenRouter,
|
||||||
|
VercelAiGateway,
|
||||||
|
}
|
||||||
|
|
||||||
impl Shortcut {
|
impl Shortcut {
|
||||||
fn suggestions(&self) -> Vec<(Provider, EcoString)> {
|
fn suggestions(&self) -> Vec<(Provider, EcoString)> {
|
||||||
use Provider as P;
|
use Provider as P;
|
||||||
@@ -52,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())]
|
||||||
}
|
}
|
||||||
@@ -96,28 +129,38 @@ pub async fn create_embeddings(
|
|||||||
let mut model_upstreams = VecDeque::new();
|
let mut model_upstreams = VecDeque::new();
|
||||||
|
|
||||||
for (provider, model) in shortcut.suggestions() {
|
for (provider, model) in shortcut.suggestions() {
|
||||||
let upstreams: Vec<Upstream> =
|
let upstreams: Vec<Upstream> = match provider {
|
||||||
match provider {
|
Provider::Cohere => state
|
||||||
Provider::CloudflareWorkersAi => state
|
|
||||||
.app_args
|
.app_args
|
||||||
.cloudflare_workers_ai_credentials
|
.cohere_api_key
|
||||||
.iter()
|
.iter()
|
||||||
.cloned()
|
.cloned()
|
||||||
.map(
|
.map(|api_key| {
|
||||||
|CloudflareCredentials {
|
|
||||||
account_id,
|
|
||||||
api_key,
|
|
||||||
}| {
|
|
||||||
Upstream::builder()
|
Upstream::builder()
|
||||||
.url(
|
.url(
|
||||||
format!("https://api.cloudflare.com/client/v4/accounts/{account_id}/ai/v1/embeddings")
|
"https://api.cohere.com/compatibility/v1/embeddings"
|
||||||
.parse()
|
.parse()
|
||||||
.expect("TODO"),
|
.expect("TODO"),
|
||||||
)
|
)
|
||||||
.api_key(api_key)
|
.api_key(api_key)
|
||||||
.build()
|
.build()
|
||||||
},
|
})
|
||||||
|
.collect(),
|
||||||
|
Provider::Mistral => state
|
||||||
|
.app_args
|
||||||
|
.mistral_api_key
|
||||||
|
.iter()
|
||||||
|
.cloned()
|
||||||
|
.map(|api_key| {
|
||||||
|
Upstream::builder()
|
||||||
|
.url(
|
||||||
|
"https://api.mistral.ai/v1/embeddings"
|
||||||
|
.parse()
|
||||||
|
.expect("TODO"),
|
||||||
)
|
)
|
||||||
|
.api_key(api_key)
|
||||||
|
.build()
|
||||||
|
})
|
||||||
.collect(),
|
.collect(),
|
||||||
Provider::GoogleAiStudio => state
|
Provider::GoogleAiStudio => state
|
||||||
.app_args
|
.app_args
|
||||||
@@ -127,7 +170,7 @@ pub async fn create_embeddings(
|
|||||||
.map(|api_key| {
|
.map(|api_key| {
|
||||||
Upstream::builder()
|
Upstream::builder()
|
||||||
.url(
|
.url(
|
||||||
"https://generativelanguage.googleapis.com/v1beta/openai/v1/embeddings"
|
"https://generativelanguage.googleapis.com/v1beta/openai/embeddings"
|
||||||
.parse()
|
.parse()
|
||||||
.expect("TODO"),
|
.expect("TODO"),
|
||||||
)
|
)
|
||||||
@@ -135,43 +178,6 @@ pub async fn create_embeddings(
|
|||||||
.build()
|
.build()
|
||||||
})
|
})
|
||||||
.collect(),
|
.collect(),
|
||||||
Provider::Groq => state
|
|
||||||
.app_args
|
|
||||||
.groq_api_key
|
|
||||||
.iter()
|
|
||||||
.cloned()
|
|
||||||
.map(|api_key| {
|
|
||||||
Upstream::builder()
|
|
||||||
.url("https://api.groq.com/openai/v1/embeddings".parse().expect("TODO"))
|
|
||||||
.api_key(api_key)
|
|
||||||
.build()
|
|
||||||
})
|
|
||||||
.collect(),
|
|
||||||
Provider::HuggingFace => state
|
|
||||||
.app_args
|
|
||||||
.hugging_face_api_key
|
|
||||||
.iter()
|
|
||||||
.cloned()
|
|
||||||
.map(|api_key| {
|
|
||||||
Upstream::builder()
|
|
||||||
.url("https://router.huggingface.co/v1/embeddings".parse().expect("TODO"))
|
|
||||||
.api_key(api_key)
|
|
||||||
.build()
|
|
||||||
})
|
|
||||||
.collect(),
|
|
||||||
Provider::LiquidAi => unreachable!(),
|
|
||||||
Provider::Mistral => state
|
|
||||||
.app_args
|
|
||||||
.mistral_api_key
|
|
||||||
.iter()
|
|
||||||
.cloned()
|
|
||||||
.map(|api_key| {
|
|
||||||
Upstream::builder()
|
|
||||||
.url("https://api.mistral.ai/v1/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
|
||||||
@@ -179,12 +185,15 @@ pub async fn create_embeddings(
|
|||||||
.cloned()
|
.cloned()
|
||||||
.map(|api_key| {
|
.map(|api_key| {
|
||||||
Upstream::builder()
|
Upstream::builder()
|
||||||
.url("https://integrate.api.nvidia.com/v1/embeddings".parse().expect("TODO"))
|
.url(
|
||||||
|
"https://integrate.api.nvidia.com/v1/embeddings"
|
||||||
|
.parse()
|
||||||
|
.expect("TODO"),
|
||||||
|
)
|
||||||
.api_key(api_key)
|
.api_key(api_key)
|
||||||
.build()
|
.build()
|
||||||
})
|
})
|
||||||
.collect(),
|
.collect(),
|
||||||
Provider::NovitaAi => unreachable!(),
|
|
||||||
Provider::OpenRouter => state
|
Provider::OpenRouter => state
|
||||||
.app_args
|
.app_args
|
||||||
.openrouter_api_key
|
.openrouter_api_key
|
||||||
@@ -192,13 +201,31 @@ pub async fn create_embeddings(
|
|||||||
.cloned()
|
.cloned()
|
||||||
.map(|api_key| {
|
.map(|api_key| {
|
||||||
Upstream::builder()
|
Upstream::builder()
|
||||||
.url("https://openrouter.ai/api/v1/embeddings".parse().expect("TODO"))
|
.url(
|
||||||
|
"https://openrouter.ai/api/v1/embeddings"
|
||||||
|
.parse()
|
||||||
|
.expect("TODO"),
|
||||||
|
)
|
||||||
|
.api_key(api_key)
|
||||||
|
.build()
|
||||||
|
})
|
||||||
|
.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)
|
.api_key(api_key)
|
||||||
.build()
|
.build()
|
||||||
})
|
})
|
||||||
.collect(),
|
.collect(),
|
||||||
Provider::VercelAiGateway => unreachable!(),
|
|
||||||
Provider::ZAi => unreachable!(),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
if let Ok(upstreams) = Vec1::try_from(upstreams) {
|
if let Ok(upstreams) = Vec1::try_from(upstreams) {
|
||||||
|
|||||||
217
src/server/extraction.rs
Normal file
217
src/server/extraction.rs
Normal 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)
|
||||||
|
}
|
||||||
12
src/server/frontend/index.html
Normal file
12
src/server/frontend/index.html
Normal 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>
|
||||||
11
src/server/frontend/mod.rs
Normal file
11
src/server/frontend/mod.rs
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
use axum::response::Html;
|
||||||
|
use axum_extra::routing::TypedPath;
|
||||||
|
use serde::Deserialize;
|
||||||
|
|
||||||
|
#[derive(Deserialize, TypedPath)]
|
||||||
|
#[typed_path("/")]
|
||||||
|
pub struct IndexPath;
|
||||||
|
|
||||||
|
pub async fn spa(_: IndexPath) -> Html<&'static str> {
|
||||||
|
Html(include_str!("index.html"))
|
||||||
|
}
|
||||||
@@ -6,12 +6,15 @@ use rand::Rng;
|
|||||||
use rand::RngExt;
|
use rand::RngExt;
|
||||||
use secrecy::SecretString;
|
use secrecy::SecretString;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
use tower_http::cors::CorsLayer;
|
||||||
use typed_builder::TypedBuilder;
|
use typed_builder::TypedBuilder;
|
||||||
use url::Url;
|
use url::Url;
|
||||||
|
|
||||||
mod chat;
|
mod chat;
|
||||||
mod decisions;
|
mod decisions;
|
||||||
mod embeddings;
|
mod embeddings;
|
||||||
|
mod extraction;
|
||||||
|
mod frontend;
|
||||||
mod models;
|
mod models;
|
||||||
mod system_one;
|
mod system_one;
|
||||||
|
|
||||||
@@ -19,22 +22,6 @@ mod system_one;
|
|||||||
pub struct AppState {
|
pub struct AppState {
|
||||||
app_args: Arc<AppArgs>,
|
app_args: Arc<AppArgs>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
enum Provider {
|
|
||||||
CloudflareWorkersAi,
|
|
||||||
GoogleAiStudio,
|
|
||||||
Groq,
|
|
||||||
HuggingFace,
|
|
||||||
LiquidAi,
|
|
||||||
Mistral,
|
|
||||||
NovitaAi,
|
|
||||||
NvidiaNim,
|
|
||||||
OpenRouter,
|
|
||||||
VercelAiGateway,
|
|
||||||
ZAi,
|
|
||||||
}
|
|
||||||
|
|
||||||
fn take_random_by_swapping<T>(mut vec1: Vec1<T>, mut rng: impl Rng) -> (T, Option<Vec1<T>>) {
|
fn take_random_by_swapping<T>(mut vec1: Vec1<T>, mut rng: impl Rng) -> (T, Option<Vec1<T>>) {
|
||||||
let end = vec1.len().get() - 1;
|
let end = vec1.len().get() - 1;
|
||||||
let index = rng.random_range(0..=end);
|
let index = rng.random_range(0..=end);
|
||||||
@@ -60,8 +47,11 @@ 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)
|
||||||
|
.layer(CorsLayer::new())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn create_app(state: AppState) -> Router<()> {
|
pub fn create_app(state: AppState) -> Router<()> {
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
use crate::CloudflareCredentials;
|
use crate::CloudflareCredentials;
|
||||||
use crate::openai;
|
use crate::openai;
|
||||||
use crate::server::AppState;
|
use crate::server::AppState;
|
||||||
use crate::server::Provider;
|
|
||||||
use crate::server::Upstream;
|
use crate::server::Upstream;
|
||||||
use crate::server::take_random_by_swapping;
|
use crate::server::take_random_by_swapping;
|
||||||
use axum::Json;
|
use axum::Json;
|
||||||
@@ -34,6 +33,16 @@ pub struct V1SystemOnePath;
|
|||||||
pub enum Shortcut {
|
pub enum Shortcut {
|
||||||
S,
|
S,
|
||||||
M,
|
M,
|
||||||
|
L,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
enum Provider {
|
||||||
|
CloudflareWorkersAi,
|
||||||
|
LiquidAi,
|
||||||
|
OpenCode,
|
||||||
|
OpenRouter,
|
||||||
|
VercelAiGateway,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Shortcut {
|
impl Shortcut {
|
||||||
@@ -44,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()),
|
||||||
@@ -53,12 +66,19 @@ impl Shortcut {
|
|||||||
(P::VercelAiGateway, "convaiinnovations/laya".into()),
|
(P::VercelAiGateway, "convaiinnovations/laya".into()),
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
S::M => {
|
S::M => {
|
||||||
vec![
|
vec![
|
||||||
|
(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())]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -118,9 +138,6 @@ pub async fn create_system_one(
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
.collect(),
|
.collect(),
|
||||||
Provider::GoogleAiStudio => unreachable!(),
|
|
||||||
Provider::Groq => unreachable!(),
|
|
||||||
Provider::HuggingFace => unreachable!(),
|
|
||||||
Provider::LiquidAi => state
|
Provider::LiquidAi => state
|
||||||
.app_args
|
.app_args
|
||||||
.liquid_ai_api_key
|
.liquid_ai_api_key
|
||||||
@@ -133,9 +150,18 @@ pub async fn create_system_one(
|
|||||||
.build()
|
.build()
|
||||||
})
|
})
|
||||||
.collect(),
|
.collect(),
|
||||||
Provider::Mistral => unreachable!(),
|
Provider::OpenCode => state
|
||||||
Provider::NovitaAi => unreachable!(),
|
.app_args
|
||||||
Provider::NvidiaNim => unreachable!(),
|
.opencode_console_api_key
|
||||||
|
.iter()
|
||||||
|
.cloned()
|
||||||
|
.map(|api_key| {
|
||||||
|
Upstream::builder()
|
||||||
|
.url("https://opencode.ai/inference/systemone/v1/systemone".parse().expect("TODO"))
|
||||||
|
.api_key(api_key)
|
||||||
|
.build()
|
||||||
|
})
|
||||||
|
.collect(),
|
||||||
Provider::OpenRouter => state
|
Provider::OpenRouter => state
|
||||||
.app_args
|
.app_args
|
||||||
.openrouter_api_key
|
.openrouter_api_key
|
||||||
@@ -160,7 +186,6 @@ pub async fn create_system_one(
|
|||||||
.build()
|
.build()
|
||||||
})
|
})
|
||||||
.collect(),
|
.collect(),
|
||||||
Provider::ZAi => unreachable!(),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
if let Ok(upstreams) = Vec1::try_from(upstreams) {
|
if let Ok(upstreams) = Vec1::try_from(upstreams) {
|
||||||
|
|||||||
Reference in New Issue
Block a user