Skip to content

Add hydrate and persist endpoints. - #216

Open
Mohammad-nassar10 wants to merge 1 commit into
vllm-project:mainfrom
Mohammad-nassar10:feat/internal-hydrate-persist
Open

Add hydrate and persist endpoints.#216
Mohammad-nassar10 wants to merge 1 commit into
vllm-project:mainfrom
Mohammad-nassar10:feat/internal-hydrate-persist

Conversation

@Mohammad-nassar10

@Mohammad-nassar10 Mohammad-nassar10 commented Aug 27, 2026

Copy link
Copy Markdown

Summary

Exposes the two halves of a stateful Responses turn as separate endpoints, so an external orchestrator (the llm-d coordinator) can make the inference call itself:

  • POST /internal/hydrate: expands previous_response_id into a stateless upstream request, plus an opaque context.
  • POST /internal/persist: takes that context and the model's response (a JSON body, or the SSE frames a streaming caller relayed), stores the turn, and returns the envelope carrying the stored resp_ id.

Both compose existing in-process steps (rehydrate_conversation, payload_from_upstream, persist_if_needed). So split.rs adds no parsing, storage, or request building of its own. RequestContext is unchanged for in-process callers; it now serializes through a reduced wire form, SplitContext.

The endpoints ship as a new crate and binary, agentic-llm-d, depending on agentic-server-core alone.

Test Plan

cargo test - new split_execution_integration.rs covers the two-turn replay, the error paths, and the context round-trip. Existing suites pass unchanged.
Verified on a cluster with a multi-turn conversation.

Closes #215.

Signed-off-by: Mohammad <mohammad.nassar@ibm.com>
.route("/health", get(handler::health))
.route("/ready", get(handler::ready))
.route("/internal/hydrate", post(handler::internal_hydrate))
.route("/internal/persist", post(handler::internal_persist))

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

these routes have no workload authentication or object-level authorization. once this listener is reachable by the coordinator, any reachable workload with a known response ID can hydrate its full stored history or write a turn. we should authenticate both endpoints and scope every read and write to the authenticated tenant.

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SplitContext {
/// Response id reserved for this turn (`resp_` prefix).
pub response_id: String,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SplitContext is accepted as caller-authored authority: persist never proves it came from hydrate and directly trusts its response_id, original request, continuation link, and effective configuration. a caller can skip hydrate and forge durable records; an empty response_id also returns success because persistence silently skips the write. we should use a signed, expiring context or a one-time server-side handle, and reject malformed reserved IDs.

exec_ctx: &ExecutionContext,
) -> ExecutorResult<ResponsePayload> {
let ctx = RequestContext::from(context);
let payload = payload_from_upstream(&ctx, upstream)?;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

at the new HTTP persistence boundary, a response containing only {"id":"x"} defaults to Completed with empty output, while malformed output entries are silently dropped. this lets an invalid model response persist a completed turn with no valid output. we should deserialize a strict terminal response here and reject missing status, missing output, and malformed output items.

)));
}

persist_if_needed(

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this insert makes /internal/persist non-idempotent. if the transaction commits but the response is lost, retrying the exact request hits the same primary key and returns HTTP 500 even though the turn was stored. we should use the reserved response ID as an idempotency key: return the existing result for an identical retry and 409 for mismatched reuse.


// The in-process flow silently skips non-terminal statuses; here that would
// return an envelope whose id can never be continued, so reject it.
if !matches!(

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

response.failed is a terminal upstream result, but it becomes ResponseStatus::Error and is rejected here as InvalidRequest, so /internal/persist reports a model failure as HTTP 400. we should return the normalized failed response or a dedicated upstream-error status, and reserve 400 for malformed or non-terminal input.

let shutdown = CancellationToken::new();
let on_signal = shutdown.clone();
tokio::spawn(async move {
if tokio::signal::ctrl_c().await.is_ok() {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this only handles Ctrl-C, while Kubernetes terminates containers with SIGTERM. the default SIGTERM action bypasses the graceful-shutdown future and can cut off in-flight hydrate or persist requests. we should reuse the gateway's SIGTERM-aware shutdown path and bounded drain timeout.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add llm-d coordinator integration: hydrate and persist endpoints

2 participants