Sign inSign up

chiefj/forecast-worker

By chiefj

•Updated 4 months ago

Proactive LLM cost intelligence powered by Google TimesFM.

Image
Developer tools
0

160

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_HOST✅—ClickHouse hostname
CLICKHOUSE_PORT❌8123ClickHouse HTTP port
CLICKHOUSE_DATABASE❌llm_observabilityDatabase name
CLICKHOUSE_USER❌defaultClickHouse user
CLICKHOUSE_PASSWORD✅—ClickHouse password
INSTRUMENTATION_SDK_URL✅—Base URL of the SDK API (e.g. http://observability-stack:8000)
TEMPORAL_HOST✅—Temporal server address (e.g. temporal:7233)
TEMPORAL_NAMESPACE❌defaultTemporal namespace
FORECAST_CRON_SCHEDULE❌5 * * * *Cron expression for forecast cadence
MIN_HISTORY_HOURS❌48Minimum 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

4 months ago

docker pull chiefj/forecast-worker