mirror of
https://github.com/outbackdingo/optimclaw.git
synced 2026-08-26 15:40:18 +00:00
Add OpenAI-compatible HTTP API (/v1/chat/completions, /v1/models) (#31)
* feat: add OpenAI-compatible HTTP API (/v1/chat/completions, /v1/models)
* - Reject model mismatches: validate req.model against the active model
and return 404 model_not_found instead of silently ignoring it
- Add x-ironclaw-streaming: simulated response header so clients know
streaming is not true token-by-token delivery
- Use SSE event type "error" for mid-stream LLM failures so clients can
distinguish errors from content chunks
- Mark docker-compose credentials as dev-only
- Add integration tests for model mismatch, streaming header, and body
size limit (axum's default 2MB)
* fix: address Copilot review feedback on OpenAI-compat API
- Wire chat_rate_limiter into /v1/chat/completions handler
- Execute LLM before starting SSE stream so failures return proper HTTP
errors instead of SSE error events
- Validate tool-role messages require tool_call_id and name fields
- Surface list_models() errors in models_handler via map_llm_error
- Reject unknown roles with 400 instead of defaulting to User
Co-Authored-By: Claude Opus 4.6 <[email protected]>
---------
Co-authored-by: firat.sertgoz <[email protected]>
Co-authored-by: Claude Opus 4.6 <[email protected]>
This commit is contained in:
co-authored by
firat.sertgoz
Claude Opus 4.6
parent
b3dee13954
commit
bbb68f7490
@@ -16,6 +16,7 @@
|
||||
|
||||
pub mod auth;
|
||||
pub mod log_layer;
|
||||
pub mod openai_compat;
|
||||
pub mod server;
|
||||
pub mod sse;
|
||||
pub mod types;
|
||||
@@ -81,6 +82,7 @@ impl GatewayChannel {
|
||||
user_id: config.user_id.clone(),
|
||||
shutdown_tx: tokio::sync::RwLock::new(None),
|
||||
ws_tracker: Some(Arc::new(ws::WsConnectionTracker::new())),
|
||||
llm_provider: None,
|
||||
chat_rate_limiter: server::RateLimiter::new(30, 60),
|
||||
});
|
||||
|
||||
@@ -107,6 +109,7 @@ impl GatewayChannel {
|
||||
user_id: self.state.user_id.clone(),
|
||||
shutdown_tx: tokio::sync::RwLock::new(None),
|
||||
ws_tracker: self.state.ws_tracker.clone(),
|
||||
llm_provider: self.state.llm_provider.clone(),
|
||||
chat_rate_limiter: server::RateLimiter::new(30, 60),
|
||||
};
|
||||
mutate(&mut new_state);
|
||||
@@ -171,6 +174,12 @@ impl GatewayChannel {
|
||||
self
|
||||
}
|
||||
|
||||
/// Inject the LLM provider for OpenAI-compatible API proxy.
|
||||
pub fn with_llm_provider(mut self, llm: Arc<dyn crate::llm::LlmProvider>) -> Self {
|
||||
self.rebuild_state(|s| s.llm_provider = Some(llm));
|
||||
self
|
||||
}
|
||||
|
||||
/// Get the auth token (for printing to console on startup).
|
||||
pub fn auth_token(&self) -> &str {
|
||||
&self.auth_token
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -137,6 +137,8 @@ pub struct GatewayState {
|
||||
pub shutdown_tx: tokio::sync::RwLock<Option<oneshot::Sender<()>>>,
|
||||
/// WebSocket connection tracker.
|
||||
pub ws_tracker: Option<Arc<crate::channels::web::ws::WsConnectionTracker>>,
|
||||
/// LLM provider for OpenAI-compatible API proxy.
|
||||
pub llm_provider: Option<Arc<dyn crate::llm::LlmProvider>>,
|
||||
/// Rate limiter for chat endpoints (30 messages per 60 seconds).
|
||||
pub chat_rate_limiter: RateLimiter,
|
||||
}
|
||||
@@ -235,6 +237,12 @@ pub async fn start_server(
|
||||
)
|
||||
// Gateway control plane
|
||||
.route("/api/gateway/status", get(gateway_status_handler))
|
||||
// OpenAI-compatible API
|
||||
.route(
|
||||
"/v1/chat/completions",
|
||||
post(super::openai_compat::chat_completions_handler),
|
||||
)
|
||||
.route("/v1/models", get(super::openai_compat::models_handler))
|
||||
.route_layer(middleware::from_fn_with_state(
|
||||
auth_state.clone(),
|
||||
auth_middleware,
|
||||
|
||||
@@ -485,6 +485,7 @@ mod tests {
|
||||
user_id: "test".to_string(),
|
||||
shutdown_tx: tokio::sync::RwLock::new(None),
|
||||
ws_tracker: Some(Arc::new(WsConnectionTracker::new())),
|
||||
llm_provider: None,
|
||||
chat_rate_limiter: crate::channels::web::server::RateLimiter::new(30, 60),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -997,6 +997,7 @@ async fn main() -> anyhow::Result<()> {
|
||||
if let Some(ref jm) = container_job_manager {
|
||||
gw = gw.with_job_manager(Arc::clone(jm));
|
||||
}
|
||||
gw = gw.with_llm_provider(Arc::clone(&llm));
|
||||
if config.sandbox.enabled {
|
||||
gw = gw.with_prompt_queue(Arc::clone(&prompt_queue));
|
||||
|
||||
|
||||
Reference in New Issue
Block a user