Compare commits

...

42 Commits

Author SHA1 Message Date
7f1290a86a feat: extraction models 2026-10-24 16:18:32 -04:00
566cc56896 chore: extraction model api (no proxying implemented yet) 2026-10-24 02:33:31 -04:00
302496c12c feat: timeout 2026-10-24 01:57:21 -04:00
1b0ca23448 feat: add cloudflare as a provider for deepseek v4.1 flash 1017 2026-10-24 01:12:06 -04:00
94803c699a feat: nemotron 3 ultra on openrouter free 2026-10-19 16:19:57 -04:00
bc0223539c chore: remove no longer available (or free) ling 3.0 openrouter models 2026-10-19 16:18:03 -04:00
69789fbd87 fix: remove models on novita that are no longer free 2026-10-19 16:16:10 -04:00
5dedf5fa07 feat: more models 2026-10-19 16:12:49 -04:00
b9daf9d6bf feat: poolside 2026-10-19 15:59:24 -04:00
d11c2d2589 feat: cohere + more models 2026-10-19 15:56:06 -04:00
94a9594130 chore: move glm 5.3 flash above step 5 preview 2026-10-18 15:35:29 -04:00
692a1a56a7 feat: frontend routes (just the index html file for now) 2026-10-18 15:31:19 -04:00
3be16d3d58 feat: opencode console support 2026-10-16 19:05:02 -04:00
J / Jacob Babich
6e3a283d81 feat: ollama cloud support 2026-10-16 12:57:27 -04:00
e74b5f6a4b feat: kilo gateway 2026-10-15 14:40:14 -04:00
J / Jacob Babich
831b6f568b feat+fix: add huggingface as a provider for ling 3.1 flash and remove openrouter now that it's no longer free on there 2026-10-15 14:09:57 -04:00
J / Jacob Babich
12eb6f2aee feat: new models 2026-10-15 13:49:18 -04:00
J / Jacob Babich
59e9d3648a chore: move groq to the top of its available models 2026-10-15 13:43:40 -04:00
f4f657c219 feat: novita, nvidia nim, vercel ai gateway 2026-10-13 23:37:24 -04:00
a0d95d52e4 fix: decisions from system one correctly 2026-10-13 21:25:25 -04:00
f778fae70c fix: parse cloudflare run results 2026-10-10 20:15:25 -04:00
c88874948a fix: correct cloudflare system one url 2026-10-10 19:28:15 -04:00
154cbac120 feat: finish implementation of decisions -> system one 2026-10-10 19:23:40 -04:00
e0e1f0781e feat: more models, make decisions a wrapper for system one 2026-10-10 19:06:02 -04:00
9a848e2c78 feat: LiquidAI and decisions and system one implementations 2026-10-09 22:58:34 -04:00
8452bb9088 chore: remove unused types 2026-10-09 02:53:57 -04:00
d869809768 chore: system one api types 2026-10-09 02:45:29 -04:00
0a111172f5 feat: openai decisions types 2026-10-09 02:28:26 -04:00
f2108ae35a feat: z.ai + more models 2026-10-09 02:19:53 -04:00
9dd13f374f feat: implement embeddings 2026-10-09 01:52:29 -04:00
226e0fb0ff chore: make some stuff in embeddings less stringly typed 2026-10-09 01:02:44 -04:00
da04b5dd62 feat: add qwen3.6 35b-a3b as an xs model 2026-10-09 00:48:52 -04:00
J / Jacob Babich
ff621d759c chore: groundwork for embeddings 2026-10-08 22:58:07 -04:00
0521a264dc chore: more 2026-10-08 01:48:13 -04:00
5ce36e3b8c feat: stuff 2026-10-08 01:24:23 -04:00
e161693587 chore: update default reasoning levels and slightly update shortcut model lists 2026-09-30 02:51:15 -04:00
0974f640bb feat: specify reasoning effort in model ID, or use a default 2026-09-30 02:34:36 -04:00
91ac1145ce chore: rename project to Katniss AI 2026-09-30 01:43:17 -04:00
f0e8516d7a feat: implement models endpoint 2026-09-30 00:56:09 -04:00
c6c0849cf5 fix: use correct webpki roots crate 2026-09-30 00:25:36 -04:00
7b3ac7acbb chore: improve default tracing directives 2026-09-30 00:03:15 -04:00
e49befb03f fix: use webpki roots 2026-09-30 00:02:30 -04:00
24 changed files with 3712 additions and 699 deletions

2
.gitignore vendored
View File

@@ -1 +1,3 @@
/target
.history/

543
Cargo.lock generated
View File

@@ -32,6 +32,15 @@ dependencies = [
"alloc-no-stdlib",
]
[[package]]
name = "android_system_properties"
version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ae221649c9976a6f6c56ae1facf410f3ddb33cc661c4b7b61020a912d4237fbc"
dependencies = [
"libc",
]
[[package]]
name = "anstream"
version = "1.0.0"
@@ -282,6 +291,12 @@ version = "0.23.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ac07cdecf99051d9a5238b80f35af32cdeba5b336e55d957b318b50137e18da5"
[[package]]
name = "bitflags"
version = "1.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
[[package]]
name = "bitflags"
version = "2.13.2"
@@ -318,6 +333,15 @@ dependencies = [
"alloc-stdlib",
]
[[package]]
name = "bs58"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bf88ba1141d185c399bee5288d850d63b8369520c1eafc32a0430b5b6c287bf4"
dependencies = [
"tinyvec",
]
[[package]]
name = "bumpalo"
version = "3.20.3"
@@ -348,12 +372,6 @@ version = "1.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4e7648175b45a9a48536d676f68d918270699102aa8dab5496df06904c914600"
[[package]]
name = "cfg_aliases"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f079e83a288787bcd14a6aea84cee5c87a67c5a3e660c30f557a3d24761b3527"
[[package]]
name = "chacha20"
version = "0.10.2"
@@ -365,6 +383,18 @@ dependencies = [
"rand_core 0.10.1",
]
[[package]]
name = "chrono"
version = "0.4.45"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1aa79e62e7697b8e29b513a68abacf485adcd1fe8284a4316c5ae868e6633327"
dependencies = [
"iana-time-zone",
"num-traits",
"serde",
"windows-link",
]
[[package]]
name = "clap"
version = "4.6.7"
@@ -405,35 +435,6 @@ version = "1.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1c133bc6a41be0d194c306b5506d15e6feeea7b1d6604bd3f8310dfb2ca96486"
[[package]]
name = "cloud-llms"
version = "0.1.0"
dependencies = [
"async-channel",
"axum",
"axum-extra",
"clap",
"ecow",
"enum-map",
"enumset",
"futures",
"headers",
"indexmap",
"mitsein",
"rand 0.10.3",
"reqwest",
"reqwest-sse",
"schemars",
"secrecy",
"serde",
"serde_json",
"tokio",
"tracing",
"tracing-subscriber",
"typed-builder",
"url",
]
[[package]]
name = "cmake"
version = "0.1.58"
@@ -622,8 +623,18 @@ version = "0.21.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9cdf337090841a411e2a7f3deb9187445851f91b309c0c0a29e05f74a00a48c0"
dependencies = [
"darling_core",
"darling_macro",
"darling_core 0.21.3",
"darling_macro 0.21.3",
]
[[package]]
name = "darling"
version = "0.24.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed17f5901b6630b993ca003def43f2f8ef4014fc13b047b57aad617ff32bc2ec"
dependencies = [
"darling_core 0.24.1",
"darling_macro 0.24.1",
]
[[package]]
@@ -639,28 +650,86 @@ dependencies = [
"syn 2.0.119",
]
[[package]]
name = "darling_core"
version = "0.24.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6837e2cf7485aaae18f86181d2f0e9a7ed297a025e220aeabf63fdebd3a2ddff"
dependencies = [
"ident_case",
"proc-macro2",
"quote",
"strsim",
"syn 3.0.6",
]
[[package]]
name = "darling_macro"
version = "0.21.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d38308df82d1080de0afee5d069fa14b0326a88c14f15c5ccda35b4a6c414c81"
dependencies = [
"darling_core",
"darling_core 0.21.3",
"quote",
"syn 2.0.119",
]
[[package]]
name = "darling_macro"
version = "0.24.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2ac7135c3ef02b2f7833bbeb1be5ba7f966dcde8a87c6b87f65a778d71a02785"
dependencies = [
"darling_core 0.24.1",
"quote",
"syn 3.0.6",
]
[[package]]
name = "data-encoding"
version = "2.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4583a4551df46e2792f82ceeac45e850d2e2d5debba0b91f102385cda5b11f06"
[[package]]
name = "defmt"
version = "1.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e2953bfe4f93bbd20cc71198842756f77d161884c99ebbabc41d80231ded88d1"
dependencies = [
"bitflags 1.3.2",
"defmt-macros",
]
[[package]]
name = "defmt-macros"
version = "1.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bad9c72e7ca2137e0dc3813245a0d282fd6daad32fd800af018306a9169b5fe8"
dependencies = [
"defmt-parser",
"proc-macro2",
"quote",
"syn 2.0.119",
]
[[package]]
name = "defmt-parser"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "10d60334b3b2e7c9d91ef8150abfb6fa4c1c39ebbcf4a81c2e346aad939fee3e"
dependencies = [
"thiserror",
]
[[package]]
name = "deranged"
version = "0.5.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c"
dependencies = [
"serde_core",
]
[[package]]
name = "digest"
@@ -770,7 +839,7 @@ version = "0.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4bd536557b58c682b217b8fb199afdff47cd3eff260623f19e77074eb073d63a"
dependencies = [
"darling",
"darling 0.21.3",
"proc-macro2",
"quote",
"syn 2.0.119",
@@ -945,10 +1014,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0"
dependencies = [
"cfg-if",
"js-sys",
"libc",
"wasi",
"wasm-bindgen",
]
[[package]]
@@ -970,11 +1037,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099"
dependencies = [
"cfg-if",
"js-sys",
"libc",
"r-efi 6.0.0",
"rand_core 0.10.1",
"wasm-bindgen",
]
[[package]]
@@ -989,13 +1054,19 @@ dependencies = [
"futures-core",
"futures-sink",
"http",
"indexmap",
"indexmap 2.14.2",
"slab",
"tokio",
"tokio-util",
"tracing",
]
[[package]]
name = "hashbrown"
version = "0.12.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888"
[[package]]
name = "hashbrown"
version = "0.17.1"
@@ -1032,6 +1103,12 @@ version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
[[package]]
name = "hex"
version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
[[package]]
name = "hickory-net"
version = "0.26.3"
@@ -1203,11 +1280,33 @@ dependencies = [
"percent-encoding",
"pin-project-lite",
"socket2",
"system-configuration",
"tokio",
"tower-service",
"tracing",
"windows-registry",
]
[[package]]
name = "iana-time-zone"
version = "0.1.65"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470"
dependencies = [
"android_system_properties",
"core-foundation-sys",
"iana-time-zone-haiku",
"js-sys",
"log",
"wasm-bindgen",
"windows-core",
]
[[package]]
name = "iana-time-zone-haiku"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f"
dependencies = [
"cc",
]
[[package]]
@@ -1320,6 +1419,17 @@ dependencies = [
"icu_properties",
]
[[package]]
name = "indexmap"
version = "1.9.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99"
dependencies = [
"autocfg",
"hashbrown 0.12.3",
"serde",
]
[[package]]
name = "indexmap"
version = "2.14.2"
@@ -1327,7 +1437,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cc4e190f5d26ca7051642629da2c52fc03bde85a03197c99408dcd291734c855"
dependencies = [
"equivalent",
"hashbrown",
"hashbrown 0.17.1",
"serde",
"serde_core",
]
[[package]]
@@ -1367,12 +1479,75 @@ dependencies = [
"either",
]
[[package]]
name = "itertools"
version = "0.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b4baf93f58d4425749ca49a51c50ebab072c5df6994d08fed93541c331481dc"
dependencies = [
"either",
]
[[package]]
name = "itoa"
version = "1.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
[[package]]
name = "jiff"
version = "0.2.38"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b2b005715dcbeb0089a3c0dab99f2ff1cc3b2525323552703d648585d342a383"
dependencies = [
"defmt",
"jiff-core",
"jiff-static",
"jiff-tzdb-platform",
"log",
"portable-atomic",
"portable-atomic-util",
"serde_core",
"windows-link",
]
[[package]]
name = "jiff-core"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5e52fe76043ccecc9005d2305ebaadf7d7fc0cc89ca6baa10a94d6bc68c7128c"
dependencies = [
"defmt",
"log",
]
[[package]]
name = "jiff-static"
version = "0.2.38"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2cc9817253cf7c7ee4684451bd327e88d6f3658014e54a29198625590650695c"
dependencies = [
"jiff-core",
"proc-macro2",
"quote",
"syn 2.0.119",
]
[[package]]
name = "jiff-tzdb"
version = "0.1.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "142bd39932ad231f10513df9ab62661fead8719872150b7ad02a2df79f4e141e"
[[package]]
name = "jiff-tzdb-platform"
version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "875a5a69ac2bab1a891711cf5eccbec1ce0341ea805560dcd90b7a2e925132e8"
dependencies = [
"jiff-tzdb",
]
[[package]]
name = "jni"
version = "0.22.4"
@@ -1443,6 +1618,42 @@ dependencies = [
"wasm-bindgen",
]
[[package]]
name = "katniss-ai"
version = "0.1.0"
dependencies = [
"async-channel",
"axum",
"axum-extra",
"clap",
"ecow",
"enum-map",
"enumset",
"futures",
"headers",
"indexmap 2.14.2",
"itertools 0.15.0",
"jiff",
"mitsein",
"rand 0.10.3",
"reqwest",
"reqwest-sse",
"rustls",
"schemars 1.2.2",
"secrecy",
"serde",
"serde_json",
"serde_with",
"strum",
"tokio",
"tower-http 0.7.1",
"tracing",
"tracing-subscriber",
"typed-builder",
"url",
"webpki-root-certs",
]
[[package]]
name = "lazy_static"
version = "1.5.0"
@@ -1482,12 +1693,6 @@ version = "0.4.34"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6"
[[package]]
name = "lru-slab"
version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4050469837a6ff301cd14c1f8f24f88549e6d548f24f64e2148eb0f72cebc51f"
[[package]]
name = "matchers"
version = "0.2.0"
@@ -1553,9 +1758,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8a49aef75c004fb496a23f175395415ecfe2a630e4c6175ae6f7d9cc1a19332e"
dependencies = [
"either",
"indexmap",
"itertools",
"schemars",
"indexmap 2.14.2",
"itertools 0.14.0",
"schemars 1.2.2",
"serde",
"smallvec",
]
@@ -1705,6 +1910,15 @@ version = "1.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "05c8b63e8d9609db387f0324918f81d68fe27748f084ef092fb35954d0539a85"
[[package]]
name = "portable-atomic-util"
version = "0.2.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "10ab3eb7f3becc3a1cbc4f2c6f20267996cfc1a6467a873763411b136a122715"
dependencies = [
"portable-atomic",
]
[[package]]
name = "potential_utf"
version = "0.1.6"
@@ -1765,63 +1979,6 @@ dependencies = [
"psl-types",
]
[[package]]
name = "quinn"
version = "0.11.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4051e23e9185c255a7e33ef59cdbca87a22d359052eecd22fc6b901fb37d9d11"
dependencies = [
"bytes",
"cfg_aliases",
"pin-project-lite",
"quinn-proto",
"quinn-udp",
"rustc-hash",
"rustls",
"socket2",
"thiserror",
"tokio",
"tracing",
"web-time",
]
[[package]]
name = "quinn-proto"
version = "0.11.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a9746dbde176634f4f2f1faf2404e30a31b2bc1e9cafb5329c95d8177a18c9fc"
dependencies = [
"aws-lc-rs",
"bytes",
"getrandom 0.4.3",
"lru-slab",
"rand 0.10.3",
"rand_pcg",
"ring",
"rustc-hash",
"rustls",
"rustls-pki-types",
"slab",
"thiserror",
"tinyvec",
"tracing",
"web-time",
]
[[package]]
name = "quinn-udp"
version = "0.5.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "35a133f956daabe89a61a685c2649f13d82d5aa4bd5d12d1277e1072a21c0694"
dependencies = [
"cfg_aliases",
"libc",
"once_cell",
"socket2",
"tracing",
"windows-sys 0.61.2",
]
[[package]]
name = "quote"
version = "1.0.47"
@@ -1889,22 +2046,13 @@ version = "0.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69"
[[package]]
name = "rand_pcg"
version = "0.10.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "caa0f4137e1c0a72f4c651489402276c8e8e1cf081f3b0ba156d2cbeef09e86a"
dependencies = [
"rand_core 0.10.1",
]
[[package]]
name = "redox_syscall"
version = "0.5.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d"
dependencies = [
"bitflags",
"bitflags 2.13.2",
]
[[package]]
@@ -1954,7 +2102,6 @@ dependencies = [
"bytes",
"cookie",
"cookie_store",
"encoding_rs",
"futures-core",
"futures-util",
"h2",
@@ -1967,12 +2114,10 @@ dependencies = [
"hyper-util",
"js-sys",
"log",
"mime",
"mime_guess",
"once_cell",
"percent-encoding",
"pin-project-lite",
"quinn",
"rustls",
"rustls-pki-types",
"rustls-platform-verifier",
@@ -1984,7 +2129,7 @@ dependencies = [
"tokio-rustls",
"tokio-util",
"tower",
"tower-http",
"tower-http 0.6.11",
"tower-service",
"url",
"wasm-bindgen",
@@ -2026,12 +2171,6 @@ dependencies = [
"windows-sys 0.52.0",
]
[[package]]
name = "rustc-hash"
version = "2.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6b1e7f9a428571be2dc5bc0505c13fb6bf936822b894ec87abf8a08a4e51742d"
[[package]]
name = "rustc_version"
version = "0.4.1"
@@ -2048,6 +2187,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0d41d731c7d2f962d1ccc364cec258de3c0e93b38c2fb3ba97ac74513048d634"
dependencies = [
"aws-lc-rs",
"log",
"once_cell",
"rustls-pki-types",
"rustls-webpki",
@@ -2073,7 +2213,6 @@ version = "1.15.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2f4925028c7eb5d1fcdaf196971378ed9d2c1c4efc7dc5d011256f76c99c0a96"
dependencies = [
"web-time",
"zeroize",
]
@@ -2146,6 +2285,18 @@ dependencies = [
"windows-sys 0.61.2",
]
[[package]]
name = "schemars"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4cd191f9397d57d581cddd31014772520aa448f65ef991055d7f61582c65165f"
dependencies = [
"dyn-clone",
"ref-cast",
"serde",
"serde_json",
]
[[package]]
name = "schemars"
version = "1.2.2"
@@ -2153,7 +2304,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "687274d293b6cdc6e73e0fee520bf2049650090d7164f87672d212a3c530cf4a"
dependencies = [
"dyn-clone",
"indexmap",
"indexmap 2.14.2",
"ref-cast",
"schemars_derive",
"serde",
@@ -2194,7 +2345,7 @@ version = "3.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d"
dependencies = [
"bitflags",
"bitflags 2.13.2",
"core-foundation 0.10.1",
"core-foundation-sys",
"libc",
@@ -2265,7 +2416,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b2f2d7ff8a2140333718bb329f5c40fc5f0865b84c426183ce14c97d2ab8154f"
dependencies = [
"form_urlencoded",
"indexmap",
"indexmap 2.14.2",
"itoa",
"ryu",
"serde_core",
@@ -2307,6 +2458,39 @@ dependencies = [
"serde",
]
[[package]]
name = "serde_with"
version = "3.24.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "df9adc193c780ef8f159aee8b61e2d5801aaa555e6eb0947fe45530ec506296f"
dependencies = [
"base64 0.23.1",
"bs58",
"chrono",
"hex",
"indexmap 1.9.3",
"indexmap 2.14.2",
"jiff",
"schemars 0.9.0",
"schemars 1.2.2",
"serde_core",
"serde_json",
"serde_with_macros",
"time",
]
[[package]]
name = "serde_with_macros"
version = "3.24.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3e17bbc68e28663bbbb90df47e058aa7eda4fb445b89fe70457bb94fbccf6e49"
dependencies = [
"darling 0.24.1",
"proc-macro2",
"quote",
"syn 3.0.6",
]
[[package]]
name = "sha1"
version = "0.10.7"
@@ -2395,6 +2579,27 @@ version = "0.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
[[package]]
name = "strum"
version = "0.28.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9628de9b8791db39ceda2b119bbe13134770b56c138ec1d3af810d045c04f9bd"
dependencies = [
"strum_macros",
]
[[package]]
name = "strum_macros"
version = "0.28.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ab85eea0270ee17587ed4156089e10b9e6880ee688791d45a905f5b1ca36f664"
dependencies = [
"heck",
"proc-macro2",
"quote",
"syn 2.0.119",
]
[[package]]
name = "subtle"
version = "2.6.1"
@@ -2449,7 +2654,7 @@ version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a13f3d0daba03132c0aa9767f98351b3488edc2c100cda2d2ec2b04f3d8d3c8b"
dependencies = [
"bitflags",
"bitflags 2.13.2",
"core-foundation 0.9.4",
"system-configuration-sys",
]
@@ -2641,7 +2846,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4cfcf7e2740e6fc6d4d688b4ef00650406bb94adf4731e43c096c3a19fe40840"
dependencies = [
"async-compression",
"bitflags",
"bitflags 2.13.2",
"bytes",
"futures-core",
"futures-util",
@@ -2657,6 +2862,21 @@ dependencies = [
"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]]
name = "tower-layer"
version = "0.3.3"
@@ -2957,16 +3177,6 @@ dependencies = [
"wasm-bindgen",
]
[[package]]
name = "web-time"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb"
dependencies = [
"js-sys",
"wasm-bindgen",
]
[[package]]
name = "webpki-root-certs"
version = "1.0.9"
@@ -2991,6 +3201,41 @@ dependencies = [
"windows-sys 0.61.2",
]
[[package]]
name = "windows-core"
version = "0.62.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb"
dependencies = [
"windows-implement",
"windows-interface",
"windows-link",
"windows-result",
"windows-strings",
]
[[package]]
name = "windows-implement"
version = "0.60.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.119",
]
[[package]]
name = "windows-interface"
version = "0.59.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.119",
]
[[package]]
name = "windows-link"
version = "0.2.1"

View File

@@ -1,5 +1,5 @@
[package]
name = "cloud-llms"
name = "katniss-ai"
version = "0.1.0"
edition = "2024"
license = "Unlicense"
@@ -15,16 +15,23 @@ enumset = { version = "1.1.14", features = ["serde"] }
futures = "0.3.34"
headers = "0.4.2"
indexmap = "2.14.2"
itertools = "0.15.0"
jiff = { version = "0.2.38", features = ["serde"] }
mitsein = { version = "0.9.0", features = ["indexmap"] }
rand = "0.10.3"
reqwest = { version = "0.13.5", features = ["brotli", "cookies", "deflate", "form", "gzip", "hickory-dns", "json", "multipart", "query", "socks", "stream", "zstd"] }
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-sse = "0.2.0"
rustls = "0.23.45"
schemars = "1.2.2"
secrecy = { version = "0.10.3", features = ["serde"] }
serde = { version = "1.0.229", features = ["derive"] }
serde_json = "1.0.151"
serde_with = "3.24.0"
strum = { version = "0.28.0", features = ["derive"] }
tokio = { version = "1.53.1", features = ["macros", "rt-multi-thread"] }
tower-http = { version = "0.7.1", features = ["cors"] }
tracing = "0.1.44"
tracing-subscriber = { version = "0.3.23", features = ["env-filter"] }
typed-builder = "0.23.2"
url = "2.5.8"
webpki-root-certs = "1.0.9"

View File

@@ -16,7 +16,7 @@ RUN \
--mount=type=cache,target=/usr/local/cargo/registry/ \
cd /root/app && \
cargo build --release && \
cp target/release/cloud-llms /root/program
cp target/release/katniss-ai /root/program
FROM scratch AS runner

99
src/lib.rs Normal file
View File

@@ -0,0 +1,99 @@
use clap::Parser;
use secrecy::SecretString;
use std::{convert::Infallible, net::IpAddr, str::FromStr};
use tracing_subscriber::EnvFilter;
pub mod openai;
pub mod server;
#[derive(Debug, Clone)]
pub struct CloudflareCredentials {
pub account_id: String,
pub api_key: SecretString,
}
impl FromStr for CloudflareCredentials {
type Err = Infallible; // TODO
fn from_str(s: &str) -> Result<Self, Self::Err> {
let (account_id, api_key) = s.split_once(":").expect("TODO");
let account_id = account_id.into();
let api_key = api_key.into();
Ok(Self {
account_id,
api_key,
})
}
}
#[derive(Debug, Clone, Parser)]
pub struct AppArgs {
#[arg(long, env, default_value_t = [127, 0, 0, 1].into())]
pub ip: IpAddr,
#[arg(long, env, default_value_t = 3126)]
pub port: u16,
#[arg(long)]
pub cloudflare_workers_ai_credentials: Vec<CloudflareCredentials>,
#[arg(long)]
pub cohere_api_key: Vec<SecretString>,
#[arg(long)]
pub google_ai_studio_api_key: Vec<SecretString>,
#[arg(long)]
pub groq_api_key: Vec<SecretString>,
#[arg(long)]
pub hugging_face_api_key: Vec<SecretString>,
#[arg(long)]
pub kilo_api_key: Vec<SecretString>,
#[arg(long)]
pub liquid_ai_api_key: Vec<SecretString>,
#[arg(long)]
pub mistral_api_key: Vec<SecretString>,
#[arg(long)]
pub novita_ai_api_key: Vec<SecretString>,
#[arg(long)]
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)]
pub openrouter_api_key: Vec<SecretString>,
#[arg(long)]
pub poolside_api_key: Vec<SecretString>,
#[arg(long)]
pub vercel_ai_gateway_api_key: Vec<SecretString>,
#[arg(long)]
pub z_ai_api_key: Vec<SecretString>,
}
#[derive(Parser)]
pub struct Args {
#[arg(
long = "tracing-directives",
env = "RUST_LOG",
default_value = "warn,katniss_ai=debug"
)]
pub env_filter: EnvFilter,
#[command(flatten)]
pub app_args: AppArgs,
}

View File

@@ -1,55 +1,11 @@
use std::net::{IpAddr, SocketAddr};
use clap::Parser;
use futures::StreamExt as _;
use reqwest_sse::EventSource;
use secrecy::SecretString;
use tokio::{join, pin, task::LocalSet};
use tracing_subscriber::{EnvFilter, fmt::format::FmtSpan};
mod openai;
mod server;
#[derive(Parser, Debug)]
struct AppArgs {
#[arg(long, env, default_value_t = [127, 0, 0, 1].into())]
ip: IpAddr,
#[arg(long, env, default_value_t = 3126)]
port: u16,
#[arg(long)]
cloudflare_workers_ai_api_key: Vec<SecretString>,
#[arg(long)]
google_ai_studio_api_key: Vec<SecretString>,
#[arg(long)]
groq_api_key: Vec<SecretString>,
#[arg(long)]
hugging_face_api_key: Vec<SecretString>,
#[arg(long)]
mistral_api_key: Vec<SecretString>,
#[arg(long)]
openrouter_api_key: Vec<SecretString>,
}
#[derive(Parser)]
struct Args {
#[arg(long = "tracing-directives", env = "RUST_LOG", default_value_t = Default::default())]
env_filter: EnvFilter,
#[command(flatten)]
app_args: AppArgs,
}
use std::net::SocketAddr;
use tracing_subscriber::fmt::format::FmtSpan;
#[tokio::main]
async fn main() {
let args = Parser::parse();
let Args {
let katniss_ai::Args {
env_filter,
app_args,
} = args;
@@ -61,52 +17,20 @@ async fn main() {
.init();
tracing::debug!(?app_args, "using");
rustls::crypto::aws_lc_rs::default_provider()
.install_default()
.unwrap();
let addr = SocketAddr::new(app_args.ip, app_args.port);
let (tx, rx) = async_channel::unbounded();
let app = server::create_app(
server::AppState::builder()
let app = katniss_ai::server::create_app(
katniss_ai::server::AppState::builder()
.app_args(app_args.into())
.stream_requester(tx)
.build(),
);
let listener = tokio::net::TcpListener::bind(addr).await.expect("TODO");
let stream_responses = async move {
let local_set = LocalSet::new();
local_set
.run_until(async {
pin!(rx);
while let Some((response, sse_sink)) = rx.next().await {
local_set.spawn_local(async move {
match response.events().await {
Ok(mut events) => {
while let Some(event_result) = events.next().await {
let message = event_result
.map(|event| {
axum::response::sse::Event::default().data(event.data)
})
.map_err(Into::into);
let _ = sse_sink.send(message).await;
}
}
Err(event_source_error) => {
let _ = sse_sink.send(Err(event_source_error.into())).await;
}
};
});
}
})
.await;
};
join!(stream_responses, async move {
tracing::info!(?addr, "listening");
axum::serve(listener, app).await.expect("TODO");
});
tracing::info!(?addr, "listening");
axum::serve(listener, app).await.expect("TODO");
}

View File

@@ -1,5 +1,7 @@
use serde::{Deserialize, Serialize};
use serde_with::{DeserializeFromStr, SerializeDisplay};
use std::collections::HashMap;
use strum::{EnumString, VariantArray};
// ============================================================================
// Request: Create Chat Completion
@@ -22,7 +24,7 @@ pub struct CreateChatCompletionRequest<Model> {
/// Number between -2.0 and 2.0. Positive values penalize new tokens based on their existing
/// frequency in the text so far, decreasing the model's likelihood to repeat the same line verbatim.
#[serde(skip_serializing_if = "Option::is_none")]
pub frequency_penalty: Option<f32>,
pub frequency_penalty: Option<f64>,
/// Deprecated in favor of `tool_choice`. Controls which (if any) function is called by the model.
#[serde(skip_serializing_if = "Option::is_none")]
@@ -35,7 +37,7 @@ pub struct CreateChatCompletionRequest<Model> {
/// Modify the likelihood of specified tokens appearing in the completion. Accepts a JSON object
/// that maps tokens (specified by their token ID in the tokenizer) to an associated bias value from -100 to 100.
#[serde(skip_serializing_if = "Option::is_none")]
pub logit_bias: Option<HashMap<String, f32>>,
pub logit_bias: Option<HashMap<String, f64>>,
/// Whether to return log probabilities of the output tokens or not. If true, returns the log
/// probabilities of each output token returned in the `content` of `message`.
@@ -81,7 +83,7 @@ pub struct CreateChatCompletionRequest<Model> {
/// Number between -2.0 and 2.0. Positive values penalize new tokens based on whether they appear in
/// the text so far, increasing the model's likelihood to talk about new topics.
#[serde(skip_serializing_if = "Option::is_none")]
pub presence_penalty: Option<f32>,
pub presence_penalty: Option<f64>,
/// Used by OpenAI to cache responses for similar requests to optimize your cache hit rates. Replaces the `user` field.
#[serde(skip_serializing_if = "Option::is_none")]
@@ -135,7 +137,7 @@ pub struct CreateChatCompletionRequest<Model> {
/// What sampling temperature to use, between 0 and 2. Higher values like 0.8 will make the output more random.
#[serde(skip_serializing_if = "Option::is_none")]
pub temperature: Option<f32>,
pub temperature: Option<f64>,
/// Controls which (if any) tool is called by the model.
#[serde(skip_serializing_if = "Option::is_none")]
@@ -153,7 +155,7 @@ pub struct CreateChatCompletionRequest<Model> {
/// An alternative to sampling with temperature, called nucleus sampling, where the model considers the
/// results of the tokens with top_p probability mass.
#[serde(skip_serializing_if = "Option::is_none")]
pub top_p: Option<f32>,
pub top_p: Option<f64>,
/// Deprecated in favor of `safety_identifier` and `prompt_cache_key`. A stable identifier for your end-users.
#[serde(skip_serializing_if = "Option::is_none")]
@@ -916,8 +918,10 @@ pub enum PromptCacheRetention {
}
/// Constrains reasoning effort for reasoning models.
#[derive(Debug, Clone, Deserialize, Serialize)]
#[serde(rename_all = "lowercase")]
#[derive(
Clone, Debug, EnumString, DeserializeFromStr, strum::Display, SerializeDisplay, VariantArray,
)]
#[strum(serialize_all = "lowercase")]
pub enum ReasoningEffort {
/// Disable reasoning.
None,
@@ -930,7 +934,8 @@ pub enum ReasoningEffort {
/// High reasoning effort.
High,
/// Extra high reasoning effort.
Xhigh,
#[strum(serialize = "xhigh")]
ExtraHigh,
/// Maximum reasoning effort.
Max,
}
@@ -1087,7 +1092,7 @@ pub struct ApproximateLocation {
/// Represents a chat completion response returned by model, based on the provided input.
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct ChatCompletion {
pub struct ChatCompletion<Model> {
/// A unique identifier for the chat completion.
pub id: String,
@@ -1098,7 +1103,7 @@ pub struct ChatCompletion {
pub created: i64,
/// The model used for the chat completion.
pub model: String,
pub model: Model,
/// The object type, which is always `chat.completion`.
pub object: String,

308
src/openai/decisions.rs Normal file
View File

@@ -0,0 +1,308 @@
use serde::{Deserialize, Serialize};
/// Represents the main request to the Decisions API
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct DecisionRequest<Model> {
/// The model that evaluates the request. Currently, only `gpt-6-luna` is supported.
pub model: Model,
/// The text or images to evaluate for every question.
/// Can be a simple string or an array of user messages containing text and images.
pub input: DecisionInput,
/// Array of questions to evaluate against the input
#[serde(rename = "questions")]
pub questions: Vec<DecisionQuestion>,
}
/// Represents the input to be evaluated, which can be either a string or an array of messages
#[derive(Debug, Clone, Deserialize, Serialize)]
#[serde(untagged)]
pub enum DecisionInput {
/// Simple text input
Text(String),
/// Array of user messages containing text and images
Messages(Vec<DecisionInputMessage>),
}
/// Represents a message in the input, typically from a user
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct DecisionInputMessage {
/// The role of the message sender (always "user" for decisions)
pub role: String,
/// The content of the message, which can be a string or an array of parts
#[serde(flatten)]
pub content: DecisionInputContent,
}
/// Represents the content of a message, which can be a string or an array of parts
#[derive(Debug, Clone, Deserialize, Serialize)]
#[serde(untagged)]
pub enum DecisionInputContent {
/// Simple text content
Text(String),
/// Array of text and image parts
Parts(Vec<DecisionInputPart>),
}
/// Represents a part of a message, which can be text or an image
#[derive(Debug, Clone, Deserialize, Serialize)]
#[serde(tag = "type")]
pub enum DecisionInputPart {
/// Text part of a message
#[serde(rename = "input_text")]
Text {
/// The text content
text: String,
},
/// Image part of a message
#[serde(rename = "input_image")]
Image {
/// The image URL (base64 data URL or HTTP(S) URL)
image_url: String,
/// Optional detail level for the image
#[serde(skip_serializing_if = "Option::is_none")]
detail: Option<ImageDetail>,
},
}
/// Represents the detail level for an image
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
#[serde(rename_all = "lowercase")]
pub enum ImageDetail {
/// Low detail
Low,
/// High detail
High,
/// Auto detail (default)
Auto,
/// Original detail
Original,
}
/// Represents a question in the Decisions API
#[derive(Debug, Clone, Deserialize, Serialize)]
#[serde(tag = "type", rename_all = "lowercase")]
pub enum DecisionQuestion {
/// A predicate question checks a condition
Predicate(PredicateQuestion),
/// A choice question selects from fixed options
Choice(ChoiceQuestion),
/// A score question evaluates against ordered levels
Score(ScoreQuestion),
}
/// A predicate question checks a condition and returns a probability
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct PredicateQuestion {
/// Optional name to identify the question in the response
#[serde(skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
/// Instructions for evaluating the condition
pub instructions: String,
}
/// A choice question selects from fixed options
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct ChoiceQuestion {
/// Optional name to identify the question in the response
#[serde(skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
/// Instructions for making the choice
pub instructions: String,
/// Array of available choices
pub choices: Vec<ChoiceOption>,
}
/// Represents a single choice option
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct ChoiceOption {
/// The value of the choice
pub value: Choice,
/// Optional description of when this choice applies
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
}
/// A score question evaluates against ordered levels
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct ScoreQuestion {
/// Optional name to identify the question in the response
#[serde(skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
/// Instructions for scoring
pub instructions: String,
/// Array of ordered levels for scoring
pub levels: Vec<ScoreLevel>,
}
/// Represents a single score level
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct ScoreLevel {
/// The label for this level
pub label: String,
/// Optional description of this level
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
}
/// Represents the response from the Decisions API
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct DecisionResponse<Model> {
/// Array of answers to the questions
pub answers: Vec<DecisionAnswer>,
/// The model used for the decision
pub model: Model,
/// Token usage information
pub usage: TokenUsage,
}
/// Represents an answer to a question
#[derive(Debug, Clone, Deserialize, Serialize)]
#[serde(tag = "type", rename_all = "lowercase")]
pub enum DecisionAnswer {
/// Answer to a predicate question
Predicate(PredicateAnswer),
/// Answer to a choice question
Choice(ChoiceAnswer),
/// Answer to a score question
Score(ScoreAnswer),
/// The model refused to answer this question
Refusal(RefusalAnswer),
}
/// Answer to a predicate question
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct PredicateAnswer {
/// Optional name of the question
#[serde(skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
/// Probability that the condition is true (0.0 to 1.0)
pub probability: f64,
}
/// Choice values are typed: a string and a boolean with the same text are distinct
#[derive(Debug, Clone, Deserialize, Serialize)]
#[serde(untagged)]
pub enum Choice {
/// String
String(String),
/// Boolean
Boolean(bool),
}
/// Answer to a choice question
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct ChoiceAnswer {
/// Optional name of the question
#[serde(skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
/// The selected choice value
pub choice: Choice,
/// Confidence score (0.0 to 1.0)
pub confidence: f64,
/// Probabilities for each choice option
pub probabilities: Vec<ChoiceProbability>,
}
/// Represents the probability distribution for a choice answer
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct ChoiceProbability {
/// The choice value
pub value: Choice,
/// Probability of this choice (0.0 to 1.0)
pub probability: f64,
}
/// Answer to a score question
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct ScoreAnswer {
/// Optional name of the question
#[serde(skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
/// The computed score (can be between level indices)
pub score: f64,
/// Confidence score (0.0 to 1.0)
pub confidence: f64,
/// Probabilities for each score level
pub probabilities: Vec<ScoreProbability>,
}
/// Represents the probability distribution for a score answer
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct ScoreProbability {
/// The level index
pub value: i32,
/// The level label
pub label: String,
/// Probability of this level (0.0 to 1.0)
pub probability: f64,
}
/// Answer indicating the model refused to answer a question
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct RefusalAnswer {
/// Optional name of the question
#[serde(skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
}
/// Represents token usage information
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct TokenUsage {
/// Total number of input tokens
pub input_tokens: i32,
/// Details about input tokens
pub input_tokens_details: TokenUsageDetails,
/// Total number of output tokens
pub output_tokens: i32,
/// Details about output tokens
pub output_tokens_details: OutputTokenDetails,
/// Total number of tokens (input + output)
pub total_tokens: i32,
}
/// Represents details about input tokens
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct TokenUsageDetails {
/// Number of tokens written to cache
pub cache_write_tokens: i32,
/// Number of tokens read from cache
pub cached_tokens: i32,
}
/// Represents details about output tokens
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct OutputTokenDetails {
/// Number of reasoning tokens used
pub reasoning_tokens: i32,
}
/// Optional safety identifier provided by the caller
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct SafetyIdentifier(pub String);

113
src/openai/embeddings.rs Normal file
View File

@@ -0,0 +1,113 @@
use serde::{Deserialize, Serialize};
/// Represents the possible input types for embedding requests
#[derive(Debug, Clone, Deserialize, Serialize)]
#[serde(untagged)]
pub enum EmbeddingInput {
/// A single string input
String(String),
/// An array of string inputs
StringArray(Vec<String>),
/// An array of token IDs (numbers)
NumberArray(Vec<i32>),
/// An array of arrays of token IDs (numbers)
NumberMatrix(Vec<Vec<i32>>),
}
/// Represents the encoding format for embeddings
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
#[serde(rename_all = "lowercase")]
pub enum EncodingFormat {
/// Return embeddings as an array of floats
Float,
/// Return embeddings as a base64-encoded string
Base64,
}
/// Request body for creating embeddings
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct CreateEmbeddingRequest<Model> {
/// Input text to embed
pub input: EmbeddingInput,
/// ID of the model to use for generating embeddings
pub model: Model,
/// Optional number of dimensions for the output embeddings
#[serde(skip_serializing_if = "Option::is_none")]
pub dimensions: Option<u32>,
/// Format to return the embeddings in
#[serde(skip_serializing_if = "Option::is_none")]
pub encoding_format: Option<EncodingFormat>,
/// Optional unique identifier for tracking end-user
#[serde(skip_serializing_if = "Option::is_none")]
pub user: Option<String>,
}
/// Embedding object type
#[derive(Debug, Clone, Deserialize, Serialize)]
#[serde(rename_all = "lowercase")]
pub enum EmbeddingObject {
/// Embedding
Embedding,
}
/// Represents a single embedding in the response
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct Embedding {
/// The embedding vector, returned as an array of floats or a base64 string
/// depending on the encoding_format parameter
pub embedding: EmbeddingOutput,
/// The index of this embedding in the list of embeddings
pub index: u32,
/// The object type, which is always "embedding"
pub object: EmbeddingObject,
}
/// Represents the possible output formats for embeddings
#[derive(Debug, Clone, Deserialize, Serialize)]
#[serde(untagged)]
pub enum EmbeddingOutput {
/// Array of floats when encoding_format is "float"
FloatArray(Vec<f64>),
/// Base64-encoded string when encoding_format is "base64"
Base64String(String),
}
/// Usage information for an embeddings request
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct Usage {
/// Number of tokens used by the prompt
pub prompt_tokens: u32,
/// Total number of tokens used by the request
pub total_tokens: u32,
}
/// List object type
#[derive(Debug, Clone, Deserialize, Serialize)]
#[serde(rename_all = "lowercase")]
pub enum ListObject {
/// List
List,
}
/// Response from the embeddings API
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct CreateEmbeddingResponse<Model> {
/// List of generated embeddings
pub data: Vec<Embedding>,
/// The name of the model used to generate the embeddings
pub model: Model,
/// The object type, which is always "list"
pub object: ListObject,
/// Usage information for the request
pub usage: Usage,
}

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 +1,6 @@
pub mod chat;
pub mod decisions;
pub mod embeddings;
pub mod extraction;
pub mod models;
pub mod system_one;

43
src/openai/models.rs Normal file
View File

@@ -0,0 +1,43 @@
use serde::{Deserialize, Serialize};
/// The object type, which is always "model".
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ModelObject {
Model,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Model<Id> {
/// The model identifier, which can be referenced in the API endpoints.
pub id: Id,
/// The Unix timestamp (in seconds) when the model was created.
pub created: i64,
/// The object type, which is always "model".
pub object: ModelObject,
/// The organization that owns the model.
pub owned_by: String,
/// The date when the model will shut down, or null if not announced.
#[serde(skip_serializing_if = "Option::is_none")]
pub shutdown_date: Option<String>,
}
/// The object type, which is always "list".
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ListObject {
List,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ListModelsResponse<Id> {
/// The list of currently available models.
pub data: Vec<Model<Id>>,
/// The object type, which is always "list".
pub object: ListObject,
}

233
src/openai/system_one.rs Normal file
View File

@@ -0,0 +1,233 @@
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
/// The top-level request structure for the system one endpoint.
///
/// This represents the complete request body that can be sent to the API.
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct SystemOneRequest<Model> {
/// The model that handles the request.
pub model: Model,
/// The content to evaluate. Can be a string, object, or array.
///
/// For text content, use a plain string. For structured data (like chat logs or application state),
/// use an object or array.
pub state: State,
/// Optional file inputs (images, audio, etc.).
///
/// Array of data URLs for input files.
///
/// For images: data:image/...;base64,...
/// For audio: data:audio/...;base64,...
#[serde(skip_serializing_if = "Option::is_none")]
pub files: Option<Vec<String>>,
/// Alternative name for file inputs (alias for `files`).
#[serde(skip_serializing_if = "Option::is_none")]
pub images: Option<Vec<String>>,
/// A map of typed questions to evaluate against the state.
///
/// Each key is a question ID you choose, and the value is a [Question] object.
/// The answers will be returned under the same keys. The question ID is not sent to the model.
pub questions: HashMap<String, Question>,
}
/// Represents the possible state formats that can be evaluated.
///
/// This is a convenience enum that wraps the different possible state formats:
/// - Plain text (String)
/// - Structured data (Object/Array)
/// - Null (for models that support it)
#[derive(Debug, Clone, Deserialize, Serialize)]
#[serde(untagged)]
pub enum State {
/// Plain text state
Text(String),
/// Structured state (object or array)
Structured(serde_json::Value),
/// Null state (for models that support it)
Null,
}
/// The main question type enum that represents all possible question types.
///
/// Each variant corresponds to a different question type (Noul, Choice, Score)
/// and contains the appropriate fields for that type.
#[derive(Debug, Clone, Deserialize, Serialize)]
#[serde(tag = "type", rename_all = "lowercase")]
pub enum Question {
/// Yes/no question type
Noul(NoulQuestion),
/// Multiple choice question type
Choice(ChoiceQuestion),
/// Scoring question type
Score(ScoreQuestion),
}
/// Represents a yes/no question (Noul = "no/yes/uncertain/likely" type)
///
/// Returns the probability that the answer is yes (value near 1) or no (value near 0).
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct NoulQuestion {
/// The yes/no question to evaluate.
///
/// Can be a plain string, or a structured object containing the question and referenced data.
pub instructions: QuestionInstructions,
/// Optional descriptions of what a yes and no mean.
///
/// Provides additional context for interpreting the yes/no answer.
#[serde(skip_serializing_if = "Option::is_none")]
pub criteria: Option<NoulCriteria>,
}
/// Criteria for a Noul question, describing what true/false mean.
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct NoulCriteria {
/// What a yes (value near 1) means.
pub r#true: QuestionInstructions,
/// What a no (value near 0) means.
pub r#false: QuestionInstructions,
}
/// Represents a multiple choice question.
///
/// Returns the chosen option and the full probability distribution.
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct ChoiceQuestion {
/// What the model should decide.
///
/// Can be a plain string, or a structured object containing the question and referenced data.
pub instructions: QuestionInstructions,
/// A map of options to their descriptions.
///
/// Each key is an option you define, and the value is its description.
/// Use null for options that don't need additional description.
/// Maximum of 255 options per Choice question.
pub criteria: HashMap<String, Option<QuestionInstructions>>,
}
/// Represents a scoring question.
///
/// Returns a probability-weighted value across the defined levels.
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct ScoreQuestion {
/// What the model should rate.
///
/// Can be a plain string, or a structured object containing the question and referenced data.
pub instructions: QuestionInstructions,
/// An ordered array of level descriptions.
///
/// Must have at least 2 levels, maximum of 10. The first element is the lowest level,
/// the last is the highest.
pub criteria: Vec<QuestionInstructions>,
}
/// Represents the possible formats for question instructions and criteria.
///
/// This is a convenience enum that wraps different possible formats:
/// - Plain text (String)
/// - Structured data (Object/Array)
#[derive(Debug, Clone, Deserialize, Serialize)]
#[serde(untagged)]
pub enum QuestionInstructions {
/// Plain text instructions
Text(String),
/// Structured instructions (object or array)
Structured(serde_json::Value),
}
/// The top-level response structure from the system one endpoint.
///
/// This represents the complete response body returned by the API.
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct SystemOneResponse<Model> {
/// The model that performed the evaluation.
pub model: Model,
/// A map of answers, keyed by the same IDs used in the request questions.
pub answers: HashMap<String, Answer>,
/// Token usage information for the request.
pub usage: Usage,
}
/// Represents the possible answer types that can be returned.
///
/// Each variant corresponds to a different question type (Noul, Choice, Score)
/// and contains the appropriate fields for that type.
#[derive(Debug, Clone, Deserialize, Serialize)]
#[serde(tag = "type", rename_all = "lowercase")]
pub enum Answer {
/// Answer to a Noul question
Noul(NoulAnswer),
/// Answer to a Choice question
Choice(ChoiceAnswer),
/// Answer to a Score question
Score(ScoreAnswer),
}
/// Answer to a Noul question (yes/no question).
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct NoulAnswer {
/// The yes/no answer on a scale from 0 (no) to 1 (yes).
pub noul: f64,
}
/// Answer to a Choice question (multiple choice).
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct ChoiceAnswer {
/// The option with the highest probability.
pub choice: String,
/// The probability of each option, they sum to 1.
pub probabilities: HashMap<String, f64>,
/// How certain the model is (0 to 1).
///
/// 0 means all options are equally likely, 1 means maximum certainty.
pub confidence: f64,
}
/// Answer to a Score question.
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct ScoreAnswer {
/// The expected level index, weighted by probability.
///
/// Can be between two levels (e.g., 1.5 would be halfway between level 1 and 2).
pub score: f64,
/// The description of each level index.
pub legend: HashMap<String, String>,
/// The probability of each level index, they sum to 1.
pub probabilities: HashMap<String, f64>,
/// How certain the model is (0 to 1).
///
/// Derived from the probabilities distribution.
pub confidence: f64,
}
/// Token usage information for the request.
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct Usage {
/// Number of input tokens (prompt tokens for all questions).
pub input_tokens: u32,
/// Number of output tokens (always 0 for this API).
pub output_tokens: u32,
}

View File

@@ -1,450 +0,0 @@
use crate::AppArgs;
use crate::openai;
use axum::Json;
use axum::Router;
use axum::extract::State;
use axum::http::StatusCode;
use axum::response::IntoResponse;
use axum::response::Response;
use axum::response::Sse;
use axum_extra::routing::RouterExt;
use axum_extra::routing::TypedPath;
use ecow::EcoString;
use headers::Header;
use headers::authorization::Bearer;
use mitsein::vec1::Vec1;
use rand::Rng;
use rand::RngExt;
use secrecy::ExposeSecret;
use secrecy::SecretString;
use serde::Deserialize;
use serde::Serialize;
use std::collections::VecDeque;
use std::sync::Arc;
use typed_builder::TypedBuilder;
use url::Url;
type SSEEventResult = Result<axum::response::sse::Event, Box<dyn std::error::Error + Sync + Send>>;
type StreamRequest = (reqwest::Response, async_channel::Sender<SSEEventResult>);
type StreamRequester = async_channel::Sender<StreamRequest>;
#[derive(Debug, Clone, TypedBuilder)]
pub struct AppState {
app_args: Arc<AppArgs>,
stream_requester: StreamRequester,
}
#[derive(Deserialize, TypedPath)]
#[typed_path("/v1/chat/completions")]
struct V1ChatCompletionsPath;
#[derive(Debug, Clone)]
enum Provider {
CloudflareWorkersAi,
GoogleAiStudio,
Groq,
HuggingFace,
Mistral,
OpenRouter,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
#[serde(rename_all = "lowercase")]
enum Shortcut {
XS,
S,
M,
L,
XL,
}
impl Shortcut {
fn suggestions(&self) -> Vec<(Provider, EcoString)> {
use Provider as P;
use Shortcut as S;
match self {
S::XS => vec![
(P::GoogleAiStudio, "gemini-3.5-flash-lite".into()),
(P::OpenRouter, "poolside/laguna-xs-2.1:free".into()),
(P::OpenRouter, "nex-agi/nex-n2.5-mini:free".into()),
(P::OpenRouter, "cohere/north-mini-code:free".into()),
(P::OpenRouter, "nvidia/nemotron-3.5-lightning:free".into()),
(P::OpenRouter, "google/gemma-4-26b-a4b-it:free".into()),
(P::OpenRouter, "poolside/laguna-xs.2:free".into()),
(
P::OpenRouter,
"nvidia/nemotron-3-nano-omni-30b-a3b-reasoning:free".into(),
),
(P::CloudflareWorkersAi, "@cf/openai/gpt-oss-20b".into()),
(P::OpenRouter, "openai/gpt-oss-20b:free".into()),
(P::Groq, "openai/gpt-oss-20b".into()),
(P::OpenRouter, "liquid/lfm-2.5-2.6b:free".into()),
(P::Groq, "groq/compound-mini".into()),
(P::OpenRouter, "openrouter/free".into()),
],
S::S => vec![
(P::CloudflareWorkersAi, "@cf/qwen/qwen3.8-27b".into()),
(P::OpenRouter, "qwen/qwen3.8-27b:free".into()),
(P::Groq, "qwen/qwen3.8-27b".into()),
(P::HuggingFace, "Qwen/Qwen3.8-27B".into()),
(P::HuggingFace, "meta-models/Muse-Glimmer-30B".into()),
(P::OpenRouter, "google/gemma-4-31b-it:free".into()),
(P::Mistral, "mistral-small-2603".into()),
(P::Groq, "groq/compound".into()),
(P::OpenRouter, "openrouter/fusion".into()),
],
S::M => vec![
(P::GoogleAiStudio, "gemini-3.8-flash".into()),
(P::GoogleAiStudio, "gemini-3.7-flash".into()),
(P::Mistral, "mistral-medium-3.5".into()),
(P::OpenRouter, "inclusionai/ling-3.0-flash-vl:free".into()),
(P::OpenRouter, "poolside/laguna-s-2.1:free".into()),
(
P::OpenRouter,
"nvidia/nemotron-3-super-120b-a12b:free".into(),
),
(P::CloudflareWorkersAi, "@cf/openai/gpt-oss-120b".into()),
(P::OpenRouter, "openai/gpt-oss-120b:free".into()),
(P::Groq, "openai/gpt-oss-120b".into()),
],
S::L => vec![
(P::CloudflareWorkersAi, "@cf/zai-org/glm-5.3-flash".into()),
(P::HuggingFace, "zai-org/GLM-5.3-Flash".into()),
(P::HuggingFace, "deepseek-ai/DeepSeek-V4.1-Flash".into()),
(P::OpenRouter, "nex-agi/nex-n2.5-pro:free".into()),
(P::HuggingFace, "MiniMaxAI/MiniMax-M3".into()),
(P::OpenRouter, "thinkingmachines/inkling-small:free".into()),
(P::HuggingFace, "thinkingmachines/Inkling-Small".into()),
],
S::XL => vec![
(P::CloudflareWorkersAi, "@cf/zai-org/glm-5.3".into()),
(P::HuggingFace, "zai-org/GLM-5.3".into()),
(P::Mistral, "zai-glm-5-3".into()),
(P::HuggingFace, "moonshotai/Kimi-K3".into()),
(P::HuggingFace, "Qwen/Qwen3.8-2.4T-A95B".into()),
(P::OpenRouter, "thinkingmachines/inkling:free".into()),
(P::HuggingFace, "thinkingmachines/Inkling".into()),
(P::HuggingFace, "tencent/Hy4-preview".into()),
(P::HuggingFace, "deepseek-ai/DeepSeek-V4-Pro".into()),
],
}
}
}
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 index = rng.random_range(0..=end);
vec1.swap(index, end);
let (body, element) = vec1.into_rtail_and_head();
let body = body.try_into().ok();
(element, body)
}
#[derive(Debug, TypedBuilder)]
struct Upstream {
base_url: Url,
#[builder(setter(strip_option))]
api_key: Option<SecretString>,
}
enum ChatCompletionResponse {
Immediate(openai::chat::completion::ChatCompletion),
Stream {
response: reqwest::Response,
stream_requester: StreamRequester,
},
}
impl IntoResponse for ChatCompletionResponse {
fn into_response(self) -> Response {
match self {
ChatCompletionResponse::Immediate(chat_completion) => {
Json(chat_completion).into_response()
}
ChatCompletionResponse::Stream {
response,
stream_requester,
} => {
let (sse_sink, sse_stream) = async_channel::bounded(256);
stream_requester
.try_send((response, sse_sink))
.expect("this is an unbounded channel");
Sse::new(sse_stream).into_response()
}
}
}
}
enum ChatCompletionError {
NoneSucceeded,
}
impl IntoResponse for ChatCompletionError {
fn into_response(self) -> Response {
match self {
ChatCompletionError::NoneSucceeded => StatusCode::SERVICE_UNAVAILABLE.into_response(),
}
}
}
#[axum::debug_handler]
#[tracing::instrument(skip(state, request))]
async fn create_chat_completion(
_: V1ChatCompletionsPath,
State(state): State<AppState>,
Json(request): Json<openai::chat::completion::CreateChatCompletionRequest<Shortcut>>,
) -> Result<ChatCompletionResponse, ChatCompletionError> {
let suggestions = request.model.suggestions();
let mut model_upstreams = VecDeque::new();
for (provider, model) in suggestions {
let upstreams = match provider {
Provider::CloudflareWorkersAi => vec![], // TODO
Provider::GoogleAiStudio => state
.app_args
.google_ai_studio_api_key
.iter()
.cloned()
.map(|api_key| {
Upstream::builder()
.base_url(
"https://generativelanguage.googleapis.com/v1beta/openai"
.parse()
.expect("TODO"),
)
.api_key(api_key)
.build()
})
.collect(),
Provider::Groq => state
.app_args
.groq_api_key
.iter()
.cloned()
.map(|api_key| {
Upstream::builder()
.base_url("https://api.groq.com/openai".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()
.base_url("https://router.huggingface.co".parse().expect("TODO"))
.api_key(api_key)
.build()
})
.collect(),
Provider::Mistral => state
.app_args
.mistral_api_key
.iter()
.cloned()
.map(|api_key| {
Upstream::builder()
.base_url("https://api.mistral.ai".parse().expect("TODO"))
.api_key(api_key)
.build()
})
.collect(),
Provider::OpenRouter => state
.app_args
.openrouter_api_key
.iter()
.cloned()
.map(|api_key| {
Upstream::builder()
.base_url("https://openrouter.ai/api".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::chat::completion::CreateChatCompletionRequest {
model: _shortcut,
messages,
audio,
frequency_penalty,
function_call,
functions,
logit_bias,
logprobs,
max_completion_tokens,
max_tokens,
metadata,
modalities,
moderation,
n,
parallel_tool_calls,
prediction,
presence_penalty,
prompt_cache_key,
prompt_cache_options,
prompt_cache_retention,
reasoning_effort,
response_format,
safety_identifier,
seed,
service_tier,
stop,
store,
stream,
stream_options,
temperature,
tool_choice,
tools,
top_logprobs,
top_p,
user,
verbosity,
web_search_options,
} = request.clone();
let request = openai::chat::completion::CreateChatCompletionRequest {
model,
messages,
audio,
frequency_penalty,
function_call,
functions,
logit_bias,
logprobs,
max_completion_tokens,
max_tokens,
metadata,
modalities,
moderation,
n,
parallel_tool_calls,
prediction,
presence_penalty,
prompt_cache_key,
prompt_cache_options,
prompt_cache_retention,
reasoning_effort,
response_format,
safety_identifier,
seed,
service_tier,
stop,
store,
stream,
stream_options,
temperature,
tool_choice,
tools,
top_logprobs,
top_p,
user,
verbosity,
web_search_options,
};
let client = reqwest::Client::new();
let mut upstream_request = client
.post({
let mut url = upstream.base_url;
{
let mut path_segments = url.path_segments_mut().expect("TODO");
let s = V1ChatCompletionsPath.to_string();
let s = s.strip_prefix("/").unwrap_or(&s);
let s = s.strip_suffix("/").unwrap_or(&s);
path_segments.extend(s.split("/"));
}
tracing::info!(?url);
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;
};
if request.stream.unwrap_or(false) {
return Ok(ChatCompletionResponse::Stream {
response: upstream_response,
stream_requester: state.stream_requester,
});
} else {
let response = match upstream_response.json().await {
Ok(response) => response,
Err(json_error) => {
tracing::error!(?json_error);
continue;
}
};
return Ok(ChatCompletionResponse::Immediate(response));
}
}
Err(ChatCompletionError::NoneSucceeded)
}
pub fn create_router() -> Router<AppState> {
Router::new().typed_post(create_chat_completion)
}
pub fn create_app(state: AppState) -> Router<()> {
create_router().with_state(state)
}

View File

@@ -0,0 +1,738 @@
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::response::Sse;
use axum_extra::routing::TypedPath;
use ecow::EcoString;
use futures::StreamExt;
use headers::Header;
use headers::authorization::Bearer;
use mitsein::vec1::Vec1;
use reqwest::Certificate;
use reqwest_sse::EventSource as _;
use secrecy::ExposeSecret;
use serde::Deserialize;
use serde_with::DeserializeFromStr;
use serde_with::SerializeDisplay;
use std::collections::VecDeque;
use std::convert::Infallible;
use std::fmt::Display;
use std::str::FromStr;
use std::time::Duration;
use strum::EnumString;
use strum::VariantArray;
use tokio::runtime::Handle;
use tokio::select;
use tokio::time::sleep;
#[derive(Deserialize, TypedPath)]
#[typed_path("/v1/chat/completions")]
pub struct V1ChatCompletionsPath;
#[derive(
Clone, Debug, EnumString, DeserializeFromStr, strum::Display, SerializeDisplay, VariantArray,
)]
#[strum(serialize_all = "lowercase")]
pub enum Shortcut {
XS,
S,
M,
L,
XL,
Experimental,
}
#[derive(Debug, Clone, DeserializeFromStr, SerializeDisplay)]
pub struct ShortcutAndMaybeReasoning(
pub Shortcut,
pub Option<openai::chat::completion::ReasoningEffort>,
);
impl FromStr for ShortcutAndMaybeReasoning {
type Err = Infallible; // TODO
fn from_str(s: &str) -> Result<Self, Self::Err> {
if let Some((model, reasoning)) = s.split_once(":") {
let model = model.parse().expect("TODO");
let reasoning = reasoning.parse().expect("TODO");
Ok(Self(model, Some(reasoning)))
} else {
let model = s.parse().expect("TODO");
let reasoning = None;
Ok(Self(model, reasoning))
}
}
}
impl Display for ShortcutAndMaybeReasoning {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let Self(model, reasoning) = self;
write!(f, "{model}")?;
if let Some(reasoning) = reasoning {
write!(f, ":")?;
write!(f, "{reasoning}")?;
}
Ok(())
}
}
#[derive(Debug, Clone)]
enum Provider {
CloudflareWorkersAi,
Cohere,
GoogleAiStudio,
Groq,
HuggingFace,
Kilo,
Mistral,
NovitaAi,
NvidiaNim,
Ollama,
OpenCode,
OpenRouter,
Poolside,
VercelAiGateway,
ZAi,
}
impl Shortcut {
fn suggestions(&self) -> Vec<(Provider, EcoString)> {
use Provider as P;
use Shortcut as S;
match self {
S::XS => vec![
(P::GoogleAiStudio, "gemini-3.5-flash-lite".into()),
(P::VercelAiGateway, "openai/gpt-5.4-nano".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::GoogleAiStudio, "gemma-4-26b-a4b-it".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::Poolside, "poolside/laguna-xs-2.1".into()),
(P::NvidiaNim, "poolside/laguna-xs-2.1".into()),
(P::OpenRouter, "poolside/laguna-xs-2.1:free".into()),
(P::ZAi, "glm-4.7-flash".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::NvidiaNim, "nvidia/nemotron-3.5-lightning-30b-a3b".into()),
(
P::VercelAiGateway,
"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.5-flash".into()),
(P::OpenRouter, "poolside/laguna-xs.2:free".into()),
(
P::NvidiaNim,
"nvidia/nemotron-3-nano-omni-30b-a3b-reasoning".into(),
),
(
P::OpenRouter,
"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::Ollama, "gpt-oss:20b".into()),
(P::NvidiaNim, "openai/gpt-oss-20b".into()),
(P::CloudflareWorkersAi, "@cf/openai/gpt-oss-20b".into()),
(P::OpenRouter, "openai/gpt-oss-20b:free".into()),
(P::HuggingFace, "openai/gpt-oss-20b".into()),
(P::NvidiaNim, "meta/llama-3.2-11b-vision-instruct".into()),
(P::OpenRouter, "liquid/lfm-2.5-2.6b:free".into()),
(P::Groq, "groq/compound-mini".into()),
(P::Kilo, "kilo-auto/free".into()),
(P::OpenRouter, "openrouter/free".into()),
],
S::S => vec![
(P::Groq, "qwen/qwen3.8-27b".into()),
(P::CloudflareWorkersAi, "@cf/qwen/qwen3.8-27b".into()),
(P::OpenRouter, "qwen/qwen3.8-27b:free".into()),
(P::HuggingFace, "Qwen/Qwen3.8-27B".into()),
(P::NvidiaNim, "meta/muse-glimmer-30b".into()),
(P::HuggingFace, "meta-models/Muse-Glimmer-30B".into()),
(P::GoogleAiStudio, "gemma-4-31b-it".into()),
(P::Ollama, "gemma4:31b".into()),
(P::NvidiaNim, "google/gemma-4-31b-it".into()),
(P::OpenRouter, "google/gemma-4-31b-it:free".into()),
(P::HuggingFace, "google/gemma-4-31B-it".into()),
(P::Mistral, "mistral-small-2603".into()),
(P::Groq, "groq/compound".into()),
(P::OpenRouter, "openrouter/fusion".into()),
],
S::M => vec![
(P::GoogleAiStudio, "gemini-3.8-flash".into()),
(P::VercelAiGateway, "alibaba/qwen3.7-flash".into()),
(P::VercelAiGateway, "alibaba/qwen3.8-omni-flash".into()),
(
P::NvidiaNim,
"nvidia/nemotron-3.5-super-vl-120b-a12b".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::Kilo, "inclusionai-ling-3-0-flash-free".into()),
(P::Kilo, "inclusionai-ling-3-0-flash-vl-free".into()),
(P::VercelAiGateway, "inclusionai/ling-3.0-flash-vl".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::VercelAiGateway, "stepfun/step-3.7-flash".into()),
(P::VercelAiGateway, "inclusionai/ling-3.0-flash-fin".into()),
(
P::VercelAiGateway,
"inclusionai/ling-3.0-flash-sante".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::Kilo, "nvidia-nemotron-3-super-120b-a12b-free".into()),
(
P::OpenRouter,
"nvidia/nemotron-3-super-120b-a12b:free".into(),
),
(P::Groq, "openai/gpt-oss-120b".into()),
(P::Ollama, "gpt-oss:120b".into()),
(P::CloudflareWorkersAi, "@cf/openai/gpt-oss-120b".into()),
(P::OpenRouter, "openai/gpt-oss-120b:free".into()),
(P::HuggingFace, "openai/gpt-oss-120b".into()),
(P::NvidiaNim, "meta/llama-3.2-90b-vision-instruct".into()),
],
S::L => vec![
(P::CloudflareWorkersAi, "@cf/zai-org/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-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::NvidiaNim, "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::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::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::Kilo, "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::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::HuggingFace, "thinkingmachines/Inkling-Small".into()),
(P::Ollama, "nemotron-3-ultra".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![
(P::NvidiaNim, "z-ai/glm-5-3".into()),
(P::HuggingFace, "zai-org/GLM-5.3".into()),
(P::CloudflareWorkersAi, "@cf/zai-org/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, "XiaomiMiMo/MiMo-V2.6-Pro-RL".into()),
(P::Mistral, "mistral-large-4".into()),
(P::HuggingFace, "Qwen/Qwen3.8-2.4T-A95B".into()),
(P::VercelAiGateway, "xiaomi/mimo-v2.5-pro".into()),
(P::OpenCode, "longcat-2.5-preview-free".into()),
(P::OpenRouter, "thinkingmachines/inkling:free".into()),
(P::HuggingFace, "thinkingmachines/Inkling".into()),
(P::VercelAiGateway, "thinkingmachines/inkling".into()),
(P::GoogleAiStudio, "gemini-3.1-pro-preview".into()),
(P::VercelAiGateway, "moonshotai/kimi-k2.7-code".into()),
],
S::Experimental => vec![
(P::OpenRouter, "space-bunny-alpha".into()),
(P::OpenCode, "space-bunny-free".into()),
(P::OpenCode, "big-pickle".into()),
(P::VercelAiGateway, "stealth/glyph-cluster".into()),
],
}
}
}
pub enum ChatCompletionResponse {
Immediate(openai::chat::completion::ChatCompletion<String>),
Stream(reqwest::Response),
}
impl IntoResponse for ChatCompletionResponse {
fn into_response(self) -> Response {
match self {
ChatCompletionResponse::Immediate(chat_completion) => {
Json(chat_completion).into_response()
}
ChatCompletionResponse::Stream(response) => {
let (sink, stream) = async_channel::bounded(256);
let handle = Handle::current();
std::thread::spawn(move || {
handle.block_on(async move {
match response.events().await {
Ok(mut events) => {
while let Some(event_result) = events.next().await {
let message = event_result
.map(|event| {
axum::response::sse::Event::default().data(event.data)
})
.map_err(Box::<dyn std::error::Error + Send + Sync>::from);
let _ = sink.send(message).await;
}
}
Err(event_source_error) => {
let _ = sink.send(Err(event_source_error.into())).await;
}
};
});
});
Sse::new(stream).into_response()
}
}
}
}
pub enum ChatCompletionError {
NoneSucceeded,
}
impl IntoResponse for ChatCompletionError {
fn into_response(self) -> Response {
match self {
ChatCompletionError::NoneSucceeded => StatusCode::SERVICE_UNAVAILABLE.into_response(),
}
}
}
#[axum::debug_handler]
#[tracing::instrument(skip(state, request))]
pub async fn create_chat_completion(
_: V1ChatCompletionsPath,
State(state): State<AppState>,
Json(request): Json<
openai::chat::completion::CreateChatCompletionRequest<ShortcutAndMaybeReasoning>,
>,
) -> Result<ChatCompletionResponse, ChatCompletionError> {
let ShortcutAndMaybeReasoning(shortcut, reasoning_in_id) = request.model.clone();
let mut model_upstreams = VecDeque::new();
for (provider, model) in shortcut.suggestions() {
let upstreams: Vec<Upstream> =
match provider {
Provider::CloudflareWorkersAi => 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/v1/chat/completions")
.parse()
.expect("TODO"),
)
.api_key(api_key)
.build()
},
)
.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/v1/chat/completions"
.parse()
.expect("TODO"),
)
.api_key(api_key)
.build()
})
.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
.app_args
.groq_api_key
.iter()
.cloned()
.map(|api_key| {
Upstream::builder()
.url("https://api.groq.com/openai/v1/chat/completions".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/chat/completions".parse().expect("TODO"))
.api_key(api_key)
.build()
})
.collect(),
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
.app_args
.mistral_api_key
.iter()
.cloned()
.map(|api_key| {
Upstream::builder()
.url("https://api.mistral.ai/v1/chat/completions".parse().expect("TODO"))
.api_key(api_key)
.build()
})
.collect(),
Provider::NvidiaNim => state
.app_args
.nvidia_nim_api_key
.iter()
.cloned()
.map(|api_key| {
Upstream::builder()
.url("https://integrate.api.nvidia.com/v1/chat/completions".parse().expect("TODO"))
.api_key(api_key)
.build()
})
.collect(),
Provider::NovitaAi => state
.app_args
.novita_ai_api_key
.iter()
.cloned()
.map(|api_key| {
Upstream::builder()
.url("https://api.novita.ai/openai/v1/chat/completions".parse().expect("TODO"))
.api_key(api_key)
.build()
})
.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
.app_args
.openrouter_api_key
.iter()
.cloned()
.map(|api_key| {
Upstream::builder()
.url("https://openrouter.ai/api/v1/chat/completions".parse().expect("TODO"))
.api_key(api_key)
.build()
})
.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
.app_args
.vercel_ai_gateway_api_key
.iter()
.cloned()
.map(|api_key| {
Upstream::builder()
.url("https://ai-gateway.vercel.sh/v1/chat/completions".parse().expect("TODO"))
.api_key(api_key)
.build()
})
.collect(),
Provider::ZAi => state
.app_args
.z_ai_api_key
.iter()
.cloned()
.map(|api_key| {
Upstream::builder()
.url("https://api.z.ai/api/paas/v4/chat/completions".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::chat::completion::CreateChatCompletionRequest {
model: _shortcut,
messages,
audio,
frequency_penalty,
function_call,
functions,
logit_bias,
logprobs,
max_completion_tokens,
max_tokens,
metadata,
modalities,
moderation,
n,
parallel_tool_calls,
prediction,
presence_penalty,
prompt_cache_key,
prompt_cache_options,
prompt_cache_retention,
reasoning_effort,
response_format,
safety_identifier,
seed,
service_tier,
stop,
store,
stream,
stream_options,
temperature,
tool_choice,
tools,
top_logprobs,
top_p,
user,
verbosity,
web_search_options,
} = request.clone();
let reasoning_effort = reasoning_effort.or(reasoning_in_id.clone());
let request = openai::chat::completion::CreateChatCompletionRequest {
model,
messages,
audio,
frequency_penalty,
function_call,
functions,
logit_bias,
logprobs,
max_completion_tokens,
max_tokens,
metadata,
modalities,
moderation,
n,
parallel_tool_calls,
prediction,
presence_penalty,
prompt_cache_key,
prompt_cache_options,
prompt_cache_retention,
reasoning_effort,
response_format,
safety_identifier,
seed,
service_tier,
stop,
store,
stream,
stream_options,
temperature,
tool_choice,
tools,
top_logprobs,
top_p,
user,
verbosity,
web_search_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 = 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,
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;
};
if request.stream.unwrap_or(false) {
return Ok(ChatCompletionResponse::Stream(upstream_response));
} else {
let response = match upstream_response.json().await {
Ok(response) => response,
Err(json_error) => {
tracing::error!(?json_error);
continue;
}
};
return Ok(ChatCompletionResponse::Immediate(response));
}
}
Err(ChatCompletionError::NoneSucceeded)
}

1
src/server/chat/mod.rs Normal file
View File

@@ -0,0 +1 @@
pub mod completions;

274
src/server/decisions.rs Normal file
View File

@@ -0,0 +1,274 @@
use crate::openai;
use crate::server::AppState;
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 serde::Deserialize;
use serde_with::DeserializeFromStr;
use serde_with::SerializeDisplay;
use std::collections::HashMap;
use std::hash::RandomState;
use strum::EnumString;
use strum::VariantArray;
#[derive(Deserialize, TypedPath)]
#[typed_path("/v1/decisions")]
pub struct V1DecisionsPath;
#[derive(
Clone, Debug, EnumString, DeserializeFromStr, strum::Display, SerializeDisplay, VariantArray,
)]
#[strum(serialize_all = "lowercase")]
pub enum Shortcut {
S,
M,
}
pub struct DecisionsResponse(openai::decisions::DecisionResponse<String>);
impl IntoResponse for DecisionsResponse {
fn into_response(self) -> Response {
let Self(embeddings) = self;
Json(embeddings).into_response()
}
}
pub enum DecisionsError {
NoneSucceeded,
}
impl IntoResponse for DecisionsError {
fn into_response(self) -> Response {
match self {
DecisionsError::NoneSucceeded => StatusCode::SERVICE_UNAVAILABLE.into_response(),
}
}
}
#[tracing::instrument(skip(app_state, request))]
pub async fn create_decision(
_: V1DecisionsPath,
State(app_state): State<AppState>,
Json(request): Json<openai::decisions::DecisionRequest<Shortcut>>,
) -> Result<DecisionsResponse, DecisionsError> {
use super::system_one;
let openai::decisions::DecisionRequest {
model,
input,
questions,
} = request;
let model = match model {
Shortcut::S => system_one::Shortcut::S,
Shortcut::M => system_one::Shortcut::M,
};
let (state, images, files) = match input {
openai::decisions::DecisionInput::Messages(messages) => {
let mut images = vec![];
let mut texts = vec![];
for openai::decisions::DecisionInputMessage { role, content } in messages {
match content {
openai::decisions::DecisionInputContent::Text(text) => texts.push(text),
openai::decisions::DecisionInputContent::Parts(decision_input_parts) => {
for decision_input_part in decision_input_parts {
match decision_input_part {
openai::decisions::DecisionInputPart::Image {
image_url,
detail,
} => images.push(image_url),
openai::decisions::DecisionInputPart::Text { text } => {
texts.push(text)
}
}
}
}
}
}
let text = texts.join("\n\n");
let files = images.clone();
(
openai::system_one::State::Text(text),
Some(images),
Some(files),
)
}
openai::decisions::DecisionInput::Text(text) => {
(openai::system_one::State::Text(text), None, None)
}
};
let questions = questions
.into_iter()
.map(|question| match question {
openai::decisions::DecisionQuestion::Predicate(
openai::decisions::PredicateQuestion { name, instructions },
) => (
name.expect("TODO"),
openai::system_one::Question::Noul(openai::system_one::NoulQuestion {
instructions: openai::system_one::QuestionInstructions::Text(instructions),
criteria: None,
}),
),
openai::decisions::DecisionQuestion::Choice(openai::decisions::ChoiceQuestion {
name,
instructions,
choices,
}) => (
name.expect("TODO"),
openai::system_one::Question::Choice(openai::system_one::ChoiceQuestion {
instructions: openai::system_one::QuestionInstructions::Text(instructions),
criteria: choices
.into_iter()
.map(|openai::decisions::ChoiceOption { value, description }| {
(
match value {
openai::decisions::Choice::String(s) => s,
openai::decisions::Choice::Boolean(b) => b.to_string(),
},
description.map(|description| {
openai::system_one::QuestionInstructions::Text(description)
}),
)
})
.collect(),
}),
),
openai::decisions::DecisionQuestion::Score(openai::decisions::ScoreQuestion {
name,
instructions,
levels,
}) => (
name.expect("TODO"),
openai::system_one::Question::Score(openai::system_one::ScoreQuestion {
instructions: openai::system_one::QuestionInstructions::Text(instructions),
criteria: levels
.into_iter()
.map(|openai::decisions::ScoreLevel { label, description }| {
openai::system_one::QuestionInstructions::Text(label)
})
.collect(),
}),
),
})
.collect();
let request = openai::system_one::SystemOneRequest {
model,
state,
files,
images,
questions,
};
system_one::create_system_one(system_one::V1SystemOnePath, State(app_state), Json(request))
.await
.map(
|system_one::SystemOneResponse(openai::system_one::SystemOneResponse {
model,
answers,
usage,
})| {
let answers = answers
.into_iter()
.map(|(id, answer)| match answer {
openai::system_one::Answer::Noul(openai::system_one::NoulAnswer {
noul,
}) => openai::decisions::DecisionAnswer::Predicate(
openai::decisions::PredicateAnswer {
name: Some(id),
probability: noul,
},
),
openai::system_one::Answer::Choice(openai::system_one::ChoiceAnswer {
choice,
probabilities,
confidence,
}) => openai::decisions::DecisionAnswer::Choice(
openai::decisions::ChoiceAnswer {
name: Some(id),
choice: openai::decisions::Choice::String(choice),
confidence,
probabilities: probabilities
.into_iter()
.map(|(choice_id, probability)| {
openai::decisions::ChoiceProbability {
value: openai::decisions::Choice::String(choice_id),
probability,
}
})
.collect(),
},
),
openai::system_one::Answer::Score(openai::system_one::ScoreAnswer {
score,
legend,
probabilities,
confidence,
}) => openai::decisions::DecisionAnswer::Score(
openai::decisions::ScoreAnswer {
name: Some(id),
score,
confidence,
probabilities: {
probabilities
.into_iter()
.map(|(choice_id, probability)| {
openai::decisions::ScoreProbability {
value: choice_id
.parse()
.expect("it should be a number, I think"),
label: legend
.get(&choice_id)
.expect("it should exist, I think")
.clone(),
probability,
}
})
.collect()
},
},
),
})
.collect();
let openai::system_one::Usage {
input_tokens,
output_tokens,
} = usage;
let input_tokens = input_tokens as i32;
let output_tokens = output_tokens as i32;
let total_tokens = input_tokens + output_tokens;
let input_tokens_details = openai::decisions::TokenUsageDetails {
cache_write_tokens: 0,
cached_tokens: 0,
};
let output_tokens_details = openai::decisions::OutputTokenDetails {
reasoning_tokens: 0,
};
let usage = openai::decisions::TokenUsage {
input_tokens,
input_tokens_details,
output_tokens,
output_tokens_details,
total_tokens,
};
DecisionsResponse(openai::decisions::DecisionResponse {
model,
answers,
usage,
})
},
)
.map_err(|_| DecisionsError::NoneSucceeded)
}

313
src/server/embeddings.rs Normal file
View File

@@ -0,0 +1,313 @@
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/embeddings")]
pub struct V1EmbeddingsPath;
#[derive(
Clone, Debug, EnumString, DeserializeFromStr, strum::Display, SerializeDisplay, VariantArray,
)]
#[strum(serialize_all = "lowercase")]
pub enum Shortcut {
#[strum(serialize = "codestral-embed-2505")]
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")]
Lfm2_5Embedding350M,
#[strum(serialize = "mistral-embed-2312")]
MistralEmbed2312,
#[strum(serialize = "nemotron-3-embed-1b")]
Nemotron3Embed1B,
}
#[derive(Debug, Clone)]
enum Provider {
Cohere,
GoogleAiStudio,
Mistral,
NvidiaNim,
OpenRouter,
VercelAiGateway,
}
impl Shortcut {
fn suggestions(&self) -> Vec<(Provider, EcoString)> {
use Provider as P;
use Shortcut as S;
match self {
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 => {
vec![(P::OpenRouter, "liquid/lfm-2.5-embedding-350m:free".into())]
}
S::MistralEmbed2312 => vec![(P::Mistral, "mistral-embed-2312".into())],
S::Nemotron3Embed1B => vec![
(P::NvidiaNim, "nvidia/nemotron-3-embed-1b".into()),
(P::OpenRouter, "nvidia/nemotron-3-embed-1b:free".into()),
],
}
}
}
pub struct EmbeddingsResponse(openai::embeddings::CreateEmbeddingResponse<String>);
impl IntoResponse for EmbeddingsResponse {
fn into_response(self) -> Response {
let Self(embeddings) = self;
Json(embeddings).into_response()
}
}
pub enum EmbeddingsError {
NoneSucceeded,
}
impl IntoResponse for EmbeddingsError {
fn into_response(self) -> Response {
match self {
EmbeddingsError::NoneSucceeded => StatusCode::SERVICE_UNAVAILABLE.into_response(),
}
}
}
#[tracing::instrument(skip(state, request))]
pub async fn create_embeddings(
_: V1EmbeddingsPath,
State(state): State<AppState>,
Json(request): Json<openai::embeddings::CreateEmbeddingRequest<Shortcut>>,
) -> Result<EmbeddingsResponse, EmbeddingsError> {
let shortcut = request.model.clone();
let mut model_upstreams = VecDeque::new();
for (provider, model) in shortcut.suggestions() {
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
.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::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
.app_args
.nvidia_nim_api_key
.iter()
.cloned()
.map(|api_key| {
Upstream::builder()
.url(
"https://integrate.api.nvidia.com/v1/embeddings"
.parse()
.expect("TODO"),
)
.api_key(api_key)
.build()
})
.collect(),
Provider::OpenRouter => state
.app_args
.openrouter_api_key
.iter()
.cloned()
.map(|api_key| {
Upstream::builder()
.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)
.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::embeddings::CreateEmbeddingRequest {
model: _shortcut,
input,
dimensions,
encoding_format,
user,
} = request.clone();
let request = openai::embeddings::CreateEmbeddingRequest {
model,
input,
dimensions,
encoding_format,
user,
};
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 = match upstream_response.json().await {
Ok(response) => response,
Err(json_error) => {
tracing::error!(?json_error);
continue;
}
};
return Ok(EmbeddingsResponse(response));
}
Err(EmbeddingsError::NoneSucceeded)
}

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

@@ -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"))
}

59
src/server/mod.rs Normal file
View File

@@ -0,0 +1,59 @@
use crate::AppArgs;
use axum::Router;
use axum_extra::routing::RouterExt;
use mitsein::vec1::Vec1;
use rand::Rng;
use rand::RngExt;
use secrecy::SecretString;
use std::sync::Arc;
use tower_http::cors::CorsLayer;
use typed_builder::TypedBuilder;
use url::Url;
mod chat;
mod decisions;
mod embeddings;
mod extraction;
mod frontend;
mod models;
mod system_one;
#[derive(Debug, Clone, TypedBuilder)]
pub struct AppState {
app_args: Arc<AppArgs>,
}
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 index = rng.random_range(0..=end);
vec1.swap(index, end);
let (body, element) = vec1.into_rtail_and_head();
let body = body.try_into().ok();
(element, body)
}
#[derive(Debug, TypedBuilder)]
struct Upstream {
url: Url,
#[builder(setter(strip_option))]
api_key: Option<SecretString>,
}
pub fn create_router() -> Router<AppState> {
Router::new()
.typed_post(chat::completions::create_chat_completion)
.typed_post(decisions::create_decision)
.typed_post(embeddings::create_embeddings)
.typed_post(extraction::create_extraction)
.typed_post(system_one::create_system_one)
.typed_get(models::list_models)
.typed_get(frontend::spa)
.layer(CorsLayer::new())
}
pub fn create_app(state: AppState) -> Router<()> {
create_router().with_state(state)
}

36
src/server/models.rs Normal file
View File

@@ -0,0 +1,36 @@
use crate::openai;
use axum::Json;
use axum_extra::routing::TypedPath;
use itertools::iproduct;
use serde::Deserialize;
use strum::VariantArray;
#[derive(Deserialize, TypedPath)]
#[typed_path("/v1/models")]
pub struct V1ModelsPath;
#[tracing::instrument]
pub async fn list_models(_: V1ModelsPath) -> Json<openai::models::ListModelsResponse<String>> {
let shortcuts = super::chat::completions::Shortcut::VARIANTS.iter().cloned();
let reasoning_efforts = openai::chat::completion::ReasoningEffort::VARIANTS
.iter()
.cloned()
.map(Some)
.chain(None);
let models = iproduct!(shortcuts, reasoning_efforts).map(|(shortcut, reasoning)| {
openai::models::Model {
id: super::chat::completions::ShortcutAndMaybeReasoning(shortcut, reasoning)
.to_string(),
created: 0,
object: openai::models::ModelObject::Model,
owned_by: "nobody".into(),
shutdown_date: None,
}
});
Json(openai::models::ListModelsResponse {
data: models.collect(),
object: openai::models::ListObject::List,
})
}

294
src/server/system_one.rs Normal file
View File

@@ -0,0 +1,294 @@
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/systemone")]
pub struct V1SystemOnePath;
#[derive(
Clone, Debug, EnumString, DeserializeFromStr, strum::Display, SerializeDisplay, VariantArray,
)]
#[strum(serialize_all = "lowercase")]
pub enum Shortcut {
S,
M,
L,
}
#[derive(Debug, Clone)]
enum Provider {
CloudflareWorkersAi,
LiquidAi,
OpenCode,
OpenRouter,
VercelAiGateway,
}
impl Shortcut {
fn suggestions(&self) -> Vec<(Provider, EcoString)> {
use Provider as P;
use Shortcut as S;
match self {
S::S => {
vec![
(
P::VercelAiGateway,
"perplexity/pplx-decider-flash-v1.2".into(),
),
(P::LiquidAi, "d1:free".into()),
(P::CloudflareWorkersAi, "clef-omni".into()),
(P::CloudflareWorkersAi, "clef-flash".into()),
(P::OpenRouter, "respan/span-01-lite:free".into()),
(P::OpenRouter, "respan/span-01-lite".into()),
(P::VercelAiGateway, "convaiinnovations/laya-free".into()),
(P::VercelAiGateway, "convaiinnovations/laya".into()),
]
}
S::M => {
vec![
(P::OpenCode, "jev-1.13-free".into()),
(P::VercelAiGateway, "perplexity/pplx-decider-v1.2".into()),
(P::CloudflareWorkersAi, "clef".into()),
(P::OpenRouter, "inception/mercury-decide:free".into()),
]
}
S::L => {
vec![(P::VercelAiGateway, "liquid/d1-ultra".into())]
}
}
}
}
pub struct SystemOneResponse(pub openai::system_one::SystemOneResponse<String>);
impl IntoResponse for SystemOneResponse {
fn into_response(self) -> Response {
let Self(system_one) = self;
Json(system_one).into_response()
}
}
pub enum SystemOneError {
NoneSucceeded,
}
impl IntoResponse for SystemOneError {
fn into_response(self) -> Response {
match self {
SystemOneError::NoneSucceeded => StatusCode::SERVICE_UNAVAILABLE.into_response(),
}
}
}
#[tracing::instrument(skip(state, request))]
pub async fn create_system_one(
_: V1SystemOnePath,
State(state): State<AppState>,
Json(request): Json<openai::system_one::SystemOneRequest<Shortcut>>,
) -> Result<SystemOneResponse, SystemOneError> {
let shortcut = request.model.clone();
let mut model_upstreams = VecDeque::new();
for (provider, model) in shortcut.suggestions() {
let upstreams: Vec<Upstream> =
match provider {
Provider::CloudflareWorkersAi => 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/@cf/cloudflare/{model}")
.parse()
.expect("TODO"),
)
.api_key(api_key)
.build()
},
)
.collect(),
Provider::LiquidAi => state
.app_args
.liquid_ai_api_key
.iter()
.cloned()
.map(|api_key| {
Upstream::builder()
.url("https://api.liquid.ai/decisions/v1/systemone".parse().expect("TODO"))
.api_key(api_key)
.build()
})
.collect(),
Provider::OpenCode => state
.app_args
.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
.app_args
.openrouter_api_key
.iter()
.cloned()
.map(|api_key| {
Upstream::builder()
.url("https://openrouter.ai/api/v1/systemone".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/typesafe/v1/systemone".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::system_one::SystemOneRequest {
model: _shortcut,
state,
files,
images,
questions,
} = request.clone();
let request = openai::system_one::SystemOneRequest {
model,
state,
files,
images,
questions,
};
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::system_one::SystemOneResponse::deserialize(
response.clone(), // TODO: not need to clone
) {
Ok(response) => return Ok(SystemOneResponse(response)),
Err(structure_error) => {
#[derive(Deserialize)]
struct CloudflareRunResponse {
result: openai::system_one::SystemOneResponse<String>,
success: bool,
errors: Vec<serde_json::Value>, // TODO
messages: Vec<serde_json::Value>, // TODO
}
match CloudflareRunResponse::deserialize(response) {
Ok(response) => return Ok(SystemOneResponse(response.result)),
Err(cloudflare_structure_error) => {
tracing::error!(?structure_error);
tracing::error!(?cloudflare_structure_error);
}
}
}
}
}
Err(SystemOneError::NoneSucceeded)
}