sim-lib-openai-server 0.1.2

OpenAI-compatible gateway skeleton for SIM.
Documentation
use serde_json::Value;
use sim_kernel::Expr;

use crate::{
    DeterministicGatewayClock, GatewayEvent, GatewayRequest, GatewayStore, MemoryGatewayStore,
    OpenAiPlanCache, PlanCacheKey, PlanCacheWriteTarget, RESPONSES_PATH, ResponseIdGenerators,
    ai_runner_cache_capability, execute_response_request_with_cache,
    openai_gateway_plan_capability,
};

#[test]
fn repeated_cache_plan_returns_hit_without_backend_branch() {
    let mut cx = super::cx();
    cx.grant(openai_gateway_plan_capability());
    let mut store = MemoryGatewayStore::new();
    let mut cache = OpenAiPlanCache::new();
    let mut ids = ResponseIdGenerators::deterministic(1);
    let mut clock = DeterministicGatewayClock::new(1_000, 10);
    let request =
        responses_request(r#"{"model":"cache(fixture/echo)","input":"cache me","store":true}"#);

    let first = execute_response_request_with_cache(
        &mut cx, &mut store, &mut cache, &mut ids, &mut clock, &request,
    );
    let second = execute_response_request_with_cache(
        &mut cx, &mut store, &mut cache, &mut ids, &mut clock, &request,
    );

    assert_eq!(first.response().status(), 200);
    assert_eq!(second.response().status(), 200);
    assert_eq!(response_json(second.response())["output_text"], "cache me");
    assert_eq!(cache.len(), 1);
    assert!(event_kind(first.events(), "branch-start"));
    assert!(!event_kind(first.events(), "cache-hit"));
    assert!(event_kind(second.events(), "cache-hit"));
    assert!(!event_kind(second.events(), "branch-start"));
    assert!(
        stored_events(&store, second.event_content_ids())
            .iter()
            .any(|event| event.kind().name.as_ref() == "cache-hit")
    );
}

#[test]
fn refresh_cache_mode_bypasses_hit_and_rewrites_entry() {
    let mut cx = super::cx();
    cx.grant(openai_gateway_plan_capability());
    let mut store = MemoryGatewayStore::new();
    let mut cache = OpenAiPlanCache::new();
    let mut ids = ResponseIdGenerators::deterministic(20);
    let mut clock = DeterministicGatewayClock::new(2_000, 10);
    let read_through =
        responses_request(r#"{"model":"cache(fixture/echo)","input":"refresh me","store":true}"#);
    let refresh = responses_request(
        r#"{"model":"cache(fixture/echo, mode: refresh)","input":"refresh me","store":true}"#,
    );

    let first = execute_response_request_with_cache(
        &mut cx,
        &mut store,
        &mut cache,
        &mut ids,
        &mut clock,
        &read_through,
    );
    let refreshed = execute_response_request_with_cache(
        &mut cx, &mut store, &mut cache, &mut ids, &mut clock, &refresh,
    );
    let hit = execute_response_request_with_cache(
        &mut cx,
        &mut store,
        &mut cache,
        &mut ids,
        &mut clock,
        &read_through,
    );

    assert_eq!(first.response().status(), 200);
    assert_eq!(refreshed.response().status(), 200);
    assert!(event_kind(refreshed.events(), "branch-start"));
    assert!(!event_kind(refreshed.events(), "cache-hit"));
    assert!(event_kind(hit.events(), "cache-hit"));
}

#[test]
fn persistent_plan_cache_writes_require_ai_runner_cache() {
    let mut cx = super::cx();
    let key = PlanCacheKey::for_request_plan(
        &Expr::String("request".to_owned()),
        &Expr::String("plan".to_owned()),
    )
    .unwrap();
    let response = Expr::String("cached response".to_owned());
    let mut cache = OpenAiPlanCache::new();

    let err = cache
        .put(
            &mut cx,
            PlanCacheWriteTarget::Persistent,
            key.clone(),
            response.clone(),
        )
        .unwrap_err();
    assert!(
        matches!(err, sim_kernel::Error::CapabilityDenied { capability } if capability == ai_runner_cache_capability())
    );

    cx.grant(ai_runner_cache_capability());
    cache
        .put(
            &mut cx,
            PlanCacheWriteTarget::Persistent,
            key.clone(),
            response.clone(),
        )
        .unwrap();
    assert_eq!(cache.get(&key), Some(&response));
}

fn responses_request(body: &str) -> GatewayRequest {
    GatewayRequest::new(
        "POST",
        RESPONSES_PATH,
        vec![("Content-Type".to_owned(), "application/json".to_owned())],
        body.as_bytes().to_vec(),
    )
}

fn response_json(response: &crate::GatewayResponse) -> Value {
    serde_json::from_slice(response.body()).unwrap()
}

fn stored_events(store: &MemoryGatewayStore, ids: &[sim_kernel::ContentId]) -> Vec<GatewayEvent> {
    ids.iter().map(|id| store.event(id).unwrap()).collect()
}

fn event_kind(events: &[GatewayEvent], kind: &str) -> bool {
    events
        .iter()
        .any(|event| event.kind().name.as_ref() == kind)
}