Files
cortiq-gateway/src/protocols/native_passthrough.rs
T
infosave2007andClaude Opus 4.8 e015555dff
Release / crates-io (push) Canceled after 0s
Release / release (macos-14, aarch64-apple-darwin) (push) Canceled after 0s
Release / release (ubuntu-latest, x86_64-unknown-linux-gnu) (push) Canceled after 0s
Release / release (windows-latest, x86_64-pc-windows-msvc) (push) Canceled after 0s
feat: finish remaining inbound protocols (models, completions, native /route)
- GET /v1/models: pool + virtual cortiq-auto
- POST /v1/completions (legacy): non-stream + streaming text_completion
- POST /route: native passthrough returning the routing decision
- all inbound protocol adapters now implemented; admin meta + README/CHANGELOG updated
- bump to 0.2.1

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-30 17:42:45 +03:00

87 lines
2.7 KiB
Rust

//! Native passthrough adapter: `POST /route`.
//! Returns the gateway's routing decision (task type, complexity, candidate models)
//! without calling a model — direct access for clients that route themselves.
use crate::error::{GatewayError, Result};
use crate::state::SharedState;
use axum::{extract::State, response::IntoResponse, routing::post, Json, Router};
use serde::Deserialize;
pub fn routes() -> Router<SharedState> {
Router::new().route("/route", post(handler))
}
#[derive(Deserialize)]
struct RouteRequest {
/// `{ "input": { "text": "..." } }` (router-style) — optional.
#[serde(default)]
input: serde_json::Value,
/// `{ "text": "..." }` — convenience shorthand.
#[serde(default)]
text: Option<String>,
#[serde(default)]
profile: Option<String>,
}
async fn handler(
State(state): State<SharedState>,
Json(req): Json<RouteRequest>,
) -> Result<impl IntoResponse> {
let live = state.live();
if !live.cfg.protocols.native_passthrough {
return Err(GatewayError::InvalidRequest(
"native_passthrough protocol is disabled".into(),
));
}
let text = req
.text
.clone()
.or_else(|| req.input["text"].as_str().map(|s| s.to_string()))
.or_else(|| req.input.as_str().map(|s| s.to_string()))
.unwrap_or_default();
if text.is_empty() {
return Err(GatewayError::InvalidRequest(
"missing input text (use {\"text\": ...} or {\"input\": {\"text\": ...}})".into(),
));
}
let profile = req
.profile
.clone()
.unwrap_or_else(|| live.cfg.route.profile.clone());
let (decision, source, candidates) = match live.router.route(&text, &profile).await {
Ok(Some(d)) => {
let cands = live.routing.candidates(&d.complexity_tier);
(Some(d), "router", cands)
}
_ => (
None,
"fallback",
vec![live.routing.default_model().to_string()],
),
};
let selected = candidates.first().cloned().unwrap_or_default();
let decision_json = match &decision {
Some(d) => serde_json::json!({
"task_label": d.task_label,
"complexity": { "score": d.complexity_score, "tier": d.complexity_tier },
"router_request_id": d.router_request_id,
}),
None => serde_json::json!({
"task_label": "degraded",
"complexity": { "score": 0.5, "tier": "degraded" },
"router_request_id": serde_json::Value::Null,
}),
};
Ok(Json(serde_json::json!({
"decision": decision_json,
"source": source,
"candidates": candidates,
"selected_model": selected,
})))
}