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.
Overview
Section titled “Overview”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 usualInternally 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.
Step 1 — Choose a credential
Section titled “Step 1 — Choose a credential”Every call needs an LLM provider API key. Two options, resolved in this order:
- Inline — pass
api_keyin the request body. Never stored. - Stored — omit
api_key; the server looks up a credential saved for the org viaPOST /orgs/{org_id}/llm-credentials.
# 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.
Step 2 — Call /analyze
Section titled “Step 2 — Call /analyze”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 @-| Field | Required | Description |
|---|---|---|
llm_backend | Yes | anthropic, mistral, or openai |
model | Yes | Provider-specific model name, e.g. claude-sonnet-5 |
api_key | No | Inline credential — omit to use a stored one |
raw_input | Yes | Same raw text you’d send to .../ingest |
rule_type | No | Rule 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}Step 3b — Larger input: async job
Section titled “Step 3b — Larger input: async job”Above the threshold, the same call returns immediately with a job to poll:
{"job_id": "b7e2c1a4-...", "status": "queued"}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 queued → running → done | failed. Once done, fetch the report the normal way:
curl "http://localhost:8000/prod-deploy-42/report?raw=true"Billing / quota
Section titled “Billing / quota”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
/analyzecall that would blow through the cap partway through stops early instead of failing outright: the response/job comes back withquota_exceeded: trueand a partial report covering whatever was analysed before the budget ran out.
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.
Error reference
Section titled “Error reference”| Status | Meaning |
|---|---|
200 | Analysis complete (sync) — full report |
202 | Job queued (async) — poll GET /jobs/{job_id} |
400 | No api_key given and none stored for this org/provider |
402 | Org’s monthly LLM budget already exceeded — checked before any work starts |
404 | Unknown analyzer, or analyzer disabled for this org |
413 | Input exceeds server.max_input_size_mb |
501 | Analyzer doesn’t support ingestion (e.g. a placeholder analyzer) |
502 | LLM provider returned output that couldn’t be parsed (sync path only — async failures land on the job doc instead) |
Server-side vs. client-led — which one?
Section titled “Server-side vs. client-led — which one?”Server-side /analyze | Client-led loop | |
|---|---|---|
| Calls needed | 1 (+ optional job poll) | 1 per unit, plus ingest/report |
| Who calls the LLM | The server | Your client/agent |
| Prompt control | Fixed system prompt (rules + generic instructions) | Full control |
| Best for | Scripts, internal tooling, the GitHub App | Custom 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.