├── .dockerignore ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── dependabot.yml └── workflows │ ├── docker-nightly.yml │ ├── lints.yml │ └── nightly.yml ├── .gitignore ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── Cargo.lock ├── Cargo.toml ├── Dockerfile ├── LICENSE ├── README.md ├── acl.toml ├── config.toml ├── openai-hub-core ├── Cargo.toml └── src │ ├── acl.rs │ ├── audit.rs │ ├── config │ ├── audit.rs │ ├── jwt_auth.rs │ └── mod.rs │ ├── error.rs │ ├── handler │ ├── acl.rs │ ├── audit │ │ ├── access.rs │ │ ├── mod.rs │ │ └── tokens.rs │ ├── helpers.rs │ ├── jwt.rs │ └── mod.rs │ ├── helpers.rs │ ├── key.rs │ └── lib.rs ├── openai-hub-jwt-token-gen ├── Cargo.toml └── src │ └── main.rs └── openai-hubd ├── Cargo.toml └── src └── main.rs /.dockerignore: -------------------------------------------------------------------------------- 1 | .git 2 | .github 3 | .idea 4 | 5 | target 6 | 7 | *.md -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. iOS] 28 | - Browser [e.g. chrome, safari] 29 | - Version [e.g. 22] 30 | 31 | **Smartphone (please complete the following information):** 32 | - Device: [e.g. iPhone6] 33 | - OS: [e.g. iOS8.1] 34 | - Browser [e.g. stock browser, safari] 35 | - Version [e.g. 22] 36 | 37 | **Additional context** 38 | Add any other context about the problem here. 39 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "cargo" # See documentation for possible values 9 | directory: "/" # Location of package manifests 10 | schedule: 11 | interval: "weekly" 12 | -------------------------------------------------------------------------------- /.github/workflows/docker-nightly.yml: -------------------------------------------------------------------------------- 1 | name: Docker Nightly 2 | 3 | on: 4 | schedule: 5 | - cron: '0 0 * * *' 6 | push: 7 | branches: [ "master" ] 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Check out code 14 | uses: actions/checkout@v2 15 | 16 | - name: Set up Docker Buildx 17 | uses: docker/setup-buildx-action@v1 18 | 19 | - name: Cache Docker layers 20 | uses: actions/cache@v2 21 | with: 22 | path: /tmp/.buildx-cache 23 | key: ${{ runner.os }}-buildx-${{ github.sha }} 24 | restore-keys: | 25 | ${{ runner.os }}-buildx- 26 | 27 | - name: Login to DockerHub 28 | uses: docker/login-action@v1 29 | with: 30 | username: ${{ secrets.DOCKERHUB_USERNAME }} 31 | password: ${{ secrets.DOCKERHUB_TOKEN }} 32 | 33 | - name: Build and push 34 | id: docker_build 35 | uses: docker/build-push-action@v2 36 | with: 37 | context: . 38 | push: true 39 | tags: lightsing/openai-hub:latest 40 | 41 | - name: Image digest 42 | run: echo ${{ steps.docker_build.outputs.digest }} 43 | -------------------------------------------------------------------------------- /.github/workflows/lints.yml: -------------------------------------------------------------------------------- 1 | name: Rust Lints 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | jobs: 10 | lint: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v2 14 | 15 | - name: Setup Rust 16 | uses: actions-rs/toolchain@v1 17 | with: 18 | toolchain: stable 19 | components: rustfmt, clippy 20 | 21 | - name: Cache Cargo 22 | uses: actions/cache@v2 23 | with: 24 | path: | 25 | ~/.cargo/registry 26 | ~/.cargo/git 27 | target 28 | key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} 29 | 30 | - name: Format Check 31 | uses: actions-rs/cargo@v1 32 | with: 33 | command: fmt 34 | args: -- --check 35 | 36 | - name: Clippy 37 | uses: actions-rs/clippy-check@v1 38 | with: 39 | token: ${{ secrets.GITHUB_TOKEN }} 40 | args: --all-features -- -D warnings 41 | -------------------------------------------------------------------------------- /.github/workflows/nightly.yml: -------------------------------------------------------------------------------- 1 | name: Nightly Build 2 | 3 | on: 4 | schedule: 5 | - cron: '0 0 * * *' 6 | push: 7 | branches: [ master ] 8 | pull_request: 9 | branches: [ master ] 10 | 11 | jobs: 12 | build: 13 | strategy: 14 | matrix: 15 | os: [windows-latest, ubuntu-latest, macos-latest] 16 | 17 | runs-on: ${{ matrix.os }} 18 | 19 | steps: 20 | - name: Checkout code 21 | uses: actions/checkout@v2 22 | 23 | - name: Setup Rust 24 | uses: actions-rs/toolchain@v1 25 | with: 26 | toolchain: nightly 27 | override: true 28 | 29 | - name: Cache Cargo 30 | uses: actions/cache@v2 31 | with: 32 | path: | 33 | ~/.cargo/registry 34 | ~/.cargo/git 35 | target 36 | key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} 37 | 38 | - name: Build 39 | run: cargo build --all-features --release 40 | 41 | - name: Archive production artifacts 42 | uses: actions/upload-artifact@v3 43 | with: 44 | name: ${{ matrix.os }}-nightly-build 45 | if-no-files-found: ignore 46 | path: | 47 | target/release/openai-hubd 48 | target/release/openai-hubd.exe 49 | target/release/openai-hub-jwt-token-gen 50 | target/release/openai-hub-jwt-token-gen.exe 51 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | 3 | .idea -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our 6 | community a harassment-free experience for everyone, regardless of age, body 7 | size, visible or invisible disability, ethnicity, sex characteristics, gender 8 | identity and expression, level of experience, education, socio-economic status, 9 | nationality, personal appearance, race, religion, or sexual identity 10 | and orientation. 11 | 12 | We pledge to act and interact in ways that contribute to an open, welcoming, 13 | diverse, inclusive, and healthy community. 14 | 15 | ## Our Standards 16 | 17 | Examples of behavior that contributes to a positive environment for our 18 | community include: 19 | 20 | * Demonstrating empathy and kindness toward other people 21 | * Being respectful of differing opinions, viewpoints, and experiences 22 | * Giving and gracefully accepting constructive feedback 23 | * Accepting responsibility and apologizing to those affected by our mistakes, 24 | and learning from the experience 25 | * Focusing on what is best not just for us as individuals, but for the 26 | overall community 27 | 28 | Examples of unacceptable behavior include: 29 | 30 | * The use of sexualized language or imagery, and sexual attention or 31 | advances of any kind 32 | * Trolling, insulting or derogatory comments, and personal or political attacks 33 | * Public or private harassment 34 | * Publishing others' private information, such as a physical or email 35 | address, without their explicit permission 36 | * Other conduct which could reasonably be considered inappropriate in a 37 | professional setting 38 | 39 | ## Enforcement Responsibilities 40 | 41 | Community leaders are responsible for clarifying and enforcing our standards of 42 | acceptable behavior and will take appropriate and fair corrective action in 43 | response to any behavior that they deem inappropriate, threatening, offensive, 44 | or harmful. 45 | 46 | Community leaders have the right and responsibility to remove, edit, or reject 47 | comments, commits, code, wiki edits, issues, and other contributions that are 48 | not aligned to this Code of Conduct, and will communicate reasons for moderation 49 | decisions when appropriate. 50 | 51 | ## Scope 52 | 53 | This Code of Conduct applies within all community spaces, and also applies when 54 | an individual is officially representing the community in public spaces. 55 | Examples of representing our community include using an official e-mail address, 56 | posting via an official social media account, or acting as an appointed 57 | representative at an online or offline event. 58 | 59 | ## Enforcement 60 | 61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 62 | reported to the community leaders responsible for enforcement at 63 | light.tsing@gmail.com. 64 | All complaints will be reviewed and investigated promptly and fairly. 65 | 66 | All community leaders are obligated to respect the privacy and security of the 67 | reporter of any incident. 68 | 69 | ## Enforcement Guidelines 70 | 71 | Community leaders will follow these Community Impact Guidelines in determining 72 | the consequences for any action they deem in violation of this Code of Conduct: 73 | 74 | ### 1. Correction 75 | 76 | **Community Impact**: Use of inappropriate language or other behavior deemed 77 | unprofessional or unwelcome in the community. 78 | 79 | **Consequence**: A private, written warning from community leaders, providing 80 | clarity around the nature of the violation and an explanation of why the 81 | behavior was inappropriate. A public apology may be requested. 82 | 83 | ### 2. Warning 84 | 85 | **Community Impact**: A violation through a single incident or series 86 | of actions. 87 | 88 | **Consequence**: A warning with consequences for continued behavior. No 89 | interaction with the people involved, including unsolicited interaction with 90 | those enforcing the Code of Conduct, for a specified period of time. This 91 | includes avoiding interactions in community spaces as well as external channels 92 | like social media. Violating these terms may lead to a temporary or 93 | permanent ban. 94 | 95 | ### 3. Temporary Ban 96 | 97 | **Community Impact**: A serious violation of community standards, including 98 | sustained inappropriate behavior. 99 | 100 | **Consequence**: A temporary ban from any sort of interaction or public 101 | communication with the community for a specified period of time. No public or 102 | private interaction with the people involved, including unsolicited interaction 103 | with those enforcing the Code of Conduct, is allowed during this period. 104 | Violating these terms may lead to a permanent ban. 105 | 106 | ### 4. Permanent Ban 107 | 108 | **Community Impact**: Demonstrating a pattern of violation of community 109 | standards, including sustained inappropriate behavior, harassment of an 110 | individual, or aggression toward or disparagement of classes of individuals. 111 | 112 | **Consequence**: A permanent ban from any sort of public interaction within 113 | the community. 114 | 115 | ## Attribution 116 | 117 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 118 | version 2.0, available at 119 | https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. 120 | 121 | Community Impact Guidelines were inspired by [Mozilla's code of conduct 122 | enforcement ladder](https://github.com/mozilla/diversity). 123 | 124 | [homepage]: https://www.contributor-covenant.org 125 | 126 | For answers to common questions about this code of conduct, see the FAQ at 127 | https://www.contributor-covenant.org/faq. Translations are available at 128 | https://www.contributor-covenant.org/translations. 129 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to OpenAI Hub 2 | 3 | We love your input! We want to make contributing to this project as easy and transparent as possible, whether it's: 4 | 5 | - Reporting a bug 6 | - Discussing the current state of the code 7 | - Submitting a fix 8 | - Proposing new features 9 | - Becoming a maintainer 10 | 11 | ## We Develop with GitHub 12 | We use GitHub to host code, to track issues and feature requests, as well as accept pull requests. 13 | 14 | ## Any Contributions You Make Will Be Under The MIT Software License 15 | In short, when you submit code changes, your submissions are understood to be under the same [MIT License](LICENSE) that covers the project. Feel free to contact the maintainers if that's a concern. 16 | 17 | ## Report Bugs Using Github's [Issues](https://github.com/lightsing/openai-hub/issues) 18 | We use GitHub issues to track public bugs. Report a bug by opening a new issue; it's that easy! 19 | 20 | ## Write Bug Reports With Detail, Background, And Sample Code 21 | Here's an example of a bug report: 22 | 23 | > **Short and descriptive example bug report title** 24 | > 25 | > **A summary of the issue and the browser/OS environment in which it occurs. If suitable, include the steps to reproduce the bug** 26 | > 27 | > 1. First step 28 | > 2. Second step 29 | > 3. And so on... 30 | > 31 | > **Any other information you want to share that is relevant to the issue being reported. This might include the lines of code that you have identified as causing the bug, and potential solutions (and your opinions on their merits).** 32 | 33 | ## Use a Consistent Coding Style 34 | We recommend using a consistent coding style to maintain the readability and maintainability of the project. Please follow the coding style guidelines provided in the repository. 35 | 36 | ## License 37 | By contributing, you agree that your contributions will be licensed under its MIT License. 38 | 39 | ## References 40 | This document was adapted from the open-source contribution guidelines template available at https://gist.github.com/briandk/3d2e8b3ec8daf5a27a62. 41 | 42 | Thank you for considering contributing to OpenAI Hub! -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 3 4 | 5 | [[package]] 6 | name = "addr2line" 7 | version = "0.21.0" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "8a30b2e23b9e17a9f90641c7ab1549cd9b44f296d3ccbf309d2863cfe398a0cb" 10 | dependencies = [ 11 | "gimli", 12 | ] 13 | 14 | [[package]] 15 | name = "adler" 16 | version = "1.0.2" 17 | source = "registry+https://github.com/rust-lang/crates.io-index" 18 | checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" 19 | 20 | [[package]] 21 | name = "ahash" 22 | version = "0.8.3" 23 | source = "registry+https://github.com/rust-lang/crates.io-index" 24 | checksum = "2c99f64d1e06488f620f932677e24bc6e2897582980441ae90a671415bd7ec2f" 25 | dependencies = [ 26 | "cfg-if", 27 | "getrandom", 28 | "once_cell", 29 | "version_check", 30 | ] 31 | 32 | [[package]] 33 | name = "aho-corasick" 34 | version = "1.1.0" 35 | source = "registry+https://github.com/rust-lang/crates.io-index" 36 | checksum = "0f2135563fb5c609d2b2b87c1e8ce7bc41b0b45430fa9661f457981503dd5bf0" 37 | dependencies = [ 38 | "memchr", 39 | ] 40 | 41 | [[package]] 42 | name = "allocator-api2" 43 | version = "0.2.16" 44 | source = "registry+https://github.com/rust-lang/crates.io-index" 45 | checksum = "0942ffc6dcaadf03badf6e6a2d0228460359d5e34b57ccdc720b7382dfbd5ec5" 46 | 47 | [[package]] 48 | name = "android-tzdata" 49 | version = "0.1.1" 50 | source = "registry+https://github.com/rust-lang/crates.io-index" 51 | checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" 52 | 53 | [[package]] 54 | name = "android_system_properties" 55 | version = "0.1.5" 56 | source = "registry+https://github.com/rust-lang/crates.io-index" 57 | checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" 58 | dependencies = [ 59 | "libc", 60 | ] 61 | 62 | [[package]] 63 | name = "anstream" 64 | version = "0.5.0" 65 | source = "registry+https://github.com/rust-lang/crates.io-index" 66 | checksum = "b1f58811cfac344940f1a400b6e6231ce35171f614f26439e80f8c1465c5cc0c" 67 | dependencies = [ 68 | "anstyle", 69 | "anstyle-parse", 70 | "anstyle-query", 71 | "anstyle-wincon", 72 | "colorchoice", 73 | "utf8parse", 74 | ] 75 | 76 | [[package]] 77 | name = "anstyle" 78 | version = "1.0.3" 79 | source = "registry+https://github.com/rust-lang/crates.io-index" 80 | checksum = "b84bf0a05bbb2a83e5eb6fa36bb6e87baa08193c35ff52bbf6b38d8af2890e46" 81 | 82 | [[package]] 83 | name = "anstyle-parse" 84 | version = "0.2.1" 85 | source = "registry+https://github.com/rust-lang/crates.io-index" 86 | checksum = "938874ff5980b03a87c5524b3ae5b59cf99b1d6bc836848df7bc5ada9643c333" 87 | dependencies = [ 88 | "utf8parse", 89 | ] 90 | 91 | [[package]] 92 | name = "anstyle-query" 93 | version = "1.0.0" 94 | source = "registry+https://github.com/rust-lang/crates.io-index" 95 | checksum = "5ca11d4be1bab0c8bc8734a9aa7bf4ee8316d462a08c6ac5052f888fef5b494b" 96 | dependencies = [ 97 | "windows-sys", 98 | ] 99 | 100 | [[package]] 101 | name = "anstyle-wincon" 102 | version = "2.1.0" 103 | source = "registry+https://github.com/rust-lang/crates.io-index" 104 | checksum = "58f54d10c6dfa51283a066ceab3ec1ab78d13fae00aa49243a45e4571fb79dfd" 105 | dependencies = [ 106 | "anstyle", 107 | "windows-sys", 108 | ] 109 | 110 | [[package]] 111 | name = "anyhow" 112 | version = "1.0.75" 113 | source = "registry+https://github.com/rust-lang/crates.io-index" 114 | checksum = "a4668cab20f66d8d020e1fbc0ebe47217433c1b6c8f2040faf858554e394ace6" 115 | 116 | [[package]] 117 | name = "async-trait" 118 | version = "0.1.73" 119 | source = "registry+https://github.com/rust-lang/crates.io-index" 120 | checksum = "bc00ceb34980c03614e35a3a4e218276a0a824e911d07651cd0d858a51e8c0f0" 121 | dependencies = [ 122 | "proc-macro2", 123 | "quote", 124 | "syn 2.0.37", 125 | ] 126 | 127 | [[package]] 128 | name = "atoi" 129 | version = "2.0.0" 130 | source = "registry+https://github.com/rust-lang/crates.io-index" 131 | checksum = "f28d99ec8bfea296261ca1af174f24225171fea9664ba9003cbebee704810528" 132 | dependencies = [ 133 | "num-traits", 134 | ] 135 | 136 | [[package]] 137 | name = "autocfg" 138 | version = "1.1.0" 139 | source = "registry+https://github.com/rust-lang/crates.io-index" 140 | checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" 141 | 142 | [[package]] 143 | name = "axum" 144 | version = "0.6.16" 145 | source = "git+https://github.com/tokio-rs/axum?rev=786329d85d06549aa1b15f9e4c5d8225c658f468#786329d85d06549aa1b15f9e4c5d8225c658f468" 146 | dependencies = [ 147 | "async-trait", 148 | "axum-core", 149 | "bytes", 150 | "futures-util", 151 | "http", 152 | "http-body 0.4.5", 153 | "hyper 0.14.27", 154 | "hyper 1.0.0-rc.4", 155 | "itoa", 156 | "matchit", 157 | "memchr", 158 | "mime", 159 | "percent-encoding", 160 | "pin-project-lite", 161 | "rustversion", 162 | "serde", 163 | "serde_json", 164 | "serde_path_to_error", 165 | "serde_urlencoded", 166 | "sync_wrapper", 167 | "tokio", 168 | "tower", 169 | "tower-hyper-http-body-compat", 170 | "tower-layer", 171 | "tower-service", 172 | ] 173 | 174 | [[package]] 175 | name = "axum-core" 176 | version = "0.3.4" 177 | source = "git+https://github.com/tokio-rs/axum?rev=786329d85d06549aa1b15f9e4c5d8225c658f468#786329d85d06549aa1b15f9e4c5d8225c658f468" 178 | dependencies = [ 179 | "async-trait", 180 | "bytes", 181 | "futures-util", 182 | "http", 183 | "http-body 0.4.5", 184 | "mime", 185 | "pin-project-lite", 186 | "rustversion", 187 | "sync_wrapper", 188 | "tower-layer", 189 | "tower-service", 190 | ] 191 | 192 | [[package]] 193 | name = "backtrace" 194 | version = "0.3.69" 195 | source = "registry+https://github.com/rust-lang/crates.io-index" 196 | checksum = "2089b7e3f35b9dd2d0ed921ead4f6d318c27680d4a5bd167b3ee120edb105837" 197 | dependencies = [ 198 | "addr2line", 199 | "cc", 200 | "cfg-if", 201 | "libc", 202 | "miniz_oxide", 203 | "object", 204 | "rustc-demangle", 205 | ] 206 | 207 | [[package]] 208 | name = "base64" 209 | version = "0.13.1" 210 | source = "registry+https://github.com/rust-lang/crates.io-index" 211 | checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" 212 | 213 | [[package]] 214 | name = "base64" 215 | version = "0.21.4" 216 | source = "registry+https://github.com/rust-lang/crates.io-index" 217 | checksum = "9ba43ea6f343b788c8764558649e08df62f86c6ef251fdaeb1ffd010a9ae50a2" 218 | 219 | [[package]] 220 | name = "base64ct" 221 | version = "1.6.0" 222 | source = "registry+https://github.com/rust-lang/crates.io-index" 223 | checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" 224 | 225 | [[package]] 226 | name = "bit-set" 227 | version = "0.5.3" 228 | source = "registry+https://github.com/rust-lang/crates.io-index" 229 | checksum = "0700ddab506f33b20a03b13996eccd309a48e5ff77d0d95926aa0210fb4e95f1" 230 | dependencies = [ 231 | "bit-vec", 232 | ] 233 | 234 | [[package]] 235 | name = "bit-vec" 236 | version = "0.6.3" 237 | source = "registry+https://github.com/rust-lang/crates.io-index" 238 | checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb" 239 | 240 | [[package]] 241 | name = "bitflags" 242 | version = "1.3.2" 243 | source = "registry+https://github.com/rust-lang/crates.io-index" 244 | checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" 245 | 246 | [[package]] 247 | name = "bitflags" 248 | version = "2.4.0" 249 | source = "registry+https://github.com/rust-lang/crates.io-index" 250 | checksum = "b4682ae6287fcf752ecaabbfcc7b6f9b72aa33933dc23a554d853aea8eea8635" 251 | dependencies = [ 252 | "serde", 253 | ] 254 | 255 | [[package]] 256 | name = "block-buffer" 257 | version = "0.10.4" 258 | source = "registry+https://github.com/rust-lang/crates.io-index" 259 | checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" 260 | dependencies = [ 261 | "generic-array", 262 | ] 263 | 264 | [[package]] 265 | name = "bstr" 266 | version = "1.6.2" 267 | source = "registry+https://github.com/rust-lang/crates.io-index" 268 | checksum = "4c2f7349907b712260e64b0afe2f84692af14a454be26187d9df565c7f69266a" 269 | dependencies = [ 270 | "memchr", 271 | "regex-automata 0.3.8", 272 | "serde", 273 | ] 274 | 275 | [[package]] 276 | name = "bumpalo" 277 | version = "3.14.0" 278 | source = "registry+https://github.com/rust-lang/crates.io-index" 279 | checksum = "7f30e7476521f6f8af1a1c4c0b8cc94f0bee37d91763d0ca2665f299b6cd8aec" 280 | 281 | [[package]] 282 | name = "byteorder" 283 | version = "1.4.3" 284 | source = "registry+https://github.com/rust-lang/crates.io-index" 285 | checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" 286 | 287 | [[package]] 288 | name = "bytes" 289 | version = "1.5.0" 290 | source = "registry+https://github.com/rust-lang/crates.io-index" 291 | checksum = "a2bd12c1caf447e69cd4528f47f94d203fd2582878ecb9e9465484c4148a8223" 292 | 293 | [[package]] 294 | name = "cc" 295 | version = "1.0.83" 296 | source = "registry+https://github.com/rust-lang/crates.io-index" 297 | checksum = "f1174fb0b6ec23863f8b971027804a42614e347eafb0a95bf0b12cdae21fc4d0" 298 | dependencies = [ 299 | "libc", 300 | ] 301 | 302 | [[package]] 303 | name = "cfg-if" 304 | version = "1.0.0" 305 | source = "registry+https://github.com/rust-lang/crates.io-index" 306 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 307 | 308 | [[package]] 309 | name = "chrono" 310 | version = "0.4.31" 311 | source = "registry+https://github.com/rust-lang/crates.io-index" 312 | checksum = "7f2c685bad3eb3d45a01354cedb7d5faa66194d1d58ba6e267a8de788f79db38" 313 | dependencies = [ 314 | "android-tzdata", 315 | "iana-time-zone", 316 | "js-sys", 317 | "num-traits", 318 | "serde", 319 | "wasm-bindgen", 320 | "windows-targets", 321 | ] 322 | 323 | [[package]] 324 | name = "clap" 325 | version = "4.4.4" 326 | source = "registry+https://github.com/rust-lang/crates.io-index" 327 | checksum = "b1d7b8d5ec32af0fadc644bf1fd509a688c2103b185644bb1e29d164e0703136" 328 | dependencies = [ 329 | "clap_builder", 330 | "clap_derive", 331 | ] 332 | 333 | [[package]] 334 | name = "clap_builder" 335 | version = "4.4.4" 336 | source = "registry+https://github.com/rust-lang/crates.io-index" 337 | checksum = "5179bb514e4d7c2051749d8fcefa2ed6d06a9f4e6d69faf3805f5d80b8cf8d56" 338 | dependencies = [ 339 | "anstream", 340 | "anstyle", 341 | "clap_lex", 342 | "strsim", 343 | ] 344 | 345 | [[package]] 346 | name = "clap_derive" 347 | version = "4.4.2" 348 | source = "registry+https://github.com/rust-lang/crates.io-index" 349 | checksum = "0862016ff20d69b84ef8247369fabf5c008a7417002411897d40ee1f4532b873" 350 | dependencies = [ 351 | "heck", 352 | "proc-macro2", 353 | "quote", 354 | "syn 2.0.37", 355 | ] 356 | 357 | [[package]] 358 | name = "clap_lex" 359 | version = "0.5.1" 360 | source = "registry+https://github.com/rust-lang/crates.io-index" 361 | checksum = "cd7cc57abe963c6d3b9d8be5b06ba7c8957a930305ca90304f24ef040aa6f961" 362 | 363 | [[package]] 364 | name = "colorchoice" 365 | version = "1.0.0" 366 | source = "registry+https://github.com/rust-lang/crates.io-index" 367 | checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" 368 | 369 | [[package]] 370 | name = "const-oid" 371 | version = "0.9.5" 372 | source = "registry+https://github.com/rust-lang/crates.io-index" 373 | checksum = "28c122c3980598d243d63d9a704629a2d748d101f278052ff068be5a4423ab6f" 374 | 375 | [[package]] 376 | name = "core-foundation" 377 | version = "0.9.3" 378 | source = "registry+https://github.com/rust-lang/crates.io-index" 379 | checksum = "194a7a9e6de53fa55116934067c844d9d749312f75c6f6d0980e8c252f8c2146" 380 | dependencies = [ 381 | "core-foundation-sys", 382 | "libc", 383 | ] 384 | 385 | [[package]] 386 | name = "core-foundation-sys" 387 | version = "0.8.4" 388 | source = "registry+https://github.com/rust-lang/crates.io-index" 389 | checksum = "e496a50fda8aacccc86d7529e2c1e0892dbd0f898a6b5645b5561b89c3210efa" 390 | 391 | [[package]] 392 | name = "cpufeatures" 393 | version = "0.2.9" 394 | source = "registry+https://github.com/rust-lang/crates.io-index" 395 | checksum = "a17b76ff3a4162b0b27f354a0c87015ddad39d35f9c0c36607a3bdd175dde1f1" 396 | dependencies = [ 397 | "libc", 398 | ] 399 | 400 | [[package]] 401 | name = "crc" 402 | version = "3.0.1" 403 | source = "registry+https://github.com/rust-lang/crates.io-index" 404 | checksum = "86ec7a15cbe22e59248fc7eadb1907dab5ba09372595da4d73dd805ed4417dfe" 405 | dependencies = [ 406 | "crc-catalog", 407 | ] 408 | 409 | [[package]] 410 | name = "crc-catalog" 411 | version = "2.2.0" 412 | source = "registry+https://github.com/rust-lang/crates.io-index" 413 | checksum = "9cace84e55f07e7301bae1c519df89cdad8cc3cd868413d3fdbdeca9ff3db484" 414 | 415 | [[package]] 416 | name = "crossbeam-queue" 417 | version = "0.3.8" 418 | source = "registry+https://github.com/rust-lang/crates.io-index" 419 | checksum = "d1cfb3ea8a53f37c40dea2c7bedcbd88bdfae54f5e2175d6ecaff1c988353add" 420 | dependencies = [ 421 | "cfg-if", 422 | "crossbeam-utils", 423 | ] 424 | 425 | [[package]] 426 | name = "crossbeam-utils" 427 | version = "0.8.16" 428 | source = "registry+https://github.com/rust-lang/crates.io-index" 429 | checksum = "5a22b2d63d4d1dc0b7f1b6b2747dd0088008a9be28b6ddf0b1e7d335e3037294" 430 | dependencies = [ 431 | "cfg-if", 432 | ] 433 | 434 | [[package]] 435 | name = "crypto-common" 436 | version = "0.1.6" 437 | source = "registry+https://github.com/rust-lang/crates.io-index" 438 | checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" 439 | dependencies = [ 440 | "generic-array", 441 | "typenum", 442 | ] 443 | 444 | [[package]] 445 | name = "der" 446 | version = "0.7.8" 447 | source = "registry+https://github.com/rust-lang/crates.io-index" 448 | checksum = "fffa369a668c8af7dbf8b5e56c9f744fbd399949ed171606040001947de40b1c" 449 | dependencies = [ 450 | "const-oid", 451 | "pem-rfc7468", 452 | "zeroize", 453 | ] 454 | 455 | [[package]] 456 | name = "digest" 457 | version = "0.10.7" 458 | source = "registry+https://github.com/rust-lang/crates.io-index" 459 | checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" 460 | dependencies = [ 461 | "block-buffer", 462 | "const-oid", 463 | "crypto-common", 464 | "subtle", 465 | ] 466 | 467 | [[package]] 468 | name = "dotenvy" 469 | version = "0.15.7" 470 | source = "registry+https://github.com/rust-lang/crates.io-index" 471 | checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b" 472 | 473 | [[package]] 474 | name = "either" 475 | version = "1.9.0" 476 | source = "registry+https://github.com/rust-lang/crates.io-index" 477 | checksum = "a26ae43d7bcc3b814de94796a5e736d4029efb0ee900c12e2d54c993ad1a1e07" 478 | dependencies = [ 479 | "serde", 480 | ] 481 | 482 | [[package]] 483 | name = "encoding_rs" 484 | version = "0.8.33" 485 | source = "registry+https://github.com/rust-lang/crates.io-index" 486 | checksum = "7268b386296a025e474d5140678f75d6de9493ae55a5d709eeb9dd08149945e1" 487 | dependencies = [ 488 | "cfg-if", 489 | ] 490 | 491 | [[package]] 492 | name = "equivalent" 493 | version = "1.0.1" 494 | source = "registry+https://github.com/rust-lang/crates.io-index" 495 | checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" 496 | 497 | [[package]] 498 | name = "errno" 499 | version = "0.3.3" 500 | source = "registry+https://github.com/rust-lang/crates.io-index" 501 | checksum = "136526188508e25c6fef639d7927dfb3e0e3084488bf202267829cf7fc23dbdd" 502 | dependencies = [ 503 | "errno-dragonfly", 504 | "libc", 505 | "windows-sys", 506 | ] 507 | 508 | [[package]] 509 | name = "errno-dragonfly" 510 | version = "0.1.2" 511 | source = "registry+https://github.com/rust-lang/crates.io-index" 512 | checksum = "aa68f1b12764fab894d2755d2518754e71b4fd80ecfb822714a1206c2aab39bf" 513 | dependencies = [ 514 | "cc", 515 | "libc", 516 | ] 517 | 518 | [[package]] 519 | name = "etcetera" 520 | version = "0.8.0" 521 | source = "registry+https://github.com/rust-lang/crates.io-index" 522 | checksum = "136d1b5283a1ab77bd9257427ffd09d8667ced0570b6f938942bc7568ed5b943" 523 | dependencies = [ 524 | "cfg-if", 525 | "home", 526 | "windows-sys", 527 | ] 528 | 529 | [[package]] 530 | name = "event-listener" 531 | version = "2.5.3" 532 | source = "registry+https://github.com/rust-lang/crates.io-index" 533 | checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0" 534 | 535 | [[package]] 536 | name = "fancy-regex" 537 | version = "0.11.0" 538 | source = "registry+https://github.com/rust-lang/crates.io-index" 539 | checksum = "b95f7c0680e4142284cf8b22c14a476e87d61b004a3a0861872b32ef7ead40a2" 540 | dependencies = [ 541 | "bit-set", 542 | "regex", 543 | ] 544 | 545 | [[package]] 546 | name = "fastrand" 547 | version = "2.0.0" 548 | source = "registry+https://github.com/rust-lang/crates.io-index" 549 | checksum = "6999dc1837253364c2ebb0704ba97994bd874e8f195d665c50b7548f6ea92764" 550 | 551 | [[package]] 552 | name = "finl_unicode" 553 | version = "1.2.0" 554 | source = "registry+https://github.com/rust-lang/crates.io-index" 555 | checksum = "8fcfdc7a0362c9f4444381a9e697c79d435fe65b52a37466fc2c1184cee9edc6" 556 | 557 | [[package]] 558 | name = "flume" 559 | version = "0.10.14" 560 | source = "registry+https://github.com/rust-lang/crates.io-index" 561 | checksum = "1657b4441c3403d9f7b3409e47575237dac27b1b5726df654a6ecbf92f0f7577" 562 | dependencies = [ 563 | "futures-core", 564 | "futures-sink", 565 | "pin-project", 566 | "spin 0.9.8", 567 | ] 568 | 569 | [[package]] 570 | name = "fnv" 571 | version = "1.0.7" 572 | source = "registry+https://github.com/rust-lang/crates.io-index" 573 | checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" 574 | 575 | [[package]] 576 | name = "foreign-types" 577 | version = "0.3.2" 578 | source = "registry+https://github.com/rust-lang/crates.io-index" 579 | checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" 580 | dependencies = [ 581 | "foreign-types-shared", 582 | ] 583 | 584 | [[package]] 585 | name = "foreign-types-shared" 586 | version = "0.1.1" 587 | source = "registry+https://github.com/rust-lang/crates.io-index" 588 | checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" 589 | 590 | [[package]] 591 | name = "form_urlencoded" 592 | version = "1.2.0" 593 | source = "registry+https://github.com/rust-lang/crates.io-index" 594 | checksum = "a62bc1cf6f830c2ec14a513a9fb124d0a213a629668a4186f329db21fe045652" 595 | dependencies = [ 596 | "percent-encoding", 597 | ] 598 | 599 | [[package]] 600 | name = "futures" 601 | version = "0.3.28" 602 | source = "registry+https://github.com/rust-lang/crates.io-index" 603 | checksum = "23342abe12aba583913b2e62f22225ff9c950774065e4bfb61a19cd9770fec40" 604 | dependencies = [ 605 | "futures-channel", 606 | "futures-core", 607 | "futures-executor", 608 | "futures-io", 609 | "futures-sink", 610 | "futures-task", 611 | "futures-util", 612 | ] 613 | 614 | [[package]] 615 | name = "futures-channel" 616 | version = "0.3.28" 617 | source = "registry+https://github.com/rust-lang/crates.io-index" 618 | checksum = "955518d47e09b25bbebc7a18df10b81f0c766eaf4c4f1cccef2fca5f2a4fb5f2" 619 | dependencies = [ 620 | "futures-core", 621 | "futures-sink", 622 | ] 623 | 624 | [[package]] 625 | name = "futures-core" 626 | version = "0.3.28" 627 | source = "registry+https://github.com/rust-lang/crates.io-index" 628 | checksum = "4bca583b7e26f571124fe5b7561d49cb2868d79116cfa0eefce955557c6fee8c" 629 | 630 | [[package]] 631 | name = "futures-executor" 632 | version = "0.3.28" 633 | source = "registry+https://github.com/rust-lang/crates.io-index" 634 | checksum = "ccecee823288125bd88b4d7f565c9e58e41858e47ab72e8ea2d64e93624386e0" 635 | dependencies = [ 636 | "futures-core", 637 | "futures-task", 638 | "futures-util", 639 | ] 640 | 641 | [[package]] 642 | name = "futures-intrusive" 643 | version = "0.5.0" 644 | source = "registry+https://github.com/rust-lang/crates.io-index" 645 | checksum = "1d930c203dd0b6ff06e0201a4a2fe9149b43c684fd4420555b26d21b1a02956f" 646 | dependencies = [ 647 | "futures-core", 648 | "lock_api", 649 | "parking_lot", 650 | ] 651 | 652 | [[package]] 653 | name = "futures-io" 654 | version = "0.3.28" 655 | source = "registry+https://github.com/rust-lang/crates.io-index" 656 | checksum = "4fff74096e71ed47f8e023204cfd0aa1289cd54ae5430a9523be060cdb849964" 657 | 658 | [[package]] 659 | name = "futures-macro" 660 | version = "0.3.28" 661 | source = "registry+https://github.com/rust-lang/crates.io-index" 662 | checksum = "89ca545a94061b6365f2c7355b4b32bd20df3ff95f02da9329b34ccc3bd6ee72" 663 | dependencies = [ 664 | "proc-macro2", 665 | "quote", 666 | "syn 2.0.37", 667 | ] 668 | 669 | [[package]] 670 | name = "futures-sink" 671 | version = "0.3.28" 672 | source = "registry+https://github.com/rust-lang/crates.io-index" 673 | checksum = "f43be4fe21a13b9781a69afa4985b0f6ee0e1afab2c6f454a8cf30e2b2237b6e" 674 | 675 | [[package]] 676 | name = "futures-task" 677 | version = "0.3.28" 678 | source = "registry+https://github.com/rust-lang/crates.io-index" 679 | checksum = "76d3d132be6c0e6aa1534069c705a74a5997a356c0dc2f86a47765e5617c5b65" 680 | 681 | [[package]] 682 | name = "futures-util" 683 | version = "0.3.28" 684 | source = "registry+https://github.com/rust-lang/crates.io-index" 685 | checksum = "26b01e40b772d54cf6c6d721c1d1abd0647a0106a12ecaa1c186273392a69533" 686 | dependencies = [ 687 | "futures-channel", 688 | "futures-core", 689 | "futures-io", 690 | "futures-macro", 691 | "futures-sink", 692 | "futures-task", 693 | "memchr", 694 | "pin-project-lite", 695 | "pin-utils", 696 | "slab", 697 | ] 698 | 699 | [[package]] 700 | name = "generic-array" 701 | version = "0.14.7" 702 | source = "registry+https://github.com/rust-lang/crates.io-index" 703 | checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" 704 | dependencies = [ 705 | "typenum", 706 | "version_check", 707 | ] 708 | 709 | [[package]] 710 | name = "getrandom" 711 | version = "0.2.10" 712 | source = "registry+https://github.com/rust-lang/crates.io-index" 713 | checksum = "be4136b2a15dd319360be1c07d9933517ccf0be8f16bf62a3bee4f0d618df427" 714 | dependencies = [ 715 | "cfg-if", 716 | "libc", 717 | "wasi", 718 | ] 719 | 720 | [[package]] 721 | name = "gimli" 722 | version = "0.28.0" 723 | source = "registry+https://github.com/rust-lang/crates.io-index" 724 | checksum = "6fb8d784f27acf97159b40fc4db5ecd8aa23b9ad5ef69cdd136d3bc80665f0c0" 725 | 726 | [[package]] 727 | name = "h2" 728 | version = "0.3.21" 729 | source = "registry+https://github.com/rust-lang/crates.io-index" 730 | checksum = "91fc23aa11be92976ef4729127f1a74adf36d8436f7816b185d18df956790833" 731 | dependencies = [ 732 | "bytes", 733 | "fnv", 734 | "futures-core", 735 | "futures-sink", 736 | "futures-util", 737 | "http", 738 | "indexmap 1.9.3", 739 | "slab", 740 | "tokio", 741 | "tokio-util", 742 | "tracing", 743 | ] 744 | 745 | [[package]] 746 | name = "hashbrown" 747 | version = "0.12.3" 748 | source = "registry+https://github.com/rust-lang/crates.io-index" 749 | checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" 750 | 751 | [[package]] 752 | name = "hashbrown" 753 | version = "0.14.0" 754 | source = "registry+https://github.com/rust-lang/crates.io-index" 755 | checksum = "2c6201b9ff9fd90a5a3bac2e56a830d0caa509576f0e503818ee82c181b3437a" 756 | dependencies = [ 757 | "ahash", 758 | "allocator-api2", 759 | ] 760 | 761 | [[package]] 762 | name = "hashlink" 763 | version = "0.8.4" 764 | source = "registry+https://github.com/rust-lang/crates.io-index" 765 | checksum = "e8094feaf31ff591f651a2664fb9cfd92bba7a60ce3197265e9482ebe753c8f7" 766 | dependencies = [ 767 | "hashbrown 0.14.0", 768 | ] 769 | 770 | [[package]] 771 | name = "heck" 772 | version = "0.4.1" 773 | source = "registry+https://github.com/rust-lang/crates.io-index" 774 | checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" 775 | dependencies = [ 776 | "unicode-segmentation", 777 | ] 778 | 779 | [[package]] 780 | name = "hermit-abi" 781 | version = "0.3.3" 782 | source = "registry+https://github.com/rust-lang/crates.io-index" 783 | checksum = "d77f7ec81a6d05a3abb01ab6eb7590f6083d08449fe5a1c8b1e620283546ccb7" 784 | 785 | [[package]] 786 | name = "hex" 787 | version = "0.4.3" 788 | source = "registry+https://github.com/rust-lang/crates.io-index" 789 | checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" 790 | 791 | [[package]] 792 | name = "hkdf" 793 | version = "0.12.3" 794 | source = "registry+https://github.com/rust-lang/crates.io-index" 795 | checksum = "791a029f6b9fc27657f6f188ec6e5e43f6911f6f878e0dc5501396e09809d437" 796 | dependencies = [ 797 | "hmac", 798 | ] 799 | 800 | [[package]] 801 | name = "hmac" 802 | version = "0.12.1" 803 | source = "registry+https://github.com/rust-lang/crates.io-index" 804 | checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" 805 | dependencies = [ 806 | "digest", 807 | ] 808 | 809 | [[package]] 810 | name = "home" 811 | version = "0.5.5" 812 | source = "registry+https://github.com/rust-lang/crates.io-index" 813 | checksum = "5444c27eef6923071f7ebcc33e3444508466a76f7a2b93da00ed6e19f30c1ddb" 814 | dependencies = [ 815 | "windows-sys", 816 | ] 817 | 818 | [[package]] 819 | name = "http" 820 | version = "0.2.9" 821 | source = "registry+https://github.com/rust-lang/crates.io-index" 822 | checksum = "bd6effc99afb63425aff9b05836f029929e345a6148a14b7ecd5ab67af944482" 823 | dependencies = [ 824 | "bytes", 825 | "fnv", 826 | "itoa", 827 | ] 828 | 829 | [[package]] 830 | name = "http-body" 831 | version = "0.4.5" 832 | source = "registry+https://github.com/rust-lang/crates.io-index" 833 | checksum = "d5f38f16d184e36f2408a55281cd658ecbd3ca05cce6d6510a176eca393e26d1" 834 | dependencies = [ 835 | "bytes", 836 | "http", 837 | "pin-project-lite", 838 | ] 839 | 840 | [[package]] 841 | name = "http-body" 842 | version = "1.0.0-rc.2" 843 | source = "registry+https://github.com/rust-lang/crates.io-index" 844 | checksum = "951dfc2e32ac02d67c90c0d65bd27009a635dc9b381a2cc7d284ab01e3a0150d" 845 | dependencies = [ 846 | "bytes", 847 | "http", 848 | ] 849 | 850 | [[package]] 851 | name = "http-serde" 852 | version = "1.1.3" 853 | source = "registry+https://github.com/rust-lang/crates.io-index" 854 | checksum = "6f560b665ad9f1572cfcaf034f7fb84338a7ce945216d64a90fd81f046a3caee" 855 | dependencies = [ 856 | "http", 857 | "serde", 858 | ] 859 | 860 | [[package]] 861 | name = "httparse" 862 | version = "1.8.0" 863 | source = "registry+https://github.com/rust-lang/crates.io-index" 864 | checksum = "d897f394bad6a705d5f4104762e116a75639e470d80901eed05a860a95cb1904" 865 | 866 | [[package]] 867 | name = "httpdate" 868 | version = "1.0.3" 869 | source = "registry+https://github.com/rust-lang/crates.io-index" 870 | checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" 871 | 872 | [[package]] 873 | name = "hyper" 874 | version = "0.14.27" 875 | source = "registry+https://github.com/rust-lang/crates.io-index" 876 | checksum = "ffb1cfd654a8219eaef89881fdb3bb3b1cdc5fa75ded05d6933b2b382e395468" 877 | dependencies = [ 878 | "bytes", 879 | "futures-channel", 880 | "futures-core", 881 | "futures-util", 882 | "h2", 883 | "http", 884 | "http-body 0.4.5", 885 | "httparse", 886 | "httpdate", 887 | "itoa", 888 | "pin-project-lite", 889 | "socket2 0.4.9", 890 | "tokio", 891 | "tower-service", 892 | "tracing", 893 | "want", 894 | ] 895 | 896 | [[package]] 897 | name = "hyper" 898 | version = "1.0.0-rc.4" 899 | source = "registry+https://github.com/rust-lang/crates.io-index" 900 | checksum = "d280a71f348bcc670fc55b02b63c53a04ac0bf2daff2980795aeaf53edae10e6" 901 | dependencies = [ 902 | "bytes", 903 | "futures-channel", 904 | "futures-util", 905 | "http", 906 | "http-body 1.0.0-rc.2", 907 | "httparse", 908 | "httpdate", 909 | "itoa", 910 | "pin-project-lite", 911 | "tokio", 912 | "tracing", 913 | "want", 914 | ] 915 | 916 | [[package]] 917 | name = "hyper-tls" 918 | version = "0.5.0" 919 | source = "registry+https://github.com/rust-lang/crates.io-index" 920 | checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905" 921 | dependencies = [ 922 | "bytes", 923 | "hyper 0.14.27", 924 | "native-tls", 925 | "tokio", 926 | "tokio-native-tls", 927 | ] 928 | 929 | [[package]] 930 | name = "iana-time-zone" 931 | version = "0.1.57" 932 | source = "registry+https://github.com/rust-lang/crates.io-index" 933 | checksum = "2fad5b825842d2b38bd206f3e81d6957625fd7f0a361e345c30e01a0ae2dd613" 934 | dependencies = [ 935 | "android_system_properties", 936 | "core-foundation-sys", 937 | "iana-time-zone-haiku", 938 | "js-sys", 939 | "wasm-bindgen", 940 | "windows", 941 | ] 942 | 943 | [[package]] 944 | name = "iana-time-zone-haiku" 945 | version = "0.1.2" 946 | source = "registry+https://github.com/rust-lang/crates.io-index" 947 | checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" 948 | dependencies = [ 949 | "cc", 950 | ] 951 | 952 | [[package]] 953 | name = "idna" 954 | version = "0.4.0" 955 | source = "registry+https://github.com/rust-lang/crates.io-index" 956 | checksum = "7d20d6b07bfbc108882d88ed8e37d39636dcc260e15e30c45e6ba089610b917c" 957 | dependencies = [ 958 | "unicode-bidi", 959 | "unicode-normalization", 960 | ] 961 | 962 | [[package]] 963 | name = "indexmap" 964 | version = "1.9.3" 965 | source = "registry+https://github.com/rust-lang/crates.io-index" 966 | checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" 967 | dependencies = [ 968 | "autocfg", 969 | "hashbrown 0.12.3", 970 | ] 971 | 972 | [[package]] 973 | name = "indexmap" 974 | version = "2.0.0" 975 | source = "registry+https://github.com/rust-lang/crates.io-index" 976 | checksum = "d5477fe2230a79769d8dc68e0eabf5437907c0457a5614a9e8dddb67f65eb65d" 977 | dependencies = [ 978 | "equivalent", 979 | "hashbrown 0.14.0", 980 | ] 981 | 982 | [[package]] 983 | name = "ipnet" 984 | version = "2.8.0" 985 | source = "registry+https://github.com/rust-lang/crates.io-index" 986 | checksum = "28b29a3cd74f0f4598934efe3aeba42bae0eb4680554128851ebbecb02af14e6" 987 | 988 | [[package]] 989 | name = "itertools" 990 | version = "0.11.0" 991 | source = "registry+https://github.com/rust-lang/crates.io-index" 992 | checksum = "b1c173a5686ce8bfa551b3563d0c2170bf24ca44da99c7ca4bfdab5418c3fe57" 993 | dependencies = [ 994 | "either", 995 | ] 996 | 997 | [[package]] 998 | name = "itoa" 999 | version = "1.0.9" 1000 | source = "registry+https://github.com/rust-lang/crates.io-index" 1001 | checksum = "af150ab688ff2122fcef229be89cb50dd66af9e01a4ff320cc137eecc9bacc38" 1002 | 1003 | [[package]] 1004 | name = "js-sys" 1005 | version = "0.3.64" 1006 | source = "registry+https://github.com/rust-lang/crates.io-index" 1007 | checksum = "c5f195fe497f702db0f318b07fdd68edb16955aed830df8363d837542f8f935a" 1008 | dependencies = [ 1009 | "wasm-bindgen", 1010 | ] 1011 | 1012 | [[package]] 1013 | name = "jwt" 1014 | version = "0.16.0" 1015 | source = "registry+https://github.com/rust-lang/crates.io-index" 1016 | checksum = "6204285f77fe7d9784db3fdc449ecce1a0114927a51d5a41c4c7a292011c015f" 1017 | dependencies = [ 1018 | "base64 0.13.1", 1019 | "crypto-common", 1020 | "digest", 1021 | "hmac", 1022 | "serde", 1023 | "serde_json", 1024 | "sha2", 1025 | ] 1026 | 1027 | [[package]] 1028 | name = "lazy_static" 1029 | version = "1.4.0" 1030 | source = "registry+https://github.com/rust-lang/crates.io-index" 1031 | checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" 1032 | dependencies = [ 1033 | "spin 0.5.2", 1034 | ] 1035 | 1036 | [[package]] 1037 | name = "libc" 1038 | version = "0.2.148" 1039 | source = "registry+https://github.com/rust-lang/crates.io-index" 1040 | checksum = "9cdc71e17332e86d2e1d38c1f99edcb6288ee11b815fb1a4b049eaa2114d369b" 1041 | 1042 | [[package]] 1043 | name = "libm" 1044 | version = "0.2.7" 1045 | source = "registry+https://github.com/rust-lang/crates.io-index" 1046 | checksum = "f7012b1bbb0719e1097c47611d3898568c546d597c2e74d66f6087edd5233ff4" 1047 | 1048 | [[package]] 1049 | name = "libsqlite3-sys" 1050 | version = "0.26.0" 1051 | source = "registry+https://github.com/rust-lang/crates.io-index" 1052 | checksum = "afc22eff61b133b115c6e8c74e818c628d6d5e7a502afea6f64dee076dd94326" 1053 | dependencies = [ 1054 | "cc", 1055 | "pkg-config", 1056 | "vcpkg", 1057 | ] 1058 | 1059 | [[package]] 1060 | name = "linux-raw-sys" 1061 | version = "0.4.7" 1062 | source = "registry+https://github.com/rust-lang/crates.io-index" 1063 | checksum = "1a9bad9f94746442c783ca431b22403b519cd7fbeed0533fdd6328b2f2212128" 1064 | 1065 | [[package]] 1066 | name = "lock_api" 1067 | version = "0.4.10" 1068 | source = "registry+https://github.com/rust-lang/crates.io-index" 1069 | checksum = "c1cc9717a20b1bb222f333e6a92fd32f7d8a18ddc5a3191a11af45dcbf4dcd16" 1070 | dependencies = [ 1071 | "autocfg", 1072 | "scopeguard", 1073 | ] 1074 | 1075 | [[package]] 1076 | name = "log" 1077 | version = "0.4.20" 1078 | source = "registry+https://github.com/rust-lang/crates.io-index" 1079 | checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" 1080 | 1081 | [[package]] 1082 | name = "matchers" 1083 | version = "0.1.0" 1084 | source = "registry+https://github.com/rust-lang/crates.io-index" 1085 | checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558" 1086 | dependencies = [ 1087 | "regex-automata 0.1.10", 1088 | ] 1089 | 1090 | [[package]] 1091 | name = "matchit" 1092 | version = "0.7.2" 1093 | source = "registry+https://github.com/rust-lang/crates.io-index" 1094 | checksum = "ed1202b2a6f884ae56f04cff409ab315c5ce26b5e58d7412e484f01fd52f52ef" 1095 | 1096 | [[package]] 1097 | name = "md-5" 1098 | version = "0.10.5" 1099 | source = "registry+https://github.com/rust-lang/crates.io-index" 1100 | checksum = "6365506850d44bff6e2fbcb5176cf63650e48bd45ef2fe2665ae1570e0f4b9ca" 1101 | dependencies = [ 1102 | "digest", 1103 | ] 1104 | 1105 | [[package]] 1106 | name = "memchr" 1107 | version = "2.6.3" 1108 | source = "registry+https://github.com/rust-lang/crates.io-index" 1109 | checksum = "8f232d6ef707e1956a43342693d2a31e72989554d58299d7a88738cc95b0d35c" 1110 | 1111 | [[package]] 1112 | name = "mime" 1113 | version = "0.3.17" 1114 | source = "registry+https://github.com/rust-lang/crates.io-index" 1115 | checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" 1116 | 1117 | [[package]] 1118 | name = "minimal-lexical" 1119 | version = "0.2.1" 1120 | source = "registry+https://github.com/rust-lang/crates.io-index" 1121 | checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" 1122 | 1123 | [[package]] 1124 | name = "miniz_oxide" 1125 | version = "0.7.1" 1126 | source = "registry+https://github.com/rust-lang/crates.io-index" 1127 | checksum = "e7810e0be55b428ada41041c41f32c9f1a42817901b4ccf45fa3d4b6561e74c7" 1128 | dependencies = [ 1129 | "adler", 1130 | ] 1131 | 1132 | [[package]] 1133 | name = "mio" 1134 | version = "0.8.8" 1135 | source = "registry+https://github.com/rust-lang/crates.io-index" 1136 | checksum = "927a765cd3fc26206e66b296465fa9d3e5ab003e651c1b3c060e7956d96b19d2" 1137 | dependencies = [ 1138 | "libc", 1139 | "wasi", 1140 | "windows-sys", 1141 | ] 1142 | 1143 | [[package]] 1144 | name = "native-tls" 1145 | version = "0.2.11" 1146 | source = "registry+https://github.com/rust-lang/crates.io-index" 1147 | checksum = "07226173c32f2926027b63cce4bcd8076c3552846cbe7925f3aaffeac0a3b92e" 1148 | dependencies = [ 1149 | "lazy_static", 1150 | "libc", 1151 | "log", 1152 | "openssl", 1153 | "openssl-probe", 1154 | "openssl-sys", 1155 | "schannel", 1156 | "security-framework", 1157 | "security-framework-sys", 1158 | "tempfile", 1159 | ] 1160 | 1161 | [[package]] 1162 | name = "nom" 1163 | version = "7.1.3" 1164 | source = "registry+https://github.com/rust-lang/crates.io-index" 1165 | checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" 1166 | dependencies = [ 1167 | "memchr", 1168 | "minimal-lexical", 1169 | ] 1170 | 1171 | [[package]] 1172 | name = "nu-ansi-term" 1173 | version = "0.46.0" 1174 | source = "registry+https://github.com/rust-lang/crates.io-index" 1175 | checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84" 1176 | dependencies = [ 1177 | "overload", 1178 | "winapi", 1179 | ] 1180 | 1181 | [[package]] 1182 | name = "num-bigint-dig" 1183 | version = "0.8.4" 1184 | source = "registry+https://github.com/rust-lang/crates.io-index" 1185 | checksum = "dc84195820f291c7697304f3cbdadd1cb7199c0efc917ff5eafd71225c136151" 1186 | dependencies = [ 1187 | "byteorder", 1188 | "lazy_static", 1189 | "libm", 1190 | "num-integer", 1191 | "num-iter", 1192 | "num-traits", 1193 | "rand", 1194 | "smallvec", 1195 | "zeroize", 1196 | ] 1197 | 1198 | [[package]] 1199 | name = "num-integer" 1200 | version = "0.1.45" 1201 | source = "registry+https://github.com/rust-lang/crates.io-index" 1202 | checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9" 1203 | dependencies = [ 1204 | "autocfg", 1205 | "num-traits", 1206 | ] 1207 | 1208 | [[package]] 1209 | name = "num-iter" 1210 | version = "0.1.43" 1211 | source = "registry+https://github.com/rust-lang/crates.io-index" 1212 | checksum = "7d03e6c028c5dc5cac6e2dec0efda81fc887605bb3d884578bb6d6bf7514e252" 1213 | dependencies = [ 1214 | "autocfg", 1215 | "num-integer", 1216 | "num-traits", 1217 | ] 1218 | 1219 | [[package]] 1220 | name = "num-traits" 1221 | version = "0.2.16" 1222 | source = "registry+https://github.com/rust-lang/crates.io-index" 1223 | checksum = "f30b0abd723be7e2ffca1272140fac1a2f084c77ec3e123c192b66af1ee9e6c2" 1224 | dependencies = [ 1225 | "autocfg", 1226 | "libm", 1227 | ] 1228 | 1229 | [[package]] 1230 | name = "num_cpus" 1231 | version = "1.16.0" 1232 | source = "registry+https://github.com/rust-lang/crates.io-index" 1233 | checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" 1234 | dependencies = [ 1235 | "hermit-abi", 1236 | "libc", 1237 | ] 1238 | 1239 | [[package]] 1240 | name = "object" 1241 | version = "0.32.1" 1242 | source = "registry+https://github.com/rust-lang/crates.io-index" 1243 | checksum = "9cf5f9dd3933bd50a9e1f149ec995f39ae2c496d31fd772c1fd45ebc27e902b0" 1244 | dependencies = [ 1245 | "memchr", 1246 | ] 1247 | 1248 | [[package]] 1249 | name = "once_cell" 1250 | version = "1.18.0" 1251 | source = "registry+https://github.com/rust-lang/crates.io-index" 1252 | checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d" 1253 | 1254 | [[package]] 1255 | name = "openai-hub-core" 1256 | version = "0.1.0" 1257 | dependencies = [ 1258 | "async-trait", 1259 | "axum", 1260 | "base64 0.21.4", 1261 | "chrono", 1262 | "futures", 1263 | "hmac", 1264 | "http-serde", 1265 | "hyper 0.14.27", 1266 | "jwt", 1267 | "once_cell", 1268 | "parking_lot", 1269 | "pin-project", 1270 | "rand", 1271 | "regex", 1272 | "reqwest", 1273 | "serde", 1274 | "serde_json", 1275 | "sha2", 1276 | "sqlx", 1277 | "sync_wrapper", 1278 | "thiserror", 1279 | "tiktoken-rs", 1280 | "tokio", 1281 | "tokio-stream", 1282 | "tokio-util", 1283 | "toml", 1284 | "tower", 1285 | "tracing", 1286 | ] 1287 | 1288 | [[package]] 1289 | name = "openai-hub-jwt-token-gen" 1290 | version = "0.1.0" 1291 | dependencies = [ 1292 | "chrono", 1293 | "clap", 1294 | "hmac", 1295 | "jwt", 1296 | "openai-hub-core", 1297 | "sha2", 1298 | "tracing-subscriber", 1299 | ] 1300 | 1301 | [[package]] 1302 | name = "openai-hubd" 1303 | version = "0.1.0" 1304 | dependencies = [ 1305 | "clap", 1306 | "openai-hub-core", 1307 | "tokio", 1308 | "tracing-subscriber", 1309 | ] 1310 | 1311 | [[package]] 1312 | name = "openssl" 1313 | version = "0.10.57" 1314 | source = "registry+https://github.com/rust-lang/crates.io-index" 1315 | checksum = "bac25ee399abb46215765b1cb35bc0212377e58a061560d8b29b024fd0430e7c" 1316 | dependencies = [ 1317 | "bitflags 2.4.0", 1318 | "cfg-if", 1319 | "foreign-types", 1320 | "libc", 1321 | "once_cell", 1322 | "openssl-macros", 1323 | "openssl-sys", 1324 | ] 1325 | 1326 | [[package]] 1327 | name = "openssl-macros" 1328 | version = "0.1.1" 1329 | source = "registry+https://github.com/rust-lang/crates.io-index" 1330 | checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" 1331 | dependencies = [ 1332 | "proc-macro2", 1333 | "quote", 1334 | "syn 2.0.37", 1335 | ] 1336 | 1337 | [[package]] 1338 | name = "openssl-probe" 1339 | version = "0.1.5" 1340 | source = "registry+https://github.com/rust-lang/crates.io-index" 1341 | checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" 1342 | 1343 | [[package]] 1344 | name = "openssl-sys" 1345 | version = "0.9.93" 1346 | source = "registry+https://github.com/rust-lang/crates.io-index" 1347 | checksum = "db4d56a4c0478783083cfafcc42493dd4a981d41669da64b4572a2a089b51b1d" 1348 | dependencies = [ 1349 | "cc", 1350 | "libc", 1351 | "pkg-config", 1352 | "vcpkg", 1353 | ] 1354 | 1355 | [[package]] 1356 | name = "overload" 1357 | version = "0.1.1" 1358 | source = "registry+https://github.com/rust-lang/crates.io-index" 1359 | checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" 1360 | 1361 | [[package]] 1362 | name = "parking_lot" 1363 | version = "0.12.1" 1364 | source = "registry+https://github.com/rust-lang/crates.io-index" 1365 | checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" 1366 | dependencies = [ 1367 | "lock_api", 1368 | "parking_lot_core", 1369 | ] 1370 | 1371 | [[package]] 1372 | name = "parking_lot_core" 1373 | version = "0.9.8" 1374 | source = "registry+https://github.com/rust-lang/crates.io-index" 1375 | checksum = "93f00c865fe7cabf650081affecd3871070f26767e7b2070a3ffae14c654b447" 1376 | dependencies = [ 1377 | "cfg-if", 1378 | "libc", 1379 | "redox_syscall", 1380 | "smallvec", 1381 | "windows-targets", 1382 | ] 1383 | 1384 | [[package]] 1385 | name = "paste" 1386 | version = "1.0.14" 1387 | source = "registry+https://github.com/rust-lang/crates.io-index" 1388 | checksum = "de3145af08024dea9fa9914f381a17b8fc6034dfb00f3a84013f7ff43f29ed4c" 1389 | 1390 | [[package]] 1391 | name = "pem-rfc7468" 1392 | version = "0.7.0" 1393 | source = "registry+https://github.com/rust-lang/crates.io-index" 1394 | checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412" 1395 | dependencies = [ 1396 | "base64ct", 1397 | ] 1398 | 1399 | [[package]] 1400 | name = "percent-encoding" 1401 | version = "2.3.0" 1402 | source = "registry+https://github.com/rust-lang/crates.io-index" 1403 | checksum = "9b2a4787296e9989611394c33f193f676704af1686e70b8f8033ab5ba9a35a94" 1404 | 1405 | [[package]] 1406 | name = "pin-project" 1407 | version = "1.1.3" 1408 | source = "registry+https://github.com/rust-lang/crates.io-index" 1409 | checksum = "fda4ed1c6c173e3fc7a83629421152e01d7b1f9b7f65fb301e490e8cfc656422" 1410 | dependencies = [ 1411 | "pin-project-internal", 1412 | ] 1413 | 1414 | [[package]] 1415 | name = "pin-project-internal" 1416 | version = "1.1.3" 1417 | source = "registry+https://github.com/rust-lang/crates.io-index" 1418 | checksum = "4359fd9c9171ec6e8c62926d6faaf553a8dc3f64e1507e76da7911b4f6a04405" 1419 | dependencies = [ 1420 | "proc-macro2", 1421 | "quote", 1422 | "syn 2.0.37", 1423 | ] 1424 | 1425 | [[package]] 1426 | name = "pin-project-lite" 1427 | version = "0.2.13" 1428 | source = "registry+https://github.com/rust-lang/crates.io-index" 1429 | checksum = "8afb450f006bf6385ca15ef45d71d2288452bc3683ce2e2cacc0d18e4be60b58" 1430 | 1431 | [[package]] 1432 | name = "pin-utils" 1433 | version = "0.1.0" 1434 | source = "registry+https://github.com/rust-lang/crates.io-index" 1435 | checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" 1436 | 1437 | [[package]] 1438 | name = "pkcs1" 1439 | version = "0.7.5" 1440 | source = "registry+https://github.com/rust-lang/crates.io-index" 1441 | checksum = "c8ffb9f10fa047879315e6625af03c164b16962a5368d724ed16323b68ace47f" 1442 | dependencies = [ 1443 | "der", 1444 | "pkcs8", 1445 | "spki", 1446 | ] 1447 | 1448 | [[package]] 1449 | name = "pkcs8" 1450 | version = "0.10.2" 1451 | source = "registry+https://github.com/rust-lang/crates.io-index" 1452 | checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" 1453 | dependencies = [ 1454 | "der", 1455 | "spki", 1456 | ] 1457 | 1458 | [[package]] 1459 | name = "pkg-config" 1460 | version = "0.3.27" 1461 | source = "registry+https://github.com/rust-lang/crates.io-index" 1462 | checksum = "26072860ba924cbfa98ea39c8c19b4dd6a4a25423dbdf219c1eca91aa0cf6964" 1463 | 1464 | [[package]] 1465 | name = "ppv-lite86" 1466 | version = "0.2.17" 1467 | source = "registry+https://github.com/rust-lang/crates.io-index" 1468 | checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" 1469 | 1470 | [[package]] 1471 | name = "proc-macro2" 1472 | version = "1.0.67" 1473 | source = "registry+https://github.com/rust-lang/crates.io-index" 1474 | checksum = "3d433d9f1a3e8c1263d9456598b16fec66f4acc9a74dacffd35c7bb09b3a1328" 1475 | dependencies = [ 1476 | "unicode-ident", 1477 | ] 1478 | 1479 | [[package]] 1480 | name = "quote" 1481 | version = "1.0.33" 1482 | source = "registry+https://github.com/rust-lang/crates.io-index" 1483 | checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae" 1484 | dependencies = [ 1485 | "proc-macro2", 1486 | ] 1487 | 1488 | [[package]] 1489 | name = "rand" 1490 | version = "0.8.5" 1491 | source = "registry+https://github.com/rust-lang/crates.io-index" 1492 | checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" 1493 | dependencies = [ 1494 | "libc", 1495 | "rand_chacha", 1496 | "rand_core", 1497 | ] 1498 | 1499 | [[package]] 1500 | name = "rand_chacha" 1501 | version = "0.3.1" 1502 | source = "registry+https://github.com/rust-lang/crates.io-index" 1503 | checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" 1504 | dependencies = [ 1505 | "ppv-lite86", 1506 | "rand_core", 1507 | ] 1508 | 1509 | [[package]] 1510 | name = "rand_core" 1511 | version = "0.6.4" 1512 | source = "registry+https://github.com/rust-lang/crates.io-index" 1513 | checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" 1514 | dependencies = [ 1515 | "getrandom", 1516 | ] 1517 | 1518 | [[package]] 1519 | name = "redox_syscall" 1520 | version = "0.3.5" 1521 | source = "registry+https://github.com/rust-lang/crates.io-index" 1522 | checksum = "567664f262709473930a4bf9e51bf2ebf3348f2e748ccc50dea20646858f8f29" 1523 | dependencies = [ 1524 | "bitflags 1.3.2", 1525 | ] 1526 | 1527 | [[package]] 1528 | name = "regex" 1529 | version = "1.9.5" 1530 | source = "registry+https://github.com/rust-lang/crates.io-index" 1531 | checksum = "697061221ea1b4a94a624f67d0ae2bfe4e22b8a17b6a192afb11046542cc8c47" 1532 | dependencies = [ 1533 | "aho-corasick", 1534 | "memchr", 1535 | "regex-automata 0.3.8", 1536 | "regex-syntax 0.7.5", 1537 | ] 1538 | 1539 | [[package]] 1540 | name = "regex-automata" 1541 | version = "0.1.10" 1542 | source = "registry+https://github.com/rust-lang/crates.io-index" 1543 | checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" 1544 | dependencies = [ 1545 | "regex-syntax 0.6.29", 1546 | ] 1547 | 1548 | [[package]] 1549 | name = "regex-automata" 1550 | version = "0.3.8" 1551 | source = "registry+https://github.com/rust-lang/crates.io-index" 1552 | checksum = "c2f401f4955220693b56f8ec66ee9c78abffd8d1c4f23dc41a23839eb88f0795" 1553 | dependencies = [ 1554 | "aho-corasick", 1555 | "memchr", 1556 | "regex-syntax 0.7.5", 1557 | ] 1558 | 1559 | [[package]] 1560 | name = "regex-syntax" 1561 | version = "0.6.29" 1562 | source = "registry+https://github.com/rust-lang/crates.io-index" 1563 | checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" 1564 | 1565 | [[package]] 1566 | name = "regex-syntax" 1567 | version = "0.7.5" 1568 | source = "registry+https://github.com/rust-lang/crates.io-index" 1569 | checksum = "dbb5fb1acd8a1a18b3dd5be62d25485eb770e05afb408a9627d14d451bae12da" 1570 | 1571 | [[package]] 1572 | name = "reqwest" 1573 | version = "0.11.20" 1574 | source = "registry+https://github.com/rust-lang/crates.io-index" 1575 | checksum = "3e9ad3fe7488d7e34558a2033d45a0c90b72d97b4f80705666fea71472e2e6a1" 1576 | dependencies = [ 1577 | "base64 0.21.4", 1578 | "bytes", 1579 | "encoding_rs", 1580 | "futures-core", 1581 | "futures-util", 1582 | "h2", 1583 | "http", 1584 | "http-body 0.4.5", 1585 | "hyper 0.14.27", 1586 | "hyper-tls", 1587 | "ipnet", 1588 | "js-sys", 1589 | "log", 1590 | "mime", 1591 | "native-tls", 1592 | "once_cell", 1593 | "percent-encoding", 1594 | "pin-project-lite", 1595 | "serde", 1596 | "serde_json", 1597 | "serde_urlencoded", 1598 | "tokio", 1599 | "tokio-native-tls", 1600 | "tokio-util", 1601 | "tower-service", 1602 | "url", 1603 | "wasm-bindgen", 1604 | "wasm-bindgen-futures", 1605 | "wasm-streams", 1606 | "web-sys", 1607 | "winreg", 1608 | ] 1609 | 1610 | [[package]] 1611 | name = "rsa" 1612 | version = "0.9.2" 1613 | source = "registry+https://github.com/rust-lang/crates.io-index" 1614 | checksum = "6ab43bb47d23c1a631b4b680199a45255dce26fa9ab2fa902581f624ff13e6a8" 1615 | dependencies = [ 1616 | "byteorder", 1617 | "const-oid", 1618 | "digest", 1619 | "num-bigint-dig", 1620 | "num-integer", 1621 | "num-iter", 1622 | "num-traits", 1623 | "pkcs1", 1624 | "pkcs8", 1625 | "rand_core", 1626 | "signature", 1627 | "spki", 1628 | "subtle", 1629 | "zeroize", 1630 | ] 1631 | 1632 | [[package]] 1633 | name = "rustc-demangle" 1634 | version = "0.1.23" 1635 | source = "registry+https://github.com/rust-lang/crates.io-index" 1636 | checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76" 1637 | 1638 | [[package]] 1639 | name = "rustc-hash" 1640 | version = "1.1.0" 1641 | source = "registry+https://github.com/rust-lang/crates.io-index" 1642 | checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" 1643 | 1644 | [[package]] 1645 | name = "rustix" 1646 | version = "0.38.13" 1647 | source = "registry+https://github.com/rust-lang/crates.io-index" 1648 | checksum = "d7db8590df6dfcd144d22afd1b83b36c21a18d7cbc1dc4bb5295a8712e9eb662" 1649 | dependencies = [ 1650 | "bitflags 2.4.0", 1651 | "errno", 1652 | "libc", 1653 | "linux-raw-sys", 1654 | "windows-sys", 1655 | ] 1656 | 1657 | [[package]] 1658 | name = "rustversion" 1659 | version = "1.0.14" 1660 | source = "registry+https://github.com/rust-lang/crates.io-index" 1661 | checksum = "7ffc183a10b4478d04cbbbfc96d0873219d962dd5accaff2ffbd4ceb7df837f4" 1662 | 1663 | [[package]] 1664 | name = "ryu" 1665 | version = "1.0.15" 1666 | source = "registry+https://github.com/rust-lang/crates.io-index" 1667 | checksum = "1ad4cc8da4ef723ed60bced201181d83791ad433213d8c24efffda1eec85d741" 1668 | 1669 | [[package]] 1670 | name = "schannel" 1671 | version = "0.1.22" 1672 | source = "registry+https://github.com/rust-lang/crates.io-index" 1673 | checksum = "0c3733bf4cf7ea0880754e19cb5a462007c4a8c1914bff372ccc95b464f1df88" 1674 | dependencies = [ 1675 | "windows-sys", 1676 | ] 1677 | 1678 | [[package]] 1679 | name = "scopeguard" 1680 | version = "1.2.0" 1681 | source = "registry+https://github.com/rust-lang/crates.io-index" 1682 | checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" 1683 | 1684 | [[package]] 1685 | name = "security-framework" 1686 | version = "2.9.2" 1687 | source = "registry+https://github.com/rust-lang/crates.io-index" 1688 | checksum = "05b64fb303737d99b81884b2c63433e9ae28abebe5eb5045dcdd175dc2ecf4de" 1689 | dependencies = [ 1690 | "bitflags 1.3.2", 1691 | "core-foundation", 1692 | "core-foundation-sys", 1693 | "libc", 1694 | "security-framework-sys", 1695 | ] 1696 | 1697 | [[package]] 1698 | name = "security-framework-sys" 1699 | version = "2.9.1" 1700 | source = "registry+https://github.com/rust-lang/crates.io-index" 1701 | checksum = "e932934257d3b408ed8f30db49d85ea163bfe74961f017f405b025af298f0c7a" 1702 | dependencies = [ 1703 | "core-foundation-sys", 1704 | "libc", 1705 | ] 1706 | 1707 | [[package]] 1708 | name = "serde" 1709 | version = "1.0.188" 1710 | source = "registry+https://github.com/rust-lang/crates.io-index" 1711 | checksum = "cf9e0fcba69a370eed61bcf2b728575f726b50b55cba78064753d708ddc7549e" 1712 | dependencies = [ 1713 | "serde_derive", 1714 | ] 1715 | 1716 | [[package]] 1717 | name = "serde_derive" 1718 | version = "1.0.188" 1719 | source = "registry+https://github.com/rust-lang/crates.io-index" 1720 | checksum = "4eca7ac642d82aa35b60049a6eccb4be6be75e599bd2e9adb5f875a737654af2" 1721 | dependencies = [ 1722 | "proc-macro2", 1723 | "quote", 1724 | "syn 2.0.37", 1725 | ] 1726 | 1727 | [[package]] 1728 | name = "serde_json" 1729 | version = "1.0.107" 1730 | source = "registry+https://github.com/rust-lang/crates.io-index" 1731 | checksum = "6b420ce6e3d8bd882e9b243c6eed35dbc9a6110c9769e74b584e0d68d1f20c65" 1732 | dependencies = [ 1733 | "itoa", 1734 | "ryu", 1735 | "serde", 1736 | ] 1737 | 1738 | [[package]] 1739 | name = "serde_path_to_error" 1740 | version = "0.1.14" 1741 | source = "registry+https://github.com/rust-lang/crates.io-index" 1742 | checksum = "4beec8bce849d58d06238cb50db2e1c417cfeafa4c63f692b15c82b7c80f8335" 1743 | dependencies = [ 1744 | "itoa", 1745 | "serde", 1746 | ] 1747 | 1748 | [[package]] 1749 | name = "serde_spanned" 1750 | version = "0.6.3" 1751 | source = "registry+https://github.com/rust-lang/crates.io-index" 1752 | checksum = "96426c9936fd7a0124915f9185ea1d20aa9445cc9821142f0a73bc9207a2e186" 1753 | dependencies = [ 1754 | "serde", 1755 | ] 1756 | 1757 | [[package]] 1758 | name = "serde_urlencoded" 1759 | version = "0.7.1" 1760 | source = "registry+https://github.com/rust-lang/crates.io-index" 1761 | checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" 1762 | dependencies = [ 1763 | "form_urlencoded", 1764 | "itoa", 1765 | "ryu", 1766 | "serde", 1767 | ] 1768 | 1769 | [[package]] 1770 | name = "sha1" 1771 | version = "0.10.5" 1772 | source = "registry+https://github.com/rust-lang/crates.io-index" 1773 | checksum = "f04293dc80c3993519f2d7f6f511707ee7094fe0c6d3406feb330cdb3540eba3" 1774 | dependencies = [ 1775 | "cfg-if", 1776 | "cpufeatures", 1777 | "digest", 1778 | ] 1779 | 1780 | [[package]] 1781 | name = "sha2" 1782 | version = "0.10.7" 1783 | source = "registry+https://github.com/rust-lang/crates.io-index" 1784 | checksum = "479fb9d862239e610720565ca91403019f2f00410f1864c5aa7479b950a76ed8" 1785 | dependencies = [ 1786 | "cfg-if", 1787 | "cpufeatures", 1788 | "digest", 1789 | ] 1790 | 1791 | [[package]] 1792 | name = "sharded-slab" 1793 | version = "0.1.4" 1794 | source = "registry+https://github.com/rust-lang/crates.io-index" 1795 | checksum = "900fba806f70c630b0a382d0d825e17a0f19fcd059a2ade1ff237bcddf446b31" 1796 | dependencies = [ 1797 | "lazy_static", 1798 | ] 1799 | 1800 | [[package]] 1801 | name = "signature" 1802 | version = "2.1.0" 1803 | source = "registry+https://github.com/rust-lang/crates.io-index" 1804 | checksum = "5e1788eed21689f9cf370582dfc467ef36ed9c707f073528ddafa8d83e3b8500" 1805 | dependencies = [ 1806 | "digest", 1807 | "rand_core", 1808 | ] 1809 | 1810 | [[package]] 1811 | name = "slab" 1812 | version = "0.4.9" 1813 | source = "registry+https://github.com/rust-lang/crates.io-index" 1814 | checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" 1815 | dependencies = [ 1816 | "autocfg", 1817 | ] 1818 | 1819 | [[package]] 1820 | name = "smallvec" 1821 | version = "1.11.0" 1822 | source = "registry+https://github.com/rust-lang/crates.io-index" 1823 | checksum = "62bb4feee49fdd9f707ef802e22365a35de4b7b299de4763d44bfea899442ff9" 1824 | 1825 | [[package]] 1826 | name = "socket2" 1827 | version = "0.4.9" 1828 | source = "registry+https://github.com/rust-lang/crates.io-index" 1829 | checksum = "64a4a911eed85daf18834cfaa86a79b7d266ff93ff5ba14005426219480ed662" 1830 | dependencies = [ 1831 | "libc", 1832 | "winapi", 1833 | ] 1834 | 1835 | [[package]] 1836 | name = "socket2" 1837 | version = "0.5.4" 1838 | source = "registry+https://github.com/rust-lang/crates.io-index" 1839 | checksum = "4031e820eb552adee9295814c0ced9e5cf38ddf1e8b7d566d6de8e2538ea989e" 1840 | dependencies = [ 1841 | "libc", 1842 | "windows-sys", 1843 | ] 1844 | 1845 | [[package]] 1846 | name = "spin" 1847 | version = "0.5.2" 1848 | source = "registry+https://github.com/rust-lang/crates.io-index" 1849 | checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" 1850 | 1851 | [[package]] 1852 | name = "spin" 1853 | version = "0.9.8" 1854 | source = "registry+https://github.com/rust-lang/crates.io-index" 1855 | checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" 1856 | dependencies = [ 1857 | "lock_api", 1858 | ] 1859 | 1860 | [[package]] 1861 | name = "spki" 1862 | version = "0.7.2" 1863 | source = "registry+https://github.com/rust-lang/crates.io-index" 1864 | checksum = "9d1e996ef02c474957d681f1b05213dfb0abab947b446a62d37770b23500184a" 1865 | dependencies = [ 1866 | "base64ct", 1867 | "der", 1868 | ] 1869 | 1870 | [[package]] 1871 | name = "sqlformat" 1872 | version = "0.2.2" 1873 | source = "registry+https://github.com/rust-lang/crates.io-index" 1874 | checksum = "6b7b278788e7be4d0d29c0f39497a0eef3fba6bbc8e70d8bf7fde46edeaa9e85" 1875 | dependencies = [ 1876 | "itertools", 1877 | "nom", 1878 | "unicode_categories", 1879 | ] 1880 | 1881 | [[package]] 1882 | name = "sqlx" 1883 | version = "0.7.1" 1884 | source = "registry+https://github.com/rust-lang/crates.io-index" 1885 | checksum = "8e58421b6bc416714d5115a2ca953718f6c621a51b68e4f4922aea5a4391a721" 1886 | dependencies = [ 1887 | "sqlx-core", 1888 | "sqlx-macros", 1889 | "sqlx-mysql", 1890 | "sqlx-postgres", 1891 | "sqlx-sqlite", 1892 | ] 1893 | 1894 | [[package]] 1895 | name = "sqlx-core" 1896 | version = "0.7.1" 1897 | source = "registry+https://github.com/rust-lang/crates.io-index" 1898 | checksum = "dd4cef4251aabbae751a3710927945901ee1d97ee96d757f6880ebb9a79bfd53" 1899 | dependencies = [ 1900 | "ahash", 1901 | "atoi", 1902 | "byteorder", 1903 | "bytes", 1904 | "chrono", 1905 | "crc", 1906 | "crossbeam-queue", 1907 | "dotenvy", 1908 | "either", 1909 | "event-listener", 1910 | "futures-channel", 1911 | "futures-core", 1912 | "futures-intrusive", 1913 | "futures-io", 1914 | "futures-util", 1915 | "hashlink", 1916 | "hex", 1917 | "indexmap 2.0.0", 1918 | "log", 1919 | "memchr", 1920 | "native-tls", 1921 | "once_cell", 1922 | "paste", 1923 | "percent-encoding", 1924 | "serde", 1925 | "serde_json", 1926 | "sha2", 1927 | "smallvec", 1928 | "sqlformat", 1929 | "thiserror", 1930 | "tokio", 1931 | "tokio-stream", 1932 | "tracing", 1933 | "url", 1934 | ] 1935 | 1936 | [[package]] 1937 | name = "sqlx-macros" 1938 | version = "0.7.1" 1939 | source = "registry+https://github.com/rust-lang/crates.io-index" 1940 | checksum = "208e3165167afd7f3881b16c1ef3f2af69fa75980897aac8874a0696516d12c2" 1941 | dependencies = [ 1942 | "proc-macro2", 1943 | "quote", 1944 | "sqlx-core", 1945 | "sqlx-macros-core", 1946 | "syn 1.0.109", 1947 | ] 1948 | 1949 | [[package]] 1950 | name = "sqlx-macros-core" 1951 | version = "0.7.1" 1952 | source = "registry+https://github.com/rust-lang/crates.io-index" 1953 | checksum = "8a4a8336d278c62231d87f24e8a7a74898156e34c1c18942857be2acb29c7dfc" 1954 | dependencies = [ 1955 | "dotenvy", 1956 | "either", 1957 | "heck", 1958 | "hex", 1959 | "once_cell", 1960 | "proc-macro2", 1961 | "quote", 1962 | "serde", 1963 | "serde_json", 1964 | "sha2", 1965 | "sqlx-core", 1966 | "sqlx-mysql", 1967 | "sqlx-postgres", 1968 | "sqlx-sqlite", 1969 | "syn 1.0.109", 1970 | "tempfile", 1971 | "tokio", 1972 | "url", 1973 | ] 1974 | 1975 | [[package]] 1976 | name = "sqlx-mysql" 1977 | version = "0.7.1" 1978 | source = "registry+https://github.com/rust-lang/crates.io-index" 1979 | checksum = "8ca69bf415b93b60b80dc8fda3cb4ef52b2336614d8da2de5456cc942a110482" 1980 | dependencies = [ 1981 | "atoi", 1982 | "base64 0.21.4", 1983 | "bitflags 2.4.0", 1984 | "byteorder", 1985 | "bytes", 1986 | "chrono", 1987 | "crc", 1988 | "digest", 1989 | "dotenvy", 1990 | "either", 1991 | "futures-channel", 1992 | "futures-core", 1993 | "futures-io", 1994 | "futures-util", 1995 | "generic-array", 1996 | "hex", 1997 | "hkdf", 1998 | "hmac", 1999 | "itoa", 2000 | "log", 2001 | "md-5", 2002 | "memchr", 2003 | "once_cell", 2004 | "percent-encoding", 2005 | "rand", 2006 | "rsa", 2007 | "serde", 2008 | "sha1", 2009 | "sha2", 2010 | "smallvec", 2011 | "sqlx-core", 2012 | "stringprep", 2013 | "thiserror", 2014 | "tracing", 2015 | "whoami", 2016 | ] 2017 | 2018 | [[package]] 2019 | name = "sqlx-postgres" 2020 | version = "0.7.1" 2021 | source = "registry+https://github.com/rust-lang/crates.io-index" 2022 | checksum = "a0db2df1b8731c3651e204629dd55e52adbae0462fa1bdcbed56a2302c18181e" 2023 | dependencies = [ 2024 | "atoi", 2025 | "base64 0.21.4", 2026 | "bitflags 2.4.0", 2027 | "byteorder", 2028 | "chrono", 2029 | "crc", 2030 | "dotenvy", 2031 | "etcetera", 2032 | "futures-channel", 2033 | "futures-core", 2034 | "futures-io", 2035 | "futures-util", 2036 | "hex", 2037 | "hkdf", 2038 | "hmac", 2039 | "home", 2040 | "itoa", 2041 | "log", 2042 | "md-5", 2043 | "memchr", 2044 | "once_cell", 2045 | "rand", 2046 | "serde", 2047 | "serde_json", 2048 | "sha1", 2049 | "sha2", 2050 | "smallvec", 2051 | "sqlx-core", 2052 | "stringprep", 2053 | "thiserror", 2054 | "tracing", 2055 | "whoami", 2056 | ] 2057 | 2058 | [[package]] 2059 | name = "sqlx-sqlite" 2060 | version = "0.7.1" 2061 | source = "registry+https://github.com/rust-lang/crates.io-index" 2062 | checksum = "be4c21bf34c7cae5b283efb3ac1bcc7670df7561124dc2f8bdc0b59be40f79a2" 2063 | dependencies = [ 2064 | "atoi", 2065 | "chrono", 2066 | "flume", 2067 | "futures-channel", 2068 | "futures-core", 2069 | "futures-executor", 2070 | "futures-intrusive", 2071 | "futures-util", 2072 | "libsqlite3-sys", 2073 | "log", 2074 | "percent-encoding", 2075 | "serde", 2076 | "sqlx-core", 2077 | "tracing", 2078 | "url", 2079 | ] 2080 | 2081 | [[package]] 2082 | name = "stringprep" 2083 | version = "0.1.4" 2084 | source = "registry+https://github.com/rust-lang/crates.io-index" 2085 | checksum = "bb41d74e231a107a1b4ee36bd1214b11285b77768d2e3824aedafa988fd36ee6" 2086 | dependencies = [ 2087 | "finl_unicode", 2088 | "unicode-bidi", 2089 | "unicode-normalization", 2090 | ] 2091 | 2092 | [[package]] 2093 | name = "strsim" 2094 | version = "0.10.0" 2095 | source = "registry+https://github.com/rust-lang/crates.io-index" 2096 | checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" 2097 | 2098 | [[package]] 2099 | name = "subtle" 2100 | version = "2.5.0" 2101 | source = "registry+https://github.com/rust-lang/crates.io-index" 2102 | checksum = "81cdd64d312baedb58e21336b31bc043b77e01cc99033ce76ef539f78e965ebc" 2103 | 2104 | [[package]] 2105 | name = "syn" 2106 | version = "1.0.109" 2107 | source = "registry+https://github.com/rust-lang/crates.io-index" 2108 | checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" 2109 | dependencies = [ 2110 | "proc-macro2", 2111 | "quote", 2112 | "unicode-ident", 2113 | ] 2114 | 2115 | [[package]] 2116 | name = "syn" 2117 | version = "2.0.37" 2118 | source = "registry+https://github.com/rust-lang/crates.io-index" 2119 | checksum = "7303ef2c05cd654186cb250d29049a24840ca25d2747c25c0381c8d9e2f582e8" 2120 | dependencies = [ 2121 | "proc-macro2", 2122 | "quote", 2123 | "unicode-ident", 2124 | ] 2125 | 2126 | [[package]] 2127 | name = "sync_wrapper" 2128 | version = "0.1.2" 2129 | source = "registry+https://github.com/rust-lang/crates.io-index" 2130 | checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160" 2131 | dependencies = [ 2132 | "futures-core", 2133 | ] 2134 | 2135 | [[package]] 2136 | name = "tempfile" 2137 | version = "3.8.0" 2138 | source = "registry+https://github.com/rust-lang/crates.io-index" 2139 | checksum = "cb94d2f3cc536af71caac6b6fcebf65860b347e7ce0cc9ebe8f70d3e521054ef" 2140 | dependencies = [ 2141 | "cfg-if", 2142 | "fastrand", 2143 | "redox_syscall", 2144 | "rustix", 2145 | "windows-sys", 2146 | ] 2147 | 2148 | [[package]] 2149 | name = "thiserror" 2150 | version = "1.0.48" 2151 | source = "registry+https://github.com/rust-lang/crates.io-index" 2152 | checksum = "9d6d7a740b8a666a7e828dd00da9c0dc290dff53154ea77ac109281de90589b7" 2153 | dependencies = [ 2154 | "thiserror-impl", 2155 | ] 2156 | 2157 | [[package]] 2158 | name = "thiserror-impl" 2159 | version = "1.0.48" 2160 | source = "registry+https://github.com/rust-lang/crates.io-index" 2161 | checksum = "49922ecae66cc8a249b77e68d1d0623c1b2c514f0060c27cdc68bd62a1219d35" 2162 | dependencies = [ 2163 | "proc-macro2", 2164 | "quote", 2165 | "syn 2.0.37", 2166 | ] 2167 | 2168 | [[package]] 2169 | name = "thread_local" 2170 | version = "1.1.7" 2171 | source = "registry+https://github.com/rust-lang/crates.io-index" 2172 | checksum = "3fdd6f064ccff2d6567adcb3873ca630700f00b5ad3f060c25b5dcfd9a4ce152" 2173 | dependencies = [ 2174 | "cfg-if", 2175 | "once_cell", 2176 | ] 2177 | 2178 | [[package]] 2179 | name = "tiktoken-rs" 2180 | version = "0.5.4" 2181 | source = "registry+https://github.com/rust-lang/crates.io-index" 2182 | checksum = "f9ae5a3c24361e5f038af22517ba7f8e3af4099e30e78a3d56f86b48238fce9d" 2183 | dependencies = [ 2184 | "anyhow", 2185 | "base64 0.21.4", 2186 | "bstr", 2187 | "fancy-regex", 2188 | "lazy_static", 2189 | "parking_lot", 2190 | "rustc-hash", 2191 | ] 2192 | 2193 | [[package]] 2194 | name = "tinyvec" 2195 | version = "1.6.0" 2196 | source = "registry+https://github.com/rust-lang/crates.io-index" 2197 | checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50" 2198 | dependencies = [ 2199 | "tinyvec_macros", 2200 | ] 2201 | 2202 | [[package]] 2203 | name = "tinyvec_macros" 2204 | version = "0.1.1" 2205 | source = "registry+https://github.com/rust-lang/crates.io-index" 2206 | checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" 2207 | 2208 | [[package]] 2209 | name = "tokio" 2210 | version = "1.32.0" 2211 | source = "registry+https://github.com/rust-lang/crates.io-index" 2212 | checksum = "17ed6077ed6cd6c74735e21f37eb16dc3935f96878b1fe961074089cc80893f9" 2213 | dependencies = [ 2214 | "backtrace", 2215 | "bytes", 2216 | "libc", 2217 | "mio", 2218 | "num_cpus", 2219 | "pin-project-lite", 2220 | "socket2 0.5.4", 2221 | "tokio-macros", 2222 | "windows-sys", 2223 | ] 2224 | 2225 | [[package]] 2226 | name = "tokio-macros" 2227 | version = "2.1.0" 2228 | source = "registry+https://github.com/rust-lang/crates.io-index" 2229 | checksum = "630bdcf245f78637c13ec01ffae6187cca34625e8c63150d424b59e55af2675e" 2230 | dependencies = [ 2231 | "proc-macro2", 2232 | "quote", 2233 | "syn 2.0.37", 2234 | ] 2235 | 2236 | [[package]] 2237 | name = "tokio-native-tls" 2238 | version = "0.3.1" 2239 | source = "registry+https://github.com/rust-lang/crates.io-index" 2240 | checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" 2241 | dependencies = [ 2242 | "native-tls", 2243 | "tokio", 2244 | ] 2245 | 2246 | [[package]] 2247 | name = "tokio-stream" 2248 | version = "0.1.14" 2249 | source = "registry+https://github.com/rust-lang/crates.io-index" 2250 | checksum = "397c988d37662c7dda6d2208364a706264bf3d6138b11d436cbac0ad38832842" 2251 | dependencies = [ 2252 | "futures-core", 2253 | "pin-project-lite", 2254 | "tokio", 2255 | ] 2256 | 2257 | [[package]] 2258 | name = "tokio-util" 2259 | version = "0.7.8" 2260 | source = "registry+https://github.com/rust-lang/crates.io-index" 2261 | checksum = "806fe8c2c87eccc8b3267cbae29ed3ab2d0bd37fca70ab622e46aaa9375ddb7d" 2262 | dependencies = [ 2263 | "bytes", 2264 | "futures-core", 2265 | "futures-sink", 2266 | "pin-project-lite", 2267 | "tokio", 2268 | "tracing", 2269 | ] 2270 | 2271 | [[package]] 2272 | name = "toml" 2273 | version = "0.7.8" 2274 | source = "registry+https://github.com/rust-lang/crates.io-index" 2275 | checksum = "dd79e69d3b627db300ff956027cc6c3798cef26d22526befdfcd12feeb6d2257" 2276 | dependencies = [ 2277 | "serde", 2278 | "serde_spanned", 2279 | "toml_datetime", 2280 | "toml_edit", 2281 | ] 2282 | 2283 | [[package]] 2284 | name = "toml_datetime" 2285 | version = "0.6.3" 2286 | source = "registry+https://github.com/rust-lang/crates.io-index" 2287 | checksum = "7cda73e2f1397b1262d6dfdcef8aafae14d1de7748d66822d3bfeeb6d03e5e4b" 2288 | dependencies = [ 2289 | "serde", 2290 | ] 2291 | 2292 | [[package]] 2293 | name = "toml_edit" 2294 | version = "0.19.15" 2295 | source = "registry+https://github.com/rust-lang/crates.io-index" 2296 | checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" 2297 | dependencies = [ 2298 | "indexmap 2.0.0", 2299 | "serde", 2300 | "serde_spanned", 2301 | "toml_datetime", 2302 | "winnow", 2303 | ] 2304 | 2305 | [[package]] 2306 | name = "tower" 2307 | version = "0.4.13" 2308 | source = "registry+https://github.com/rust-lang/crates.io-index" 2309 | checksum = "b8fa9be0de6cf49e536ce1851f987bd21a43b771b09473c3549a6c853db37c1c" 2310 | dependencies = [ 2311 | "futures-core", 2312 | "futures-util", 2313 | "pin-project", 2314 | "pin-project-lite", 2315 | "tokio", 2316 | "tower-layer", 2317 | "tower-service", 2318 | "tracing", 2319 | ] 2320 | 2321 | [[package]] 2322 | name = "tower-hyper-http-body-compat" 2323 | version = "0.2.0" 2324 | source = "registry+https://github.com/rust-lang/crates.io-index" 2325 | checksum = "a7ea3e622710ee44a8255baa6fcb2a34d67450e2ffb48e5e58d5a7bd6ff55a21" 2326 | dependencies = [ 2327 | "http", 2328 | "http-body 0.4.5", 2329 | "http-body 1.0.0-rc.2", 2330 | "hyper 1.0.0-rc.4", 2331 | "pin-project-lite", 2332 | "tower", 2333 | "tower-service", 2334 | ] 2335 | 2336 | [[package]] 2337 | name = "tower-layer" 2338 | version = "0.3.2" 2339 | source = "registry+https://github.com/rust-lang/crates.io-index" 2340 | checksum = "c20c8dbed6283a09604c3e69b4b7eeb54e298b8a600d4d5ecb5ad39de609f1d0" 2341 | 2342 | [[package]] 2343 | name = "tower-service" 2344 | version = "0.3.2" 2345 | source = "registry+https://github.com/rust-lang/crates.io-index" 2346 | checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52" 2347 | 2348 | [[package]] 2349 | name = "tracing" 2350 | version = "0.1.37" 2351 | source = "registry+https://github.com/rust-lang/crates.io-index" 2352 | checksum = "8ce8c33a8d48bd45d624a6e523445fd21ec13d3653cd51f681abf67418f54eb8" 2353 | dependencies = [ 2354 | "cfg-if", 2355 | "log", 2356 | "pin-project-lite", 2357 | "tracing-attributes", 2358 | "tracing-core", 2359 | ] 2360 | 2361 | [[package]] 2362 | name = "tracing-attributes" 2363 | version = "0.1.26" 2364 | source = "registry+https://github.com/rust-lang/crates.io-index" 2365 | checksum = "5f4f31f56159e98206da9efd823404b79b6ef3143b4a7ab76e67b1751b25a4ab" 2366 | dependencies = [ 2367 | "proc-macro2", 2368 | "quote", 2369 | "syn 2.0.37", 2370 | ] 2371 | 2372 | [[package]] 2373 | name = "tracing-core" 2374 | version = "0.1.31" 2375 | source = "registry+https://github.com/rust-lang/crates.io-index" 2376 | checksum = "0955b8137a1df6f1a2e9a37d8a6656291ff0297c1a97c24e0d8425fe2312f79a" 2377 | dependencies = [ 2378 | "once_cell", 2379 | "valuable", 2380 | ] 2381 | 2382 | [[package]] 2383 | name = "tracing-log" 2384 | version = "0.1.3" 2385 | source = "registry+https://github.com/rust-lang/crates.io-index" 2386 | checksum = "78ddad33d2d10b1ed7eb9d1f518a5674713876e97e5bb9b7345a7984fbb4f922" 2387 | dependencies = [ 2388 | "lazy_static", 2389 | "log", 2390 | "tracing-core", 2391 | ] 2392 | 2393 | [[package]] 2394 | name = "tracing-subscriber" 2395 | version = "0.3.17" 2396 | source = "registry+https://github.com/rust-lang/crates.io-index" 2397 | checksum = "30a651bc37f915e81f087d86e62a18eec5f79550c7faff886f7090b4ea757c77" 2398 | dependencies = [ 2399 | "matchers", 2400 | "nu-ansi-term", 2401 | "once_cell", 2402 | "regex", 2403 | "sharded-slab", 2404 | "smallvec", 2405 | "thread_local", 2406 | "tracing", 2407 | "tracing-core", 2408 | "tracing-log", 2409 | ] 2410 | 2411 | [[package]] 2412 | name = "try-lock" 2413 | version = "0.2.4" 2414 | source = "registry+https://github.com/rust-lang/crates.io-index" 2415 | checksum = "3528ecfd12c466c6f163363caf2d02a71161dd5e1cc6ae7b34207ea2d42d81ed" 2416 | 2417 | [[package]] 2418 | name = "typenum" 2419 | version = "1.17.0" 2420 | source = "registry+https://github.com/rust-lang/crates.io-index" 2421 | checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" 2422 | 2423 | [[package]] 2424 | name = "unicode-bidi" 2425 | version = "0.3.13" 2426 | source = "registry+https://github.com/rust-lang/crates.io-index" 2427 | checksum = "92888ba5573ff080736b3648696b70cafad7d250551175acbaa4e0385b3e1460" 2428 | 2429 | [[package]] 2430 | name = "unicode-ident" 2431 | version = "1.0.12" 2432 | source = "registry+https://github.com/rust-lang/crates.io-index" 2433 | checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" 2434 | 2435 | [[package]] 2436 | name = "unicode-normalization" 2437 | version = "0.1.22" 2438 | source = "registry+https://github.com/rust-lang/crates.io-index" 2439 | checksum = "5c5713f0fc4b5db668a2ac63cdb7bb4469d8c9fed047b1d0292cc7b0ce2ba921" 2440 | dependencies = [ 2441 | "tinyvec", 2442 | ] 2443 | 2444 | [[package]] 2445 | name = "unicode-segmentation" 2446 | version = "1.10.1" 2447 | source = "registry+https://github.com/rust-lang/crates.io-index" 2448 | checksum = "1dd624098567895118886609431a7c3b8f516e41d30e0643f03d94592a147e36" 2449 | 2450 | [[package]] 2451 | name = "unicode_categories" 2452 | version = "0.1.1" 2453 | source = "registry+https://github.com/rust-lang/crates.io-index" 2454 | checksum = "39ec24b3121d976906ece63c9daad25b85969647682eee313cb5779fdd69e14e" 2455 | 2456 | [[package]] 2457 | name = "url" 2458 | version = "2.4.1" 2459 | source = "registry+https://github.com/rust-lang/crates.io-index" 2460 | checksum = "143b538f18257fac9cad154828a57c6bf5157e1aa604d4816b5995bf6de87ae5" 2461 | dependencies = [ 2462 | "form_urlencoded", 2463 | "idna", 2464 | "percent-encoding", 2465 | ] 2466 | 2467 | [[package]] 2468 | name = "utf8parse" 2469 | version = "0.2.1" 2470 | source = "registry+https://github.com/rust-lang/crates.io-index" 2471 | checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" 2472 | 2473 | [[package]] 2474 | name = "valuable" 2475 | version = "0.1.0" 2476 | source = "registry+https://github.com/rust-lang/crates.io-index" 2477 | checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" 2478 | 2479 | [[package]] 2480 | name = "vcpkg" 2481 | version = "0.2.15" 2482 | source = "registry+https://github.com/rust-lang/crates.io-index" 2483 | checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" 2484 | 2485 | [[package]] 2486 | name = "version_check" 2487 | version = "0.9.4" 2488 | source = "registry+https://github.com/rust-lang/crates.io-index" 2489 | checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" 2490 | 2491 | [[package]] 2492 | name = "want" 2493 | version = "0.3.1" 2494 | source = "registry+https://github.com/rust-lang/crates.io-index" 2495 | checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" 2496 | dependencies = [ 2497 | "try-lock", 2498 | ] 2499 | 2500 | [[package]] 2501 | name = "wasi" 2502 | version = "0.11.0+wasi-snapshot-preview1" 2503 | source = "registry+https://github.com/rust-lang/crates.io-index" 2504 | checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" 2505 | 2506 | [[package]] 2507 | name = "wasm-bindgen" 2508 | version = "0.2.87" 2509 | source = "registry+https://github.com/rust-lang/crates.io-index" 2510 | checksum = "7706a72ab36d8cb1f80ffbf0e071533974a60d0a308d01a5d0375bf60499a342" 2511 | dependencies = [ 2512 | "cfg-if", 2513 | "wasm-bindgen-macro", 2514 | ] 2515 | 2516 | [[package]] 2517 | name = "wasm-bindgen-backend" 2518 | version = "0.2.87" 2519 | source = "registry+https://github.com/rust-lang/crates.io-index" 2520 | checksum = "5ef2b6d3c510e9625e5fe6f509ab07d66a760f0885d858736483c32ed7809abd" 2521 | dependencies = [ 2522 | "bumpalo", 2523 | "log", 2524 | "once_cell", 2525 | "proc-macro2", 2526 | "quote", 2527 | "syn 2.0.37", 2528 | "wasm-bindgen-shared", 2529 | ] 2530 | 2531 | [[package]] 2532 | name = "wasm-bindgen-futures" 2533 | version = "0.4.37" 2534 | source = "registry+https://github.com/rust-lang/crates.io-index" 2535 | checksum = "c02dbc21516f9f1f04f187958890d7e6026df8d16540b7ad9492bc34a67cea03" 2536 | dependencies = [ 2537 | "cfg-if", 2538 | "js-sys", 2539 | "wasm-bindgen", 2540 | "web-sys", 2541 | ] 2542 | 2543 | [[package]] 2544 | name = "wasm-bindgen-macro" 2545 | version = "0.2.87" 2546 | source = "registry+https://github.com/rust-lang/crates.io-index" 2547 | checksum = "dee495e55982a3bd48105a7b947fd2a9b4a8ae3010041b9e0faab3f9cd028f1d" 2548 | dependencies = [ 2549 | "quote", 2550 | "wasm-bindgen-macro-support", 2551 | ] 2552 | 2553 | [[package]] 2554 | name = "wasm-bindgen-macro-support" 2555 | version = "0.2.87" 2556 | source = "registry+https://github.com/rust-lang/crates.io-index" 2557 | checksum = "54681b18a46765f095758388f2d0cf16eb8d4169b639ab575a8f5693af210c7b" 2558 | dependencies = [ 2559 | "proc-macro2", 2560 | "quote", 2561 | "syn 2.0.37", 2562 | "wasm-bindgen-backend", 2563 | "wasm-bindgen-shared", 2564 | ] 2565 | 2566 | [[package]] 2567 | name = "wasm-bindgen-shared" 2568 | version = "0.2.87" 2569 | source = "registry+https://github.com/rust-lang/crates.io-index" 2570 | checksum = "ca6ad05a4870b2bf5fe995117d3728437bd27d7cd5f06f13c17443ef369775a1" 2571 | 2572 | [[package]] 2573 | name = "wasm-streams" 2574 | version = "0.3.0" 2575 | source = "registry+https://github.com/rust-lang/crates.io-index" 2576 | checksum = "b4609d447824375f43e1ffbc051b50ad8f4b3ae8219680c94452ea05eb240ac7" 2577 | dependencies = [ 2578 | "futures-util", 2579 | "js-sys", 2580 | "wasm-bindgen", 2581 | "wasm-bindgen-futures", 2582 | "web-sys", 2583 | ] 2584 | 2585 | [[package]] 2586 | name = "web-sys" 2587 | version = "0.3.64" 2588 | source = "registry+https://github.com/rust-lang/crates.io-index" 2589 | checksum = "9b85cbef8c220a6abc02aefd892dfc0fc23afb1c6a426316ec33253a3877249b" 2590 | dependencies = [ 2591 | "js-sys", 2592 | "wasm-bindgen", 2593 | ] 2594 | 2595 | [[package]] 2596 | name = "whoami" 2597 | version = "1.4.1" 2598 | source = "registry+https://github.com/rust-lang/crates.io-index" 2599 | checksum = "22fc3756b8a9133049b26c7f61ab35416c130e8c09b660f5b3958b446f52cc50" 2600 | 2601 | [[package]] 2602 | name = "winapi" 2603 | version = "0.3.9" 2604 | source = "registry+https://github.com/rust-lang/crates.io-index" 2605 | checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" 2606 | dependencies = [ 2607 | "winapi-i686-pc-windows-gnu", 2608 | "winapi-x86_64-pc-windows-gnu", 2609 | ] 2610 | 2611 | [[package]] 2612 | name = "winapi-i686-pc-windows-gnu" 2613 | version = "0.4.0" 2614 | source = "registry+https://github.com/rust-lang/crates.io-index" 2615 | checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 2616 | 2617 | [[package]] 2618 | name = "winapi-x86_64-pc-windows-gnu" 2619 | version = "0.4.0" 2620 | source = "registry+https://github.com/rust-lang/crates.io-index" 2621 | checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 2622 | 2623 | [[package]] 2624 | name = "windows" 2625 | version = "0.48.0" 2626 | source = "registry+https://github.com/rust-lang/crates.io-index" 2627 | checksum = "e686886bc078bc1b0b600cac0147aadb815089b6e4da64016cbd754b6342700f" 2628 | dependencies = [ 2629 | "windows-targets", 2630 | ] 2631 | 2632 | [[package]] 2633 | name = "windows-sys" 2634 | version = "0.48.0" 2635 | source = "registry+https://github.com/rust-lang/crates.io-index" 2636 | checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" 2637 | dependencies = [ 2638 | "windows-targets", 2639 | ] 2640 | 2641 | [[package]] 2642 | name = "windows-targets" 2643 | version = "0.48.5" 2644 | source = "registry+https://github.com/rust-lang/crates.io-index" 2645 | checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" 2646 | dependencies = [ 2647 | "windows_aarch64_gnullvm", 2648 | "windows_aarch64_msvc", 2649 | "windows_i686_gnu", 2650 | "windows_i686_msvc", 2651 | "windows_x86_64_gnu", 2652 | "windows_x86_64_gnullvm", 2653 | "windows_x86_64_msvc", 2654 | ] 2655 | 2656 | [[package]] 2657 | name = "windows_aarch64_gnullvm" 2658 | version = "0.48.5" 2659 | source = "registry+https://github.com/rust-lang/crates.io-index" 2660 | checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" 2661 | 2662 | [[package]] 2663 | name = "windows_aarch64_msvc" 2664 | version = "0.48.5" 2665 | source = "registry+https://github.com/rust-lang/crates.io-index" 2666 | checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" 2667 | 2668 | [[package]] 2669 | name = "windows_i686_gnu" 2670 | version = "0.48.5" 2671 | source = "registry+https://github.com/rust-lang/crates.io-index" 2672 | checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" 2673 | 2674 | [[package]] 2675 | name = "windows_i686_msvc" 2676 | version = "0.48.5" 2677 | source = "registry+https://github.com/rust-lang/crates.io-index" 2678 | checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" 2679 | 2680 | [[package]] 2681 | name = "windows_x86_64_gnu" 2682 | version = "0.48.5" 2683 | source = "registry+https://github.com/rust-lang/crates.io-index" 2684 | checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" 2685 | 2686 | [[package]] 2687 | name = "windows_x86_64_gnullvm" 2688 | version = "0.48.5" 2689 | source = "registry+https://github.com/rust-lang/crates.io-index" 2690 | checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" 2691 | 2692 | [[package]] 2693 | name = "windows_x86_64_msvc" 2694 | version = "0.48.5" 2695 | source = "registry+https://github.com/rust-lang/crates.io-index" 2696 | checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" 2697 | 2698 | [[package]] 2699 | name = "winnow" 2700 | version = "0.5.15" 2701 | source = "registry+https://github.com/rust-lang/crates.io-index" 2702 | checksum = "7c2e3184b9c4e92ad5167ca73039d0c42476302ab603e2fec4487511f38ccefc" 2703 | dependencies = [ 2704 | "memchr", 2705 | ] 2706 | 2707 | [[package]] 2708 | name = "winreg" 2709 | version = "0.50.0" 2710 | source = "registry+https://github.com/rust-lang/crates.io-index" 2711 | checksum = "524e57b2c537c0f9b1e69f1965311ec12182b4122e45035b1508cd24d2adadb1" 2712 | dependencies = [ 2713 | "cfg-if", 2714 | "windows-sys", 2715 | ] 2716 | 2717 | [[package]] 2718 | name = "zeroize" 2719 | version = "1.6.0" 2720 | source = "registry+https://github.com/rust-lang/crates.io-index" 2721 | checksum = "2a0956f1ba7c7909bfb66c2e9e4124ab6f6482560f6628b5aaeba39207c9aad9" 2722 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | resolver = "2" 3 | members = [ 4 | "openai-hub-core", 5 | "openai-hubd", 6 | "openai-hub-jwt-token-gen" 7 | ] 8 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM rust:1.72-bookworm as chef 2 | RUN cargo install cargo-chef cargo-zigbuild 3 | RUN apt update && apt install -y python3-pip libssl-dev && rm -rf /var/lib/apt/lists/* 4 | RUN pip3 install --break-system-packages ziglang 5 | WORKDIR openai-hub 6 | 7 | FROM chef AS planner 8 | COPY . . 9 | RUN cargo chef prepare --recipe-path recipe.json 10 | 11 | FROM chef AS builder 12 | COPY --from=planner /openai-hub/recipe.json recipe.json 13 | RUN cargo chef cook --zigbuild --workspace --release --recipe-path recipe.json 14 | COPY . . 15 | RUN cargo zigbuild --release --all-features && \ 16 | mkdir build && \ 17 | cp /openai-hub/target/release/openai* build/ 18 | 19 | FROM debian:bookworm-slim AS runtime-base 20 | RUN apt update && apt install -y libssl3 && rm -rf /var/lib/apt/lists/* 21 | 22 | FROM runtime-base 23 | WORKDIR /opt/openai-hub 24 | RUN mkdir -p /opt/openai-hub 25 | COPY --from=builder /openai-hub/build/openai-hubd /opt/openai-hub/ 26 | COPY --from=builder /openai-hub/build/openai-hub-jwt-token-gen /opt/openai-hub/ 27 | COPY config.toml acl.toml /opt/openai-hub/config/ 28 | CMD ["/opt/openai-hub/openai-hubd", "-c", "config/config.toml", "-a", "config/acl.toml"] 29 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 lightsing 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # OpenAI Hub 2 | [![Licence](https://img.shields.io/github/license/Ileriayo/markdown-badges?style=for-the-badge)](./LICENSE) 3 | [![Docker](https://img.shields.io/badge/docker-%230db7ed.svg?style=for-the-badge&logo=docker&logoColor=white)](https://hub.docker.com/repository/docker/lightsing/openai-hub) 4 | 5 | OpenAI Hub is a comprehensive and robust tool designed to streamline and enhance your interaction with OpenAI's API. It features an innovative way to load balance multiple API keys, allowing users to make requests without needing individual OpenAI API keys. Additionally, it employs a global access control list (ACL) that gives you the power to regulate which APIs and models users can utilize. The Hub also includes JWT Authentication for secure and reliable user authentication, and now, an Access Log feature for tracking API usage and token consumption. 6 | 7 | ## Key Features 8 | - **Load Balancing:** Utilize multiple API keys efficiently, preventing the overuse of any single key. 9 | - **API Key Protection:** Allow users to make requests without the need for an individual OpenAI API key, enhancing security and ease of use. 10 | - **Global ACL:** Regulate user access to specific APIs and models, ensuring the right people have access to the right resources. 11 | - **JWT Authentication:** Secure and reliable user authentication system using JSON Web Tokens (JWT). 12 | - **Access Log:** Keep track of API usage and token consumption with our newly implemented access log feature. You can choose to store logs in file, SQLite, MySQL, or PostgreSQL backends. 13 | 14 | ## Getting Started 15 | 16 | You can run OpenAI Hub either by cloning the repository and using Cargo, or by using Docker. 17 | 18 | ### Running with Cargo 19 | 20 | ```bash 21 | git clone https://github.com/lightsing/openai-hub.git 22 | cd openai-hub 23 | 24 | # build and run 25 | cargo run run --bin openai-hubd --all-features --release 26 | ``` 27 | 28 | ### Running with Docker 29 | 30 | ```bash 31 | # pull the Docker image 32 | docker pull lightsing/openai-hub:latest 33 | 34 | # run the Docker container 35 | docker run -p 8080:8080 lightsing/openai-hub 36 | 37 | # or with your custom configs 38 | docker run -v /your/path/to/config:/opt/openai-hub/config -p lightsing/openai-hub 39 | ``` 40 | 41 | Please replace `username` with the appropriate GitHub username. 42 | 43 | ## Upcoming Features (To-Do List) 44 | - [ ] **Per User/RBAC ACL:** We're developing a more granular access control system to allow permissions to be set on a per-user basis, and Role-Based Access Control (RBAC) to allow users to have roles that define their access levels. 45 | 46 | We're always working to improve OpenAI Hub and add new features. Stay tuned for these exciting updates! 47 | 48 | ## Contributing 49 | We encourage you to contribute to OpenAI Hub! Please check out the [Contributing to OpenAI Hub guide](CONTRIBUTING.md) for guidelines about how to proceed. 50 | 51 | ## License 52 | OpenAI Hub is released under the [MIT License](LICENSE). 53 | 54 | ## Contact 55 | If you have any questions, issues, or suggestions for improvement, please feel free to open an issue in this repository or contact us directly. 56 | 57 | We're excited to see how you'll use OpenAI Hub! -------------------------------------------------------------------------------- /acl.toml: -------------------------------------------------------------------------------- 1 | [global] 2 | whitelist = true # default reject unlisted requests, allow when `method.endpoint = true` 3 | # set whitelist to false if you want to default allow requests, reject when `method.endpoint = false` 4 | allow_deployments = [] # allow requests with azure deployment-id 5 | 6 | [global.methods] 7 | GET = false # default disallow GET requests 8 | POST = true # default allow POST requests 9 | DELETE = false # default disallow DELETE requests 10 | 11 | [endpoint.GET] 12 | "/models" = true # allow list models 13 | "/models/{model}" = true # allow retrive model 14 | "/files" = true # allow list uploaded files 15 | "/files/{file_id}/content" = true # allow to retrieve file content 16 | "/fine-tunes" = true # allow to list fine-tunes 17 | "/fine-tunes/{fine_tune_id}" = true # allow to retrieve fine-tune 18 | "/fine-tunes/{fine_tune_id}/events" = true # allow to retrieve fine-tune events 19 | "/engines" = true # allow to list engines [deprecated] 20 | "/engines/{engine_id}" = true # allow to retrieve engine 21 | 22 | [endpoint.POST] 23 | "/completions" = true # allow create completion 24 | "/chat/completions" = true # allow create chat completion 25 | "/edits" = true # allow create edit 26 | "/images/generations" = true # allow create image 27 | "/images/edits" = true # allow create image edit 28 | "/images/variations" = true # allow create image variantion 29 | "/embeddings" = true # allow create embeddings 30 | "/audio/transcriptions" = true # allow create audio transcription 31 | "/audio/translations" = true # allow create audio translation 32 | "/files" = true # allow upload files 33 | "/fine-tunes" = true # allwo to create fine-tune 34 | "/fine-tunes/{fine_tune_id}/cancel" = true # allow to cancel fine-tune 35 | "/moderations" = true # allow to create moderation 36 | 37 | [endpoint.DELETE] 38 | "/files/{file_id}" = true # allow to delete uploaded file 39 | "/models/{model}" = true # allow to delete a finetune model 40 | 41 | [model.GET."/models/{model}"] 42 | path = true # filter model in path 43 | allows = ["*"] # default allow any model 44 | disallows = [] 45 | 46 | [model.POST."/completions"] 47 | allows = ["text-davinci-003", "text-davinci-002", "text-curie-001", "text-babbage-001", "text-ada-001"] 48 | disallows = [] 49 | 50 | [model.POST."/chat/completions"] 51 | allows = ["gpt-4", "gpt-4-0314", "gpt-4-32k", "gpt-4-32k-0314", "gpt-3.5-turbo", "gpt-3.5-turbo-0301"] 52 | disallows = [] 53 | 54 | [model.POST."/edits"] 55 | allows = ["text-davinci-edit-001", "code-davinci-edit-001"] 56 | disallows = [] 57 | 58 | [model.POST."/embeddings"] 59 | allows = ["text-embedding-ada-002", "text-search-ada-doc-001"] 60 | disallows = [] 61 | 62 | [model.POST."/audio/transcriptions"] 63 | allows = ["whisper-1"] 64 | disallows = [] 65 | 66 | [model.POST."/audio/translations"] 67 | allows = ["whisper-1"] 68 | disallows = [] 69 | 70 | [model.POST."/fine-tunes"] 71 | allows = ["davinci", "curie", "babbage", "ada"] 72 | disallows = [] 73 | allow_omitted = true 74 | 75 | [model.POST."/moderations"] 76 | allows = ["text-moderation-stable", "text-moderation-latest"] 77 | disallows = [] 78 | 79 | [model.DELETE."/models/{model}"] 80 | path = true # filter model in path 81 | allows = [] 82 | disallows = [] -------------------------------------------------------------------------------- /config.toml: -------------------------------------------------------------------------------- 1 | # The IP address and port that the server will bind to 2 | bind = "0.0.0.0:8080" 3 | 4 | # The API keys for OpenAI. You can add multiple keys as needed. 5 | api_keys = [""] 6 | 7 | # The organization ID for OpenAI. Uncomment and fill in if applicable. 8 | # organization = "" 9 | 10 | # The base URL for the OpenAI API. The default is OpenAI's official API. 11 | # Uncomment and replace with your own endpoint if needed. 12 | # api_base = "https://api.openai.com/v1" # default 13 | # api_base = "https://example-endpoint.openai.azure.com" 14 | 15 | # The type of the API. By default, it's set to OpenAI's official API. 16 | # Uncomment and replace as needed for other API types. 17 | # api_type = "open_ai" # default 18 | # api_type = "azure" 19 | # api_type = "azure_ad" 20 | 21 | # The version of the API. Uncomment and fill in if using Azure. 22 | # api_version = "2023-05-15" # uncomment when using azure 23 | 24 | # Uncomment the following section to enable JWT authentication. 25 | # Provide the secret for JWT token generation and verification. 26 | # [jwt-auth] 27 | # secret = "some-secret" 28 | 29 | # The audit configuration. 30 | # Specifies where and how access logs should be stored. 31 | [audit] 32 | backend = "file" # possible backends: file, sqlite, mysql, postgres 33 | 34 | [audit.filters.access] 35 | enable = true # enable this filter 36 | 37 | method = true # log request method 38 | uri = true # log request origin uri 39 | headers = true # log request origin headers 40 | body = true # log request body (be careful when enable with file upload apis (like audio, files)) 41 | response = true # log openai response 42 | 43 | [audit.filters.tokens] 44 | enable = true # enable this filter 45 | 46 | # logging tokens on thoses endpoints 47 | endpoints = ["/completions", "/chat/completions", "/edits", "/embeddings"] 48 | # openai api won't return token consumption in stream mode 49 | # skip: skip calcuate token consumption for stream request 50 | # reject: reject stream request 51 | # estimate: estimate the token consumption using tiktoken (may be inaccurate) 52 | stream_tokens = "estimate" 53 | 54 | # For file backend, specify the file path for the access log. 55 | [audit.backends.file] 56 | filename = "access.log" 57 | 58 | # For SQLite backend, specify the SQLite database file path. 59 | # [audit.backends.sqlite] 60 | # filename = "audit.sqlite" 61 | 62 | # For MySQL backend, specify the host, port, and optional socket for a MySQL database, 63 | # as well as the username, password, and database name for access log storage. 64 | # [audit.backends.mysql] 65 | # host = "localhost" 66 | # port = 3306 67 | # socket = "/var/run/mysqld/mysqld.sock" # using UNIX socket instead of TCP 68 | # username = "username" 69 | # password = "password" 70 | # database = "access_log" 71 | 72 | # For PostgreSQL backend, specify the host, port, and socket for a PostgreSQL database, 73 | # as well as the username, password, and database name for access log storage. 74 | # [audit.backends.postgres] 75 | # host = "localhost" 76 | # port = 5432 77 | # socket = "/var/run/postgresql/.s.PGSQL.5432" 78 | # username = "postgres" 79 | # password = "password" 80 | # database = "access_log" 81 | -------------------------------------------------------------------------------- /openai-hub-core/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "openai-hub-core" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | async-trait = "0.1" 8 | axum = { git = "https://github.com/tokio-rs/axum", rev = "786329d85d06549aa1b15f9e4c5d8225c658f468" } 9 | base64 = { version = "0.21", optional = true } 10 | chrono = { version = "0.4", optional = true } 11 | futures = "0.3" 12 | hmac = { version = "0.12", optional = true} 13 | http-serde = "1.1" 14 | hyper = { version = "0.14", features = ["full"] } 15 | jwt = { version = "0.16", optional = true } 16 | once_cell = { version = "1.18", optional = true } 17 | parking_lot = "0.12" 18 | pin-project = "1.1" 19 | rand = { version = "0.8", optional = true} 20 | regex = { version = "1.8", optional = true } 21 | reqwest = { version = "0.11", features = ["stream"] } 22 | serde = { version = "1", features = ["derive"] } 23 | serde_json = "1.0" 24 | sha2 = { version = "0.10", optional = true } 25 | sqlx = { version = "0.7", optional = true } 26 | sync_wrapper = { version = "0.1", features = ["futures"] } 27 | thiserror = "1.0" 28 | tiktoken-rs = { version = "0.5", optional = true } 29 | tokio = { version = "1", features = ["rt", "net"] } 30 | tokio-stream = "0.1" 31 | tokio-util = { version = "0.7", features = ["io-util"] } 32 | toml = "0.7" 33 | tower = "0.4" 34 | tracing = "0.1" 35 | 36 | [features] 37 | defutures = ["acl", "jwt-auth", "audit", "sqlite", "mysql", "postgres"] 38 | acl = ["once_cell", "regex"] 39 | jwt-auth = ["jwt", "hmac", "sha2", "chrono"] 40 | audit = ["sqlx", "sqlx/runtime-tokio-native-tls", "sqlx/chrono", "chrono", "chrono/serde", "base64-serialize", "rand", "estimate-tokens"] 41 | sqlite = ["sqlx/sqlite"] 42 | mysql = ["sqlx/mysql"] 43 | postgres = ["sqlx/postgres"] 44 | estimate-tokens = ["tiktoken-rs"] 45 | base64-serialize = ["base64"] 46 | -------------------------------------------------------------------------------- /openai-hub-core/src/acl.rs: -------------------------------------------------------------------------------- 1 | use crate::helpers::{endpoints_to_regex, wildcards_to_regex}; 2 | 3 | use axum::http::{Method, StatusCode}; 4 | 5 | use once_cell::sync::Lazy; 6 | use regex::Regex; 7 | use serde::{Deserialize, Deserializer}; 8 | use serde_json::Value; 9 | use std::collections::{BTreeMap, HashMap, HashSet}; 10 | use std::fmt::Debug; 11 | use std::hash::{Hash, Hasher}; 12 | use tracing::{event, instrument, Level}; 13 | 14 | static DEPLOYMENT_ID_REGEX: Lazy = 15 | Lazy::new(|| Regex::new(r#"^/engines/([^/]+)/.+$"#).unwrap()); 16 | 17 | #[derive(Clone)] 18 | struct MethodSerde(Method); 19 | 20 | impl Eq for MethodSerde {} 21 | 22 | impl PartialEq for MethodSerde { 23 | fn eq(&self, other: &Self) -> bool { 24 | self.0 == other.0 25 | } 26 | } 27 | 28 | impl Hash for MethodSerde { 29 | fn hash(&self, state: &mut H) { 30 | self.0.hash(state); 31 | } 32 | } 33 | 34 | impl<'de> Deserialize<'de> for MethodSerde { 35 | fn deserialize>(deserializer: D) -> Result { 36 | Ok(Self(http_serde::method::deserialize(deserializer)?)) 37 | } 38 | } 39 | 40 | #[derive(Debug, Clone, Default)] 41 | pub struct ApiAcl { 42 | pub global: Global, 43 | pub endpoint: HashMap, 44 | pub model_body: HashMap>, 45 | pub model_path: HashMap>, 46 | } 47 | 48 | #[derive(Debug, Clone)] 49 | pub struct Global { 50 | pub whitelist: bool, 51 | pub methods: HashMap, 52 | pub allow_deployments: HashSet, 53 | } 54 | 55 | #[derive(Debug, Clone)] 56 | pub struct ModelOption { 57 | pub allows: Regex, 58 | pub disallows: Regex, 59 | pub allow_omitted: bool, 60 | } 61 | 62 | impl Default for ModelOption { 63 | fn default() -> Self { 64 | Self { 65 | allows: Regex::new("^.*$").unwrap(), 66 | disallows: Regex::new("^$").unwrap(), 67 | allow_omitted: false, 68 | } 69 | } 70 | } 71 | 72 | pub trait ModelValidator: Send { 73 | fn validate_path(&self, _path: &str) -> Result<(), AclError> { 74 | Ok(()) 75 | } 76 | 77 | fn validate_body(&self, _body: &Value) -> Result<(), AclError> { 78 | Ok(()) 79 | } 80 | } 81 | 82 | impl Default for Global { 83 | fn default() -> Self { 84 | Self { 85 | whitelist: true, 86 | methods: HashMap::from_iter([(Method::POST, true)]), 87 | allow_deployments: HashSet::new(), 88 | } 89 | } 90 | } 91 | 92 | pub enum AclError { 93 | MethodNotAllowed(Method), 94 | DeploymentNotAllowed(String), 95 | EndpointNotAllowed(Method, String), 96 | ModelNotAllowed(String), 97 | MissingModel, 98 | } 99 | 100 | #[derive(Debug, thiserror::Error)] 101 | pub enum LoadError { 102 | #[error(transparent)] 103 | InvalidToml(#[from] toml::de::Error), 104 | #[error(transparent)] 105 | InvalidRegex(#[from] regex::Error), 106 | } 107 | 108 | impl ApiAcl { 109 | #[instrument(skip_all)] 110 | pub fn load(s: &str) -> Result { 111 | #[derive(Deserialize)] 112 | struct GlobalDe { 113 | #[serde(default = "default_true")] 114 | whitelist: bool, 115 | #[serde(default)] 116 | methods: HashMap, 117 | #[serde(default)] 118 | allow_deployments: HashSet, 119 | } 120 | 121 | #[derive(Deserialize)] 122 | struct ModelOptionDe { 123 | #[serde(default)] 124 | path: bool, 125 | #[serde(default)] 126 | allows: Vec, 127 | #[serde(default)] 128 | disallows: Vec, 129 | #[serde(default)] 130 | allow_omitted: bool, 131 | } 132 | 133 | #[derive(Deserialize)] 134 | struct ApiAclDe { 135 | pub global: GlobalDe, 136 | #[serde(default)] 137 | pub endpoint: HashMap>, 138 | #[serde(default)] 139 | pub model: HashMap>, 140 | } 141 | 142 | let ApiAclDe { 143 | global: global_de, 144 | endpoint, 145 | model: model_de, 146 | } = toml::from_str(s)?; 147 | 148 | let global = Global { 149 | whitelist: global_de.whitelist, 150 | methods: global_de 151 | .methods 152 | .into_iter() 153 | .map(|(k, v)| (k.0, v)) 154 | .collect(), 155 | allow_deployments: global_de.allow_deployments, 156 | }; 157 | 158 | let mut endpoint_regex: HashMap = HashMap::new(); 159 | for (method, endpoints) in endpoint.into_iter() { 160 | let endpoints = endpoints 161 | .into_iter() 162 | .filter(|(_, allow)| if global.whitelist { *allow } else { !*allow }) 163 | .map(|(endpoint, _)| endpoint); 164 | endpoint_regex.insert(method.0, endpoints_to_regex(endpoints)?); 165 | } 166 | 167 | let mut model_body = HashMap::new(); 168 | let mut model_path = HashMap::new(); 169 | 170 | for (method, models) in model_de.into_iter() { 171 | model_body.insert(method.0.clone(), HashMap::new()); 172 | model_path.insert(method.0.clone(), Vec::new()); 173 | for (path, model_de) in models.into_iter() { 174 | let option = ModelOption { 175 | allows: wildcards_to_regex(model_de.allows.into_iter())?, 176 | disallows: wildcards_to_regex(model_de.disallows.into_iter())?, 177 | allow_omitted: model_de.allow_omitted, 178 | }; 179 | if model_de.path { 180 | event!(Level::DEBUG, "should be a regex rule: {}", path); 181 | let path = path.replace("{model}", "(?P[^/]+)"); 182 | event!(Level::DEBUG, "transformed regex rule: {}", path); 183 | model_path 184 | .get_mut(&method.0) 185 | .unwrap() 186 | .push((Regex::new(&path).unwrap(), option)); 187 | } else { 188 | event!(Level::DEBUG, "seems to be a normal rule: {}", path); 189 | model_body.get_mut(&method.0).unwrap().insert(path, option); 190 | } 191 | } 192 | } 193 | 194 | Ok(Self { 195 | global, 196 | endpoint: endpoint_regex, 197 | model_body, 198 | model_path, 199 | }) 200 | } 201 | 202 | #[instrument(skip_all)] 203 | pub fn validate( 204 | &self, 205 | method: &Method, 206 | path: &str, 207 | ) -> Result>, AclError> { 208 | // global method check 209 | event!( 210 | Level::DEBUG, 211 | "method: {}, config: {:?}", 212 | method, 213 | self.global.methods.get(method) 214 | ); 215 | if !self.global.methods.get(method).unwrap_or(&false) { 216 | event!(Level::DEBUG, "method not allowed: {:?}", method); 217 | return Err(AclError::MethodNotAllowed(method.clone())); 218 | } 219 | event!(Level::DEBUG, "path: {}", path); 220 | 221 | // deployment check 222 | let endpoint = if let Some(deployment_id) = DEPLOYMENT_ID_REGEX.captures(path) { 223 | let id = deployment_id.get(1).unwrap(); 224 | event!( 225 | Level::DEBUG, 226 | "seems contains deployment id: {}", 227 | id.as_str() 228 | ); 229 | if !self.global.allow_deployments.contains(id.as_str()) { 230 | event!(Level::DEBUG, "deployment {} not allowed", id.as_str()); 231 | return Err(AclError::DeploymentNotAllowed(id.as_str().to_string())); 232 | } 233 | &path[id.end()..] 234 | } else { 235 | path 236 | }; 237 | event!(Level::DEBUG, "endpoint: {}", endpoint); 238 | 239 | // per endpoint check 240 | let matched = self 241 | .endpoint 242 | .get(method) 243 | .map(|re| re.is_match(endpoint)) 244 | .unwrap_or(false); 245 | event!(Level::DEBUG, "rule matched: {}", matched); 246 | if (self.global.whitelist && !matched) || (!self.global.whitelist && matched) { 247 | event!( 248 | Level::DEBUG, 249 | "endpoint not allowed: {} {}", 250 | method, 251 | endpoint 252 | ); 253 | return Err(AclError::EndpointNotAllowed( 254 | method.clone(), 255 | endpoint.to_string(), 256 | )); 257 | } 258 | 259 | Ok(self 260 | .model_body 261 | .get(method) 262 | .and_then(|per_method| { 263 | per_method 264 | .get(endpoint) 265 | .map(|o| Box::new(o.clone()) as Box) 266 | }) 267 | .or_else(|| { 268 | self.model_path.get(method).and_then(|regexes| { 269 | event!(Level::DEBUG, "not found in plain rules, try regexes"); 270 | regexes 271 | .iter() 272 | .find(|(re, _)| re.is_match(endpoint)) 273 | .map(|o| Box::new(o.clone()) as Box) 274 | }) 275 | })) 276 | } 277 | } 278 | 279 | impl ModelOption { 280 | #[instrument(skip(self))] 281 | fn validate(&self, model: Option<&str>) -> Result<(), AclError> { 282 | match model { 283 | None => { 284 | if self.allow_omitted { 285 | event!(Level::DEBUG, "model is omitted and allowed"); 286 | Ok(()) 287 | } else { 288 | event!(Level::DEBUG, "model is missing"); 289 | Err(AclError::MissingModel) 290 | } 291 | } 292 | Some(model) => { 293 | if self.disallows.is_match(model) || !self.allows.is_match(model) { 294 | event!(Level::DEBUG, "model is not allowed"); 295 | Err(AclError::ModelNotAllowed(model.to_string())) 296 | } else { 297 | event!(Level::DEBUG, "model is allowed"); 298 | Ok(()) 299 | } 300 | } 301 | } 302 | } 303 | } 304 | 305 | impl ModelValidator for (Regex, ModelOption) { 306 | #[instrument(skip(self))] 307 | fn validate_path(&self, path: &str) -> Result<(), AclError> { 308 | debug_assert!(self.0.is_match(path)); 309 | let model = self 310 | .0 311 | .captures(path) 312 | .unwrap() 313 | .name("model") 314 | .unwrap() 315 | .as_str(); 316 | self.1.validate(Some(model)) 317 | } 318 | } 319 | 320 | impl ModelValidator for ModelOption { 321 | #[instrument(skip(self))] 322 | fn validate_body(&self, body: &Value) -> Result<(), AclError> { 323 | self.validate(body.get("model").and_then(|m| m.as_str())) 324 | } 325 | } 326 | 327 | impl AclError { 328 | pub(crate) fn status_code(&self) -> StatusCode { 329 | match self { 330 | AclError::MethodNotAllowed(_) => StatusCode::METHOD_NOT_ALLOWED, 331 | _ => StatusCode::FORBIDDEN, 332 | } 333 | } 334 | } 335 | 336 | impl ToString for AclError { 337 | fn to_string(&self) -> String { 338 | match self { 339 | AclError::MethodNotAllowed(method) => format!("Method {} not allowed", method.as_str()), 340 | AclError::DeploymentNotAllowed(id) => format!("Deployment {} not allowed", id), 341 | AclError::EndpointNotAllowed(method, endpoint) => { 342 | format!("Endpoint {} {} not allowed", method.as_str(), endpoint) 343 | } 344 | AclError::ModelNotAllowed(model) => { 345 | format!("Model {} not allowed", model) 346 | } 347 | AclError::MissingModel => "Missing model".to_string(), 348 | } 349 | } 350 | } 351 | 352 | const fn default_true() -> bool { 353 | true 354 | } 355 | -------------------------------------------------------------------------------- /openai-hub-core/src/audit.rs: -------------------------------------------------------------------------------- 1 | use crate::config::{AuditBackendType, AuditConfig}; 2 | use base64::engine::general_purpose; 3 | use base64::Engine; 4 | use chrono::serde::ts_milliseconds; 5 | use rand::distributions::{Alphanumeric, DistString}; 6 | use rand::thread_rng; 7 | use serde::{Deserialize, Serialize, Serializer}; 8 | use sqlx::{MySql, Pool, Postgres, Sqlite}; 9 | use std::collections::BTreeMap; 10 | use std::ops::Deref; 11 | use std::sync::Arc; 12 | use tokio::io::AsyncWriteExt; 13 | use tokio::sync::Mutex; 14 | use tracing::{event, Level}; 15 | 16 | #[async_trait::async_trait] 17 | pub trait BackendEngine { 18 | async fn init(&self) -> Result<(), BackendCreationError> { 19 | Ok(()) 20 | } 21 | async fn log_access(&self, access: AccessLog); 22 | async fn log_tokens(&self, tokens: TokenUsageLog); 23 | } 24 | 25 | #[derive(Default, Debug, Serialize)] 26 | pub struct AccessLog { 27 | #[serde(with = "ts_milliseconds")] 28 | pub timestamp: chrono::DateTime, 29 | #[serde(skip_serializing_if = "Option::is_none")] 30 | pub user: Option, 31 | pub ray_id: String, 32 | #[serde(skip_serializing_if = "Option::is_none")] 33 | pub method: Option, 34 | #[serde(skip_serializing_if = "Option::is_none")] 35 | pub uri: Option, 36 | #[serde(skip_serializing_if = "Option::is_none")] 37 | pub headers: Option>, 38 | #[serde( 39 | skip_serializing_if = "Option::is_none", 40 | serialize_with = "might_as_base64_option" 41 | )] 42 | pub body: Option>, 43 | #[serde(skip_serializing_if = "Option::is_none")] 44 | pub response_status: Option, 45 | #[serde(skip_serializing_if = "Option::is_none")] 46 | pub response_headers: Option>, 47 | #[serde( 48 | skip_serializing_if = "Option::is_none", 49 | serialize_with = "might_as_base64_option" 50 | )] 51 | pub response_body: Option>, 52 | } 53 | 54 | impl AccessLog { 55 | pub fn now() -> Self { 56 | let ray_id = Alphanumeric.sample_string(&mut thread_rng(), 16); 57 | Self { 58 | timestamp: chrono::Utc::now(), 59 | ray_id, 60 | ..Default::default() 61 | } 62 | } 63 | 64 | fn body_as_string(&self) -> Option { 65 | self.body.as_ref().map(|b| { 66 | String::from_utf8(b.clone()).unwrap_or_else(|_| general_purpose::STANDARD.encode(b)) 67 | }) 68 | } 69 | 70 | fn response_body_as_string(&self) -> Option { 71 | self.response_body.as_ref().map(|b| { 72 | String::from_utf8(b.clone()).unwrap_or_else(|_| general_purpose::STANDARD.encode(b)) 73 | }) 74 | } 75 | } 76 | 77 | #[derive(Debug, Serialize)] 78 | pub struct TokenUsageLog { 79 | #[serde(with = "ts_milliseconds")] 80 | pub timestamp: chrono::DateTime, 81 | #[serde(skip_serializing_if = "Option::is_none")] 82 | pub user: Option, 83 | pub ray_id: String, 84 | pub model: String, 85 | pub usage: TokenUsage, 86 | pub is_estimated: bool, 87 | } 88 | 89 | #[derive(Debug, Serialize, Deserialize)] 90 | pub struct TokenUsage { 91 | pub prompt_tokens: usize, 92 | pub completion_tokens: usize, 93 | pub total_tokens: usize, 94 | } 95 | 96 | #[derive(Debug, thiserror::Error)] 97 | pub enum BackendCreationError { 98 | #[error(transparent)] 99 | Io(#[from] std::io::Error), 100 | #[error(transparent)] 101 | DatabaseError(#[from] sqlx::Error), 102 | } 103 | 104 | #[derive(Clone)] 105 | pub enum Backend { 106 | Text(TextBackend), 107 | Database(DatabaseBackend), 108 | } 109 | 110 | impl Backend { 111 | pub async fn create_with(config: &AuditConfig) -> Result { 112 | let this = match config.backend { 113 | AuditBackendType::File => Self::Text(TextBackend::create_with(config).await?), 114 | _ => Self::Database(DatabaseBackend::create_with(config).await?), 115 | }; 116 | this.init().await?; 117 | Ok(this) 118 | } 119 | } 120 | 121 | #[async_trait::async_trait] 122 | impl BackendEngine for Backend { 123 | async fn init(&self) -> Result<(), BackendCreationError> { 124 | match self { 125 | Self::Text(backend) => backend.init().await, 126 | Self::Database(backend) => backend.init().await, 127 | } 128 | } 129 | 130 | async fn log_access(&self, access: AccessLog) { 131 | match self { 132 | Backend::Text(backend) => backend.log_access(access).await, 133 | Backend::Database(backend) => backend.log_access(access).await, 134 | } 135 | } 136 | 137 | async fn log_tokens(&self, tokens: TokenUsageLog) { 138 | match self { 139 | Backend::Text(backend) => backend.log_tokens(tokens).await, 140 | Backend::Database(backend) => backend.log_tokens(tokens).await, 141 | } 142 | } 143 | } 144 | 145 | #[derive(Clone)] 146 | pub struct TextBackend { 147 | writer: Arc>, 148 | } 149 | 150 | impl TextBackend { 151 | async fn create_with(config: &AuditConfig) -> Result { 152 | let writer = tokio::fs::OpenOptions::new() 153 | .create(true) 154 | .append(true) 155 | .open(&config.backends.file_backend.filename) 156 | .await?; 157 | Ok(Self { 158 | writer: Arc::new(Mutex::new(writer)), 159 | }) 160 | } 161 | } 162 | 163 | #[async_trait::async_trait] 164 | impl BackendEngine for TextBackend { 165 | async fn log_access(&self, access: AccessLog) { 166 | let mut writer = self.writer.lock().await; 167 | let mut vec = serde_json::to_vec(&access).unwrap(); 168 | vec.push(b'\n'); 169 | if let Err(e) = writer.write_all(&vec).await { 170 | event!( 171 | Level::ERROR, 172 | error = ?e, 173 | "Failed to write access log to file" 174 | ); 175 | } 176 | } 177 | 178 | async fn log_tokens(&self, tokens: TokenUsageLog) { 179 | let mut writer = self.writer.lock().await; 180 | let mut vec = serde_json::to_vec(&tokens).unwrap(); 181 | vec.push(b'\n'); 182 | if let Err(e) = writer.write_all(&vec).await { 183 | event!( 184 | Level::ERROR, 185 | error = ?e, 186 | "Failed to write tokens log to file" 187 | ); 188 | } 189 | } 190 | } 191 | 192 | #[derive(Clone)] 193 | pub enum DatabaseBackend { 194 | Sqlite(Pool), 195 | MySql(Pool), 196 | Postgres(Pool), 197 | } 198 | 199 | impl DatabaseBackend { 200 | async fn create_with(config: &AuditConfig) -> Result { 201 | Ok(match config.backend { 202 | AuditBackendType::Sqlite => { 203 | Self::Sqlite(Pool::connect_with((&config.backends.sqlite_backend).into()).await?) 204 | } 205 | AuditBackendType::Mysql => { 206 | Self::MySql(Pool::connect_with((&config.backends.mysql_backend).into()).await?) 207 | } 208 | AuditBackendType::Postgres => Self::Postgres( 209 | Pool::connect_with((&config.backends.postgres_backend).into()).await?, 210 | ), 211 | _ => unreachable!(), 212 | }) 213 | } 214 | } 215 | 216 | #[async_trait::async_trait] 217 | impl BackendEngine for DatabaseBackend { 218 | async fn init(&self) -> Result<(), BackendCreationError> { 219 | match self { 220 | Self::Sqlite(pool) => pool.init().await, 221 | Self::MySql(pool) => pool.init().await, 222 | Self::Postgres(pool) => pool.init().await, 223 | } 224 | } 225 | 226 | async fn log_access(&self, access: AccessLog) { 227 | match self { 228 | DatabaseBackend::Sqlite(pool) => pool.log_access(access).await, 229 | DatabaseBackend::MySql(pool) => pool.log_access(access).await, 230 | DatabaseBackend::Postgres(pool) => pool.log_access(access).await, 231 | } 232 | } 233 | 234 | async fn log_tokens(&self, tokens: TokenUsageLog) { 235 | match self { 236 | DatabaseBackend::Sqlite(pool) => pool.log_tokens(tokens).await, 237 | DatabaseBackend::MySql(pool) => pool.log_tokens(tokens).await, 238 | DatabaseBackend::Postgres(pool) => pool.log_tokens(tokens).await, 239 | } 240 | } 241 | } 242 | 243 | #[async_trait::async_trait] 244 | impl BackendEngine for Pool { 245 | async fn init(&self) -> Result<(), BackendCreationError> { 246 | sqlx::query( 247 | r#"CREATE TABLE IF NOT EXISTS audit_log ( 248 | id INTEGER PRIMARY KEY AUTOINCREMENT, 249 | timestamp DATETIME NOT NULL, 250 | ray_id TEXT NOT NULL, 251 | user TEXT, 252 | method TEXT, 253 | uri TEXT, 254 | headers TEXT, 255 | body TEXT, 256 | response_status INTEGER, 257 | response_headers TEXT, 258 | response_body TEXT 259 | )"#, 260 | ) 261 | .execute(self) 262 | .await?; 263 | sqlx::query( 264 | r#"CREATE TABLE IF NOT EXISTS tokens_log ( 265 | id INTEGER PRIMARY KEY AUTOINCREMENT, 266 | timestamp DATETIME, 267 | ray_id TEXT NOT NULL, 268 | user TEXT, 269 | model TEXT NOT NULL, 270 | is_estimated BOOLEAN NOT NULL, 271 | prompt_tokens INTEGER NOT NULL, 272 | completion_tokens INTEGER NOT NULL, 273 | total_tokens INTEGER NOT NULL 274 | )"#, 275 | ) 276 | .execute(self) 277 | .await?; 278 | Ok(()) 279 | } 280 | async fn log_access(&self, log: AccessLog) { 281 | let body = log.body_as_string(); 282 | let response_body = log.response_body_as_string(); 283 | let result = sqlx::query(r#"INSERT INTO audit_log (timestamp, ray_id, user, method, uri, headers, body, response_status, response_headers, response_body) 284 | VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"#) 285 | .bind(log.timestamp) 286 | .bind(log.ray_id) 287 | .bind(log.user) 288 | .bind(log.method) 289 | .bind(log.uri) 290 | .bind(serde_json::to_string(&log.headers).unwrap()) 291 | .bind(body) 292 | .bind(log.response_status) 293 | .bind(serde_json::to_string(&log.response_headers).unwrap()) 294 | .bind(response_body) 295 | .execute(self) 296 | .await; 297 | if let Err(e) = result { 298 | event!( 299 | Level::ERROR, 300 | error = ?e, 301 | "Failed to write access log to sqlite" 302 | ); 303 | } 304 | } 305 | 306 | async fn log_tokens(&self, tokens: TokenUsageLog) { 307 | let result = sqlx::query(r#"INSERT INTO tokens_log (timestamp, ray_id, user, model, is_estimated, prompt_tokens, completion_tokens, total_tokens) 308 | VALUES (?, ?, ?, ?, ?, ?, ?, ?)"#) 309 | .bind(tokens.timestamp) 310 | .bind(tokens.ray_id) 311 | .bind(tokens.user) 312 | .bind(tokens.model) 313 | .bind(tokens.is_estimated) 314 | .bind(tokens.usage.prompt_tokens as u32) 315 | .bind(tokens.usage.completion_tokens as u32) 316 | .bind(tokens.usage.total_tokens as u32) 317 | .execute(self) 318 | .await; 319 | if let Err(e) = result { 320 | event!( 321 | Level::ERROR, 322 | error = ?e, 323 | "Failed to write tokens log to sqlite" 324 | ); 325 | } 326 | } 327 | } 328 | 329 | #[async_trait::async_trait] 330 | impl BackendEngine for Pool { 331 | async fn init(&self) -> Result<(), BackendCreationError> { 332 | sqlx::query( 333 | r#"CREATE TABLE IF NOT EXISTS audit_log ( 334 | id INTEGER PRIMARY KEY AUTO_INCREMENT, 335 | timestamp TIMESTAMP NOT NULL, 336 | ray_id VARCHAR(16) NOT NULL, 337 | user VARCHAR(255), 338 | method VARCHAR(10), 339 | uri VARCHAR(255), 340 | headers TEXT, 341 | body TEXT, 342 | response_status SMALLINT UNSIGNED, 343 | response_headers TEXT, 344 | response_body TEXT 345 | )"#, 346 | ) 347 | .execute(self) 348 | .await?; 349 | sqlx::query( 350 | r#"CREATE TABLE IF NOT EXISTS tokens_log ( 351 | id INTEGER PRIMARY KEY AUTO_INCREMENT, 352 | timestamp TIMESTAMP NOT NULL, 353 | ray_id VARCHAR(16) NOT NULL, 354 | user VARCHAR(255), 355 | model VARCHAR(255) NOT NULL, 356 | is_estimated BOOLEAN NOT NULL, 357 | prompt_tokens BIGINT UNSIGNED NOT NULL, 358 | completion_tokens BIGINT UNSIGNED NOT NULL, 359 | total_tokens BIGINT UNSIGNED NOT NULL 360 | )"#, 361 | ) 362 | .execute(self) 363 | .await?; 364 | Ok(()) 365 | } 366 | 367 | async fn log_access(&self, log: AccessLog) { 368 | let body = log.body_as_string(); 369 | let response_body = log.response_body_as_string(); 370 | let result = sqlx::query(r#"INSERT INTO audit_log (timestamp, ray_id, user, method, uri, headers, body, response_status, response_headers, response_body) 371 | VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"#) 372 | .bind(log.timestamp) 373 | .bind(log.ray_id) 374 | .bind(log.user) 375 | .bind(log.method) 376 | .bind(log.uri) 377 | .bind(serde_json::to_string(&log.headers).unwrap()) 378 | .bind(body) 379 | .bind(log.response_status) 380 | .bind(serde_json::to_string(&log.response_headers).unwrap()) 381 | .bind(response_body) 382 | .execute(self) 383 | .await; 384 | if let Err(e) = result { 385 | event!( 386 | Level::ERROR, 387 | error = ?e, 388 | "Failed to write access log to MySql" 389 | ); 390 | } 391 | } 392 | 393 | async fn log_tokens(&self, tokens: TokenUsageLog) { 394 | let result = sqlx::query(r#"INSERT INTO tokens_log (timestamp, ray_id, user, model, is_estimated, prompt_tokens, completion_tokens, total_tokens) 395 | VALUES (?, ?, ?, ?, ?, ?, ?, ?)"#) 396 | .bind(tokens.timestamp) 397 | .bind(tokens.ray_id) 398 | .bind(tokens.user) 399 | .bind(tokens.model) 400 | .bind(tokens.is_estimated) 401 | .bind(tokens.usage.prompt_tokens as u64) 402 | .bind(tokens.usage.completion_tokens as u64) 403 | .bind(tokens.usage.total_tokens as u64) 404 | .execute(self) 405 | .await; 406 | if let Err(e) = result { 407 | event!( 408 | Level::ERROR, 409 | error = ?e, 410 | "Failed to write tokens log to sqlite" 411 | ); 412 | } 413 | } 414 | } 415 | 416 | #[async_trait::async_trait] 417 | impl BackendEngine for Pool { 418 | async fn init(&self) -> Result<(), BackendCreationError> { 419 | sqlx::query( 420 | r#"CREATE TABLE IF NOT EXISTS audit_log ( 421 | id SERIAL PRIMARY KEY, 422 | timestamp TIMESTAMPTZ NOT NULL, 423 | ray_id VARCHAR(16) NOT NULL, 424 | user VARCHAR(255), 425 | method VARCHAR(10), 426 | uri VARCHAR(255), 427 | headers TEXT, 428 | body TEXT, 429 | response_status SMALLINT, 430 | response_headers TEXT, 431 | response_body TEXT 432 | )"#, 433 | ) 434 | .execute(self) 435 | .await?; 436 | 437 | sqlx::query( 438 | r#"CREATE TABLE IF NOT EXISTS tokens_log ( 439 | id INTEGER PRIMARY KEY AUTO_INCREMENT, 440 | timestamp TIMESTAMPTZ NOT NULL, 441 | ray_id VARCHAR(16) NOT NULL, 442 | user VARCHAR(255), 443 | model VARCHAR(255) NOT NULL, 444 | is_estimated BOOL NOT NULL, 445 | prompt_tokens BIGINT NOT NULL, 446 | completion_tokens BIGINT NOT NULL, 447 | total_tokens BIGINT NOT NULL 448 | )"#, 449 | ) 450 | .execute(self) 451 | .await?; 452 | Ok(()) 453 | } 454 | 455 | async fn log_access(&self, log: AccessLog) { 456 | let body = log.body_as_string(); 457 | let response_body = log.response_body_as_string(); 458 | let result = sqlx::query(r#"INSERT INTO audit_log (timestamp, ray_id, user, method, uri, headers, body, response_status, response_headers, response_body) 459 | VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)"#) 460 | .bind(log.timestamp) 461 | .bind(log.ray_id) 462 | .bind(log.user) 463 | .bind(log.method) 464 | .bind(log.uri) 465 | .bind(serde_json::to_string(&log.headers).unwrap()) 466 | .bind(body) 467 | .bind(log.response_status.map(|s| s as i16)) 468 | .bind(serde_json::to_string(&log.response_headers).unwrap()) 469 | .bind(response_body) 470 | .execute(self) 471 | .await; 472 | if let Err(e) = result { 473 | event!( 474 | Level::ERROR, 475 | error = ?e, 476 | "Failed to write access log to Postgres" 477 | ); 478 | } 479 | } 480 | 481 | async fn log_tokens(&self, tokens: TokenUsageLog) { 482 | let result = sqlx::query(r#"INSERT INTO tokens_log (timestamp, ray_id, user, model, is_estimated, prompt_tokens, completion_tokens, total_tokens) 483 | VALUES ($1, $2, $3, $4, $5, $6, $7, $8)"#) 484 | .bind(tokens.timestamp) 485 | .bind(tokens.ray_id) 486 | .bind(tokens.user) 487 | .bind(tokens.model) 488 | .bind(tokens.is_estimated) 489 | .bind(tokens.usage.prompt_tokens as i64) 490 | .bind(tokens.usage.completion_tokens as i64) 491 | .bind(tokens.usage.total_tokens as i64) 492 | .execute(self) 493 | .await; 494 | if let Err(e) = result { 495 | event!( 496 | Level::ERROR, 497 | error = ?e, 498 | "Failed to write tokens log to sqlite" 499 | ); 500 | } 501 | } 502 | } 503 | 504 | fn might_as_base64_option(value: &Option, serializer: S) -> Result 505 | where 506 | T: Deref, 507 | S: Serializer, 508 | { 509 | value 510 | .as_ref() 511 | .map(|v| { 512 | String::from_utf8(v.deref().to_vec()) 513 | .unwrap_or_else(|_| general_purpose::STANDARD.encode(v.deref())) 514 | }) 515 | .serialize(serializer) 516 | } 517 | -------------------------------------------------------------------------------- /openai-hub-core/src/config/audit.rs: -------------------------------------------------------------------------------- 1 | use serde::Deserialize; 2 | use sqlx::mysql::MySqlConnectOptions; 3 | use sqlx::postgres::PgConnectOptions; 4 | use sqlx::sqlite::SqliteConnectOptions; 5 | use std::collections::HashSet; 6 | 7 | #[derive(Clone, Debug, Deserialize)] 8 | pub struct AuditConfig { 9 | pub backend: AuditBackendType, 10 | #[serde(default)] 11 | pub backends: AuditBackendConfig, 12 | #[serde(default)] 13 | pub filters: AuditFiltersConfig, 14 | } 15 | 16 | #[derive(Copy, Clone, Debug, Deserialize)] 17 | #[serde(rename_all = "lowercase")] 18 | pub enum AuditBackendType { 19 | File, 20 | Sqlite, 21 | Mysql, 22 | Postgres, 23 | } 24 | 25 | #[derive(Clone, Debug, Default, Deserialize)] 26 | #[serde(default)] 27 | pub struct AuditBackendConfig { 28 | pub file_backend: FileBackendConfig, 29 | pub sqlite_backend: SqliteBackendConfig, 30 | pub mysql_backend: MySqlBackendConfig, 31 | pub postgres_backend: PostgresBackendConfig, 32 | } 33 | 34 | #[derive(Clone, Debug, Default, Deserialize)] 35 | #[serde(default)] 36 | pub struct AuditFiltersConfig { 37 | pub access: AuditAccessFilterConfig, 38 | pub tokens: AuditTokensFilterConfig, 39 | } 40 | 41 | #[derive(Clone, Debug, Deserialize)] 42 | pub struct AuditAccessFilterConfig { 43 | pub enable: bool, 44 | pub method: bool, 45 | pub uri: bool, 46 | pub headers: bool, 47 | pub body: bool, 48 | pub response: bool, 49 | } 50 | 51 | impl Default for AuditAccessFilterConfig { 52 | fn default() -> Self { 53 | Self { 54 | enable: true, 55 | method: true, 56 | uri: true, 57 | headers: false, 58 | body: false, 59 | response: false, 60 | } 61 | } 62 | } 63 | 64 | #[derive(Clone, Debug, Deserialize)] 65 | pub struct AuditTokensFilterConfig { 66 | pub enable: bool, 67 | pub endpoints: HashSet, 68 | pub stream_tokens: StreamTokensPolicy, 69 | } 70 | 71 | impl Default for AuditTokensFilterConfig { 72 | fn default() -> Self { 73 | Self { 74 | enable: true, 75 | endpoints: HashSet::from_iter([ 76 | "/completions".to_string(), 77 | "/chat/completions".to_string(), 78 | "/edits".to_string(), 79 | "/embeddings".to_string(), 80 | ]), 81 | stream_tokens: StreamTokensPolicy::default(), 82 | } 83 | } 84 | } 85 | 86 | #[derive(Copy, Clone, Debug, Eq, PartialEq, Deserialize)] 87 | #[serde(rename_all = "lowercase")] 88 | pub enum StreamTokensPolicy { 89 | Skip, 90 | Reject, 91 | Estimate, 92 | } 93 | 94 | impl Default for StreamTokensPolicy { 95 | fn default() -> Self { 96 | Self::Estimate 97 | } 98 | } 99 | 100 | #[derive(Clone, Debug, Deserialize)] 101 | #[serde(default)] 102 | pub struct FileBackendConfig { 103 | pub filename: String, 104 | } 105 | 106 | impl Default for FileBackendConfig { 107 | fn default() -> Self { 108 | Self { 109 | filename: "access.log".to_string(), 110 | } 111 | } 112 | } 113 | 114 | #[derive(Clone, Debug, Deserialize)] 115 | #[serde(default)] 116 | pub struct SqliteBackendConfig { 117 | pub filename: String, 118 | pub create_if_missing: bool, 119 | } 120 | 121 | impl Default for SqliteBackendConfig { 122 | fn default() -> Self { 123 | Self { 124 | filename: "access-log.sqlite".to_string(), 125 | create_if_missing: true, 126 | } 127 | } 128 | } 129 | 130 | impl From<&SqliteBackendConfig> for SqliteConnectOptions { 131 | fn from(config: &SqliteBackendConfig) -> Self { 132 | SqliteConnectOptions::new() 133 | .filename(&config.filename) 134 | .create_if_missing(config.create_if_missing) 135 | } 136 | } 137 | 138 | #[derive(Clone, Debug, Deserialize)] 139 | #[serde(default)] 140 | pub struct MySqlBackendConfig { 141 | pub host: Option, 142 | pub port: Option, 143 | pub socket: Option, 144 | pub username: Option, 145 | pub password: Option, 146 | pub database: String, 147 | } 148 | 149 | impl Default for MySqlBackendConfig { 150 | fn default() -> Self { 151 | Self { 152 | host: None, 153 | port: None, 154 | socket: None, 155 | username: None, 156 | password: None, 157 | database: "access_log".to_string(), 158 | } 159 | } 160 | } 161 | 162 | impl From<&MySqlBackendConfig> for MySqlConnectOptions { 163 | fn from(config: &MySqlBackendConfig) -> Self { 164 | let mut options = MySqlConnectOptions::new(); 165 | if let Some(ref host) = config.host { 166 | options = options.host(host); 167 | } 168 | if let Some(port) = config.port { 169 | options = options.port(port); 170 | } 171 | if let Some(ref socket) = config.socket { 172 | options = options.socket(socket); 173 | } 174 | if let Some(ref username) = config.username { 175 | options = options.username(username); 176 | } 177 | if let Some(ref password) = config.password { 178 | options = options.password(password); 179 | } 180 | options = options.database(&config.database); 181 | options 182 | } 183 | } 184 | 185 | #[derive(Clone, Debug, Deserialize)] 186 | #[serde(default)] 187 | pub struct PostgresBackendConfig { 188 | pub host: Option, 189 | pub port: Option, 190 | pub socket: Option, 191 | pub username: Option, 192 | pub password: Option, 193 | pub database: String, 194 | } 195 | 196 | impl Default for PostgresBackendConfig { 197 | fn default() -> Self { 198 | Self { 199 | host: None, 200 | port: None, 201 | socket: None, 202 | username: None, 203 | password: None, 204 | database: "access_log".to_string(), 205 | } 206 | } 207 | } 208 | 209 | impl From<&PostgresBackendConfig> for PgConnectOptions { 210 | fn from(config: &PostgresBackendConfig) -> Self { 211 | let mut options = PgConnectOptions::new(); 212 | if let Some(ref host) = config.host { 213 | options = options.host(host); 214 | } 215 | if let Some(port) = config.port { 216 | options = options.port(port); 217 | } 218 | if let Some(ref socket) = config.socket { 219 | options = options.socket(socket); 220 | } 221 | if let Some(ref username) = config.username { 222 | options = options.username(username); 223 | } 224 | if let Some(ref password) = config.password { 225 | options = options.password(password); 226 | } 227 | options = options.database(&config.database); 228 | options 229 | } 230 | } 231 | -------------------------------------------------------------------------------- /openai-hub-core/src/config/jwt_auth.rs: -------------------------------------------------------------------------------- 1 | use hmac::digest::KeyInit; 2 | use hmac::Hmac; 3 | use serde::Deserialize; 4 | use sha2::Sha256; 5 | 6 | #[derive(Clone, Debug)] 7 | pub struct JwtAuthConfig { 8 | pub key: Hmac, 9 | } 10 | 11 | #[derive(Clone, Deserialize)] 12 | pub struct JwtAuthConfigDe { 13 | pub secret: String, 14 | } 15 | 16 | impl From for JwtAuthConfig { 17 | fn from(de: JwtAuthConfigDe) -> Self { 18 | let key = Hmac::new_from_slice(de.secret.as_bytes()).unwrap(); 19 | Self { key } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /openai-hub-core/src/config/mod.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | use std::net::{AddrParseError, SocketAddr}; 3 | 4 | #[cfg(feature = "acl")] 5 | use crate::acl::ApiAcl; 6 | 7 | #[cfg(feature = "jwt-auth")] 8 | mod jwt_auth; 9 | #[cfg(feature = "jwt-auth")] 10 | pub use jwt_auth::JwtAuthConfig; 11 | #[cfg(feature = "jwt-auth")] 12 | use jwt_auth::JwtAuthConfigDe; 13 | 14 | #[cfg(feature = "audit")] 15 | mod audit; 16 | #[cfg(feature = "audit")] 17 | pub use audit::*; 18 | 19 | #[derive(Clone, Debug)] 20 | pub struct ServerConfig { 21 | pub addr: SocketAddr, 22 | pub api_keys: Vec, 23 | pub openai: OpenAIConfig, 24 | #[cfg(feature = "acl")] 25 | pub global_api_acl: Option, 26 | #[cfg(feature = "jwt-auth")] 27 | pub jwt_auth: Option, 28 | #[cfg(feature = "audit")] 29 | pub audit: Option, 30 | } 31 | 32 | #[derive(Clone, Debug)] 33 | pub struct OpenAIConfig { 34 | pub organization: Option, 35 | pub api_base: String, 36 | pub api_type: ApiType, 37 | pub api_version: Option, 38 | } 39 | 40 | #[derive(Copy, Clone, Debug, Eq, PartialEq, Serialize, Deserialize, Default)] 41 | pub enum ApiType { 42 | #[serde(rename = "open_ai")] 43 | #[default] 44 | OpenAI, 45 | #[serde(rename = "azure")] 46 | Azure, 47 | #[serde(rename = "azure_ad")] 48 | AzureAD, 49 | } 50 | 51 | #[derive(Debug, thiserror::Error)] 52 | pub enum LoadError { 53 | #[error(transparent)] 54 | AddrParse(#[from] AddrParseError), 55 | #[error(transparent)] 56 | Toml(#[from] toml::de::Error), 57 | } 58 | 59 | impl ServerConfig { 60 | pub fn load(s: &str) -> Result { 61 | #[derive(Deserialize)] 62 | struct ConfigDe { 63 | bind: String, 64 | api_keys: Vec, 65 | #[serde(default)] 66 | organization: Option, 67 | #[serde(default)] 68 | api_base: Option, 69 | #[serde(default)] 70 | api_type: ApiType, 71 | #[serde(default)] 72 | api_version: Option, 73 | #[cfg(feature = "jwt-auth")] 74 | #[serde(rename = "jwt-auth")] 75 | #[serde(default)] 76 | jwt_auth: Option, 77 | #[cfg(feature = "audit")] 78 | #[serde(default)] 79 | audit: Option, 80 | } 81 | let config_de: ConfigDe = toml::from_str(s)?; 82 | Ok(Self { 83 | addr: config_de.bind.parse()?, 84 | api_keys: config_de.api_keys, 85 | openai: OpenAIConfig { 86 | organization: config_de.organization, 87 | api_base: config_de 88 | .api_base 89 | .unwrap_or("https://api.openai.com/v1".to_string()), 90 | api_type: config_de.api_type, 91 | api_version: config_de.api_version, 92 | }, 93 | #[cfg(feature = "acl")] 94 | global_api_acl: None, 95 | #[cfg(feature = "jwt-auth")] 96 | jwt_auth: config_de.jwt_auth.map(Into::into), 97 | #[cfg(feature = "audit")] 98 | audit: config_de.audit, 99 | }) 100 | } 101 | 102 | #[cfg(feature = "acl")] 103 | pub fn set_global_api_acl(&mut self, acl: ApiAcl) -> &mut Self { 104 | self.global_api_acl = Some(acl); 105 | self 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /openai-hub-core/src/error.rs: -------------------------------------------------------------------------------- 1 | use axum::body::Body; 2 | use axum::http::{header, StatusCode}; 3 | use axum::response::IntoResponse; 4 | use axum::response::Response; 5 | use serde_json::json; 6 | 7 | #[cfg(feature = "acl")] 8 | use crate::acl::AclError; 9 | 10 | #[derive(Debug)] 11 | pub struct ErrorResponse { 12 | status_code: StatusCode, 13 | message: String, 14 | } 15 | 16 | impl ErrorResponse { 17 | pub fn new>(status_code: StatusCode, message: S) -> Self { 18 | Self { 19 | status_code, 20 | message: message.as_ref().to_string(), 21 | } 22 | } 23 | 24 | pub fn body(&self) -> Body { 25 | let buf = json!({ 26 | "error": { 27 | "message": &self.message, 28 | } 29 | }); 30 | Body::from(serde_json::to_string(&buf).unwrap()) 31 | } 32 | } 33 | 34 | impl IntoResponse for ErrorResponse { 35 | fn into_response(self) -> Response { 36 | Response::builder() 37 | .status(self.status_code) 38 | .header(header::CONTENT_TYPE, "application/json") 39 | .body(self.body()) 40 | .unwrap() 41 | } 42 | } 43 | 44 | #[cfg(feature = "acl")] 45 | impl From for ErrorResponse { 46 | fn from(err: AclError) -> Self { 47 | ErrorResponse { 48 | status_code: err.status_code(), 49 | message: err.to_string(), 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /openai-hub-core/src/handler/acl.rs: -------------------------------------------------------------------------------- 1 | use crate::acl::ApiAcl; 2 | use crate::error::ErrorResponse; 3 | use axum::body::Body; 4 | use axum::extract::{Request, State}; 5 | use axum::http::{header, StatusCode}; 6 | use axum::middleware::Next; 7 | use axum::response::Response; 8 | use futures::TryStreamExt; 9 | use std::io; 10 | use std::sync::Arc; 11 | use tokio::io::AsyncReadExt; 12 | use tokio_util::io::StreamReader; 13 | use tracing::{event, Level}; 14 | 15 | pub async fn global_acl_layer( 16 | State(acl): State>>, 17 | req: Request, 18 | next: Next, 19 | ) -> Result { 20 | if acl.is_none() { 21 | return Ok(next.run(req).await); 22 | } 23 | let acl = acl.unwrap(); 24 | let (parts, mut body) = req.into_parts(); 25 | event!(Level::DEBUG, "{} {}", parts.method, parts.uri.path()); 26 | 27 | let may_validate_model = acl 28 | .validate(&parts.method, parts.uri.path()) 29 | .map_err(ErrorResponse::from)?; 30 | 31 | if let Some(validator) = may_validate_model { 32 | validator 33 | .validate_path(parts.uri.path()) 34 | .map_err(ErrorResponse::from)?; 35 | if !parts.method.is_safe() { 36 | let content_type = parts 37 | .headers 38 | .get(header::CONTENT_TYPE) 39 | .and_then(|value| value.to_str().ok()) 40 | .ok_or_else(|| { 41 | ErrorResponse::new(StatusCode::BAD_REQUEST, "missing content-type header") 42 | })?; 43 | event!(Level::DEBUG, "Content-Type: {}", content_type); 44 | if content_type == "application/json" { 45 | let mut buf = vec![]; 46 | StreamReader::new(body.map_err(|e| io::Error::new(io::ErrorKind::Other, e))) 47 | .read_to_end(&mut buf) 48 | .await 49 | .map_err(|_| { 50 | ErrorResponse::new(StatusCode::BAD_REQUEST, "failed to read body") 51 | })?; 52 | let json: serde_json::Value = serde_json::from_slice(&buf) 53 | .map_err(|e| ErrorResponse::new(StatusCode::BAD_REQUEST, e.to_string()))?; 54 | event!(Level::DEBUG, "json: {:?}", json); 55 | validator 56 | .validate_body(&json) 57 | .map_err(ErrorResponse::from)?; 58 | 59 | body = Body::from(serde_json::to_string(&json).unwrap()); 60 | } 61 | } 62 | } 63 | 64 | let req = Request::from_parts(parts, body); 65 | Ok(next.run(req).await) 66 | } 67 | -------------------------------------------------------------------------------- /openai-hub-core/src/handler/audit/access.rs: -------------------------------------------------------------------------------- 1 | use crate::audit::{AccessLog, Backend, BackendEngine}; 2 | use crate::config::AuditConfig; 3 | use crate::error::ErrorResponse; 4 | use crate::handler::helpers::{stream_read_req_body, stream_read_response_body}; 5 | use crate::handler::jwt::AUTHED_HEADER; 6 | use crate::helpers::HeaderMapExt; 7 | use crate::short_circuit_if; 8 | use axum::extract::{Request, State}; 9 | use axum::middleware::Next; 10 | use axum::response::Response; 11 | use std::sync::Arc; 12 | use tokio::spawn; 13 | 14 | pub const RAY_ID_HEADER: &str = "X-Ray-Id"; 15 | 16 | pub async fn audit_access_layer( 17 | State(state): State, Backend)>>, 18 | req: Request, 19 | next: Next, 20 | ) -> Result { 21 | short_circuit_if!(req, next, state.is_none()); 22 | 23 | let (config, backend) = state.unwrap(); 24 | short_circuit_if!(req, next, !config.filters.access.enable); 25 | 26 | let (mut parts, body) = req.into_parts(); 27 | let mut log = AccessLog::now(); 28 | parts.headers.remove(RAY_ID_HEADER); 29 | parts 30 | .headers 31 | .insert(RAY_ID_HEADER, log.ray_id.as_str().parse().unwrap()); 32 | 33 | if let Some(user) = parts.headers.get(AUTHED_HEADER) { 34 | log.user = Some(user.to_str().unwrap().to_string()); 35 | } 36 | if config.filters.access.method { 37 | log.method = Some(parts.method.as_str().to_string()); 38 | } 39 | if config.filters.access.uri { 40 | log.uri = Some(parts.uri.path().to_string()); 41 | } 42 | if config.filters.access.headers { 43 | log.headers = Some(parts.headers.as_btree_map()); 44 | } 45 | let req = Request::from_parts(parts, body); 46 | let response = if config.filters.access.body { 47 | let (response, mut body_recv) = stream_read_req_body(req, next).await; 48 | log.body = body_recv.recv().await.flatten(); 49 | response 50 | } else { 51 | next.run(req).await 52 | }; 53 | 54 | let response = if config.filters.access.response { 55 | let status = response.status(); 56 | let headers = response.headers().clone(); 57 | 58 | let (response, mut body_rx) = stream_read_response_body(response); 59 | spawn(async move { 60 | log.response_status = Some(status.as_u16()); 61 | log.response_headers = Some(headers.as_btree_map()); 62 | log.response_body = body_rx.recv().await.flatten(); 63 | backend.log_access(log).await; 64 | }); 65 | 66 | response 67 | } else { 68 | spawn(async move { 69 | backend.log_access(log).await; 70 | }); 71 | response 72 | }; 73 | 74 | Ok(response) 75 | } 76 | -------------------------------------------------------------------------------- /openai-hub-core/src/handler/audit/mod.rs: -------------------------------------------------------------------------------- 1 | mod access; 2 | mod tokens; 3 | 4 | pub use access::audit_access_layer; 5 | pub use tokens::audit_tokens_layer; 6 | -------------------------------------------------------------------------------- /openai-hub-core/src/handler/audit/tokens.rs: -------------------------------------------------------------------------------- 1 | use crate::audit::{Backend, BackendEngine, TokenUsage, TokenUsageLog}; 2 | use crate::config::{AuditConfig, StreamTokensPolicy}; 3 | use crate::error::ErrorResponse; 4 | use crate::handler::audit::access::RAY_ID_HEADER; 5 | use crate::handler::helpers::stream_read_response_body; 6 | use crate::handler::jwt::AUTHED_HEADER; 7 | use crate::short_circuit_if; 8 | use axum::body::Body; 9 | use axum::extract::{Request, State}; 10 | use axum::http::StatusCode; 11 | use axum::middleware::Next; 12 | use axum::response::Response; 13 | use futures::TryStreamExt; 14 | use serde::de::DeserializeOwned; 15 | use serde::Deserialize; 16 | use serde_json::Value; 17 | use std::io; 18 | use std::sync::Arc; 19 | use tiktoken_rs::tokenizer::get_tokenizer; 20 | use tiktoken_rs::{ 21 | get_bpe_from_tokenizer, num_tokens_from_messages, ChatCompletionRequestMessage, FunctionCall, 22 | }; 23 | use tokio::io::AsyncReadExt; 24 | use tokio::spawn; 25 | use tokio::sync::mpsc::Receiver; 26 | use tokio_util::io::StreamReader; 27 | use tracing::{event, instrument, Level}; 28 | 29 | #[instrument(skip_all)] 30 | pub async fn audit_tokens_layer( 31 | State(state): State, Backend)>>, 32 | req: Request, 33 | next: Next, 34 | ) -> Result { 35 | short_circuit_if!(req, next, state.is_none()); 36 | 37 | let (config, backend) = state.unwrap(); 38 | 39 | short_circuit_if!(req, next, !config.filters.tokens.enable); 40 | short_circuit_if!( 41 | req, 42 | next, 43 | !config.filters.tokens.endpoints.contains(req.uri().path()) 44 | ); 45 | 46 | let (parts, body) = req.into_parts(); 47 | let user = parts 48 | .headers 49 | .get(AUTHED_HEADER) 50 | .map(|h| h.to_str().unwrap().to_string()); 51 | let ray_id = parts 52 | .headers 53 | .get(RAY_ID_HEADER) 54 | .unwrap() 55 | .to_str() 56 | .unwrap() 57 | .to_string(); 58 | let mut req_body = vec![]; 59 | StreamReader::new(body.map_err(|e| io::Error::new(io::ErrorKind::Other, e))) 60 | .read_to_end(&mut req_body) 61 | .await 62 | .map_err(|_| ErrorResponse::new(StatusCode::BAD_REQUEST, "failed to read body"))?; 63 | let parsed_body: Value = serde_json::from_slice(&req_body) 64 | .map_err(|_| ErrorResponse::new(StatusCode::BAD_REQUEST, "failed to parse body"))?; 65 | if parsed_body.get("model").is_none() { 66 | event!( 67 | Level::ERROR, 68 | "tokens statics require 'model' field in request body" 69 | ); 70 | return Err(ErrorResponse::new( 71 | StatusCode::BAD_REQUEST, 72 | "missing 'model' field in request body", 73 | )); 74 | } 75 | let stream = parsed_body 76 | .get("stream") 77 | .and_then(|s| s.as_bool()) 78 | .unwrap_or(false); 79 | if stream && config.filters.tokens.stream_tokens == StreamTokensPolicy::Reject { 80 | return Err(ErrorResponse::new( 81 | StatusCode::BAD_REQUEST, 82 | "stream requests are not allowed", 83 | )); 84 | } 85 | let endpoint = parts.uri.path().to_string(); 86 | 87 | let request = Request::from_parts(parts, Body::from(req_body)); 88 | let response = next.run(request).await; 89 | let (response, res_body_rx) = stream_read_response_body(response); 90 | 91 | spawn(audit_tokens_layer_inner( 92 | endpoint, 93 | user, 94 | parsed_body, 95 | res_body_rx, 96 | ray_id, 97 | config, 98 | backend, 99 | )); 100 | 101 | Ok(response) 102 | } 103 | 104 | async fn audit_tokens_layer_inner( 105 | endpoint: String, 106 | user: Option, 107 | req_body: Value, 108 | mut res_body_rx: Receiver>>, 109 | ray_id: String, 110 | config: Arc, 111 | backend: Backend, 112 | ) { 113 | // TODO: stream read response body 114 | let res_body = res_body_rx 115 | .recv() 116 | .await 117 | .flatten() 118 | .and_then(|v| String::from_utf8(v).ok()); 119 | if res_body.is_none() { 120 | event!(Level::WARN, "failed to read response body"); 121 | return; 122 | } 123 | let res_body = res_body.unwrap(); 124 | let model = req_body.get("model").unwrap().as_str().unwrap().to_string(); 125 | 126 | let stream = req_body 127 | .get("stream") 128 | .and_then(|s| s.as_bool()) 129 | .unwrap_or(false); 130 | let (usage, is_estimated) = match (stream, config.filters.tokens.stream_tokens) { 131 | (true, StreamTokensPolicy::Skip) => return, 132 | (true, StreamTokensPolicy::Reject) => unreachable!(), 133 | (true, StreamTokensPolicy::Estimate) => { 134 | let usage = match endpoint.as_str() { 135 | "/completions" => count_completions_tokens(model.as_str(), req_body, res_body), 136 | "/chat/completions" => count_chat_tokens(model.as_str(), req_body, res_body), 137 | _ => { 138 | event!(Level::ERROR, "unsupported endpoint {}", endpoint); 139 | return; 140 | } 141 | }; 142 | if usage.is_none() { 143 | event!( 144 | Level::WARN, 145 | "failed to estimate usage for request, ray id = {}", 146 | ray_id 147 | ); 148 | return; 149 | } 150 | (usage.unwrap(), true) 151 | } 152 | (false, _) => { 153 | if let Ok(res) = serde_json::from_str::(res_body.as_str()) { 154 | (res.usage, false) 155 | } else { 156 | event!(Level::WARN, "failed to parse usage from response"); 157 | return; 158 | } 159 | } 160 | }; 161 | 162 | let log = TokenUsageLog { 163 | timestamp: chrono::Utc::now(), 164 | user, 165 | ray_id, 166 | model, 167 | usage, 168 | is_estimated, 169 | }; 170 | backend.log_tokens(log).await; 171 | } 172 | 173 | fn get_events(res_body: String) -> Option>> { 174 | let events: Result>, _> = res_body 175 | .split("\n\n") 176 | .filter_map(|event| event.strip_prefix("data: ")) 177 | .filter(|event| *event != "[DONE]") 178 | .map(|event| { 179 | event!(Level::DEBUG, "paring event: {}", event); 180 | serde_json::from_str(event) 181 | }) 182 | .collect(); 183 | if events.is_err() { 184 | event!(Level::ERROR, "failed to parse response events"); 185 | return None; 186 | } 187 | Some(events.unwrap()) 188 | } 189 | 190 | fn count_completions_tokens(model: &str, req_body: Value, res_body: String) -> Option { 191 | let tokenizer = get_tokenizer(model)?; 192 | event!(Level::DEBUG, "got tokenizer {:?} for {}", tokenizer, model); 193 | let bpe = get_bpe_from_tokenizer(tokenizer).ok()?; 194 | 195 | let prompt_tokens = req_body 196 | .get("prompt") 197 | .and_then(|p| p.as_str()) 198 | .map(|s| bpe.encode_with_special_tokens(s).len()) 199 | .unwrap_or(0); 200 | 201 | let events = get_events::(res_body)?; 202 | let mut choices = vec![]; 203 | for event in events.into_iter() { 204 | for choice in event.choices.into_iter() { 205 | if choices.len() < choice.index + 1 { 206 | choices.resize(choice.index + 1, String::new()); 207 | } 208 | choices[choice.index].push_str(choice.text.as_str()); 209 | } 210 | } 211 | let completion_tokens = choices 212 | .iter() 213 | .map(|s| bpe.encode_with_special_tokens(s).len()) 214 | .sum(); 215 | 216 | Some(TokenUsage { 217 | prompt_tokens, 218 | completion_tokens, 219 | total_tokens: prompt_tokens + completion_tokens, 220 | }) 221 | } 222 | 223 | fn count_chat_tokens(model: &str, req_body: Value, res_body: String) -> Option { 224 | #[derive(Deserialize)] 225 | struct ChatCompletionRequestMessageDe { 226 | role: String, 227 | #[serde(default)] 228 | content: Option, 229 | #[serde(default)] 230 | name: Option, 231 | #[serde(default)] 232 | function_call: Option, 233 | } 234 | let prompt_messages = req_body.get("messages")?; 235 | let parsed_prompt = 236 | serde_json::from_value::>(prompt_messages.clone()) 237 | .ok()?; 238 | let prompt: Vec = parsed_prompt 239 | .into_iter() 240 | .map(|p| ChatCompletionRequestMessage { 241 | role: p.role, 242 | content: p.content, 243 | name: p.name, 244 | function_call: p.function_call.map(|f| f.into()), 245 | }) 246 | .collect(); 247 | let prompt_tokens = num_tokens_from_messages(model, &prompt).ok()?; 248 | event!(Level::DEBUG, "estimated prompt tokens: {}", prompt_tokens); 249 | 250 | let events = get_events::(res_body)?; 251 | 252 | let mut choices = vec![]; 253 | for event in events.into_iter() { 254 | for choice in event.choices.into_iter() { 255 | if choices.len() < choice.index + 1 { 256 | choices.resize(choice.index + 1, ChatCompletionRequestMessage::default()); 257 | } 258 | if let Some(r) = choice.delta.role { 259 | choices[choice.index].role = r; 260 | } 261 | if let Some(c) = choice.delta.content { 262 | if choices[choice.index].content.is_none() { 263 | choices[choice.index].content = Some(c); 264 | } else { 265 | choices[choice.index] 266 | .content 267 | .as_mut() 268 | .unwrap() 269 | .push_str(c.as_str()); 270 | } 271 | } 272 | if let Some(f) = choice.delta.function_call { 273 | choices[choice.index].function_call = Some(f.into()); 274 | } 275 | } 276 | } 277 | event!(Level::DEBUG, "completions: {:?}", choices); 278 | let completion_tokens = num_tokens_from_messages(model, &choices).ok()?; 279 | event!( 280 | Level::DEBUG, 281 | "estimated completion tokens: {}", 282 | completion_tokens 283 | ); 284 | 285 | Some(TokenUsage { 286 | prompt_tokens, 287 | completion_tokens, 288 | total_tokens: prompt_tokens + completion_tokens, 289 | }) 290 | } 291 | 292 | #[derive(Deserialize)] 293 | struct ResponseWithUsage { 294 | usage: TokenUsage, 295 | } 296 | 297 | #[derive(Deserialize)] 298 | struct StreamEvent { 299 | choices: Vec, 300 | } 301 | 302 | #[derive(Copy, Clone, Debug, Deserialize)] 303 | enum ObjectType { 304 | #[serde(rename = "chat.completion.chunk")] 305 | ChatCompletionChunk, 306 | #[serde(rename = "text_completion")] 307 | TextCompletion, 308 | } 309 | 310 | #[derive(Deserialize)] 311 | struct ChatChoice { 312 | pub delta: Delta, 313 | pub index: usize, 314 | } 315 | 316 | #[derive(Default, Deserialize)] 317 | struct Delta { 318 | pub role: Option, 319 | pub content: Option, 320 | pub function_call: Option, 321 | } 322 | 323 | #[derive(Deserialize)] 324 | pub struct FunctionCallDe { 325 | pub name: String, 326 | pub arguments: String, 327 | } 328 | 329 | #[derive(Deserialize)] 330 | struct CompletionChoice { 331 | pub text: String, 332 | pub index: usize, 333 | } 334 | 335 | impl From for FunctionCall { 336 | fn from(f: FunctionCallDe) -> Self { 337 | FunctionCall { 338 | name: f.name, 339 | arguments: f.arguments, 340 | } 341 | } 342 | } 343 | -------------------------------------------------------------------------------- /openai-hub-core/src/handler/helpers.rs: -------------------------------------------------------------------------------- 1 | use crate::helpers::{tee, ResultStream}; 2 | use axum::body::{Body, Bytes}; 3 | use axum::extract::Request; 4 | use axum::middleware::Next; 5 | use axum::response::Response; 6 | use futures::TryStreamExt; 7 | use std::io; 8 | use sync_wrapper::SyncStream; 9 | use tokio::io::AsyncReadExt; 10 | use tokio::spawn; 11 | use tokio::sync::mpsc; 12 | use tokio::sync::mpsc::Receiver; 13 | use tokio_util::io::StreamReader; 14 | 15 | #[macro_export] 16 | macro_rules! short_circuit_if { 17 | ($req:ident, $next:ident, $cond:expr) => { 18 | if $cond { 19 | return Ok($next.run($req).await); 20 | } 21 | }; 22 | } 23 | 24 | pub async fn stream_read_req_body( 25 | req: Request, 26 | next: Next, 27 | ) -> (Response, Receiver>>) { 28 | let (parts, body) = req.into_parts(); 29 | let (dup_body, body_) = tee(SyncStream::new(body)); 30 | 31 | let rx = stream_read_body(dup_body); 32 | let response = next 33 | .run(Request::from_parts(parts, Body::from_stream(body_))) 34 | .await; 35 | 36 | (response, rx) 37 | } 38 | 39 | pub fn stream_read_response_body(response: Response) -> (Response, Receiver>>) { 40 | let (parts, body) = response.into_parts(); 41 | let (dup_body, body) = tee(SyncStream::new(body)); 42 | let rx = stream_read_body(dup_body); 43 | let response = Response::from_parts(parts, Body::from_stream(body)); 44 | (response, rx) 45 | } 46 | 47 | fn stream_read_body(body: ResultStream) -> Receiver>> { 48 | let (tx, rx) = mpsc::channel(1); 49 | spawn(async move { 50 | let mut body_reader = StreamReader::new( 51 | body.map_err(|e| io::Error::new(io::ErrorKind::Other, e.to_string())), 52 | ); 53 | let mut body_buffer = vec![]; 54 | if body_reader.read_to_end(&mut body_buffer).await.is_err() { 55 | tx.send(None).await.ok(); 56 | } 57 | tx.send(Some(body_buffer)).await.ok() 58 | }); 59 | rx 60 | } 61 | -------------------------------------------------------------------------------- /openai-hub-core/src/handler/jwt.rs: -------------------------------------------------------------------------------- 1 | use crate::config::JwtAuthConfig; 2 | use crate::error::ErrorResponse; 3 | use axum::extract::{Request, State}; 4 | use axum::http::{header, StatusCode}; 5 | use axum::middleware::Next; 6 | use axum::response::Response; 7 | use jwt::{RegisteredClaims, VerifyWithKey}; 8 | use std::sync::Arc; 9 | use tracing::{event, instrument, Level}; 10 | 11 | pub const AUTHED_HEADER: &str = "X-AUTHED-SUB"; 12 | 13 | #[instrument(skip_all)] 14 | pub async fn jwt_auth_layer( 15 | State(jwt_config): State>>, 16 | req: Request, 17 | next: Next, 18 | ) -> Result { 19 | match jwt_config { 20 | Some(jwt_config) => jwt_auth_layer_inner(jwt_config, req, next) 21 | .await 22 | .map_err(|_| { 23 | event!(Level::ERROR, "Failed to authenticate request"); 24 | ErrorResponse::new(StatusCode::FORBIDDEN, "invalid authorization header") 25 | }), 26 | None => Ok(next.run(req).await), 27 | } 28 | } 29 | 30 | async fn jwt_auth_layer_inner( 31 | jwt_config: Arc, 32 | req: Request, 33 | next: Next, 34 | ) -> Result { 35 | let (mut parts, body) = req.into_parts(); 36 | 37 | parts.headers.remove(AUTHED_HEADER); 38 | 39 | let token = parts 40 | .headers 41 | .get(header::AUTHORIZATION) 42 | .ok_or_else(|| { 43 | event!(Level::ERROR, "Missing authorization header"); 44 | })? 45 | .to_str() 46 | .map_err(|_| { 47 | event!(Level::ERROR, "Invalid string in authorization header"); 48 | })? 49 | .strip_prefix("Bearer ") 50 | .ok_or_else(|| { 51 | event!(Level::ERROR, "Not start with 'Bearer '"); 52 | })?; 53 | 54 | event!(Level::DEBUG, "Token: {}", token); 55 | 56 | let claims: RegisteredClaims = 57 | VerifyWithKey::verify_with_key(token, &jwt_config.key).map_err(|e| { 58 | event!(Level::ERROR, "Failed to verify token: {}", e); 59 | })?; 60 | 61 | let now = chrono::Utc::now().timestamp() as u64; 62 | 63 | if let Some(nbf) = claims.not_before { 64 | if nbf > now { 65 | event!(Level::ERROR, "claims not valid before now: {:?}", claims); 66 | return Err(()); 67 | } 68 | } 69 | if let Some(exp) = claims.expiration { 70 | if exp < now { 71 | event!(Level::ERROR, "expired claims: {:?}", claims); 72 | return Err(()); 73 | } 74 | } 75 | 76 | event!(Level::INFO, "verified claims: {:?}", claims); 77 | match claims.subject { 78 | Some(sub) => { 79 | event!(Level::INFO, "authed subject: {}", sub); 80 | parts 81 | .headers 82 | .insert(AUTHED_HEADER, sub.parse().map_err(|_| ())?); 83 | } 84 | None => { 85 | event!(Level::INFO, "anonymous claims"); 86 | parts 87 | .headers 88 | .insert(AUTHED_HEADER, "anonymous".parse().unwrap()); 89 | } 90 | } 91 | 92 | let req = Request::from_parts(parts, body); 93 | Ok(next.run(req).await) 94 | } 95 | -------------------------------------------------------------------------------- /openai-hub-core/src/handler/mod.rs: -------------------------------------------------------------------------------- 1 | #[cfg(feature = "acl")] 2 | mod acl; 3 | #[cfg(feature = "audit")] 4 | mod audit; 5 | mod helpers; 6 | #[cfg(feature = "jwt-auth")] 7 | mod jwt; 8 | 9 | use crate::config::OpenAIConfig; 10 | use crate::error::ErrorResponse; 11 | use crate::helpers::proxy_request; 12 | use crate::key::KeyPool; 13 | use axum::extract::Request; 14 | use axum::handler::Handler; 15 | use axum::response::{IntoResponse, Response}; 16 | use futures::TryStreamExt; 17 | use std::future::Future; 18 | use std::io; 19 | use std::pin::Pin; 20 | use std::sync::Arc; 21 | use sync_wrapper::SyncStream; 22 | 23 | #[cfg(feature = "jwt-auth")] 24 | pub use self::jwt::jwt_auth_layer; 25 | #[cfg(feature = "acl")] 26 | pub use acl::global_acl_layer; 27 | #[cfg(feature = "audit")] 28 | pub use audit::{audit_access_layer, audit_tokens_layer}; 29 | 30 | #[derive(Clone)] 31 | pub struct RequestHandler { 32 | pub key_pool: Arc, 33 | pub client: reqwest::Client, 34 | pub config: Arc, 35 | } 36 | 37 | impl Handler, ()> for RequestHandler { 38 | type Future = Pin + Send>>; 39 | 40 | fn call(self, req: Request, _state: ()) -> Self::Future { 41 | Box::pin(async move { self.handle_request(req).await.into_response() }) 42 | } 43 | } 44 | 45 | impl RequestHandler { 46 | async fn handle_request(self, req: Request) -> Result { 47 | let (parts, body) = req.into_parts(); 48 | let body = body.map_err(|e| io::Error::new(io::ErrorKind::Other, e)); 49 | 50 | proxy_request( 51 | self.client, 52 | parts.method, 53 | format!("{}{}", self.config.api_base, parts.uri.path()), 54 | self.key_pool.get().await, 55 | parts.headers, 56 | reqwest::Body::wrap_stream(SyncStream::new(body)), 57 | ) 58 | .await 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /openai-hub-core/src/helpers.rs: -------------------------------------------------------------------------------- 1 | use axum::body::Body; 2 | use std::collections::BTreeMap; 3 | use std::pin::Pin; 4 | use std::task::{Context, Poll}; 5 | 6 | use axum::http::{header, HeaderMap, Method, StatusCode}; 7 | use axum::response::Response; 8 | use base64::Engine; 9 | use futures::{Stream, StreamExt}; 10 | use tokio::sync::mpsc; 11 | 12 | use tokio_stream::wrappers::ReceiverStream; 13 | 14 | use crate::error::ErrorResponse; 15 | use crate::key::KeyGuard; 16 | use tracing::{event, instrument, Level}; 17 | 18 | #[cfg(feature = "acl")] 19 | mod regex_helpers { 20 | use once_cell::sync::Lazy; 21 | use regex::Regex; 22 | use tracing::{event, instrument, Level}; 23 | 24 | static SPECIAL_CHARS_EXCEPT_START_REGEX: Lazy = 25 | Lazy::new(|| Regex::new(r"([.+?^$()\[\]{}|\\])").unwrap()); 26 | static SPECIAL_CHARS_EXCEPT_GROUP_REGEX: Lazy = 27 | Lazy::new(|| Regex::new(r"([.+?*^$()\[\]|\\])").unwrap()); 28 | static PATH_REGEX: Lazy = Lazy::new(|| Regex::new(r"/\{(?P[^/{}]+)}").unwrap()); 29 | 30 | #[instrument(skip_all)] 31 | pub fn wildcards_to_regex, I: Iterator>( 32 | wildcards: I, 33 | ) -> Result { 34 | let mut candidates = vec![]; 35 | for wildcard in wildcards { 36 | let wildcard = wildcard.as_ref(); 37 | let wildcard = SPECIAL_CHARS_EXCEPT_START_REGEX.replace(wildcard, "\\$1"); 38 | // short circuit if the wildcard is allowing anything 39 | if wildcard == "*" { 40 | event!(Level::DEBUG, "found *, skip remaining"); 41 | return Ok(Regex::new("^.*$").unwrap()); 42 | } 43 | let mut wildcard = wildcard.replace('*', ".*"); 44 | // group with non-capturing group 45 | wildcard.insert_str(0, "(?:"); 46 | wildcard.push(')'); 47 | event!(Level::DEBUG, "transformed wildcard to {}", wildcard); 48 | candidates.push(wildcard); 49 | } 50 | let mut regex = candidates.join("|"); 51 | regex.insert_str(0, "^(?:"); 52 | regex.push_str(")$"); 53 | event!(Level::DEBUG, "transformed wildcards to regex {}", regex); 54 | Regex::new(®ex) 55 | } 56 | 57 | #[instrument(skip_all)] 58 | pub fn endpoints_to_regex, I: Iterator>( 59 | endpoints: I, 60 | ) -> Result { 61 | let mut candidates = vec![]; 62 | for endpoint in endpoints { 63 | let endpoint = endpoint.as_ref(); 64 | let endpoint = SPECIAL_CHARS_EXCEPT_GROUP_REGEX.replace(endpoint, "\\$1"); 65 | let endpoint = PATH_REGEX.replace(&endpoint, "/(?:[^/]+)"); 66 | event!(Level::DEBUG, "transformed regex rule: {}", endpoint); 67 | // group with non-capturing group 68 | candidates.push(format!("(?:{endpoint})")); 69 | } 70 | let mut regex = candidates.join("|"); 71 | regex.insert_str(0, "^(?:"); 72 | regex.push_str(")$"); 73 | event!(Level::DEBUG, "transformed wildcards to regex {}", regex); 74 | Regex::new(®ex) 75 | } 76 | } 77 | 78 | #[cfg(feature = "acl")] 79 | pub use regex_helpers::*; 80 | 81 | pub fn request_error_into_response(e: reqwest::Error) -> ErrorResponse { 82 | if e.is_timeout() { 83 | return ErrorResponse::new(StatusCode::GATEWAY_TIMEOUT, "openai timeout"); 84 | } 85 | ErrorResponse::new(StatusCode::INTERNAL_SERVER_ERROR, e.to_string()) 86 | } 87 | 88 | #[pin_project::pin_project] 89 | pub struct StreamWithKey { 90 | #[pin] 91 | stream: S, 92 | key: KeyGuard, 93 | } 94 | 95 | impl StreamWithKey { 96 | pub fn new(stream: S, key: KeyGuard) -> Self { 97 | Self { stream, key } 98 | } 99 | } 100 | 101 | impl futures::Stream for StreamWithKey { 102 | type Item = S::Item; 103 | 104 | fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { 105 | let mut this = self.project(); 106 | this.stream.as_mut().poll_next(cx) 107 | } 108 | 109 | fn size_hint(&self) -> (usize, Option) { 110 | self.stream.size_hint() 111 | } 112 | } 113 | 114 | #[instrument(skip(client, key, body))] 115 | pub async fn proxy_request( 116 | client: reqwest::Client, 117 | method: Method, 118 | uri: U, 119 | key: KeyGuard, 120 | headers: HeaderMap, 121 | body: B, 122 | ) -> Result 123 | where 124 | U: reqwest::IntoUrl + std::fmt::Debug, 125 | B: Into, 126 | { 127 | let mut request = client 128 | .request(method, uri) 129 | .header(header::AUTHORIZATION, format!("Bearer {}", key.as_str())) 130 | .body(body); 131 | if let Some(content_type) = headers.get(header::CONTENT_TYPE) { 132 | request = request.header(header::CONTENT_TYPE, content_type); 133 | } 134 | if let Some(accept) = headers.get(header::ACCEPT) { 135 | request = request.header(header::ACCEPT, accept); 136 | } 137 | let result = request.send().await.map_err(request_error_into_response)?; 138 | let status = result.status(); 139 | let headers = result.headers().clone(); 140 | event!(Level::DEBUG, "openai returns status: {}", status); 141 | let body = StreamWithKey::new(result.bytes_stream(), key); 142 | let mut builder = Response::builder().status(status); 143 | for (k, v) in headers.iter() { 144 | builder = builder.header(k, v); 145 | } 146 | Ok(builder.body(Body::from_stream(body)).unwrap()) 147 | } 148 | 149 | pub type ResultStream = ReceiverStream>; 150 | 151 | pub fn tee(stream: S) -> (ResultStream, ResultStream) 152 | where 153 | S: Stream> + Send + Sync + Unpin + 'static, 154 | T: Clone + Send + Sync + 'static, 155 | E: Send + Sync + 'static, 156 | { 157 | let (tx1, rx1) = mpsc::channel(1); 158 | let (tx2, rx2) = mpsc::channel(1); 159 | 160 | tokio::spawn(async move { 161 | let mut stream = stream; 162 | 163 | while let Some(value) = stream.next().await { 164 | match value { 165 | Ok(t) => { 166 | let clone_t = t.clone(); 167 | let tx1 = tx1.clone(); 168 | tokio::spawn(async move { 169 | tx1.send(Ok(clone_t)).await.ok(); 170 | }); 171 | let tx2 = tx2.clone(); 172 | tokio::spawn(async move { 173 | tx2.send(Ok(t)).await.ok(); 174 | }); 175 | } 176 | Err(e) => { 177 | tx2.send(Err(e)).await.ok(); 178 | } 179 | } 180 | } 181 | }); 182 | 183 | (ReceiverStream::new(rx1), ReceiverStream::new(rx2)) 184 | } 185 | 186 | pub trait HeaderMapExt { 187 | fn as_btree_map(&self) -> BTreeMap; 188 | } 189 | 190 | impl HeaderMapExt for HeaderMap { 191 | fn as_btree_map(&self) -> BTreeMap { 192 | self.iter() 193 | .map(|(k, v)| match v.to_str() { 194 | Ok(s) => (k.to_string(), s.to_string()), 195 | Err(_) => ( 196 | k.to_string(), 197 | if cfg!(feature = "base64-serialize") { 198 | base64::engine::general_purpose::STANDARD.encode(v.as_bytes()) 199 | } else { 200 | "".to_string() 201 | }, 202 | ), 203 | }) 204 | .collect() 205 | } 206 | } 207 | -------------------------------------------------------------------------------- /openai-hub-core/src/key.rs: -------------------------------------------------------------------------------- 1 | use parking_lot::Mutex; 2 | use std::collections::VecDeque; 3 | use std::sync::Arc; 4 | use std::{fmt, mem}; 5 | use tokio::sync::{OwnedSemaphorePermit, Semaphore}; 6 | 7 | pub struct KeyPool { 8 | total: usize, 9 | keys: Mutex>, 10 | semaphore: Arc, 11 | } 12 | 13 | #[clippy::has_significant_drop] 14 | pub struct KeyGuard { 15 | key: String, 16 | pool: Arc, 17 | _permit: OwnedSemaphorePermit, 18 | } 19 | 20 | impl KeyPool { 21 | pub fn new(iter: impl IntoIterator) -> Self { 22 | let keys = VecDeque::from_iter(iter); 23 | let semaphore = Semaphore::new(keys.len()); 24 | 25 | Self { 26 | total: keys.len(), 27 | keys: Mutex::new(keys), 28 | semaphore: Arc::new(semaphore), 29 | } 30 | } 31 | 32 | pub async fn get(self: Arc) -> KeyGuard { 33 | let permit = self.semaphore.clone().acquire_owned().await.unwrap(); 34 | let key = self.keys.lock().pop_front().unwrap(); 35 | 36 | KeyGuard { 37 | key, 38 | pool: self.clone(), 39 | _permit: permit, 40 | } 41 | } 42 | } 43 | 44 | impl fmt::Debug for KeyPool { 45 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 46 | f.debug_struct("KeyPool") 47 | .field("available", &self.semaphore.available_permits()) 48 | .field("total", &self.total) 49 | .finish() 50 | } 51 | } 52 | 53 | impl KeyGuard { 54 | pub fn as_str(&self) -> &str { 55 | &self.key 56 | } 57 | } 58 | 59 | impl Drop for KeyGuard { 60 | fn drop(&mut self) { 61 | let key = mem::take(&mut self.key); 62 | self.pool.keys.lock().push_back(key); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /openai-hub-core/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![deny(unsafe_code)] 2 | //! This is the main module for the OpenAI Hub server. 3 | //! It handles server configuration, request handling, access control, and the server's API key pool. 4 | 5 | #[cfg(feature = "acl")] 6 | /// Access Control List (ACL) module 7 | mod acl; 8 | #[cfg(feature = "audit")] 9 | mod audit; 10 | /// Configuration 11 | pub mod config; 12 | /// Error handling 13 | mod error; 14 | /// Request handlers 15 | mod handler; 16 | /// Helpers 17 | mod helpers; 18 | /// API Key Pool 19 | mod key; 20 | 21 | #[cfg(feature = "acl")] 22 | pub use acl::ApiAcl; 23 | 24 | use crate::handler::RequestHandler; 25 | use crate::key::KeyPool; 26 | use axum::handler::HandlerWithoutStateExt; 27 | use config::ServerConfig; 28 | use std::io; 29 | use std::sync::Arc; 30 | use tokio::net::TcpListener; 31 | use tracing::{event, Level}; 32 | 33 | #[cfg(any(feature = "acl", feature = "jwt-auth", feature = "audit"))] 34 | use axum::handler::Handler; 35 | #[cfg(any(feature = "acl", feature = "jwt-auth", feature = "audit"))] 36 | use axum::middleware::from_fn_with_state; 37 | 38 | #[cfg(feature = "acl")] 39 | use crate::handler::global_acl_layer; 40 | #[cfg(feature = "jwt-auth")] 41 | use crate::handler::jwt_auth_layer; 42 | #[cfg(feature = "audit")] 43 | use crate::handler::{audit_access_layer, audit_tokens_layer}; 44 | 45 | static APP_USER_AGENT: &str = concat!(env!("CARGO_PKG_NAME"), "/", env!("CARGO_PKG_VERSION")); 46 | 47 | /// Holds the server's configuration and API key pool. 48 | pub struct Server { 49 | config: Arc, 50 | api_key_pool: Arc, 51 | } 52 | 53 | /// Server Error 54 | #[derive(Debug, thiserror::Error)] 55 | pub enum ServerError { 56 | #[error(transparent)] 57 | Io(#[from] io::Error), 58 | #[error(transparent)] 59 | AddrParse(#[from] std::net::AddrParseError), 60 | #[error(transparent)] 61 | Reqwest(#[from] reqwest::Error), 62 | #[cfg(feature = "audit")] 63 | #[error(transparent)] 64 | Audit(#[from] audit::BackendCreationError), 65 | } 66 | 67 | impl Server { 68 | /// Create a new Server from a given configuration. 69 | pub fn from_config(config: ServerConfig) -> Self { 70 | let api_key_pool = Arc::new(KeyPool::new(config.api_keys.clone())); 71 | Self { 72 | config: Arc::new(config), 73 | api_key_pool, 74 | } 75 | } 76 | 77 | /// Start the server and listen for incoming connections. 78 | pub async fn serve(self) -> Result<(), ServerError> { 79 | event!(Level::INFO, "{:?}", self.config); 80 | let listener = TcpListener::bind(self.config.addr).await?; 81 | let client = reqwest::Client::builder() 82 | .user_agent(APP_USER_AGENT) 83 | .build()?; 84 | let handler = RequestHandler { 85 | key_pool: self.api_key_pool.clone(), 86 | client, 87 | config: Arc::new(self.config.openai.clone()), 88 | }; 89 | 90 | #[cfg(feature = "audit")] 91 | let handler = { 92 | let state = if let Some(ref audit_config) = self.config.audit { 93 | let backend = audit::Backend::create_with(audit_config).await?; 94 | Some((Arc::new(audit_config.clone()), backend)) 95 | } else { 96 | None 97 | }; 98 | handler 99 | .layer(from_fn_with_state(state.clone(), audit_tokens_layer)) 100 | .layer(from_fn_with_state(state, audit_access_layer)) 101 | }; 102 | 103 | #[cfg(feature = "acl")] 104 | let handler = handler.layer(from_fn_with_state( 105 | self.config.global_api_acl.clone().map(Arc::new), 106 | global_acl_layer, 107 | )); 108 | 109 | #[cfg(feature = "jwt-auth")] 110 | let handler = handler.layer(from_fn_with_state( 111 | self.config.jwt_auth.clone().map(Arc::new), 112 | jwt_auth_layer, 113 | )); 114 | 115 | axum::serve(listener, handler.into_service()).await?; 116 | Ok(()) 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /openai-hub-jwt-token-gen/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "openai-hub-jwt-token-gen" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | openai-hub-core = { path = "../openai-hub-core", features = ["jwt-auth"] } 8 | clap = { version = "4.4", features = ["derive"] } 9 | jwt = "0.16" 10 | tracing-subscriber = "0.3" 11 | hmac = "0.12" 12 | sha2 = "0.10" 13 | chrono = { version = "0.4", default-features = false, features = ["clock"] } 14 | -------------------------------------------------------------------------------- /openai-hub-jwt-token-gen/src/main.rs: -------------------------------------------------------------------------------- 1 | use chrono::{DateTime, Days, Months, Utc}; 2 | use clap::Parser; 3 | use jwt::{RegisteredClaims, SignWithKey}; 4 | use openai_hub_core::config::ServerConfig; 5 | use std::fs::read_to_string; 6 | use std::path::PathBuf; 7 | 8 | #[derive(Parser)] 9 | #[command(author, version, about, long_about = None)] 10 | struct Cli { 11 | #[arg(short, long, value_name = "SUB")] 12 | subject: Option, 13 | #[arg(short, long, value_name = "EXP")] 14 | expiration: Option, 15 | #[arg(short, long, value_name = "FILE")] 16 | config: Option, 17 | } 18 | 19 | fn main() { 20 | let cli = Cli::parse(); 21 | 22 | let config_path = cli.config.unwrap_or_else(|| "config.toml".parse().unwrap()); 23 | if !config_path.exists() { 24 | eprintln!("Config file not found"); 25 | std::process::exit(1); 26 | } 27 | let config = 28 | ServerConfig::load(&read_to_string(config_path).unwrap()).expect("cannot load config"); 29 | let jwt_config = config.jwt_auth.expect("cannot find jwt auth config"); 30 | 31 | let mut claims = RegisteredClaims::default(); 32 | let utc: DateTime = Utc::now(); 33 | 34 | if let Some(subject) = cli.subject { 35 | claims.subject = Some(subject); 36 | } 37 | if let Some(expiration) = cli.expiration { 38 | if !expiration.is_ascii() { 39 | eprintln!("Invalid expiration time"); 40 | std::process::exit(1); 41 | } 42 | let length: u32 = expiration 43 | .get(..expiration.len() - 1) 44 | .unwrap() 45 | .parse() 46 | .expect("invalid length"); 47 | let unit = expiration.get(expiration.len() - 1..).unwrap(); 48 | let exp = match unit { 49 | "d" => utc + Days::new(length as u64), 50 | "m" => utc + Months::new(length), 51 | "y" => utc + Months::new(length * 12), 52 | _ => { 53 | eprintln!("{} is not a valid unit", unit); 54 | std::process::exit(1); 55 | } 56 | }; 57 | claims.expiration = Some(exp.timestamp() as u64); 58 | } 59 | claims.issued_at = Some(utc.timestamp() as u64); 60 | 61 | let token_str = claims.sign_with_key(&jwt_config.key).unwrap(); 62 | println!("{}", token_str); 63 | } 64 | -------------------------------------------------------------------------------- /openai-hubd/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "openai-hubd" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | clap = { version = "4.4", features = ["derive"] } 8 | openai-hub-core = { path = "../openai-hub-core" } 9 | tokio = { version = "1", features = ["rt", "net", "macros", "rt-multi-thread"] } 10 | tracing-subscriber = { version = "0.3", features = ["env-filter"] } 11 | 12 | [features] 13 | default = ["acl", "jwt-auth", "access-log"] 14 | acl = ["openai-hub-core/acl"] 15 | jwt-auth = ["openai-hub-core/jwt-auth"] 16 | access-log = ["openai-hub-core/audit", "openai-hub-core/sqlite", "openai-hub-core/mysql", "openai-hub-core/postgres"] -------------------------------------------------------------------------------- /openai-hubd/src/main.rs: -------------------------------------------------------------------------------- 1 | use clap::Parser; 2 | use openai_hub_core::config::ServerConfig; 3 | use openai_hub_core::Server; 4 | use std::fs::read_to_string; 5 | use std::path::PathBuf; 6 | use tracing_subscriber::EnvFilter; 7 | 8 | #[cfg(feature = "acl")] 9 | use openai_hub_core::ApiAcl; 10 | 11 | #[derive(Parser)] 12 | #[command(author, version, about, long_about = None)] 13 | struct Cli { 14 | #[arg(short, long, value_name = "FILE")] 15 | config: Option, 16 | #[cfg(feature = "acl")] 17 | #[arg(short, long, value_name = "FILE")] 18 | acl: Option, 19 | } 20 | 21 | #[tokio::main] 22 | async fn main() -> Result<(), Box> { 23 | tracing_subscriber::fmt() 24 | .with_env_filter( 25 | EnvFilter::builder() 26 | .with_default_directive("openai_hub_core=debug".parse().unwrap()) 27 | .from_env_lossy(), 28 | ) 29 | .init(); 30 | 31 | let cli = Cli::parse(); 32 | let config_path = cli.config.unwrap_or_else(|| PathBuf::from("config.toml")); 33 | 34 | #[allow(unused_mut)] 35 | let mut config = ServerConfig::load(&read_to_string(config_path).unwrap())?; 36 | 37 | #[cfg(feature = "acl")] 38 | { 39 | let acl_path = cli.acl.unwrap_or_else(|| PathBuf::from("acl.toml")); 40 | if let Ok(acl) = read_to_string(acl_path) { 41 | config.set_global_api_acl(ApiAcl::load(&acl)?); 42 | } 43 | } 44 | 45 | Server::from_config(config).serve().await?; 46 | Ok(()) 47 | } 48 | --------------------------------------------------------------------------------