met 1.0.0-beta-preview

Metrale Engine: pure Rust LLM inference server (HTTP API, scheduler, TUI, CLI).
// SPDX-License-Identifier: MIT OR Apache-2.0

//! 2026-09-26: Stored-object endpoints: `GET /v1/chat/completions/{id}` and the Responses
//! API retrieve, delete and `input_items` handlers, plus the assistant transcript turn
//! that Responses storage appends.
//!
//! Owner: server API.
//! Invariants:
//! - Each handler reads or deletes only entries of its own kind: `ChatCompletion` for the
//!   chat completion, `Response` for the Responses handlers.

#![allow(unused_imports, dead_code)]

use crate::main_modules::model_host::ProcessState;
use axum::extract::State;
use axum::extract::rejection::JsonRejection;
use axum::http::StatusCode;
use axum::response::sse::{Event, KeepAlive};
use axum::response::{IntoResponse, Json, Response, Sse};
use futures::StreamExt;
use std::sync::Arc;
use tokio_stream::wrappers::ReceiverStream;

use super::chat_stream::run_chat_stream;
use super::responses_stream::responses_endpoint_stream;
use super::responses_translate::{
    build_responses_usage, emit, find_frame_end, translate_chat_response_to_responses,
};
use crate::AppState;
use crate::openai::{
    ChatCompletionChunk, ChatCompletionRequest, ChatCompletionResponse, CompletionChunk,
    CompletionRequest, CompletionResponse, ModelInfo, ModelListResponse, Usage,
};
use crate::tool_parser;

use super::chat::chat_completions_inner;
use super::compact::{compact_messages, openai_error_response, openai_error_response_with_param};
use super::completions::not_supported;
use super::inference_impl::{extract_thinking, strip_stop_sequences, tokenize_stop_sequences};
use super::inference_types::{
    GrammarSpec, InferenceRequest, InferenceResponse, StreamEvent, TokenLogprobs,
};
use super::sanitizer::{
    F7_STALL_REFUSE_THRESHOLD, F7_STALL_WARN_THRESHOLD, F7StallBuckets, ToolKind, classify_tool,
    extract_bash_final_action, primary_arg_for_tool, sanitize_content_chunk,
};
use super::strip::strip_thinking_tags;

use super::inference_types::*;
use super::sanitizer::*;
/// 2026-09-26: The assistant transcript turn for the first choice of `chat`, which
/// Responses storage appends so a `previous_response_id` resume sees it. `None` when
/// `chat` has no choices.
pub(super) fn assistant_incoming_from_ir(
    chat: &crate::ir::ChatResponse,
) -> Option<crate::openai::IncomingMessage> {
    let choice = chat.choices.first()?;
    let tool_calls: Vec<tool_parser::IncomingToolCall> = choice
        .tool_calls
        .iter()
        .map(|tc| tool_parser::IncomingToolCall {
            id: Some(tc.id.clone()),
            function: tool_parser::IncomingFunction {
                name: tc.name.clone(),
                arguments: tc.arguments.to_string(),
            },
        })
        .collect();
    Some(crate::openai::IncomingMessage {
        role: "assistant".to_string(),
        content: crate::openai::ParsedContent::text_only(
            choice.content.clone().unwrap_or_default(),
        ),
        tool_calls: if tool_calls.is_empty() {
            None
        } else {
            Some(tool_calls)
        },
        tool_call_id: None,
        name: None,
        reasoning_content: choice.reasoning.clone(),
    })
}

/// 2026-09-26: `GET /v1/chat/completions/{id}`: the body a `store: true` chat completion
/// saved, as stored, or 404.
pub async fn get_stored_completion(
    ProcessState(process): ProcessState,
    axum::extract::Path(id): axum::extract::Path<String>,
) -> Response {
    match process
        .response_store
        .get(&id, crate::response_store::StoredKind::ChatCompletion)
    {
        Some(entry) => Json(entry.body).into_response(),
        None => openai_error_response(
            StatusCode::NOT_FOUND,
            format!(
                "Completion '{id}' not found. It may have expired, or was never stored (set `store: true` on the request to enable storage)."
            ),
        ),
    }
}

// 2026-09-26: Responses API handlers. They use the same store as
// `/v1/chat/completions/{id}` and look up `StoredKind::Response` only.

/// 2026-09-26: `GET /v1/responses/{id}`: the stored Response body, or 404.
pub async fn get_stored_response(
    ProcessState(process): ProcessState,
    axum::extract::Path(id): axum::extract::Path<String>,
) -> Response {
    match process
        .response_store
        .get(&id, crate::response_store::StoredKind::Response)
    {
        Some(entry) => Json(entry.body).into_response(),
        None => openai_error_response_with_param(
            StatusCode::NOT_FOUND,
            format!("Response '{id}' not found. It may have expired or was not stored."),
            Some("id"),
            Some("response_not_found"),
        ),
    }
}

/// 2026-09-26: `DELETE /v1/responses/{id}`: `{id, object:"response.deleted", deleted:true}`
/// on a hit, 404 on a miss. The entry leaves the in-memory map at once; with a disk
/// backend (`METRALE_STORE_DIR`) its file removal is queued, or done inline when the
/// backend's queue is full.
pub async fn delete_stored_response(
    ProcessState(process): ProcessState,
    axum::extract::Path(id): axum::extract::Path<String>,
) -> Response {
    let existed = process
        .response_store
        .delete(&id, crate::response_store::StoredKind::Response);
    if existed {
        Json(serde_json::json!({
            "id": id,
            "object": "response.deleted",
            "deleted": true,
        }))
        .into_response()
    } else {
        openai_error_response_with_param(
            StatusCode::NOT_FOUND,
            format!("Response '{id}' not found."),
            Some("id"),
            Some("response_not_found"),
        )
    }
}

/// 2026-09-26: `GET /v1/responses/{id}/input_items`: the stored transcript without its
/// final assistant turn, as `message` items with ids `item_{id}_{i}`. Query parameters:
/// `order` (`asc` by default, or `desc`) and `limit` (20 by default, at most 100). Other
/// parameters, including `after` and `before`, are ignored, and `has_more` is always
/// false.
pub async fn list_response_input_items(
    ProcessState(process): ProcessState,
    axum::extract::Path(id): axum::extract::Path<String>,
    axum::extract::Query(q): axum::extract::Query<std::collections::HashMap<String, String>>,
) -> Response {
    let Some(entry) = process
        .response_store
        .get(&id, crate::response_store::StoredKind::Response)
    else {
        return openai_error_response_with_param(
            StatusCode::NOT_FOUND,
            format!("Response '{id}' not found."),
            Some("id"),
            Some("response_not_found"),
        );
    };

    // 2026-09-26: Drop the trailing assistant turn: `input_items` lists what the caller
    // sent. The store keeps that turn for `previous_response_id` resumes.
    let mut msgs: Vec<crate::openai::IncomingMessage> = entry.messages;
    if msgs.last().map(|m| m.role == "assistant").unwrap_or(false) {
        msgs.pop();
    }

    let mut items: Vec<serde_json::Value> = msgs
        .iter()
        .enumerate()
        .map(|(i, m)| {
            let content = if !m.content.has_images() {
                serde_json::json!([{ "type": "input_text", "text": m.content.text }])
            } else {
                let mut parts: Vec<serde_json::Value> = Vec::new();
                if !m.content.text.is_empty() {
                    parts.push(serde_json::json!({ "type": "input_text", "text": m.content.text }));
                }
                for img in m.content.images() {
                    parts.push(serde_json::json!({
                        "type": "input_image",
                        "image_url": img,
                    }));
                }
                serde_json::Value::Array(parts)
            };
            serde_json::json!({
                "id": format!("item_{id}_{i}"),
                "type": "message",
                "role": m.role,
                "content": content,
            })
        })
        .collect();

    let order = q.get("order").map(|s| s.as_str()).unwrap_or("asc");
    if order == "desc" {
        items.reverse();
    }
    let limit: usize = q
        .get("limit")
        .and_then(|s| s.parse().ok())
        .unwrap_or(20)
        .min(100);
    if items.len() > limit {
        items.truncate(limit);
    }

    let first_id = items
        .first()
        .and_then(|v| v.get("id").and_then(|v| v.as_str()))
        .unwrap_or("")
        .to_string();
    let last_id = items
        .last()
        .and_then(|v| v.get("id").and_then(|v| v.as_str()))
        .unwrap_or("")
        .to_string();
    Json(serde_json::json!({
        "object": "list",
        "data": items,
        "first_id": first_id,
        "last_id": last_id,
        "has_more": false,
    }))
    .into_response()
}