Sign inSign up

chiefj/forecast-worker

By chiefj

Updated 3 months ago

Proactive LLM cost intelligence powered by Google TimesFM.

Image
Developer tools
0

156

chiefj/forecast-worker repository overview

Forecast Worker — chiefj/forecast-worker

Proactive LLM cost intelligence powered by Google TimesFM.
Pairs with chiefj/instrumentation-sdk-api to deliver live forecast graphs in Grafana.


Table of Contents

  1. What This Package Does
  2. How the Two Images Work Together
  3. Quick Start (Docker — End User)
  4. Sending Forecast Data from Your App
  5. Viewing Forecasts in Grafana
  6. Core Architectural Pipeline
  7. Model Parameters and Caching
  8. Database Migrations
  9. Folder Structure
  10. Development and Testing

1. What This Package Does

The Forecast Worker is a Temporal-based cron worker that solves proactive cost and latency tracking using Google TimesFM (Time Series Forecasting Model).

Unlike EWMA (Exponentially Weighted Moving Average) — which is reactive and only alerts after a budget spike or SLO breach — the Forecast Worker is proactive. It:

  1. Aggregates historical cost/token logs from ClickHouse (last 168 hours).
  2. Runs TimesFM inference to project the trend 24 hours into the future.
  3. Produces three quantile outputs: mean, p10 (optimistic), p90 (pessimistic).
  4. Pushes results to the Instrumentation SDK API via HTTP — which exposes them as Prometheus metrics visible in Grafana dashboards.

2. How the Two Images Work Together

┌─────────────────────────────────────────────────────────────────────────────┐
│                         YOUR APPLICATION STACK                              │
│                                                                             │
│  ┌──────────────────────────────┐    ┌──────────────────────────────────┐  │
│  │   chiefj/forecast-worker     │    │  chiefj/instrumentation-sdk-api  │  │
│  │                              │    │                                  │  │
│  │  ┌──────────────────────┐    │    │  ┌────────────────────────────┐  │  │
│  │  │ Temporal Cron Worker │    │    │  │ FastAPI (port 8000)        │  │  │
│  │  │ Runs every 5 minutes │    │    │  │  POST /v1/metrics/forecast │  │  │
│  │  └──────────┬───────────┘    │    │  │  POST /v1/metrics/record   │  │  │
│  │             │                │    │  │  GET  /v1/metrics/health   │  │  │
│  │  ┌──────────▼───────────┐    │    │  └────────────┬───────────────┘  │  │
│  │  │  TimesFM Inference   │    │    │               │                  │  │
│  │  │  (google/timesfm-    │    │    │  ┌────────────▼───────────────┐  │  │
│  │  │   2.5-200m-pytorch)  │    │    │  │ OTel Prometheus Adapter   │  │  │
│  │  └──────────┬───────────┘    │    │  │ Observable Gauges (p/s)   │  │  │
│  │             │                │    │  └────────────┬───────────────┘  │  │
│  │  ┌──────────▼───────────┐    │    │               │                  │  │
│  │  │  Quantile Output     │    │    │  ┌────────────▼───────────────┐  │  │
│  │  │  mean / p10 / p90    ├────┼────►  │ Prometheus Exporter :9464 │  │  │
│  │  └──────────────────────┘    │    │  └────────────┬───────────────┘  │  │
│  │                              │    │               │                  │  │
│  │  Your app also calls SDK     │    │  ┌────────────▼───────────────┐  │  │
│  │  directly for span metrics   │    │  │ Prometheus  :9090          │  │  │
│  └──────────────────────────────┘    │  └────────────┬───────────────┘  │  │
│                                      │               │                  │  │
│  ┌───────────────────────────────────┼───────────────▼───────────────┐  │  │
│  │               Grafana :3000       │   Dashboards auto-provisioned │  │  │
│  │  ┌────────────────────────────────┼──────────────────────────────┐│  │  │
│  │  │  LLM Cost Forecast Dashboard  │  Mean/p10/p90 time-series    ││  │  │
│  │  │  LLM Cost Dashboard           │  Actual vs Forecast overlay  ││  │  │
│  │  │  LLM Security Dashboard       │  PII / Injection events      ││  │  │
│  │  └────────────────────────────────┴──────────────────────────────┘│  │  │
│  └───────────────────────────────────────────────────────────────────┘  │  │
│                                      └──────────────────────────────────┘  │
└─────────────────────────────────────────────────────────────────────────────┘
Data Flow Step-by-Step
StepComponentWhat Happens
1Your AppCalls POST /v1/metrics/record with span data (tokens, cost, latency)
2instrumentation-sdk-apiRecords OTel counters/histograms → exported to Prometheus port 9464
3forecast-worker (cron, every 5m)Queries ClickHouse for last 168h of cost data
4forecast-workerRuns TimesFM to produce mean, p10, p90 for next 24h
5forecast-workerCalls POST /v1/metrics/forecast on the SDK API with results
6instrumentation-sdk-apiSets observable Prometheus gauges: llm_forecast_cost_mean_usd_micro, etc.
7PrometheusScrapes port 9464 every 5s
8GrafanaRenders forecast band charts from Prometheus data

3. Quick Start (Docker — End User)

Prerequisites
  • Docker Engine 24+ installed
  • Ports 8000, 3000, 9090, 4317 available on your host
Step 1 — Log in to Docker Hub
docker login -u chiefj
# Enter your token when prompted
Step 2 — Pull both images
docker pull chiefj/instrumentation-sdk-api:latest
docker pull chiefj/forecast-worker:latest
Step 3 — Start the Observability Stack

Start the main observability stack (FastAPI + Prometheus + Grafana + Tempo in one container):

docker run -d \
  --name observability-stack \
  -p 8000:8000 \
  -p 3000:3000 \
  -p 9090:9090 \
  -p 4317:4317 \
  chiefj/instrumentation-sdk-api:latest

Wait ~10 seconds for all services to start, then verify:

curl http://localhost:8000/v1/metrics/health
# → {"initialized": true, "message": "# HELP ..."}
Step 4 — Start the Forecast Worker

The worker needs access to your ClickHouse instance and the SDK API:

docker run -d \
  --name forecast-worker \
  -e CLICKHOUSE_HOST=your-clickhouse-host \
  -e CLICKHOUSE_PORT=8123 \
  -e CLICKHOUSE_DATABASE=llm_observability \
  -e CLICKHOUSE_USER=default \
  -e CLICKHOUSE_PASSWORD=your-password \
  -e INSTRUMENTATION_SDK_URL=http://observability-stack:8000 \
  -e TEMPORAL_HOST=your-temporal-host:7233 \
  --link observability-stack \
  chiefj/forecast-worker:latest

Tip: For local development without ClickHouse, you can push forecast results directly to the SDK API (see Step 5 below).

Step 5 — Open Grafana

Navigate to http://localhost:3000 (default credentials: admin / admin).

Go to Dashboards → LLM Observability folder. You'll see:

DashboardWhat It Shows
LLM Cost Forecast DashboardMean/p10/p90 forecast band, uncertainty width, actual vs forecast overlay
LLM Cost DashboardCumulative cost, cost by model/service, forecast panel
LLM Security & Safety DashboardPII detections, injection attempts, violation trends
LLM Latency & TTFT Dashboardp50/p95/p99 latency, time-to-first-token by model
LLM Guardrails DashboardInvariant breach tracking, human review SLO

4. Sending Forecast Data from Your App

If you're integrating your own forecasting model or want to push forecasts programmatically without the Temporal worker, you can call the SDK directly:

REST API
curl -X POST http://localhost:8000/v1/metrics/forecast \
  -H "Content-Type: application/json" \
  -d '{
    "mean": 9200,
    "p10": 3800,
    "p90": 17500,
    "model": "gpt-4o",
    "provider": "openai",
    "service_name": "my-chat-app"
  }'
FieldTypeDescription
meanintExpected cost in micro-USD (e.g. 9200 = $0.0092)
p10intOptimistic lower bound (10th percentile)
p90intPessimistic upper bound (90th percentile)
modelstrLLM model name (e.g. "gpt-4o")
providerstrLLM provider (e.g. "openai", "anthropic")
service_namestrYour service identifier
Python SDK Usage
import httpx

SDK_URL = "http://localhost:8000"

# Record a span (actual usage)
httpx.post(f"{SDK_URL}/v1/metrics/record", json={
    "model": "gpt-4o",
    "provider": "openai",
    "service_name": "my-chat-app",
    "prompt_tokens": 1500,
    "completion_tokens": 400,
    "latency_ms_total": 1800,
    "latency_ms_ttft": 320,
    "finish_reason": "stop",
    "status": "success",
    "cost_usd_micro": 7200,
    "pii_detected": False,
    "injection_attempt": False,
})

# Push a forecast (from your own model or from forecast-worker output)
httpx.post(f"{SDK_URL}/v1/metrics/forecast", json={
    "mean": 9200,
    "p10": 3800,
    "p90": 17500,
    "model": "gpt-4o",
    "provider": "openai",
    "service_name": "my-chat-app",
})

5. Viewing Forecasts in Grafana

After sending at least one forecast, open Grafana and go to:

Dashboards → LLM Observability → LLM Cost Forecast Dashboard

You'll see:

  • Top panel — Time-series with three lines: Mean (blue, solid), p10 (green, dashed), p90 (orange, dashed). This is the confidence interval band produced by TimesFM.
  • Bar gauge — Current forecast mean broken down by model.
  • Uncertainty band width — How wide the forecast spread is (p90 − p10).
  • Actual vs Forecast — Actual accumulated cost overlaid with the forecast mean line, so you can validate accuracy.
  • Stat panels — Current snapshot values for mean, p10, p90.

Metric names in Prometheus (for custom PromQL queries):

llm_forecast_cost_mean_usd_micro{model="gpt-4o", service_name="my-chat-app"}
llm_forecast_cost_p10_usd_micro{model="gpt-4o", service_name="my-chat-app"}
llm_forecast_cost_p90_usd_micro{model="gpt-4o", service_name="my-chat-app"}

Divide by 1000000 to convert from micro-USD to USD.


6. Core Architectural Pipeline

[Cron: 5 * * * *]
       │
       ▼
1. fetch_cost_series (Activity F-FM-01)
       │  Queries ClickHouse cost_by_dimension for last 168 hours
       ▼
2. ForecastService.build_dense_series (Domain Layer)
       │  Zero-pads missing hours, validates min_history_hours >= 48
       ▼
3. TimesFM Inference (Adapter Layer)
       │  google/timesfm-2.5-200m-pytorch
       │  context_len=168, patch_len=32, horizon=24
       ▼
4. Quantile Projection
       ├── forecast_mean → Expected cost trend
       ├── forecast_p10  → Optimistic lower bound
       └── forecast_p90  → Worst-case for budget breach alerting
       │
       ▼
5. POST /v1/metrics/forecast → instrumentation-sdk-api
       │
       ▼
6. OTel Observable Gauge → Prometheus :9464 → Grafana

7. Model Parameters and Caching

  • Checkpoint: google/timesfm-2.5-200m-pytorch (registered in platform Model Registry)
  • Context length: 168 hours (1 week of history)
  • Input patch length: 32
  • Generation horizon: 24 hours ahead
  • Backend: PyTorch CPU-only (torch --index-url https://download.pytorch.org/whl/cpu)
  • Weight caching: Mount ~/.cache/huggingface as a Docker volume to avoid re-downloading on restart:
docker run -d \
  --name forecast-worker \
  -v ~/.cache/huggingface:/root/.cache/huggingface \
  ... \
  chiefj/forecast-worker:latest

8. Database Migrations

Migrations live in database/migrations/ and use immutable versioned SQL files:

FilePurpose
0001_init.sqlCreates forecasts table with forecast_mean, forecast_p10, forecast_p90, forecast_time, per service/model (unique constraint)
0001_init.rollback.sqlReverts the schema
schema.lockCurrent schema hash lock

9. Folder Structure

packages/python/forecast-worker/
├── build/
│   └── Dockerfile             # CPU-optimized PyTorch container
├── contracts/
│   └── workflows/
│       └── forecast_workflow.yaml
├── database/
│   ├── migrations/
│   │   ├── 0001_init.rollback.sql
│   │   └── 0001_init.sql
│   └── schema.lock
├── outcome/                   # Generated forecast graphs (PNG/SVG)
├── scripts/
│   └── test.sh
├── src/
│   ├── features/forecast/
│   │   └── service.py         # Dense series + business validation
│   ├── infra/adapters/clickhouse/
│   │   └── clickhouse_adapter.py
│   ├── shared/
│   │   ├── contracts/validator.py
│   │   ├── errors/base.py
│   │   └── ports/clickhouse_port.py
│   └── worker/
│       └── index.py           # Entry point
├── tests/unit/
├── pyproject.toml
├── worker-registry.yaml
└── README.md

10. Development and Testing

Setup Environment
python3 -m venv --without-pip .venv
.venv/bin/python3 get-pip.py
.venv/bin/pip install -e ".[dev]"
Run Tests
./scripts/test.sh

Or with coverage:

.venv/bin/pytest --cov=src --cov-report=term-missing

Minimum coverage threshold: 80% (currently 94% package-wide).

Environment Variables
VariableRequiredDefaultDescription
CLICKHOUSE_HOSTClickHouse hostname
CLICKHOUSE_PORT8123ClickHouse HTTP port
CLICKHOUSE_DATABASEllm_observabilityDatabase name
CLICKHOUSE_USERdefaultClickHouse user
CLICKHOUSE_PASSWORDClickHouse password
INSTRUMENTATION_SDK_URLBase URL of the SDK API (e.g. http://observability-stack:8000)
TEMPORAL_HOSTTemporal server address (e.g. temporal:7233)
TEMPORAL_NAMESPACEdefaultTemporal namespace
FORECAST_CRON_SCHEDULE5 * * * *Cron expression for forecast cadence
MIN_HISTORY_HOURS48Minimum hours of history required to run forecast

Docker Image Reference

ImageTagSizeDescription
chiefj/instrumentation-sdk-apilatest~2.1 GBFastAPI + Prometheus + Grafana + Tempo all-in-one observability stack
chiefj/forecast-workerlatest~3.5 GBTimesFM CPU inference worker + Temporal integration
# Pull and run the full stack
docker pull chiefj/instrumentation-sdk-api:latest
docker pull chiefj/forecast-worker:latest

Tag summary

Content type

Image

Digest

sha256:c31b6d6c0

Size

707.9 MB

Last updated

3 months ago

docker pull chiefj/forecast-worker