Compare commits
42 Commits
f7fe0878f1
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
7f1290a86a
|
|||
| 566cc56896 | |||
| 302496c12c | |||
| 1b0ca23448 | |||
| 94803c699a | |||
| bc0223539c | |||
| 69789fbd87 | |||
| 5dedf5fa07 | |||
| b9daf9d6bf | |||
|
d11c2d2589
|
|||
| 94a9594130 | |||
| 692a1a56a7 | |||
| 3be16d3d58 | |||
|
|
6e3a283d81 | ||
| e74b5f6a4b | |||
|
|
831b6f568b | ||
|
|
12eb6f2aee | ||
|
|
59e9d3648a | ||
| f4f657c219 | |||
| a0d95d52e4 | |||
| f778fae70c | |||
| c88874948a | |||
| 154cbac120 | |||
| e0e1f0781e | |||
| 9a848e2c78 | |||
| 8452bb9088 | |||
| d869809768 | |||
| 0a111172f5 | |||
| f2108ae35a | |||
| 9dd13f374f | |||
| 226e0fb0ff | |||
| da04b5dd62 | |||
|
|
ff621d759c | ||
| 0521a264dc | |||
| 5ce36e3b8c | |||
| e161693587 | |||
| 0974f640bb | |||
| 91ac1145ce | |||
| f0e8516d7a | |||
| c6c0849cf5 | |||
| 7b3ac7acbb | |||
| e49befb03f |
2
.gitignore
vendored
2
.gitignore
vendored
@@ -1 +1,3 @@
|
||||
/target
|
||||
|
||||
.history/
|
||||
|
||||
543
Cargo.lock
generated
543
Cargo.lock
generated
@@ -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"
|
||||
|
||||
11
Cargo.toml
11
Cargo.toml
@@ -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"
|
||||
|
||||
@@ -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
99
src/lib.rs
Normal 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,
|
||||
}
|
||||
98
src/main.rs
98
src/main.rs
@@ -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");
|
||||
}
|
||||
|
||||
@@ -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
308
src/openai/decisions.rs
Normal 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
113
src/openai/embeddings.rs
Normal 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
524
src/openai/extraction.rs
Normal file
@@ -0,0 +1,524 @@
|
||||
//! System One-style models that can extract, compute, estimate,
|
||||
//! and forecast values from inputs.
|
||||
//! They are strongly typed and output confidence scores and probabilities against their outputs.
|
||||
//!
|
||||
//! Request: [`ExtractionRequest`] · Response: [`ExtractionResponse`]
|
||||
|
||||
use jiff::civil::{Date, DateTime, Time};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashMap;
|
||||
|
||||
/// The item type required for `list` fields.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum ListItems {
|
||||
/// List of text items.
|
||||
Text,
|
||||
/// List of numbers.
|
||||
Number,
|
||||
/// List of ISO dates.
|
||||
Date,
|
||||
}
|
||||
|
||||
/// Scale for estimate/forecast grids.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum Scale {
|
||||
/// Linear scale (default).
|
||||
#[default]
|
||||
Linear,
|
||||
/// Logarithmic scale.
|
||||
Log,
|
||||
}
|
||||
|
||||
/// Interval for forecast fields.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum ForecastInterval {
|
||||
/// One step of the grid.
|
||||
Step,
|
||||
/// Per minute.
|
||||
Minute,
|
||||
/// Per hour.
|
||||
Hour,
|
||||
/// Per day.
|
||||
Day,
|
||||
/// Per week.
|
||||
Week,
|
||||
/// Per month.
|
||||
Month,
|
||||
/// Per quarter.
|
||||
Quarter,
|
||||
/// Per year.
|
||||
Year,
|
||||
}
|
||||
|
||||
/// A value that may be a number or an ISO date string. Used for the
|
||||
/// `min`/`max` bounds of estimate/forecast fields (ISO date for date
|
||||
/// estimates) and for the `step` grid.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(untagged)]
|
||||
pub enum NumberOrDate {
|
||||
/// A numeric bound or step size.
|
||||
Number(f64),
|
||||
/// An ISO date string (for date estimates).
|
||||
Date(Date),
|
||||
}
|
||||
|
||||
/// A step size for estimate grids: a positive number (days for date
|
||||
/// estimates), or the strings `"month"` / `"year"`.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(untagged)]
|
||||
pub enum EstimateStep {
|
||||
/// A positive numeric step (days for date estimates).
|
||||
Number(f64),
|
||||
/// Calendar-aware step: `"month"`.
|
||||
Month,
|
||||
/// Calendar-aware step: `"year"`.
|
||||
Year,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum DurationUnit {
|
||||
Seconds,
|
||||
Minutes,
|
||||
Hours,
|
||||
Days,
|
||||
Weeks,
|
||||
Months,
|
||||
Years,
|
||||
}
|
||||
|
||||
/// A typed field specification
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(tag = "type", rename_all = "lowercase")]
|
||||
pub enum Field {
|
||||
/// A string extracted from the input.
|
||||
#[serde(rename = "text")]
|
||||
Text {
|
||||
/// What the field should contain.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
instructions: Option<String>,
|
||||
/// Probability of null above which the value is null (default 0.5).
|
||||
/// Must be in `(0, 1]`.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
null_threshold: Option<f64>,
|
||||
},
|
||||
/// A numeric value.
|
||||
#[serde(rename = "number")]
|
||||
Number {
|
||||
/// What the field should contain.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
instructions: Option<String>,
|
||||
/// Unit label for the number (e.g. `USD`).
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
unit: Option<String>,
|
||||
/// Whether the number must be an integer. Numbers and durations only.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
integer: Option<bool>,
|
||||
/// Number of decimal places (0–6). Numbers, durations and number
|
||||
/// lists only.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
decimals: Option<u8>,
|
||||
/// Probability of null above which the value is null (default 0.5).
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
null_threshold: Option<f64>,
|
||||
},
|
||||
/// An ISO date value.
|
||||
#[serde(rename = "date")]
|
||||
Date {
|
||||
/// What the field should contain.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
instructions: Option<String>,
|
||||
/// Probability of null above which the value is null (default 0.5).
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
null_threshold: Option<f64>,
|
||||
},
|
||||
/// An ISO time value.
|
||||
#[serde(rename = "time")]
|
||||
Time {
|
||||
/// What the field should contain.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
instructions: Option<String>,
|
||||
/// Probability of null above which the value is null (default 0.5).
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
null_threshold: Option<f64>,
|
||||
},
|
||||
/// An ISO date-time value.
|
||||
#[serde(rename = "datetime")]
|
||||
Datetime {
|
||||
/// What the field should contain.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
instructions: Option<String>,
|
||||
/// Probability of null above which the value is null (default 0.5).
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
null_threshold: Option<f64>,
|
||||
},
|
||||
/// A duration value.
|
||||
#[serde(rename = "duration")]
|
||||
Duration {
|
||||
/// What the field should contain.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
instructions: Option<String>,
|
||||
/// Unit label
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
unit: Option<DurationUnit>,
|
||||
/// Whether the duration must be an integer. Numbers and durations only.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
integer: Option<bool>,
|
||||
/// Number of decimal places (0–6). Numbers, durations and number
|
||||
/// lists only.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
decimals: Option<u8>,
|
||||
/// Probability of null above which the value is null (default 0.5).
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
null_threshold: Option<f64>,
|
||||
},
|
||||
/// A list of typed items.
|
||||
#[serde(rename = "list")]
|
||||
List {
|
||||
/// What the field should contain.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
instructions: Option<String>,
|
||||
/// Required for list fields: the type of each item.
|
||||
items: ListItems,
|
||||
/// Number of decimal places (0–6) for number lists.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
decimals: Option<u8>,
|
||||
/// Probability of null above which the value is null (default 0.5).
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
null_threshold: Option<f64>,
|
||||
},
|
||||
/// A probabilistic estimate over a bounded grid.
|
||||
#[serde(rename = "estimate")]
|
||||
Estimate {
|
||||
/// What the field should contain.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
instructions: Option<String>,
|
||||
/// Required for estimate/forecast: lower bound (a number, or an ISO
|
||||
/// date for date estimates).
|
||||
min: NumberOrDate,
|
||||
/// Required for estimate/forecast: upper bound (a number, or an ISO
|
||||
/// date for date estimates).
|
||||
max: NumberOrDate,
|
||||
/// Estimate grid: a positive number (days for date estimates), or
|
||||
/// `"month"` / `"year"`.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
step: Option<EstimateStep>,
|
||||
/// Grid scale: `linear` (default) or `log`.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
scale: Option<Scale>,
|
||||
/// Probability of null above which the value is null (default 0.5).
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
null_threshold: Option<f64>,
|
||||
},
|
||||
/// A forecast: a sequence of estimates over a horizon.
|
||||
#[serde(rename = "forecast")]
|
||||
Forecast {
|
||||
/// What the field should contain.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
instructions: Option<String>,
|
||||
/// Required for estimate/forecast: lower bound (a number, or an ISO
|
||||
/// date for date estimates).
|
||||
min: NumberOrDate,
|
||||
/// Required for estimate/forecast: upper bound (a number, or an ISO
|
||||
/// date for date estimates).
|
||||
max: NumberOrDate,
|
||||
/// Estimate grid: a positive number (days for date estimates), or
|
||||
/// `"month"` / `"year"`.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
step: Option<EstimateStep>,
|
||||
/// Grid scale: `linear` (default) or `log`.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
scale: Option<Scale>,
|
||||
/// Forecast only: number of steps (1–64).
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
horizon: Option<u32>,
|
||||
/// Forecast only: the interval between steps.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
interval: Option<ForecastInterval>,
|
||||
/// Probability of null above which the value is null (default 0.5).
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
null_threshold: Option<f64>,
|
||||
},
|
||||
}
|
||||
|
||||
/// An image input. Either a data URL string or an explicit base64 object.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(untagged)]
|
||||
pub enum Image {
|
||||
/// A data URL string (must start with `data:`, case-insensitive).
|
||||
DataUrl(
|
||||
/// The data URL, e.g. `data:image/png;base64,...`.
|
||||
String,
|
||||
),
|
||||
/// An explicitly split base64 image.
|
||||
Base64 {
|
||||
/// MIME content type of the image (PNG, JPEG or WebP).
|
||||
content_type: String,
|
||||
/// Base64-encoded image bytes.
|
||||
base64: String,
|
||||
},
|
||||
}
|
||||
|
||||
/// Optional request settings.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
|
||||
pub struct RequestOptions {
|
||||
/// Add 99 percentiles to estimates (default `false`).
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub include_quantiles: Option<bool>,
|
||||
/// Return the text that spans index into (default `false`).
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub return_state_text: Option<bool>,
|
||||
}
|
||||
|
||||
/// The extraction request body.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
pub struct ExtractionRequest<Model> {
|
||||
/// The model selector
|
||||
pub model: Model,
|
||||
/// The text to read and predict from. Must be non-empty unless images
|
||||
/// are sent. Text answers are spans of this string.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub state: Option<String>,
|
||||
/// Required. Map of field name to a typed field spec. At least one
|
||||
/// field; no upper limit. Fields that do not fit next to the state in
|
||||
/// one 8,192-token pass are split across parallel passes. Field names
|
||||
/// must match `^[A-Za-z_][A-Za-z0-9_.\-]{0,63}$`.
|
||||
pub fields: HashMap<String, Field>,
|
||||
/// Optional embedded PNG, JPEG or WebP images placed before the state
|
||||
/// (max 4; 4 MiB each, 8 MiB total decoded; request body max 16 MiB).
|
||||
/// Remote URLs are not accepted.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub images: Option<Vec<Image>>,
|
||||
/// ISO date that anchors dates written without a year.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub reference_date: Option<Date>,
|
||||
/// Optional request settings.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub options: Option<RequestOptions>,
|
||||
}
|
||||
|
||||
/// The value of an answer. Shape depends on the field type; `null` when the
|
||||
/// input does not determine it.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(untagged)]
|
||||
pub enum List {
|
||||
// A list of strings
|
||||
Strings(Vec<String>),
|
||||
// A list of numbers
|
||||
Numbers(Vec<f64>),
|
||||
// A list of dates
|
||||
Dates(Vec<Date>),
|
||||
}
|
||||
|
||||
/// A forecast step: one estimate per step, with median and interval bounds.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
pub struct ForecastStep {
|
||||
/// The point (median) value for this step.
|
||||
pub value: f64,
|
||||
/// Mean of the distribution for this step.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub mean: Option<f64>,
|
||||
/// Lower bound of the interval for this step.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub p10: Option<f64>,
|
||||
/// Upper bound of the interval for this step.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub p90: Option<f64>,
|
||||
}
|
||||
|
||||
/// One answer per requested field, keyed by field name. Tagged by `type`,
|
||||
/// mirroring the field spec that produced it.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(tag = "type", rename_all = "lowercase")]
|
||||
pub enum Answer {
|
||||
/// Answer for a text field: an exact span of the state, with character
|
||||
/// offsets.
|
||||
#[serde(rename = "text")]
|
||||
Text {
|
||||
/// The extracted value, or null when the input does not determine it.
|
||||
value: Option<String>,
|
||||
/// The model's best guess before applying the null threshold.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
guess: Option<String>,
|
||||
/// Probability that the field is null.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
p_null: Option<f64>,
|
||||
/// Confidence score in `[0, 1]`.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
confidence: Option<f64>,
|
||||
/// Character offsets `[start, end]` of the span into the state.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
span: Option<[usize; 2]>,
|
||||
},
|
||||
/// Answer for a number field.
|
||||
#[serde(rename = "number")]
|
||||
Number {
|
||||
/// The extracted value, or null when the input does not determine it.
|
||||
value: Option<f64>,
|
||||
/// The model's best guess before applying the null threshold.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
guess: Option<f64>,
|
||||
/// Probability that the field is null.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
p_null: Option<f64>,
|
||||
/// Confidence score in `[0, 1]`.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
confidence: Option<f64>,
|
||||
},
|
||||
/// Answer for a date field.
|
||||
#[serde(rename = "date")]
|
||||
Date {
|
||||
/// The computed/extracted ISO date, or null.
|
||||
value: Option<Date>,
|
||||
/// The model's best guess before applying the null threshold.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
guess: Option<Date>,
|
||||
/// Probability that the field is null.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
p_null: Option<f64>,
|
||||
/// Confidence score in `[0, 1]`.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
confidence: Option<f64>,
|
||||
},
|
||||
/// Answer for a time field.
|
||||
#[serde(rename = "time")]
|
||||
Time {
|
||||
/// The extracted ISO time, or null.
|
||||
value: Option<Time>,
|
||||
/// The model's best guess before applying the null threshold.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
guess: Option<Time>,
|
||||
/// Probability that the field is null.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
p_null: Option<f64>,
|
||||
/// Confidence score in `[0, 1]`.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
confidence: Option<f64>,
|
||||
},
|
||||
/// Answer for a datetime field.
|
||||
#[serde(rename = "datetime")]
|
||||
Datetime {
|
||||
/// The extracted ISO date-time, or null.
|
||||
value: Option<DateTime>,
|
||||
/// The model's best guess before applying the null threshold.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
guess: Option<DateTime>,
|
||||
/// Probability that the field is null.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
p_null: Option<f64>,
|
||||
/// Confidence score in `[0, 1]`.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
confidence: Option<f64>,
|
||||
},
|
||||
/// Answer for a duration field.
|
||||
#[serde(rename = "duration")]
|
||||
Duration {
|
||||
/// The extracted duration, or null.
|
||||
value: Option<f64>,
|
||||
/// The model's best guess before applying the null threshold.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
guess: Option<f64>,
|
||||
/// Probability that the field is null.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
p_null: Option<f64>,
|
||||
/// Confidence score in `[0, 1]`.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
confidence: Option<f64>,
|
||||
},
|
||||
/// Answer for a list field: values with per-item spans.
|
||||
#[serde(rename = "list")]
|
||||
List {
|
||||
/// The extracted items, or null.
|
||||
value: Option<List>,
|
||||
/// Per-item character offsets `[start, end]` into the state.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
spans: Option<Vec<[usize; 2]>>,
|
||||
/// Probability that the field is null.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
p_null: Option<f64>,
|
||||
/// Confidence score in `[0, 1]`.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
confidence: Option<f64>,
|
||||
},
|
||||
/// Answer for an estimate field: a distribution summary.
|
||||
#[serde(rename = "estimate")]
|
||||
Estimate {
|
||||
/// The point estimate (median).
|
||||
value: NumberOrDate,
|
||||
/// Mean of the estimated distribution.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
mean: Option<f64>,
|
||||
/// Median of the estimated distribution.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
median: Option<f64>,
|
||||
/// 10th percentile of the estimated distribution.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
p10: Option<f64>,
|
||||
/// 90th percentile of the estimated distribution.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
p90: Option<f64>,
|
||||
/// Probability the true value is below the grid minimum.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
p_below: Option<f64>,
|
||||
/// Probability the true value is above the grid maximum.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
p_above: Option<f64>,
|
||||
/// 99 percentiles, when `options.include_quantiles` is set.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
quantiles: Option<Vec<f64>>,
|
||||
},
|
||||
/// Answer for a forecast field: one estimate per step.
|
||||
#[serde(rename = "forecast")]
|
||||
Forecast {
|
||||
/// One estimate per step.
|
||||
#[serde(default)]
|
||||
steps: Vec<ForecastStep>,
|
||||
/// Probability that the field is null.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
p_null: Option<f64>,
|
||||
/// Confidence score in `[0, 1]`.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
confidence: Option<f64>,
|
||||
},
|
||||
}
|
||||
|
||||
/// Token and pass accounting for a request.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
pub struct Usage {
|
||||
/// Tokens consumed by the state text.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub state_tokens: Option<u64>,
|
||||
/// Tokens consumed by images.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub image_tokens: Option<u64>,
|
||||
/// Tokens consumed by the field schema.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub schema_tokens: Option<u64>,
|
||||
/// Total input tokens.
|
||||
pub input_tokens: u64,
|
||||
/// Total output tokens.
|
||||
pub output_tokens: u64,
|
||||
/// Number of parallel passes used.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub passes: Option<u64>,
|
||||
}
|
||||
|
||||
/// The extraction response body.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
pub struct ExtractionResponse<Model> {
|
||||
/// The model that produced this response
|
||||
pub model: Model,
|
||||
/// One answer per field, keyed by field name. `value` is null when the
|
||||
/// input does not determine it.
|
||||
pub answers: HashMap<String, Answer>,
|
||||
/// Whether the input was truncated to fit the context window.
|
||||
pub truncated: bool,
|
||||
/// The text that spans index into, when `options.return_state_text` is set.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub state_text: Option<String>,
|
||||
/// Token and pass accounting for the request.
|
||||
pub usage: Usage,
|
||||
}
|
||||
@@ -1 +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
43
src/openai/models.rs
Normal 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
233
src/openai/system_one.rs
Normal 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,
|
||||
}
|
||||
450
src/server.rs
450
src/server.rs
@@ -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)
|
||||
}
|
||||
738
src/server/chat/completions.rs
Normal file
738
src/server/chat/completions.rs
Normal 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
1
src/server/chat/mod.rs
Normal file
@@ -0,0 +1 @@
|
||||
pub mod completions;
|
||||
274
src/server/decisions.rs
Normal file
274
src/server/decisions.rs
Normal 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
313
src/server/embeddings.rs
Normal 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
217
src/server/extraction.rs
Normal file
@@ -0,0 +1,217 @@
|
||||
use crate::CloudflareCredentials;
|
||||
use crate::openai;
|
||||
use crate::server::AppState;
|
||||
use crate::server::Upstream;
|
||||
use crate::server::take_random_by_swapping;
|
||||
use axum::Json;
|
||||
use axum::extract::State;
|
||||
use axum::http::StatusCode;
|
||||
use axum::response::IntoResponse;
|
||||
use axum::response::Response;
|
||||
use axum_extra::routing::TypedPath;
|
||||
use ecow::EcoString;
|
||||
use headers::Header;
|
||||
use headers::authorization::Bearer;
|
||||
use mitsein::vec1::Vec1;
|
||||
use reqwest::Certificate;
|
||||
use secrecy::ExposeSecret;
|
||||
use serde::Deserialize;
|
||||
use serde_with::DeserializeFromStr;
|
||||
use serde_with::SerializeDisplay;
|
||||
use std::collections::VecDeque;
|
||||
use strum::EnumString;
|
||||
use strum::VariantArray;
|
||||
|
||||
#[derive(Deserialize, TypedPath)]
|
||||
#[typed_path("/v1/extraction")]
|
||||
pub struct V1ExtractionPath;
|
||||
|
||||
#[derive(
|
||||
Clone, Debug, EnumString, DeserializeFromStr, strum::Display, SerializeDisplay, VariantArray,
|
||||
)]
|
||||
#[strum(serialize_all = "lowercase")]
|
||||
pub enum Shortcut {
|
||||
M,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
enum Provider {
|
||||
Cloudflare,
|
||||
}
|
||||
|
||||
impl Shortcut {
|
||||
fn suggestions(&self) -> Vec<(Provider, EcoString)> {
|
||||
use Provider as P;
|
||||
use Shortcut as S;
|
||||
|
||||
match self {
|
||||
S::M => {
|
||||
vec![(P::Cloudflare, "@cf/cloudflare/clairic".into())]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct ExtractionResponse(pub openai::extraction::ExtractionResponse<String>);
|
||||
|
||||
impl IntoResponse for ExtractionResponse {
|
||||
fn into_response(self) -> Response {
|
||||
let Self(extraction) = self;
|
||||
|
||||
Json(extraction).into_response()
|
||||
}
|
||||
}
|
||||
|
||||
pub enum ExtractionError {
|
||||
NoneSucceeded,
|
||||
}
|
||||
|
||||
impl IntoResponse for ExtractionError {
|
||||
fn into_response(self) -> Response {
|
||||
match self {
|
||||
ExtractionError::NoneSucceeded => StatusCode::SERVICE_UNAVAILABLE.into_response(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(state, request))]
|
||||
pub async fn create_extraction(
|
||||
_: V1ExtractionPath,
|
||||
State(state): State<AppState>,
|
||||
Json(request): Json<openai::extraction::ExtractionRequest<Shortcut>>,
|
||||
) -> Result<ExtractionResponse, ExtractionError> {
|
||||
let shortcut = request.model.clone();
|
||||
let mut model_upstreams = VecDeque::new();
|
||||
|
||||
for (provider, model) in shortcut.suggestions() {
|
||||
let upstreams: Vec<Upstream> =
|
||||
match provider {
|
||||
Provider::Cloudflare => state
|
||||
.app_args
|
||||
.cloudflare_workers_ai_credentials
|
||||
.iter()
|
||||
.cloned()
|
||||
.map(
|
||||
|CloudflareCredentials {
|
||||
account_id,
|
||||
api_key,
|
||||
}| {
|
||||
Upstream::builder()
|
||||
.url(
|
||||
format!("https://api.cloudflare.com/client/v4/accounts/{account_id}/ai/run/{model}")
|
||||
.parse()
|
||||
.expect("TODO"),
|
||||
)
|
||||
.api_key(api_key)
|
||||
.build()
|
||||
},
|
||||
)
|
||||
.collect(),
|
||||
};
|
||||
|
||||
if let Ok(upstreams) = Vec1::try_from(upstreams) {
|
||||
model_upstreams.push_back((model, upstreams));
|
||||
}
|
||||
}
|
||||
|
||||
while let Some((model, upstreams)) = model_upstreams.pop_front() {
|
||||
let (upstream, upstreams) = take_random_by_swapping(upstreams, &mut rand::rng());
|
||||
if let Some(upstreams) = upstreams {
|
||||
model_upstreams.push_back((model.clone(), upstreams));
|
||||
}
|
||||
|
||||
tracing::info!(?model, ?upstream);
|
||||
|
||||
let openai::extraction::ExtractionRequest {
|
||||
model: _shortcut,
|
||||
state,
|
||||
fields,
|
||||
images,
|
||||
reference_date,
|
||||
options,
|
||||
} = request.clone();
|
||||
|
||||
let request = openai::extraction::ExtractionRequest {
|
||||
model,
|
||||
state,
|
||||
fields,
|
||||
images,
|
||||
reference_date,
|
||||
options,
|
||||
};
|
||||
|
||||
let client = reqwest::Client::builder()
|
||||
.tls_certs_only(
|
||||
webpki_root_certs::TLS_SERVER_ROOT_CERTS
|
||||
.iter()
|
||||
.filter_map(|cert| Certificate::from_der(cert).ok()),
|
||||
)
|
||||
.build()
|
||||
.expect("TODO");
|
||||
|
||||
let mut upstream_request = client.post(upstream.url).json(&request);
|
||||
|
||||
if let Some(api_key) = upstream.api_key {
|
||||
let authorization =
|
||||
headers::Authorization::bearer(api_key.expose_secret()).expect("TODO");
|
||||
upstream_request = upstream_request.header(headers::Authorization::<Bearer>::name(), {
|
||||
let mut vec = vec![];
|
||||
|
||||
authorization.encode(&mut vec);
|
||||
|
||||
let [one] = vec.try_into().expect("TODO");
|
||||
|
||||
one
|
||||
});
|
||||
}
|
||||
|
||||
let upstream_response = match upstream_request.send().await {
|
||||
Ok(upstream_response) => upstream_response,
|
||||
Err(upstream_error) => {
|
||||
tracing::error!(?upstream_error);
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
if let Err(upstream_error) = upstream_response.error_for_status_ref() {
|
||||
tracing::error!(?upstream_error);
|
||||
match upstream_response.text().await {
|
||||
Ok(body) => tracing::error!(?body),
|
||||
Err(body_error) => tracing::error!(?body_error),
|
||||
};
|
||||
continue;
|
||||
};
|
||||
|
||||
let response: serde_json::Value = match upstream_response.json().await {
|
||||
Ok(response) => response,
|
||||
Err(json_error) => {
|
||||
tracing::error!(?json_error);
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
match openai::extraction::ExtractionResponse::deserialize(
|
||||
response.clone(), // TODO: not need to clone
|
||||
) {
|
||||
Ok(response) => return Ok(ExtractionResponse(response)),
|
||||
Err(structure_error) => {
|
||||
#[derive(Deserialize)]
|
||||
struct CloudflareRunResponse {
|
||||
result: openai::extraction::ExtractionResponse<String>,
|
||||
success: bool,
|
||||
errors: Vec<serde_json::Value>, // TODO
|
||||
messages: Vec<serde_json::Value>, // TODO
|
||||
}
|
||||
|
||||
match CloudflareRunResponse::deserialize(response) {
|
||||
Ok(response) => return Ok(ExtractionResponse(response.result)),
|
||||
Err(cloudflare_structure_error) => {
|
||||
tracing::error!(?structure_error);
|
||||
tracing::error!(?cloudflare_structure_error);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(ExtractionError::NoneSucceeded)
|
||||
}
|
||||
12
src/server/frontend/index.html
Normal file
12
src/server/frontend/index.html
Normal file
@@ -0,0 +1,12 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>Basic Page</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Hello, world!</h1>
|
||||
<p>This is a basic HTML page.</p>
|
||||
</body>
|
||||
</html>
|
||||
11
src/server/frontend/mod.rs
Normal file
11
src/server/frontend/mod.rs
Normal file
@@ -0,0 +1,11 @@
|
||||
use axum::response::Html;
|
||||
use axum_extra::routing::TypedPath;
|
||||
use serde::Deserialize;
|
||||
|
||||
#[derive(Deserialize, TypedPath)]
|
||||
#[typed_path("/")]
|
||||
pub struct IndexPath;
|
||||
|
||||
pub async fn spa(_: IndexPath) -> Html<&'static str> {
|
||||
Html(include_str!("index.html"))
|
||||
}
|
||||
59
src/server/mod.rs
Normal file
59
src/server/mod.rs
Normal 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
36
src/server/models.rs
Normal 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
294
src/server/system_one.rs
Normal 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)
|
||||
}
|
||||
Reference in New Issue
Block a user