Skip to content

Server-Side Analysis

The LLM Agent Workflow has the client drive the ingest → loop → submit cycle, calling the LLM itself between each step. POST /{analyzer}/{namespace}/analyze moves that entire cycle onto the server: hand it a raw input, an LLM backend, and a model, and it ingests, analyses every unit, and hands back a report — one call in, a report out. The GitHub App integration uses this same path internally.

Use this when you don’t need fine-grained control over the LLM call itself (custom prompting, a non-tool-calling client, an internal script) and just want the report.


POST /{analyzer}/{namespace}/analyze
body: {llm_backend, model, api_key?, raw_input, rule_type?}
small input (<= server.sync_analysis_max_units units)
-> 200 {report_markdown, resource_count, quota_exceeded}
larger input
-> 202 {job_id, status: "queued"}
-> poll GET /jobs/{job_id} until status is "done" or "failed"
-> then GET /{namespace}/report as usual

Internally this reuses exactly the same ingest()/get_rules() calls the client-led path uses, so a namespace partially analysed via PUT /resource/{hash}/analysis and then finished via /analyze (or vice versa) works correctly — already-analysed units are skipped either way.


Every call needs an LLM provider API key. Two options, resolved in this order:

  1. Inline — pass api_key in the request body. Never stored.
  2. Stored — omit api_key; the server looks up a credential saved for the org via POST /orgs/{org_id}/llm-credentials.
Terminal window
# Store a credential once (org-admin)
curl -X POST http://localhost:8000/orgs/default/llm-credentials \
-H "X-API-Key: <admin-key>" -H "Content-Type: application/json" \
-d '{"provider": "anthropic", "api_key": "sk-ant-..."}'

Stored credentials are encrypted at rest (Fernet) and never returned by the API once saved — only GET /orgs/{org_id}/llm-credentials lists which providers are configured, never the key itself.


Terminal window
jq -n \
--arg raw_input "$(cat terraform_plan.txt)" \
'{llm_backend: "anthropic", model: "claude-sonnet-5", raw_input: $raw_input, rule_type: "aws"}' \
| curl -s -X POST http://localhost:8000/terraform-plan/prod-deploy-42/analyze \
-H "X-API-Key: <your-key>" -H "Content-Type: application/json" -d @-
FieldRequiredDescription
llm_backendYesanthropic, mistral, or openai
modelYesProvider-specific model name, e.g. claude-sonnet-5
api_keyNoInline credential — omit to use a stored one
raw_inputYesSame raw text you’d send to .../ingest
rule_typeNoRule sub-category filter, e.g. aws

Step 3a — Small input: synchronous response

Section titled “Step 3a — Small input: synchronous response”

At or below server.sync_analysis_max_units (default 5) ingested units, the call blocks until analysis finishes and returns the report directly:

{
"report_markdown": "# 🤖 Analysis Report\n...",
"resource_count": 3,
"quota_exceeded": false
}

Above the threshold, the same call returns immediately with a job to poll:

{"job_id": "b7e2c1a4-...", "status": "queued"}
Terminal window
curl -s http://localhost:8000/jobs/b7e2c1a4-... -H "X-API-Key: <your-key>"
{
"job_id": "b7e2c1a4-...",
"status": "done",
"mode": "async",
"trigger": "api",
"resource_count": 14,
"quota_exceeded": false,
"error": null
}

status transitions queuedrunningdone | failed. Once done, fetch the report the normal way:

Terminal window
curl "http://localhost:8000/prod-deploy-42/report?raw=true"

If the org has a monthly LLM spend cap set (PATCH /orgs/{org_id}/quota), it’s enforced two ways:

  • Pre-flight — if the cap is already met/exceeded, the call is rejected outright with 402, before any LLM call is made.
  • Mid-run — a single large /analyze call that would blow through the cap partway through stops early instead of failing outright: the response/job comes back with quota_exceeded: true and a partial report covering whatever was analysed before the budget ran out.
Terminal window
curl -X PATCH http://localhost:8000/orgs/default/quota \
-H "X-API-Key: <admin-key>" -H "Content-Type: application/json" \
-d '{"monthly_budget_usd": 50.0}'
curl http://localhost:8000/orgs/default/usage -H "X-API-Key: <admin-key>"
# {"period": "2026-07", "total_cost_usd": 12.34, "budget_usd": 50.0, "remaining_usd": 37.66, "event_count": 84}

See Billing / quota endpoints.


StatusMeaning
200Analysis complete (sync) — full report
202Job queued (async) — poll GET /jobs/{job_id}
400No api_key given and none stored for this org/provider
402Org’s monthly LLM budget already exceeded — checked before any work starts
404Unknown analyzer, or analyzer disabled for this org
413Input exceeds server.max_input_size_mb
501Analyzer doesn’t support ingestion (e.g. a placeholder analyzer)
502LLM provider returned output that couldn’t be parsed (sync path only — async failures land on the job doc instead)

Server-side /analyzeClient-led loop
Calls needed1 (+ optional job poll)1 per unit, plus ingest/report
Who calls the LLMThe serverYour client/agent
Prompt controlFixed system prompt (rules + generic instructions)Full control
Best forScripts, internal tooling, the GitHub AppCustom agents, MCP clients, non-standard LLM providers

Both paths write to the exact same data model and are fully interoperable within one namespace — see Architecture — Server-side analysis.