feat: server outline

This commit is contained in:
2026-09-26 17:29:30 -04:00
parent cf35271341
commit 9761a192c0
5 changed files with 98 additions and 74 deletions

1
Cargo.lock generated
View File

@@ -360,6 +360,7 @@ dependencies = [
"axum", "axum",
"axum-extra", "axum-extra",
"clap", "clap",
"headers",
"reqwest", "reqwest",
"schemars", "schemars",
"secrecy", "secrecy",

View File

@@ -7,6 +7,7 @@ edition = "2024"
axum = { version = "0.8.9", features = ["http2", "multipart", "ws"] } axum = { version = "0.8.9", features = ["http2", "multipart", "ws"] }
axum-extra = { version = "0.12.6", features = ["typed-header", "typed-routing"] } axum-extra = { version = "0.12.6", features = ["typed-header", "typed-routing"] }
clap = { version = "4.6.7", features = ["derive", "env"] } clap = { version = "4.6.7", features = ["derive", "env"] }
headers = "0.4.2"
reqwest = { version = "0.13.5", features = ["brotli", "cookies", "deflate", "form", "gzip", "json", "multipart", "query", "stream", "zstd"] } reqwest = { version = "0.13.5", features = ["brotli", "cookies", "deflate", "form", "gzip", "json", "multipart", "query", "stream", "zstd"] }
schemars = "1.2.2" schemars = "1.2.2"
secrecy = { version = "0.10.3", features = ["serde"] } secrecy = { version = "0.10.3", features = ["serde"] }

View File

@@ -1,11 +1,20 @@
use std::net::{IpAddr, SocketAddr};
use clap::Parser; use clap::Parser;
use secrecy::SecretString; use secrecy::SecretString;
use tracing_subscriber::{EnvFilter, fmt::format::FmtSpan}; use tracing_subscriber::{EnvFilter, fmt::format::FmtSpan};
mod openai; mod openai;
mod server;
#[derive(Parser, Debug)] #[derive(Parser, Debug)]
struct AppArgs { struct AppArgs {
#[arg(long, env)]
ip: IpAddr,
#[arg(long, env)]
port: u16,
#[arg(long)] #[arg(long)]
cloudflare_workers_api_key: Vec<SecretString>, cloudflare_workers_api_key: Vec<SecretString>,
@@ -48,12 +57,16 @@ async fn main() {
.with_env_filter(env_filter) .with_env_filter(env_filter)
.init(); .init();
tracing::debug!(?app_args, "using"); tracing::debug!(?app_args, "using");
let AppArgs {
cloudflare_workers_api_key, let addr = SocketAddr::new(app_args.ip, app_args.port);
google_ai_studio_api_key,
groq_api_key, let app = server::create_app(
hugging_face_api_key, server::AppState::builder()
mistral_api_key, .app_args(app_args.into())
openrouter_api_key, .build(),
} = app_args; );
let listener = tokio::net::TcpListener::bind(addr).await.expect("TODO");
axum::serve(listener, app).await.expect("TODO");
} }

View File

@@ -1,81 +1,44 @@
use std::marker::PhantomData; use schemars::Schema;
use serde::{Deserialize, Serialize};
use schemars::{JsonSchema, Schema, SchemaGenerator}; #[derive(Debug, Serialize, Deserialize)]
use serde::{Deserialize, Serialize, ser::SerializeMap as _}; pub struct Message {
use typed_builder::TypedBuilder;
use url::Url;
#[derive(Debug, TypedBuilder, Serialize, Deserialize)]
struct Message {
role: String, role: String,
content: String, content: String,
} }
struct StructuredOutputJson<S>(PhantomData<S>); #[derive(Debug, Deserialize, Serialize)]
pub struct ResponseFormatJsonSchema {
impl<S> Serialize for StructuredOutputJson<S> pub strict: bool,
where pub name: String,
S: JsonSchema, pub schema: Schema,
{
fn serialize<Serializer>(
&self,
serializer: Serializer,
) -> Result<Serializer::Ok, Serializer::Error>
where
Serializer: serde::Serializer,
{
let mut map = serializer.serialize_map(Some(3))?;
map.serialize_entry("strict", &true)?;
map.serialize_entry("name", &S::schema_name())?;
map.serialize_entry(
"schema",
&SchemaGenerator::default().into_root_schema_for::<S>(),
)?;
map.end()
}
} }
#[derive(Debug)] #[derive(Debug, Deserialize, Serialize)]
enum ResponseFormat<OutputFormat> { #[serde(tag = "type")]
Json { schema: PhantomData<OutputFormat> }, pub enum ResponseFormat {
JsonSchema {
json_schema: ResponseFormatJsonSchema,
},
} }
impl<OutputFormat> Serialize for ResponseFormat<OutputFormat> #[derive(Debug, Serialize, Deserialize)]
where pub struct ChatCompletionsRequest<Model> {
OutputFormat: JsonSchema, pub model: Model,
{
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> pub messages: Vec<Message>,
where
S: serde::Serializer, pub response_format: ResponseFormat,
{
let mut map = serializer.serialize_map(Some(2))?;
map.serialize_entry("type", "json_schema")?;
map.serialize_entry(
"json_schema",
&StructuredOutputJson::<OutputFormat>(Default::default()),
)?;
map.end()
}
} }
#[derive(Debug, TypedBuilder, Serialize)] #[derive(Debug, Serialize, Deserialize)]
#[serde(bound(serialize = "ResponseFormat<OutputSchema>: Serialize"))] pub struct Choice {
struct ChatCompletionsRequest<OutputSchema> { pub finish_reason: String, // I don't care
model: String,
messages: Vec<Message>, pub message: Message,
response_format: ResponseFormat<OutputSchema>,
} }
#[derive(Debug, Deserialize)] #[derive(Debug, Serialize, Deserialize)]
struct Choice { pub struct ChatCompletionsResponse {
finish_reason: String, // I don't care pub choices: Vec<Choice>,
message: Message,
}
#[derive(Debug, Deserialize)]
struct ChatCompletionsResponse {
choices: Vec<Choice>,
} }

46
src/server.rs Normal file
View File

@@ -0,0 +1,46 @@
use std::sync::Arc;
use crate::AppArgs;
use crate::openai;
use axum::Json;
use axum::Router;
use axum::extract::State;
use axum_extra::routing::RouterExt;
use axum_extra::routing::TypedPath;
use serde::Deserialize;
use serde::Serialize;
use typed_builder::TypedBuilder;
#[derive(Debug, Clone, TypedBuilder)]
pub struct AppState {
app_args: Arc<AppArgs>,
}
#[derive(Deserialize, TypedPath)]
#[typed_path("/v1/chat/completions")]
struct V1ChatCompletions;
#[derive(Debug, Serialize, Deserialize)]
enum Shortcut {
Xs,
S,
M,
L,
XL
}
async fn post_v1_chat_completions(
_: V1ChatCompletions,
State(state): State<AppState>,
Json(request): Json<openai::ChatCompletionsRequest<Shortcut>>,
) -> Result<Json<openai::ChatCompletionsResponse>, ()> {
todo!();
}
pub fn create_router() -> Router<AppState> {
Router::new().typed_post(post_v1_chat_completions)
}
pub fn create_app(state: AppState) -> Router<()> {
create_router().with_state(state)
}