Skip to content

LLM interpretations (multilingual, tiered)

Natural-language interpretations built on top of the deterministic Vedic engine. The LLM never sees a chart "raw" — it receives a structured JSON context (Complete Factor, Theme Probability, transit contacts, period modifiers) and is instructed to ground every claim in those numbers.

Two axes of control:

  • language — one of ten ISO-639-1 codes; the model produces the response in the matching language and native script.
  • tierbasic / standard / premium. Concrete models are a deployment decision; the API never reveals them.

Languages

Code Language Script
en English Latin
hi Hindi (हिन्दी) Devanagari
ta Tamil (தமிழ்) Tamil
te Telugu (తెలుగు) Telugu
kn Kannada (ಕನ್ನಡ) Kannada
ml Malayalam (മലയാളം) Malayalam
mr Marathi (मराठी) Devanagari
bn Bengali (বাংলা) Bengali
gu Gujarati (ગુજરાતી) Gujarati
es Spanish (Español) Latin

Tiers

Tier Voice Output budget Typical use
basic Concise — 3-5 short paragraphs ~700 tokens Quick chat replies, push notifications
standard Substantive — 6-10 paragraphs, every non-obvious claim grounded ~1500 tokens Default UI experience
premium Nuanced senior-Jyotisha voice — 12-20 paragraphs cross-referencing the engine ~3000 tokens Premium reports, depth analysis

Pricing per interpretation scales roughly 1× / 8× / 50× across the three tiers; pick by use-case.

POST /v1/llm/theme/{theme}

Per-life-area interpretation grounded in a Complete Factor + Theme Probability.

Param Type Notes
chart_id string An existing chart from POST /v1/charts.
language enum One of the 10 language codes.
tier enum basic / standard / premium. Default: standard.
at ISO datetime Optional — fold transit context into the interpretation.

The path {theme} is one of the 133 classical significators — career, marriage_for_man, wealth, mother, longevity, ...

curl -X POST -H "Authorization: Bearer $TOKEN" \
  "https://api.astrolinkers.com/v1/llm/theme/career\
?chart_id=$CHART&language=en&tier=standard&at=2026-11-15T12:00:00Z"
{
  "interpretation_type": "theme",
  "language": "en",
  "tier": "standard",
  "content": "Your career theme is centred on a strong Saturn karaka in the 10th house...\n\n...",
  "engine_context": {
    "theme": "career",
    "complete_factor": { /* karaka, essentials, arudhas */ },
    "probability": { /* 7 sources + failed conditions */ },
    "top_transit_contacts": [ /* 5 strongest */ ]
  },
  "input_tokens": 920,
  "output_tokens": 1480,
  "latency_ms": 4200
}

POST /v1/llm/chart-reading

Full-chart synthesis — meta-factors (planets driving multiple themes), top yogas, corrected planet natures, thematic snapshot of the five headline life areas.

curl -X POST -H "Authorization: Bearer $TOKEN" \
  "https://api.astrolinkers.com/v1/llm/chart-reading\
?chart_id=$CHART&language=hi&tier=premium"

Premium tier in Hindi will produce a multi-paragraph reading in Devanagari with cross-references to the engine's findings.

POST /v1/llm/dasha-forecast

Per-moment forecast for the current MD/AD/PD chain. Folds in:

  • Period modifiers (which planets are amplified by the current dasha lords).
  • Transit modifiers at at (SAV + Bhinnashtaka weighted).
curl -X POST -H "Authorization: Bearer $TOKEN" \
  "https://api.astrolinkers.com/v1/llm/dasha-forecast\
?chart_id=$CHART&language=en&at=2027-03-01T12:00:00Z&tier=premium"

POST /v1/llm/muhurta-reasoning

Electional reasoning — given a date window, the LLM explains the top muhurta candidates ranked by tara + day-lord.

curl -X POST -H "Authorization: Bearer $TOKEN" \
  "https://api.astrolinkers.com/v1/llm/muhurta-reasoning\
?chart_id=$CHART&language=ta&tier=standard\
&window_start=2026-12-01T00:00:00Z&window_end=2026-12-07T23:59:00Z\
&interval_minutes=180&top_n=5"

Returns the top candidates with a Tamil-language explanation of why each moment ranks where it does.

Response shape (all four endpoints)

{
  "interpretation_type": "theme | chart_reading | dasha_forecast | muhurta",
  "language": "en",
  "tier": "standard",
  "content": "Natural-language interpretation text...",
  "engine_context": { /* the structured engine output the LLM was given */ },
  "input_tokens": 920,
  "output_tokens": 1480,
  "latency_ms": 4200
}

engine_context is identical to what the LLM saw. Use it for auditing: every claim in content should be traceable to a value in engine_context.

Rate limits per tier

Every mutating /v1/llm/... endpoint (sync + stream) is protected by a per-tenant token bucket. Each tier has its own bucket: exhausting your basic quota does not block premium calls, and one customer's traffic never affects another's.

Tier Burst (capacity) Sustained refill Effective ceiling
basic 30 0.5 req/s ~1 800 req/h
standard 10 0.1 req/s ~360 req/h
premium 5 0.02 req/s ~72 req/h

Defaults are deployment-configurable; the numbers above match the out-of-the-box Settings. List / read / usage-summary endpoints are not rate-limited (they don't call the LLM).

When the bucket is empty the API returns HTTP 429 with the standard error envelope plus a Retry-After header:

HTTP/2 429
Retry-After: 46
Content-Type: application/json

{
  "error": {
    "code": "llm_tier_rate_limited",
    "message": "Llm tier rate limited",
    "details": {
      "reason": "llm_tier_rate_limited",
      "tier": "premium",
      "retry_after_seconds": 46
    }
  }
}

Retry-After is in seconds, rounded up to the nearest whole second so the value is a safe lower bound.

Response cache

Identical requests within a TTL window do not call the LLM again — the original response is replayed from the database. The cache key is a deterministic SHA-256 over:

  • tenant id, chart id;
  • interpretation type + (for theme) the theme;
  • language, tier;
  • request parameters (at, window_start, window_end, interval_minutes, top_n — whichever apply).

Sync responses gain a cached: true field on cache hit; the streaming variant replays a cached row as exactly one meta event + one delta containing the full content + one done with cached: true so the SSE contract is identical to a fresh call.

Default TTL: 24 hours. Configurable server-side via LLM_CACHE_TTL_SECONDS (0 disables the cache entirely).

Bypassing the cache

Add ?fresh=true to force a new LLM call:

curl -X POST -H "Authorization: Bearer $TOKEN" \
  "https://api.astrolinkers.com/v1/llm/theme/career\
?chart_id=$CHART&language=en&tier=premium&fresh=true"

The new response is also persisted, so subsequent non-fresh calls hit it (the cache is refreshed, not just bypassed).

What invalidates a cached entry

  • TTL expiry — older than LLM_CACHE_TTL_SECONDS.
  • Any difference in the cache-key fields: a new language, tier, or changed at/window all miss the cache and produce a fresh row.
  • The cache schema version is bumped server-side when the prompt template or engine output shape changes meaningfully; every entry becomes ineligible until the next request refreshes it.

Usage summary

GET /v1/llm/usage-summary aggregates call count, token usage and USD spend over an arbitrary window. Tenant-scoped, with optional filters and a single break-down dimension.

Query Type Notes
from ISO datetime Inclusive lower bound.
to ISO datetime Exclusive upper bound. Must be strictly after from.
chart_id string Optional — usage for one chart.
interpretation_type enum Optional — theme / chart_reading / dasha_forecast / muhurta.
language enum Optional — one of the 10 codes.
tier enum Optional — basic / standard / premium.
group_by enum none (default), interpretation_type, tier, language, day (UTC).
curl -H "Authorization: Bearer $TOKEN" \
  "https://api.astrolinkers.com/v1/llm/usage-summary\
?from=2026-05-18T00:00:00Z&to=2026-05-19T00:00:00Z&group_by=tier"
{
  "from_": "2026-05-18T00:00:00Z",
  "to": "2026-05-19T00:00:00Z",
  "group_by": "tier",
  "total": {
    "label": null,
    "call_count": 5,
    "input_tokens": 6047,
    "output_tokens": 4696,
    "cost_usd": 0.006763
  },
  "breakdown": [
    { "label": "basic",    "call_count": 3, "input_tokens": 3388, "output_tokens": 975,  "cost_usd": 0.001482 },
    { "label": "premium",  "call_count": 1, "input_tokens": 1178, "output_tokens": 3054, "cost_usd": 0.003169 },
    { "label": "standard", "call_count": 1, "input_tokens": 1481, "output_tokens": 667,  "cost_usd": 0.002112 }
  ]
}

The total bucket aggregates everything in the window before the break-down is applied, so it equals the sum of the breakdown buckets when no extra filter is in play.

Use cases:

  • Monthly bill statements (group_by=day, sum cost_usd).
  • Capacity planning (group_by=tier).
  • Per-chart cost analysis (chart_id=…, group_by=interpretation_type).
  • Language mix audit (group_by=language).

Notes:

  • Cached responses are recorded with the original call's tokens and cost so usage analytics reflect actual spend — replays do not inflate the total.
  • Aggregation runs as a single GROUP BY server-side; window scans are bounded by the (owner_tenant_id, interpretation_type, created_at) index.

Persistence and re-reading past interpretations

Every successful LLM interpretation — sync or streamed — is saved server-side. The tenant can list and re-read past interpretations without paying for another LLM round-trip, and every row carries the same engine_context so the audit trail stays intact.

A row is written only after the call succeeds:

  • Sync calls write the row after the LLM response lands; the response body now includes interpretation_id.
  • Streamed calls accumulate deltas; on the terminal done event the row is written and its id arrives in the same done payload.
  • A streaming error event leaves no row behind — half-streams never reach the table.

GET /v1/llm/interpretations

List the tenant's stored interpretations, newest first.

Query Type Default Notes
chart_id string Only rows for one chart.
interpretation_type enum theme / chart_reading / dasha_forecast / muhurta.
language enum One of the 10 ISO codes.
tier enum basic / standard / premium.
limit int 50 1..200.
offset int 0 For pagination.
curl -H "Authorization: Bearer $TOKEN" \
  "https://api.astrolinkers.com/v1/llm/interpretations\
?chart_id=$CHART&interpretation_type=theme&limit=10"
{
  "items": [
    {
      "id": "019e3d37-bd30-7973-920e-ba3030aca3d4",
      "chart_id": "019e3cdd-…",
      "interpretation_type": "theme",
      "theme": "career",
      "language": "en",
      "tier": "basic",
      "content": "**Career Theme Analysis…**",
      "engine_context": { /* same shape the LLM was given */ },
      "request_params": { "at": null },
      "input_tokens": 1130,
      "output_tokens": 256,
      "latency_ms": 12300,
      "cost_usd": 0.00044,
      "created_at": "2026-05-18T22:31:33Z"
    }
  ],
  "total": 1,
  "limit": 10,
  "offset": 0
}

total is the unpaginated count of matches so the UI can render correct pagination.

GET /v1/llm/interpretations/{interpretation_id}

Read one record. Cross-tenant ids return 404.

curl -H "Authorization: Bearer $TOKEN" \
  "https://api.astrolinkers.com/v1/llm/interpretations/019e3d37-…"

Returns one row in the same shape as a list.items[] element above.

Schema fields

Field Notes
id UUIDv7 (sortable by time).
chart_id The chart the interpretation belongs to.
interpretation_type theme / chart_reading / dasha_forecast / muhurta.
theme Populated only for interpretation_type == "theme"; one of the 133 significators.
language / tier The original request parameters.
content Full natural-language output as the LLM produced it.
engine_context The structured engine JSON that the LLM saw — every claim in content should trace back here.
request_params Endpoint-specific parameters (at, window_start, window_end, interval_minutes, top_n).
input_tokens / output_tokens / latency_ms / cost_usd Usage accounting from the underlying provider.
created_at UTC timestamp when the row was written.

Tenant isolation

Every read query is scoped to the requesting tenant's id. List filters apply on top of tenant scoping — a tenant cannot see another tenant's interpretations even with a known id.

Why this is in the API

  • Re-reading is free: the LLM is not called again.
  • engine_context is preserved with the response, so an audit of any past interpretation is one GET away.
  • Cost analytics: sum cost_usd over a window for a per-tenant spend report.
  • UI history: list interpretations per chart so users can scroll back through their previous readings.

Streaming (Server-Sent Events)

Every interpretation endpoint has a …/stream sibling that returns its output as a text/event-stream. The UI gets the structured engine context immediately (so it can render grounded blocks while the user waits), then tokens flow in as the LLM produces them.

The four streaming endpoints:

Streaming endpoint Sync sibling
POST /v1/llm/theme/{theme}/stream /v1/llm/theme/{theme}
POST /v1/llm/chart-reading/stream /v1/llm/chart-reading
POST /v1/llm/dasha-forecast/stream /v1/llm/dasha-forecast
POST /v1/llm/muhurta-reasoning/stream /v1/llm/muhurta-reasoning

All query parameters from the sync endpoint apply unchanged.

Event taxonomy

Each event is a named SSE event with a JSON payload.

  • event: meta — sent once, first. Carries the deterministic engine output that the LLM is about to interpret. Render it immediately so the user sees grounded numbers before any token arrives.

    {
      "kind": "meta",
      "interpretation_type": "theme",
      "language": "en",
      "tier": "standard",
      "engine_context": { /* same shape as the sync endpoint */ }
    }
    
  • event: delta — one per incremental token chunk. Concatenating every delta.content reconstructs the full interpretation.

    { "kind": "delta", "content": "…token chunk…" }
    
  • event: done — terminal. Carries the usage summary so you can display cost / token counts.

    {
      "kind": "done",
      "input_tokens": 1481,
      "output_tokens": 667,
      "latency_ms": 4628,
      "cost_usd": 0.00211
    }
    
  • event: error — terminal alternative. Sent if the active model fails mid-stream (after at least one delta has already flowed) or if the entire fallback chain is exhausted before any token reaches the caller.

    { "kind": "error", "error": "human-readable detail" }
    

A stream is guaranteed to end with exactly one terminal event (done or error); your consumer can stop as soon as either arrives.

Fallback semantics

The router tries the policy's primary model first, then walks down the fallback chain on retryable failures (timeout, rate-limit, 5xx, circuit-open). For streaming there is one important rule:

Fallback only happens before the first delta reaches the client. Once content has begun to flow, the active model is locked in — a mid-stream failure surfaces as a terminal error event and the client must retry from scratch.

This is unavoidable: an HTTP byte that has already left the wire cannot be unsent, and splicing two models' partial outputs together would corrupt the response.

Client example (browser / fetch)

The browser's built-in EventSource only supports GET, so for the POST-based streaming endpoints use fetch + the ReadableStream API. A minimal consumer:

async function streamInterpretation({ url, token, onMeta, onDelta, onDone, onError }) {
  const resp = await fetch(url, {
    method: "POST",
    headers: {
      "Authorization": `Bearer ${token}`,
      "Accept": "text/event-stream",
    },
  });
  if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
  const reader = resp.body.getReader();
  const decoder = new TextDecoder();
  let buffer = "";
  while (true) {
    const { value, done } = await reader.read();
    if (done) break;
    buffer += decoder.decode(value, { stream: true });
    // SSE events are separated by blank lines.
    let sep;
    while ((sep = buffer.indexOf("\n\n")) !== -1) {
      const block = buffer.slice(0, sep);
      buffer = buffer.slice(sep + 2);
      const lines = block.split("\n");
      const kind = lines.find(l => l.startsWith("event: "))?.slice(7);
      const data = lines.find(l => l.startsWith("data: "))?.slice(6);
      if (!kind || !data) continue;
      const payload = JSON.parse(data);
      if (kind === "meta")  onMeta?.(payload);
      if (kind === "delta") onDelta?.(payload.content);
      if (kind === "done")  return onDone?.(payload);
      if (kind === "error") return onError?.(payload.error);
    }
  }
}

A non-streaming reference (curl):

curl -N -X POST -H "Authorization: Bearer $TOKEN" \
  -H "Accept: text/event-stream" \
  "https://api.astrolinkers.com/v1/llm/chart-reading/stream\
?chart_id=$CHART&language=en&tier=standard"

curl -N disables output buffering so each event prints as it arrives.

Operational notes

  • The response sets Cache-Control: no-cache, no-transform and X-Accel-Buffering: no so reverse proxies do not buffer the stream.
  • If your client disconnects mid-stream the server cancels the upstream LLM call and stops charging tokens — useful when the user navigates away.
  • Streaming and sync endpoints share the same per-tenant cost cap (llm_cost_cap_per_hour_usd). A request that would exceed the cap is rejected with 402 / 429 before the stream opens, so the caller never sees a half-stream that fails to bill.

Architecture (for the curious)

  • Provider-agnostic — the underlying router accepts any provider implementing LLMProviderProtocol; switching from OpenRouter to a direct vendor is a configuration change, not code.
  • Tier → model mapping lives in config/llm_policy.yaml server-side; never echoed in the API. Tier labels (basic, standard, premium) are stable; the model behind a tier can rotate as better/cheaper options emerge.
  • Fallback chains — each tier has 2 fallback models. If the primary errors or the circuit breaker is open, the request rolls forward to the next without surfacing the failure.
  • Cost guard — the per-tenant rolling LLM spend cap (llm_cost_cap_per_hour_usd) applies to every interpretation. A tenant that hits the cap gets a 402 / 429 with a retry_after hint.
  • Prompt redaction — user-supplied text is sanitised before it reaches the provider (PII / secret patterns stripped).

What this does not replace

The existing /v1/interpretations endpoint is template-driven (deterministic, no LLM round-trip). Use it when you need:

  • Predictable, reproducible output bytes (caching, deduplication).
  • No external-LLM dependency.
  • Per-skill statements with feedback / accuracy tracking.

Use /v1/llm/... when you need:

  • Natural-language synthesis grounded in engine math.
  • Multilingual output.
  • Tier-controlled depth.