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
|
/target
|
||||||
|
|
||||||
|
.history/
|
||||||
|
|||||||
543
Cargo.lock
generated
543
Cargo.lock
generated
@@ -32,6 +32,15 @@ dependencies = [
|
|||||||
"alloc-no-stdlib",
|
"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]]
|
[[package]]
|
||||||
name = "anstream"
|
name = "anstream"
|
||||||
version = "1.0.0"
|
version = "1.0.0"
|
||||||
@@ -282,6 +291,12 @@ version = "0.23.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "ac07cdecf99051d9a5238b80f35af32cdeba5b336e55d957b318b50137e18da5"
|
checksum = "ac07cdecf99051d9a5238b80f35af32cdeba5b336e55d957b318b50137e18da5"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "bitflags"
|
||||||
|
version = "1.3.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "bitflags"
|
name = "bitflags"
|
||||||
version = "2.13.2"
|
version = "2.13.2"
|
||||||
@@ -318,6 +333,15 @@ dependencies = [
|
|||||||
"alloc-stdlib",
|
"alloc-stdlib",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "bs58"
|
||||||
|
version = "0.5.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "bf88ba1141d185c399bee5288d850d63b8369520c1eafc32a0430b5b6c287bf4"
|
||||||
|
dependencies = [
|
||||||
|
"tinyvec",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "bumpalo"
|
name = "bumpalo"
|
||||||
version = "3.20.3"
|
version = "3.20.3"
|
||||||
@@ -348,12 +372,6 @@ version = "1.0.5"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "4e7648175b45a9a48536d676f68d918270699102aa8dab5496df06904c914600"
|
checksum = "4e7648175b45a9a48536d676f68d918270699102aa8dab5496df06904c914600"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "cfg_aliases"
|
|
||||||
version = "0.2.2"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "f079e83a288787bcd14a6aea84cee5c87a67c5a3e660c30f557a3d24761b3527"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "chacha20"
|
name = "chacha20"
|
||||||
version = "0.10.2"
|
version = "0.10.2"
|
||||||
@@ -365,6 +383,18 @@ dependencies = [
|
|||||||
"rand_core 0.10.1",
|
"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]]
|
[[package]]
|
||||||
name = "clap"
|
name = "clap"
|
||||||
version = "4.6.7"
|
version = "4.6.7"
|
||||||
@@ -405,35 +435,6 @@ version = "1.1.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "1c133bc6a41be0d194c306b5506d15e6feeea7b1d6604bd3f8310dfb2ca96486"
|
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]]
|
[[package]]
|
||||||
name = "cmake"
|
name = "cmake"
|
||||||
version = "0.1.58"
|
version = "0.1.58"
|
||||||
@@ -622,8 +623,18 @@ version = "0.21.3"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "9cdf337090841a411e2a7f3deb9187445851f91b309c0c0a29e05f74a00a48c0"
|
checksum = "9cdf337090841a411e2a7f3deb9187445851f91b309c0c0a29e05f74a00a48c0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"darling_core",
|
"darling_core 0.21.3",
|
||||||
"darling_macro",
|
"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]]
|
[[package]]
|
||||||
@@ -639,28 +650,86 @@ dependencies = [
|
|||||||
"syn 2.0.119",
|
"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]]
|
[[package]]
|
||||||
name = "darling_macro"
|
name = "darling_macro"
|
||||||
version = "0.21.3"
|
version = "0.21.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d38308df82d1080de0afee5d069fa14b0326a88c14f15c5ccda35b4a6c414c81"
|
checksum = "d38308df82d1080de0afee5d069fa14b0326a88c14f15c5ccda35b4a6c414c81"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"darling_core",
|
"darling_core 0.21.3",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.119",
|
"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]]
|
[[package]]
|
||||||
name = "data-encoding"
|
name = "data-encoding"
|
||||||
version = "2.11.1"
|
version = "2.11.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "4583a4551df46e2792f82ceeac45e850d2e2d5debba0b91f102385cda5b11f06"
|
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]]
|
[[package]]
|
||||||
name = "deranged"
|
name = "deranged"
|
||||||
version = "0.5.8"
|
version = "0.5.8"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c"
|
checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c"
|
||||||
|
dependencies = [
|
||||||
|
"serde_core",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "digest"
|
name = "digest"
|
||||||
@@ -770,7 +839,7 @@ version = "0.15.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "4bd536557b58c682b217b8fb199afdff47cd3eff260623f19e77074eb073d63a"
|
checksum = "4bd536557b58c682b217b8fb199afdff47cd3eff260623f19e77074eb073d63a"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"darling",
|
"darling 0.21.3",
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.119",
|
"syn 2.0.119",
|
||||||
@@ -945,10 +1014,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0"
|
checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
"js-sys",
|
|
||||||
"libc",
|
"libc",
|
||||||
"wasi",
|
"wasi",
|
||||||
"wasm-bindgen",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -970,11 +1037,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099"
|
checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
"js-sys",
|
|
||||||
"libc",
|
"libc",
|
||||||
"r-efi 6.0.0",
|
"r-efi 6.0.0",
|
||||||
"rand_core 0.10.1",
|
"rand_core 0.10.1",
|
||||||
"wasm-bindgen",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -989,13 +1054,19 @@ dependencies = [
|
|||||||
"futures-core",
|
"futures-core",
|
||||||
"futures-sink",
|
"futures-sink",
|
||||||
"http",
|
"http",
|
||||||
"indexmap",
|
"indexmap 2.14.2",
|
||||||
"slab",
|
"slab",
|
||||||
"tokio",
|
"tokio",
|
||||||
"tokio-util",
|
"tokio-util",
|
||||||
"tracing",
|
"tracing",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "hashbrown"
|
||||||
|
version = "0.12.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "hashbrown"
|
name = "hashbrown"
|
||||||
version = "0.17.1"
|
version = "0.17.1"
|
||||||
@@ -1032,6 +1103,12 @@ version = "0.5.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
|
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "hex"
|
||||||
|
version = "0.4.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "hickory-net"
|
name = "hickory-net"
|
||||||
version = "0.26.3"
|
version = "0.26.3"
|
||||||
@@ -1203,11 +1280,33 @@ dependencies = [
|
|||||||
"percent-encoding",
|
"percent-encoding",
|
||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
"socket2",
|
"socket2",
|
||||||
"system-configuration",
|
|
||||||
"tokio",
|
"tokio",
|
||||||
"tower-service",
|
"tower-service",
|
||||||
"tracing",
|
"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]]
|
[[package]]
|
||||||
@@ -1320,6 +1419,17 @@ dependencies = [
|
|||||||
"icu_properties",
|
"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]]
|
[[package]]
|
||||||
name = "indexmap"
|
name = "indexmap"
|
||||||
version = "2.14.2"
|
version = "2.14.2"
|
||||||
@@ -1327,7 +1437,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "cc4e190f5d26ca7051642629da2c52fc03bde85a03197c99408dcd291734c855"
|
checksum = "cc4e190f5d26ca7051642629da2c52fc03bde85a03197c99408dcd291734c855"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"equivalent",
|
"equivalent",
|
||||||
"hashbrown",
|
"hashbrown 0.17.1",
|
||||||
|
"serde",
|
||||||
|
"serde_core",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -1367,12 +1479,75 @@ dependencies = [
|
|||||||
"either",
|
"either",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "itertools"
|
||||||
|
version = "0.15.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8b4baf93f58d4425749ca49a51c50ebab072c5df6994d08fed93541c331481dc"
|
||||||
|
dependencies = [
|
||||||
|
"either",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "itoa"
|
name = "itoa"
|
||||||
version = "1.0.18"
|
version = "1.0.18"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
|
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]]
|
[[package]]
|
||||||
name = "jni"
|
name = "jni"
|
||||||
version = "0.22.4"
|
version = "0.22.4"
|
||||||
@@ -1443,6 +1618,42 @@ dependencies = [
|
|||||||
"wasm-bindgen",
|
"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]]
|
[[package]]
|
||||||
name = "lazy_static"
|
name = "lazy_static"
|
||||||
version = "1.5.0"
|
version = "1.5.0"
|
||||||
@@ -1482,12 +1693,6 @@ version = "0.4.34"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6"
|
checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "lru-slab"
|
|
||||||
version = "0.1.3"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "4050469837a6ff301cd14c1f8f24f88549e6d548f24f64e2148eb0f72cebc51f"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "matchers"
|
name = "matchers"
|
||||||
version = "0.2.0"
|
version = "0.2.0"
|
||||||
@@ -1553,9 +1758,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "8a49aef75c004fb496a23f175395415ecfe2a630e4c6175ae6f7d9cc1a19332e"
|
checksum = "8a49aef75c004fb496a23f175395415ecfe2a630e4c6175ae6f7d9cc1a19332e"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"either",
|
"either",
|
||||||
"indexmap",
|
"indexmap 2.14.2",
|
||||||
"itertools",
|
"itertools 0.14.0",
|
||||||
"schemars",
|
"schemars 1.2.2",
|
||||||
"serde",
|
"serde",
|
||||||
"smallvec",
|
"smallvec",
|
||||||
]
|
]
|
||||||
@@ -1705,6 +1910,15 @@ version = "1.15.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "05c8b63e8d9609db387f0324918f81d68fe27748f084ef092fb35954d0539a85"
|
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]]
|
[[package]]
|
||||||
name = "potential_utf"
|
name = "potential_utf"
|
||||||
version = "0.1.6"
|
version = "0.1.6"
|
||||||
@@ -1765,63 +1979,6 @@ dependencies = [
|
|||||||
"psl-types",
|
"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]]
|
[[package]]
|
||||||
name = "quote"
|
name = "quote"
|
||||||
version = "1.0.47"
|
version = "1.0.47"
|
||||||
@@ -1889,22 +2046,13 @@ version = "0.10.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69"
|
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]]
|
[[package]]
|
||||||
name = "redox_syscall"
|
name = "redox_syscall"
|
||||||
version = "0.5.18"
|
version = "0.5.18"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d"
|
checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags",
|
"bitflags 2.13.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -1954,7 +2102,6 @@ dependencies = [
|
|||||||
"bytes",
|
"bytes",
|
||||||
"cookie",
|
"cookie",
|
||||||
"cookie_store",
|
"cookie_store",
|
||||||
"encoding_rs",
|
|
||||||
"futures-core",
|
"futures-core",
|
||||||
"futures-util",
|
"futures-util",
|
||||||
"h2",
|
"h2",
|
||||||
@@ -1967,12 +2114,10 @@ dependencies = [
|
|||||||
"hyper-util",
|
"hyper-util",
|
||||||
"js-sys",
|
"js-sys",
|
||||||
"log",
|
"log",
|
||||||
"mime",
|
|
||||||
"mime_guess",
|
"mime_guess",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
"percent-encoding",
|
"percent-encoding",
|
||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
"quinn",
|
|
||||||
"rustls",
|
"rustls",
|
||||||
"rustls-pki-types",
|
"rustls-pki-types",
|
||||||
"rustls-platform-verifier",
|
"rustls-platform-verifier",
|
||||||
@@ -1984,7 +2129,7 @@ dependencies = [
|
|||||||
"tokio-rustls",
|
"tokio-rustls",
|
||||||
"tokio-util",
|
"tokio-util",
|
||||||
"tower",
|
"tower",
|
||||||
"tower-http",
|
"tower-http 0.6.11",
|
||||||
"tower-service",
|
"tower-service",
|
||||||
"url",
|
"url",
|
||||||
"wasm-bindgen",
|
"wasm-bindgen",
|
||||||
@@ -2026,12 +2171,6 @@ dependencies = [
|
|||||||
"windows-sys 0.52.0",
|
"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]]
|
[[package]]
|
||||||
name = "rustc_version"
|
name = "rustc_version"
|
||||||
version = "0.4.1"
|
version = "0.4.1"
|
||||||
@@ -2048,6 +2187,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "0d41d731c7d2f962d1ccc364cec258de3c0e93b38c2fb3ba97ac74513048d634"
|
checksum = "0d41d731c7d2f962d1ccc364cec258de3c0e93b38c2fb3ba97ac74513048d634"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"aws-lc-rs",
|
"aws-lc-rs",
|
||||||
|
"log",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
"rustls-pki-types",
|
"rustls-pki-types",
|
||||||
"rustls-webpki",
|
"rustls-webpki",
|
||||||
@@ -2073,7 +2213,6 @@ version = "1.15.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "2f4925028c7eb5d1fcdaf196971378ed9d2c1c4efc7dc5d011256f76c99c0a96"
|
checksum = "2f4925028c7eb5d1fcdaf196971378ed9d2c1c4efc7dc5d011256f76c99c0a96"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"web-time",
|
|
||||||
"zeroize",
|
"zeroize",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -2146,6 +2285,18 @@ dependencies = [
|
|||||||
"windows-sys 0.61.2",
|
"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]]
|
[[package]]
|
||||||
name = "schemars"
|
name = "schemars"
|
||||||
version = "1.2.2"
|
version = "1.2.2"
|
||||||
@@ -2153,7 +2304,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "687274d293b6cdc6e73e0fee520bf2049650090d7164f87672d212a3c530cf4a"
|
checksum = "687274d293b6cdc6e73e0fee520bf2049650090d7164f87672d212a3c530cf4a"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"dyn-clone",
|
"dyn-clone",
|
||||||
"indexmap",
|
"indexmap 2.14.2",
|
||||||
"ref-cast",
|
"ref-cast",
|
||||||
"schemars_derive",
|
"schemars_derive",
|
||||||
"serde",
|
"serde",
|
||||||
@@ -2194,7 +2345,7 @@ version = "3.7.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d"
|
checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags",
|
"bitflags 2.13.2",
|
||||||
"core-foundation 0.10.1",
|
"core-foundation 0.10.1",
|
||||||
"core-foundation-sys",
|
"core-foundation-sys",
|
||||||
"libc",
|
"libc",
|
||||||
@@ -2265,7 +2416,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "b2f2d7ff8a2140333718bb329f5c40fc5f0865b84c426183ce14c97d2ab8154f"
|
checksum = "b2f2d7ff8a2140333718bb329f5c40fc5f0865b84c426183ce14c97d2ab8154f"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"form_urlencoded",
|
"form_urlencoded",
|
||||||
"indexmap",
|
"indexmap 2.14.2",
|
||||||
"itoa",
|
"itoa",
|
||||||
"ryu",
|
"ryu",
|
||||||
"serde_core",
|
"serde_core",
|
||||||
@@ -2307,6 +2458,39 @@ dependencies = [
|
|||||||
"serde",
|
"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]]
|
[[package]]
|
||||||
name = "sha1"
|
name = "sha1"
|
||||||
version = "0.10.7"
|
version = "0.10.7"
|
||||||
@@ -2395,6 +2579,27 @@ version = "0.11.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
|
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]]
|
[[package]]
|
||||||
name = "subtle"
|
name = "subtle"
|
||||||
version = "2.6.1"
|
version = "2.6.1"
|
||||||
@@ -2449,7 +2654,7 @@ version = "0.7.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "a13f3d0daba03132c0aa9767f98351b3488edc2c100cda2d2ec2b04f3d8d3c8b"
|
checksum = "a13f3d0daba03132c0aa9767f98351b3488edc2c100cda2d2ec2b04f3d8d3c8b"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags",
|
"bitflags 2.13.2",
|
||||||
"core-foundation 0.9.4",
|
"core-foundation 0.9.4",
|
||||||
"system-configuration-sys",
|
"system-configuration-sys",
|
||||||
]
|
]
|
||||||
@@ -2641,7 +2846,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "4cfcf7e2740e6fc6d4d688b4ef00650406bb94adf4731e43c096c3a19fe40840"
|
checksum = "4cfcf7e2740e6fc6d4d688b4ef00650406bb94adf4731e43c096c3a19fe40840"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"async-compression",
|
"async-compression",
|
||||||
"bitflags",
|
"bitflags 2.13.2",
|
||||||
"bytes",
|
"bytes",
|
||||||
"futures-core",
|
"futures-core",
|
||||||
"futures-util",
|
"futures-util",
|
||||||
@@ -2657,6 +2862,21 @@ dependencies = [
|
|||||||
"url",
|
"url",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tower-http"
|
||||||
|
version = "0.7.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "08a05a66a4fdd61cbbe0a1d755ffe0ca6aba159dd4820936a0ff8a8278245b9c"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags 2.13.2",
|
||||||
|
"bytes",
|
||||||
|
"http",
|
||||||
|
"percent-encoding",
|
||||||
|
"pin-project-lite",
|
||||||
|
"tower-layer",
|
||||||
|
"tower-service",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tower-layer"
|
name = "tower-layer"
|
||||||
version = "0.3.3"
|
version = "0.3.3"
|
||||||
@@ -2957,16 +3177,6 @@ dependencies = [
|
|||||||
"wasm-bindgen",
|
"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]]
|
[[package]]
|
||||||
name = "webpki-root-certs"
|
name = "webpki-root-certs"
|
||||||
version = "1.0.9"
|
version = "1.0.9"
|
||||||
@@ -2991,6 +3201,41 @@ dependencies = [
|
|||||||
"windows-sys 0.61.2",
|
"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]]
|
[[package]]
|
||||||
name = "windows-link"
|
name = "windows-link"
|
||||||
version = "0.2.1"
|
version = "0.2.1"
|
||||||
|
|||||||
11
Cargo.toml
11
Cargo.toml
@@ -1,5 +1,5 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "cloud-llms"
|
name = "katniss-ai"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
license = "Unlicense"
|
license = "Unlicense"
|
||||||
@@ -15,16 +15,23 @@ enumset = { version = "1.1.14", features = ["serde"] }
|
|||||||
futures = "0.3.34"
|
futures = "0.3.34"
|
||||||
headers = "0.4.2"
|
headers = "0.4.2"
|
||||||
indexmap = "2.14.2"
|
indexmap = "2.14.2"
|
||||||
|
itertools = "0.15.0"
|
||||||
|
jiff = { version = "0.2.38", features = ["serde"] }
|
||||||
mitsein = { version = "0.9.0", features = ["indexmap"] }
|
mitsein = { version = "0.9.0", features = ["indexmap"] }
|
||||||
rand = "0.10.3"
|
rand = "0.10.3"
|
||||||
reqwest = { version = "0.13.5", features = ["brotli", "cookies", "deflate", "form", "gzip", "hickory-dns", "json", "multipart", "query", "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"
|
reqwest-sse = "0.2.0"
|
||||||
|
rustls = "0.23.45"
|
||||||
schemars = "1.2.2"
|
schemars = "1.2.2"
|
||||||
secrecy = { version = "0.10.3", features = ["serde"] }
|
secrecy = { version = "0.10.3", features = ["serde"] }
|
||||||
serde = { version = "1.0.229", features = ["derive"] }
|
serde = { version = "1.0.229", features = ["derive"] }
|
||||||
serde_json = "1.0.151"
|
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"] }
|
tokio = { version = "1.53.1", features = ["macros", "rt-multi-thread"] }
|
||||||
|
tower-http = { version = "0.7.1", features = ["cors"] }
|
||||||
tracing = "0.1.44"
|
tracing = "0.1.44"
|
||||||
tracing-subscriber = { version = "0.3.23", features = ["env-filter"] }
|
tracing-subscriber = { version = "0.3.23", features = ["env-filter"] }
|
||||||
typed-builder = "0.23.2"
|
typed-builder = "0.23.2"
|
||||||
url = "2.5.8"
|
url = "2.5.8"
|
||||||
|
webpki-root-certs = "1.0.9"
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ RUN \
|
|||||||
--mount=type=cache,target=/usr/local/cargo/registry/ \
|
--mount=type=cache,target=/usr/local/cargo/registry/ \
|
||||||
cd /root/app && \
|
cd /root/app && \
|
||||||
cargo build --release && \
|
cargo build --release && \
|
||||||
cp target/release/cloud-llms /root/program
|
cp target/release/katniss-ai /root/program
|
||||||
|
|
||||||
FROM scratch AS runner
|
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 clap::Parser;
|
||||||
use futures::StreamExt as _;
|
use std::net::SocketAddr;
|
||||||
use reqwest_sse::EventSource;
|
use tracing_subscriber::fmt::format::FmtSpan;
|
||||||
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,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() {
|
async fn main() {
|
||||||
let args = Parser::parse();
|
let args = Parser::parse();
|
||||||
let Args {
|
let katniss_ai::Args {
|
||||||
env_filter,
|
env_filter,
|
||||||
app_args,
|
app_args,
|
||||||
} = args;
|
} = args;
|
||||||
@@ -61,52 +17,20 @@ async fn main() {
|
|||||||
.init();
|
.init();
|
||||||
tracing::debug!(?app_args, "using");
|
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 addr = SocketAddr::new(app_args.ip, app_args.port);
|
||||||
|
|
||||||
let (tx, rx) = async_channel::unbounded();
|
let app = katniss_ai::server::create_app(
|
||||||
|
katniss_ai::server::AppState::builder()
|
||||||
let app = server::create_app(
|
|
||||||
server::AppState::builder()
|
|
||||||
.app_args(app_args.into())
|
.app_args(app_args.into())
|
||||||
.stream_requester(tx)
|
|
||||||
.build(),
|
.build(),
|
||||||
);
|
);
|
||||||
|
|
||||||
let listener = tokio::net::TcpListener::bind(addr).await.expect("TODO");
|
let listener = tokio::net::TcpListener::bind(addr).await.expect("TODO");
|
||||||
|
|
||||||
let stream_responses = async move {
|
tracing::info!(?addr, "listening");
|
||||||
let local_set = LocalSet::new();
|
axum::serve(listener, app).await.expect("TODO");
|
||||||
|
|
||||||
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");
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
use serde_with::{DeserializeFromStr, SerializeDisplay};
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
use strum::{EnumString, VariantArray};
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// Request: Create Chat Completion
|
// 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
|
/// 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.
|
/// frequency in the text so far, decreasing the model's likelihood to repeat the same line verbatim.
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[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.
|
/// Deprecated in favor of `tool_choice`. Controls which (if any) function is called by the model.
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[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
|
/// 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.
|
/// 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")]
|
#[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
|
/// 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`.
|
/// 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
|
/// 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.
|
/// the text so far, increasing the model's likelihood to talk about new topics.
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[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.
|
/// 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")]
|
#[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.
|
/// 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")]
|
#[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.
|
/// Controls which (if any) tool is called by the model.
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[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
|
/// An alternative to sampling with temperature, called nucleus sampling, where the model considers the
|
||||||
/// results of the tokens with top_p probability mass.
|
/// results of the tokens with top_p probability mass.
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[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.
|
/// Deprecated in favor of `safety_identifier` and `prompt_cache_key`. A stable identifier for your end-users.
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
@@ -916,8 +918,10 @@ pub enum PromptCacheRetention {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Constrains reasoning effort for reasoning models.
|
/// Constrains reasoning effort for reasoning models.
|
||||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
#[derive(
|
||||||
#[serde(rename_all = "lowercase")]
|
Clone, Debug, EnumString, DeserializeFromStr, strum::Display, SerializeDisplay, VariantArray,
|
||||||
|
)]
|
||||||
|
#[strum(serialize_all = "lowercase")]
|
||||||
pub enum ReasoningEffort {
|
pub enum ReasoningEffort {
|
||||||
/// Disable reasoning.
|
/// Disable reasoning.
|
||||||
None,
|
None,
|
||||||
@@ -930,7 +934,8 @@ pub enum ReasoningEffort {
|
|||||||
/// High reasoning effort.
|
/// High reasoning effort.
|
||||||
High,
|
High,
|
||||||
/// Extra high reasoning effort.
|
/// Extra high reasoning effort.
|
||||||
Xhigh,
|
#[strum(serialize = "xhigh")]
|
||||||
|
ExtraHigh,
|
||||||
/// Maximum reasoning effort.
|
/// Maximum reasoning effort.
|
||||||
Max,
|
Max,
|
||||||
}
|
}
|
||||||
@@ -1087,7 +1092,7 @@ pub struct ApproximateLocation {
|
|||||||
|
|
||||||
/// Represents a chat completion response returned by model, based on the provided input.
|
/// Represents a chat completion response returned by model, based on the provided input.
|
||||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||||
pub struct ChatCompletion {
|
pub struct ChatCompletion<Model> {
|
||||||
/// A unique identifier for the chat completion.
|
/// A unique identifier for the chat completion.
|
||||||
pub id: String,
|
pub id: String,
|
||||||
|
|
||||||
@@ -1098,7 +1103,7 @@ pub struct ChatCompletion {
|
|||||||
pub created: i64,
|
pub created: i64,
|
||||||
|
|
||||||
/// The model used for the chat completion.
|
/// The model used for the chat completion.
|
||||||
pub model: String,
|
pub model: Model,
|
||||||
|
|
||||||
/// The object type, which is always `chat.completion`.
|
/// The object type, which is always `chat.completion`.
|
||||||
pub object: String,
|
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 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