Skip to content
10 changes: 10 additions & 0 deletions backend/app/api/docs/llm/get_llm_call.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
Retrieve the status and results of an LLM call job by job ID.

This endpoint allows you to poll for the status and results of an asynchronous LLM call job that was previously initiated via the POST `/llm/call` endpoint.


### Notes

- This endpoint returns both the job status AND the actual LLM response when complete
- LLM responses are also delivered asynchronously via the callback URL (if provided)
- Jobs can be queried at any time after creation
99 changes: 92 additions & 7 deletions backend/app/api/routes/llm.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,20 @@
import logging
from uuid import UUID

from fastapi import APIRouter, Depends

from app.api.deps import AuthContextDep, SessionDep
from app.api.permissions import Permission, require_permission
from app.models import LLMCallRequest, LLMCallResponse, Message
from app.core.exception_handlers import HTTPException
from app.crud.jobs import JobCrud
from app.crud.llm import get_llm_calls_by_job_id
from app.models import (
LLMCallRequest,
LLMCallResponse,
LLMJobImmediatePublic,
LLMJobPublic,
)
from app.models.llm.response import LLMResponse, Usage
from app.services.llm.jobs import start_job
from app.utils import APIResponse, validate_callback_url, load_description

Expand Down Expand Up @@ -34,7 +44,7 @@ def llm_callback_notification(body: APIResponse[LLMCallResponse]):
@router.post(
"/llm/call",
description=load_description("llm/llm_call.md"),
response_model=APIResponse[Message],
response_model=APIResponse[LLMJobImmediatePublic],
callbacks=llm_callback_router.routes,
dependencies=[Depends(require_permission(Permission.REQUIRE_PROJECT))],
)
Expand All @@ -43,22 +53,97 @@ def llm_call(
):
"""
Endpoint to initiate an LLM call as a background job.
Returns job information for polling.
"""
project_id = _current_user.project_.id
organization_id = _current_user.organization_.id

if request.callback_url:
validate_callback_url(str(request.callback_url))

start_job(
job_id = start_job(
db=session,
request=request,
project_id=project_id,
organization_id=organization_id,
)

return APIResponse.success_response(
data=Message(
message=f"Your response is being generated and will be delivered via callback."
),
# Fetch job details to return immediate response
job_crud = JobCrud(session=session)
job = job_crud.get(job_id=job_id)

if not job:
raise HTTPException(status_code=404, detail="Job not found")

if request.callback_url:
message = "Your response is being generated and will be delivered via callback."
else:
message = "Your response is being generated"

job_response = LLMJobImmediatePublic(
job_id=job.id,
status=job.status.value,
message=message,
job_inserted_at=job.created_at,
job_updated_at=job.updated_at,
)

return APIResponse.success_response(data=job_response)


@router.get(
"/llm/call/{job_id}",
description=load_description("llm/get_llm_call.md"),
response_model=APIResponse[LLMJobPublic],
dependencies=[Depends(require_permission(Permission.REQUIRE_PROJECT))],
)
def get_llm_call_status(
_current_user: AuthContextDep,
session: SessionDep,
job_id: UUID,
):
"""
Poll for LLM call job status and results.
Returns job information with nested LLM response when complete.
"""
job_crud = JobCrud(session=session)
job = job_crud.get(job_id=job_id)

if not job:
raise HTTPException(status_code=404, detail="Job not found")

llm_call_response = None
if job.status.value == "SUCCESS":
llm_calls = get_llm_calls_by_job_id(session=session, job_id=job_id)

if llm_calls:
# Get the first LLM call from the list which will be the only call for the job id
# since we initially won't be using this endpoint for llm chains
llm_call = llm_calls[0]

llm_response = LLMResponse(
provider_response_id=llm_call.provider_response_id or "",
conversation_id=llm_call.conversation_id,
provider=llm_call.provider,
model=llm_call.model,
output=llm_call.content,
)

usage = Usage(**llm_call.usage) if llm_call.usage else None

llm_call_response = LLMCallResponse(
response=llm_response,
usage=usage,
provider_raw_response=None,
)

job_response = LLMJobPublic(
job_id=job.id,
status=job.status.value,
llm_response=llm_call_response,
error_message=job.error_message,
job_inserted_at=job.created_at,
job_updated_at=job.updated_at,
)

return APIResponse.success_response(data=job_response)
3 changes: 3 additions & 0 deletions backend/app/models/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,9 @@
LLMChainRequest,
LLMChainResponse,
LlmChain,
LLMJobBasePublic,
LLMJobImmediatePublic,
LLMJobPublic,
)

from .message import Message
Expand Down
3 changes: 3 additions & 0 deletions backend/app/models/llm/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,4 +30,7 @@
AudioOutput,
LLMChainResponse,
IntermediateChainResponse,
LLMJobBasePublic,
LLMJobImmediatePublic,
LLMJobPublic,
)
26 changes: 26 additions & 0 deletions backend/app/models/llm/response.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@

This module contains structured response models for LLM API calls.
"""
from datetime import datetime
from uuid import UUID

from sqlmodel import SQLModel, Field
from typing import Literal, Annotated
from app.models.llm.request import AudioContent, TextContent
Expand Down Expand Up @@ -100,3 +103,26 @@ class IntermediateChainResponse(SQLModel):
default=None,
description="Unmodified raw response from the LLM provider from the current block",
)


# Job response models
class LLMJobBasePublic(SQLModel):
"""Base response model for LLM job information."""

job_id: UUID
status: str # JobStatus from job.py


class LLMJobImmediatePublic(LLMJobBasePublic):
"""Immediate response after creating an LLM job."""

message: str
job_inserted_at: datetime
job_updated_at: datetime


class LLMJobPublic(LLMJobBasePublic):
"""Full job response with nested LLM response when complete."""

llm_response: LLMCallResponse | None = None
error_message: str | None = None
Loading