Fran Jurmanović df4ca0bf76 Track cached prompt tokens through to Langfuse
OpenAI's chat completions response includes prompt_tokens_details.
cached_tokens when prompt caching kicks in - previously dropped
entirely. Now extracted in LlmClient and forwarded to Langfuse as the
flat cache_read_input_tokens usage key.

Used the flat key rather than OpenAI's nested prompt_tokens_details
shape: the installed @langfuse/tracing SDK's usageDetails type is
Record<string, number> | OpenAiUsage (a 3-field camelCase shape, no
_details nesting) even though the wire schema in @langfuse/core
supports nesting - a gap in the SDK's public types, not something
worth fighting with a cast.

Verified: full suite (82/82) passing, tsc/eslint clean.
2026-08-12 23:15:42 +02:00
2026-08-12 21:52:45 +02:00
2026-08-12 22:17:56 +02:00
2026-08-12 22:17:56 +02:00
2026-08-12 22:17:56 +02:00

sugarr

Movie-recommendation companion for Jellyfin: pulls your favorite/recently-watched movies, gets suggestions from an LLM (via Langfuse-managed prompts) or a TMDb similarity/frequency fallback, resolves them against TMDb, excludes anything already in your library or Radarr, and can auto-request the rest via Radarr. Runs on a cron schedule and/or as an HTTP API.

Built with a TypeScript/OOP architecture (Node + Fastify + Langfuse): a reusable src/core/ framework (HTTP client base classes, an LLM tool/pipeline abstraction with Langfuse-cached prompts, Fastify server scaffolding) and an src/app/ layer with Sugarr's concrete clients/tools/pipeline/controllers. See docs/superpowers/specs/2026-08-12-typescript-oop-rewrite-design.md and docs/superpowers/plans/2026-08-12-typescript-oop-rewrite.md for the design and implementation plan (this repo previously also had a Go implementation, now removed in favor of this one).

Running the app

pnpm install
cp .env.example .env   # fill in the values below
pnpm dev                # or: pnpm build && pnpm start

Required environment variables

See .env.example for the full list with defaults. At minimum you need:

  • JELLYFIN_URL, JELLYFIN_API_KEY, JELLYFIN_USER_ID, TMDB_API_KEY
  • LANGFUSE_PUBLIC_KEY, LANGFUSE_SECRET_KEY, LANGFUSE_BASE_URL — the app fetches its LLM prompts from Langfuse at runtime (see below), so these are required even if LLM_ENABLED=false (only the recommend/search endpoints actually call Langfuse; if you never enable the LLM path you can point these at any reachable Langfuse instance).

Docker

docker compose up -d --build

Seeding Langfuse prompts

The app fetches two chat prompts from Langfuse by name — movie-recommender and movie-search — compiles them with per-request variables, and reads model/temperature/etc. from each prompt version's config field. Ready-to-import definitions are in langfuse/:

  • langfuse/movie-recommender.prompt.json
  • langfuse/movie-search.prompt.json

Create each one in your Langfuse project (UI: Prompts → New prompt, type Chat; or via the Langfuse API/SDK) using the prompt array as the chat messages and the config object as the prompt's config. Variables use {{name}} interpolation:

  • movie-recommender: count, favorites, watched, library, previouslySuggested
  • movie-search: query, count

Prompt caching and invalidation

Compiled prompts are cached in memory per process (LANGFUSE_PROMPT_CACHE_ENABLED, default true; set to false to always fetch fresh from Langfuse — useful while iterating on a prompt). To invalidate the cache after updating a prompt in Langfuse without restarting the app, call:

POST /webhooks/langfuse/invalidate
Content-Type: application/json
Authorization: Bearer <SECRET_KEY>   # if SECRET_KEY is set

{"promptName": "movie-recommender"}   # or {} / omit body to clear the whole cache

This isn't wired to a Langfuse-native push webhook (self-hosted Langfuse has no built-in "prompt updated" event) — call it manually, from CI after a prompt change, or from Langfuse Cloud's webhook feature if you have one configured.

HTTP API

Method Path Notes
GET /health exempt from auth
GET /recommendations ?count=, ?force_llm=true, ?force_tmdb=true
POST /recommendations/search body {"query": "..."}, requires LLM_ENABLED=true
POST /recommendations/trigger runs the pipeline + auto-requests via Radarr if enabled
POST /recommendations/request body {"tmdb_id": 12345}, requires RADARR_ENABLED=true
POST /webhooks/langfuse/invalidate body {"promptName"?: string}

If SECRET_KEY is set, all routes except /health require Authorization: Bearer <SECRET_KEY>.

Description
No description provided
Readme 376 KiB
Languages
TypeScript 99.6%
Dockerfile 0.4%