{"repo": "autumnsgrove/lattice", "files": {"services/forage/wrangler.toml": {"7a487fbfde06fffe25884e24f35835c4087f1d33": {"t": 1785745329.9417303, "n": 2110, "h": "6b68aa58fae60945", "c": "# Worker configuration for Forage\nname = \"forage\"\nmain = \"src/index.ts\"\ncompatibility_date = \"2025-12-01\"\n\n# Durable Objects binding - SQLite-backed (FREE tier)\n[[durable_objects.bindings]]\nname = \"SEARCH_JOB\"\nclass_name = \"SearchJobDO\"\n\n# Workers AI binding (for Cloudflare AI models like Llama 4 Scout)\n[ai]\nbinding = \"AI\"\n\n# D1 Database binding for job index\n[[d1_databases]]\nbinding = \"DB\"\ndatabase_name = \"forage-jobs\"\ndatabase_id = \"cd493112-a901-4f6d-aadf-a5ca78929557\"\n\n# Migrations for Durable Objects\n[[migrations]]\ntag = \"v1\"\nnew_sqlite_classes = [\"SearchJobDO\"]\n\n# Service bindings (Worker-to-Worker)\n[[services]]\nbinding = \"AUTH\"\nservice = \"groveauth\"\n\n[[services]]\nbinding = \"ZEPHYR\"\nservice = \"grove-zephyr\"\n\n# Durable Objects observability (logs to CF dashboard)\n[observability.logs]\nenabled = true\nhead_sampling_rate = 1\ninvocation_logs = true\n\n# Environment variables (secrets should be set via wrangler secret)\n[vars]\nENVIRONMENT = \"production\"\nMAX_BATCHES = \"6\"\nTARGET_RESULTS = \"25\"\n# Provider selection: openrouter (primary, ZDR compliant), deepseek (fallback)\nDRIVER_PROVIDER = \"openrouter\"\nSWARM_PROVIDER = \"openrouter\"\n# RDAP checker selection: rdap (traditional), cerebras (AI-powered)\nUSE_CEREBRAS_RDAP = \"false\"\n\n# Development environment\n[env.dev]\nname = \"forage-dev\"\n\n[[env.dev.durable_objects.bindings]]\nname = \"SEARCH_JOB\"\nclass_name = \"SearchJobDO\"\n\n[env.dev.ai]\nbinding = \"AI\"\n\n[[env.dev.migrations]]\ntag = \"v1\"\nnew_sqlite_classes = [\"SearchJobDO\"]\n\n[env.dev.vars]\nENVIRONMENT = \"development\"\nMAX_BATCHES = \"2\"\nTARGET_RESULTS = \"25\"\nDRIVER_PROVIDER = \"openrouter\"\nSWARM_PROVIDER = \"openrouter\"\nUSE_CEREBRAS_RDAP = \"false\"\n\n# Secrets to configure:\n# - OPENROUTER_API_KEY: API key for OpenRouter (primary, ZDR compliant)\n# - DEEPSEEK_API_KEY: API key for DeepSeek (fallback only)\n# - ZEPHYR_API_KEY: API key for Zephyr email service binding\n#\n# Set with: wrangler secret put OPENROUTER_API_KEY\n#\n# Provider hierarchy:\n# - openrouter: OpenRouter proxy to DeepSeek V3.2 ($0.28/$0.42 per M tokens) - PRIMARY\n# - deepseek: DeepSeek V3.2 direct ($0.28/$0.42 per M tokens) - FALLBACK\n"}, "f3eeb06b3998778a9b6921e934ae42c1ae9a841e": {"t": 1785750263.4390476, "n": 2110, "h": "6b68aa58fae60945", "c": "# Worker configuration for Forage\nname = \"forage\"\nmain = \"src/index.ts\"\ncompatibility_date = \"2025-12-01\"\n\n# Durable Objects binding - SQLite-backed (FREE tier)\n[[durable_objects.bindings]]\nname = \"SEARCH_JOB\"\nclass_name = \"SearchJobDO\"\n\n# Workers AI binding (for Cloudflare AI models like Llama 4 Scout)\n[ai]\nbinding = \"AI\"\n\n# D1 Database binding for job index\n[[d1_databases]]\nbinding = \"DB\"\ndatabase_name = \"forage-jobs\"\ndatabase_id = \"cd493112-a901-4f6d-aadf-a5ca78929557\"\n\n# Migrations for Durable Objects\n[[migrations]]\ntag = \"v1\"\nnew_sqlite_classes = [\"SearchJobDO\"]\n\n# Service bindings (Worker-to-Worker)\n[[services]]\nbinding = \"AUTH\"\nservice = \"groveauth\"\n\n[[services]]\nbinding = \"ZEPHYR\"\nservice = \"grove-zephyr\"\n\n# Durable Objects observability (logs to CF dashboard)\n[observability.logs]\nenabled = true\nhead_sampling_rate = 1\ninvocation_logs = true\n\n# Environment variables (secrets should be set via wrangler secret)\n[vars]\nENVIRONMENT = \"production\"\nMAX_BATCHES = \"6\"\nTARGET_RESULTS = \"25\"\n# Provider selection: openrouter (primary, ZDR compliant), deepseek (fallback)\nDRIVER_PROVIDER = \"openrouter\"\nSWARM_PROVIDER = \"openrouter\"\n# RDAP checker selection: rdap (traditional), cerebras (AI-powered)\nUSE_CEREBRAS_RDAP = \"false\"\n\n# Development environment\n[env.dev]\nname = \"forage-dev\"\n\n[[env.dev.durable_objects.bindings]]\nname = \"SEARCH_JOB\"\nclass_name = \"SearchJobDO\"\n\n[env.dev.ai]\nbinding = \"AI\"\n\n[[env.dev.migrations]]\ntag = \"v1\"\nnew_sqlite_classes = [\"SearchJobDO\"]\n\n[env.dev.vars]\nENVIRONMENT = \"development\"\nMAX_BATCHES = \"2\"\nTARGET_RESULTS = \"25\"\nDRIVER_PROVIDER = \"openrouter\"\nSWARM_PROVIDER = \"openrouter\"\nUSE_CEREBRAS_RDAP = \"false\"\n\n# Secrets to configure:\n# - OPENROUTER_API_KEY: API key for OpenRouter (primary, ZDR compliant)\n# - DEEPSEEK_API_KEY: API key for DeepSeek (fallback only)\n# - ZEPHYR_API_KEY: API key for Zephyr email service binding\n#\n# Set with: wrangler secret put OPENROUTER_API_KEY\n#\n# Provider hierarchy:\n# - openrouter: OpenRouter proxy to DeepSeek V3.2 ($0.28/$0.42 per M tokens) - PRIMARY\n# - deepseek: DeepSeek V3.2 direct ($0.28/$0.42 per M tokens) - FALLBACK\n"}, "4d9fc0708f7deb4193151734ae240d966328feb2": {"t": 1785750264.542948, "n": 2110, "h": "19b3e5dd0efbd295", "c": "# Worker configuration for Forage\nname = \"forage\"\nmain = \"src/index.ts\"\ncompatibility_date = \"2024-11-01\"\n\n# Durable Objects binding - SQLite-backed (FREE tier)\n[[durable_objects.bindings]]\nname = \"SEARCH_JOB\"\nclass_name = \"SearchJobDO\"\n\n# Workers AI binding (for Cloudflare AI models like Llama 4 Scout)\n[ai]\nbinding = \"AI\"\n\n# D1 Database binding for job index\n[[d1_databases]]\nbinding = \"DB\"\ndatabase_name = \"forage-jobs\"\ndatabase_id = \"cd493112-a901-4f6d-aadf-a5ca78929557\"\n\n# Migrations for Durable Objects\n[[migrations]]\ntag = \"v1\"\nnew_sqlite_classes = [\"SearchJobDO\"]\n\n# Service bindings (Worker-to-Worker)\n[[services]]\nbinding = \"AUTH\"\nservice = \"groveauth\"\n\n[[services]]\nbinding = \"ZEPHYR\"\nservice = \"grove-zephyr\"\n\n# Durable Objects observability (logs to CF dashboard)\n[observability.logs]\nenabled = true\nhead_sampling_rate = 1\ninvocation_logs = true\n\n# Environment variables (secrets should be set via wrangler secret)\n[vars]\nENVIRONMENT = \"production\"\nMAX_BATCHES = \"6\"\nTARGET_RESULTS = \"25\"\n# Provider selection: openrouter (primary, ZDR compliant), deepseek (fallback)\nDRIVER_PROVIDER = \"openrouter\"\nSWARM_PROVIDER = \"openrouter\"\n# RDAP checker selection: rdap (traditional), cerebras (AI-powered)\nUSE_CEREBRAS_RDAP = \"false\"\n\n# Development environment\n[env.dev]\nname = \"forage-dev\"\n\n[[env.dev.durable_objects.bindings]]\nname = \"SEARCH_JOB\"\nclass_name = \"SearchJobDO\"\n\n[env.dev.ai]\nbinding = \"AI\"\n\n[[env.dev.migrations]]\ntag = \"v1\"\nnew_sqlite_classes = [\"SearchJobDO\"]\n\n[env.dev.vars]\nENVIRONMENT = \"development\"\nMAX_BATCHES = \"2\"\nTARGET_RESULTS = \"25\"\nDRIVER_PROVIDER = \"openrouter\"\nSWARM_PROVIDER = \"openrouter\"\nUSE_CEREBRAS_RDAP = \"false\"\n\n# Secrets to configure:\n# - OPENROUTER_API_KEY: API key for OpenRouter (primary, ZDR compliant)\n# - DEEPSEEK_API_KEY: API key for DeepSeek (fallback only)\n# - ZEPHYR_API_KEY: API key for Zephyr email service binding\n#\n# Set with: wrangler secret put OPENROUTER_API_KEY\n#\n# Provider hierarchy:\n# - openrouter: OpenRouter proxy to DeepSeek V3.2 ($0.28/$0.42 per M tokens) - PRIMARY\n# - deepseek: DeepSeek V3.2 direct ($0.28/$0.42 per M tokens) - FALLBACK\n"}, "d167fd4829bafc8f4c11b2e6843c3f1e7f4f5623": {"t": 1785750266.912402, "n": 1946, "h": "df840be0bd7866f5", "c": "# Worker configuration for Forage\nname = \"forage\"\nmain = \"src/index.ts\"\ncompatibility_date = \"2024-11-01\"\n\n# Durable Objects binding - SQLite-backed (FREE tier)\n[[durable_objects.bindings]]\nname = \"SEARCH_JOB\"\nclass_name = \"SearchJobDO\"\n\n# Workers AI binding (for Cloudflare AI models like Llama 4 Scout)\n[ai]\nbinding = \"AI\"\n\n# D1 Database binding for job index\n[[d1_databases]]\nbinding = \"DB\"\ndatabase_name = \"forage-jobs\"\ndatabase_id = \"cd493112-a901-4f6d-aadf-a5ca78929557\"\n\n# Migrations for Durable Objects\n[[migrations]]\ntag = \"v1\"\nnew_sqlite_classes = [\"SearchJobDO\"]\n\n# Durable Objects observability (logs to CF dashboard)\n[observability.logs]\nenabled = true\nhead_sampling_rate = 1\ninvocation_logs = true\n\n# Environment variables (secrets should be set via wrangler secret)\n[vars]\nENVIRONMENT = \"production\"\nMAX_BATCHES = \"6\"\nTARGET_RESULTS = \"25\"\n# Provider selection: openrouter (primary, ZDR compliant), deepseek (fallback)\nDRIVER_PROVIDER = \"openrouter\"\nSWARM_PROVIDER = \"openrouter\"\n# RDAP checker selection: rdap (traditional), cerebras (AI-powered)\nUSE_CEREBRAS_RDAP = \"false\"\n\n# Development environment\n[env.dev]\nname = \"forage-dev\"\n\n[[env.dev.durable_objects.bindings]]\nname = \"SEARCH_JOB\"\nclass_name = \"SearchJobDO\"\n\n[env.dev.ai]\nbinding = \"AI\"\n\n[[env.dev.migrations]]\ntag = \"v1\"\nnew_sqlite_classes = [\"SearchJobDO\"]\n\n[env.dev.vars]\nENVIRONMENT = \"development\"\nMAX_BATCHES = \"2\"\nTARGET_RESULTS = \"25\"\nDRIVER_PROVIDER = \"openrouter\"\nSWARM_PROVIDER = \"openrouter\"\nUSE_CEREBRAS_RDAP = \"false\"\n\n# Secrets to configure:\n# - OPENROUTER_API_KEY: API key for OpenRouter (primary, ZDR compliant)\n# - DEEPSEEK_API_KEY: API key for DeepSeek (fallback only)\n# - RESEND_API_KEY: API key for email sending\n#\n# Set with: wrangler secret put OPENROUTER_API_KEY\n#\n# Provider hierarchy:\n# - openrouter: OpenRouter proxy to DeepSeek V3.2 ($0.28/$0.42 per M tokens) - PRIMARY\n# - deepseek: DeepSeek V3.2 direct ($0.28/$0.42 per M tokens) - FALLBACK\n"}}, "docs/specs/lumen-spec.md": {"7a487fbfde06fffe25884e24f35835c4087f1d33": {"t": 1786067813.1936789, "n": 28567, "h": "79b9b5d5e8844549", "c": "---\ntitle: Lumen — AI Gateway\ndescription: Unified AI routing layer with task-based provider selection and observability\ncategory: specs\nspecCategory: operations\nicon: lamp-ceiling\nlastUpdated: \"2026-01-29\"\naliases: []\ntags:\n - ai-integration\n - infrastructure\n - cloudflare-workers\n - openrouter\n---\n\n# Lumen — AI Gateway\n\n```\n ╭─────────────────────╮\n ╱ ╲\n ╱ ╭───────────────╮ ╲\n │ ╱ ╲ │\n │ │ │ │\n │ │ · ✦ · · │ │\n │ │ · · │ │\n │ │ · LUMEN · │ │\n │ │ · · │ │\n │ │ · · ✦ · │ │\n │ │ │ │\n │ ╲ ╱ │\n ╲ ╰───────────────╯ ╱\n ╲ ╱\n ╰─────────────────────╯\n\n The hollow center of a tube.\n The void through which everything flows.\n The darkness that contains illumination.\n```\n\n> _Light from the void._\n\nGrove's unified AI gateway. Every AI request passes through this hollow center: Wisp's writing assistance, Thorn's content moderation, Timeline's summaries, Fireside's conversations. One interface. Intelligent routing. The void through which intelligence flows.\n\n**Public Name:** Lumen\n**Internal Name:** GroveLumen\n**Domain:** _(internal service)_\n**Last Updated:** January 2026\n\nIn anatomy, a lumen is the hollow center of a tube: the empty space inside blood vessels, intestines, airways. It's not the wall. It's not the tissue. It's the void through which everything flows. But lumen also means light. The same word for darkness and illumination.\n\nThe paradox is the point. The hollow that carries light.\n\n---\n\n## Overview\n\nLumen is Grove's unified AI gateway: a single interface that routes all AI requests to the appropriate models, handles authentication and rate limiting, scrubs sensitive data, normalizes responses, and logs usage.\n\n**The problem it solves:**\n\n- AI integration scattered across services (Wisp, Thorn, Timeline, Fireside)\n- Hardcoded model choices that can't adapt\n- No unified rate limiting or quota management\n- No consistent PII scrubbing or safety checks\n- Overkill models for simple tasks (DeepSeek v3 for basic moderation)\n\n**The solution:**\n\n```typescript\n// Before: Scattered, hardcoded\nconst response = await fetch(\"https://openrouter.ai/api/v1/chat\", {\n\theaders: { Authorization: `Bearer ${OPENROUTER_KEY}` },\n\tbody: JSON.stringify({ model: \"deepseek/deepseek-chat\", messages }),\n});\n\n// After: One call, intelligent routing\nconst response = await Lumen.inference({\n\ttask: \"moderation\",\n\tinput: userContent,\n\ttenant: tenantId,\n});\n```\n\n**One sentence:** _\"Grove talks to AI through Lumen.\"_\n\n---\n\n## Architecture\n\n```\n┌─────────────────────────────────────────────────────────────────────────────┐\n│ GROVE SERVICES │\n│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │\n│ │ Wisp │ │ Thorn │ │ Timeline │ │ Fireside │ │\n│ │ (writing) │ │(moderation) │ │ (summaries) │ │ (chat) │ │\n│ └──────┬──────┘ └──────┬──────┘ └──────┬──────┘ └──────┬──────┘ │\n└─────────┼────────────────┼────────────────┼────────────────┼────────────────┘\n │ │ │ │\n │ Lumen.inference({ task, input, tenant }) │\n │ │ │ │\n └────────────────┴────────┬───────┴────────────────┘\n │\n┌───────────────────────────────────┴─────────────────────────────────────────┐\n│ LUMEN (Cloudflare Worker) │\n│ │\n│ ┌────────────────────────────────────────────────────────────────────────┐ │\n│ │ Pre-Processing │ │\n│ │ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ │\n│ │ │ PII Scrubber │ │ Rate Limiter │ │ Quota Check │ │ │\n│ │ └──────────────┘ └──────────────┘ └──────────────┘ │ │\n│ └────────────────────────────────┬───────────────────────────────────────┘ │\n│ │ │\n│ ┌────────────────────────────────┴───────────────────────────────────────┐ │\n│ │ Task Router │ │\n│ │ │ │\n│ │ task: \"moderation\" → GPT-oss Safeguard 20B (OpenRouter) │ │\n│ │ task: \"generation\" → DeepSeek v3.2 (OpenRouter) │ │\n│ │ task: \"summary\" → DeepSeek v3.2 (OpenRouter) │ │\n│ │ task: \"embedding\" → BGE-M3 (OpenRouter) │ │\n│ │ task: \"chat\" → DeepSeek v3.2 (OpenRouter) │ │\n│ │ task: \"image\" → Gemini 2.5 Flash (OpenRouter) │ │\n│ │ task: \"code\" → DeepSeek v3.2 (OpenRouter) │ │\n│ │ │ │\n│ └────────────────────────────────┬───────────────────────────────────────┘ │\n│ │ │\n│ ┌────────────────────────────────┴───────────────────────────────────────┐ │\n│ │ Post-Processing │ │\n│ │ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ │\n│ │ │ Normalize │ │ Add Meta │ │ Log Usage │ │ │\n│ │ │ Response │ │ (model, │ │ (tenant, │ │ │\n│ │ │ │ │ tokens) │ │ tokens) │ │ │\n│ │ └──────────────┘ └──────────────┘ └──────────────┘ │ │\n│ └────────────────────────────────────────────────────────────────────────┘ │\n│ │\n│ ┌────────────────────────────────────────────────────────────────────────┐ │\n│ │ Cloudflare AI Gateway │ │\n│ │ (caching, rate limiting, logging, analytics) │ │\n│ └────────────────────────────────────────────────────────────────────────┘ │\n│ │\n└───────────────────────────────────┬─────────────────────────────────────────┘\n │\n┌───────────────────────────────────┴─────────────────────────────────────────┐\n│ AI PROVIDERS │\n│ │\n│ ┌──────────────────────────────────┐ ┌──────────────────────────────────┐ │\n│ │ OpenRouter │ │ Cloudflare Workers AI │ │\n│ │ (Primary) │ │ (Fallback) │ │\n│ │ │ │ │ │\n│ │ GPT-oss Safeg. Kimi K2 │ │ LlamaGuard 3 ShieldGemma │ │\n│ │ DeepSeek v3.2 Gemini Flash │ │ BGE Base │ │\n│ │ LlamaGuard 4 Claude Haiku │ │ │ │\n│ │ BGE-M3 Qwen3 Embed │ │ │ │\n│ └──────────────────────────────────┘ └──────────────────────────────────┘ │\n│ │\n└─────────────────────────────────────────────────────────────────────────────┘\n```\n\n---\n\n## Task Types & Routing\n\nLumen routes requests based on task type, selecting the optimal model for each job.\n\n### Task Registry\n\n| Task | Primary Model | Provider | Fallback Chain | Use Case |\n| --------------- | ---------------------- | ------------- | ------------------------------------ | --------------------- |\n| `moderation` | GPT-oss Safeguard 20B | OpenRouter | LlamaGuard 4 12B → DeepSeek V3.2 | Content safety checks |\n| `generation` | DeepSeek v3.2 | OpenRouter | Kimi K2 → Llama 3.3 70B | Long-form writing |\n| `summary` | DeepSeek v3.2 | OpenRouter | Kimi K2 → Llama 3.3 70B | Summarization |\n| `embedding` | BGE-M3 | OpenRouter | Qwen3 Embed → BGE Base (CF) | Vector embeddings |\n| `chat` | DeepSeek v3.2 | OpenRouter | Kimi K2 → Llama 3.3 70B | Conversational |\n| `image` | Gemini 2.5 Flash | OpenRouter | Claude Haiku 4.5 | Image analysis |\n| `code` | DeepSeek v3.2 | OpenRouter | Claude Haiku 4.5 → Kimi K2 | Code generation |\n| `transcription` | Whisper Large v3 Turbo | Cloudflare AI | Whisper → Whisper Tiny EN | Voice-to-text |\n\n### Why This Routing?\n\n**Moderation (GPT-oss Safeguard 20B):**\nGPT-oss Safeguard 20B is OpenAI's purpose-built safety reasoning model (Apache 2.0). It interprets Grove's moderation policy at inference time via chain-of-thought reasoning, returning real confidence scores and audit-ready reasoning traces. LlamaGuard 4 12B serves as first fallback (fast binary classifier), with DeepSeek V3.2 as a general-purpose last resort. All three models route through OpenRouter.\n\n**Generation/Chat/Summary (DeepSeek v3.2):**\nDeepSeek v3.2 offers excellent quality at $0.25/$0.38 per million tokens. Kimi K2 provides strong reasoning as first fallback, with Llama 3.3 70B as a reliable tertiary option. All via OpenRouter for unified access.\n\n**Embeddings (BGE-M3):**\nBGE-M3 on OpenRouter provides high-quality multilingual embeddings at $0.02/M tokens. Qwen3 Embed offers a solid fallback, with CF Workers AI's BGE Base as a free last resort.\n\n**Image Analysis (Gemini 2.5 Flash):**\nGemini 2.5 Flash offers excellent vision capabilities at just $0.15/$0.60 per million tokens. That's ~6x cheaper than Claude Haiku while delivering comparable quality. Claude Haiku 4.5 serves as premium fallback if needed.\n\n**Code (DeepSeek v3.2):**\nDeepSeek excels at code generation. Claude Haiku provides a quality fallback for complex cases, with Kimi K2's reasoning capabilities as tertiary.\n\n**Transcription (Whisper Large v3 Turbo):**\nCloudflare Workers AI provides Whisper models at zero cost. The turbo variant offers fast, accurate transcription for voice input. Fallback to standard Whisper, then Whisper Tiny EN for resilience. All transcription stays on Cloudflare's edge network.\n\n---\n\n## API Design\n\n### Core Interface\n\n```typescript\ninterface LumenRequest {\n\ttask: TaskType;\n\tinput: string | Message[];\n\ttenant?: string;\n\toptions?: {\n\t\tmodel?: string; // Override default model\n\t\tmaxTokens?: number;\n\t\ttemperature?: number;\n\t\tstream?: boolean;\n\t\tmetadata?: Record;\n\t};\n}\n\ninterface LumenResponse {\n\tcontent: string;\n\tmetadata: {\n\t\tmodel: string;\n\t\tprovider: string;\n\t\tinputTokens: number;\n\t\toutputTokens: number;\n\t\tlatencyMs: number;\n\t\tcached: boolean;\n\t};\n}\n\ntype TaskType =\n\t| \"moderation\"\n\t| \"generation\"\n\t| \"summary\"\n\t| \"embedding\"\n\t| \"chat\"\n\t| \"image\"\n\t| \"code\"\n\t| \"transcription\";\n```\n\n### Usage Examples\n\n```typescript\nimport { Lumen } from \"@autumnsgrove/lattice/lumen\";\n\n// Content moderation (Thorn)\nconst safety = await Lumen.inference({\n\ttask: \"moderation\",\n\tinput: userSubmittedContent,\n\ttenant: \"autumn\",\n});\n\nif (!safety.content.includes(\"safe\")) {\n\tthrow new Error(\"Content flagged for review\");\n}\n\n// Writing assistance (Wisp)\nconst suggestions = await Lumen.inference({\n\ttask: \"generation\",\n\tinput: [\n\t\t{\n\t\t\trole: \"system\",\n\t\t\tcontent: \"You are a writing assistant. Suggest improvements.\",\n\t\t},\n\t\t{ role: \"user\", content: draftText },\n\t],\n\ttenant: \"autumn\",\n\toptions: { maxTokens: 500 },\n});\n\n// Timeline summary\nconst summary = await Lumen.inference({\n\ttask: \"summary\",\n\tinput: `Summarize these commits:\\n${commitMessages.join(\"\\n\")}`,\n\ttenant: \"autumn\",\n});\n\n// Embedding for search\nconst embedding = await Lumen.inference({\n\ttask: \"embedding\",\n\tinput: \"How do I customize my theme?\",\n});\n\n// Voice transcription (Scribe)\nconst transcript = await Lumen.transcribe({\n\taudio: audioData, // Uint8Array\n\ttenant: \"autumn\",\n\toptions: { mode: \"draft\" }, // \"raw\" or \"draft\"\n});\n// transcript.text, transcript.gutterContent (for draft mode)\n```\n\n### Streaming Support\n\n```typescript\nconst stream = await Lumen.inference({\n\ttask: \"chat\",\n\tinput: messages,\n\toptions: { stream: true },\n});\n\nfor await (const chunk of stream) {\n\tprocess.stdout.write(chunk.content);\n}\n```\n\n---\n\n## Scribe: Voice Transcription\n\nLumen handles voice-to-text transcription via Cloudflare Workers AI's Whisper models. Two modes support different use cases.\n\n### Transcription Modes\n\n**Raw Mode** — Direct 1:1 transcription. What you say is what you get.\n\n```typescript\nconst result = await lumen.transcribe({\n\taudio: audioData,\n\ttenant: \"autumn\",\n\toptions: { mode: \"raw\" },\n});\n// result.text = \"Hello world, this is exactly what was said.\"\n// result.wordCount = 8\n// result.duration = 2.5\n```\n\n**Draft Mode** — AI-structured transcription with auto-generated Vines. The raw transcript passes through an LLM that cleans filler words, structures the content, and extracts tangents as gutter content.\n\n```typescript\nconst result = await lumen.transcribe({\n\taudio: audioData,\n\ttenant: \"autumn\",\n\toptions: { mode: \"draft\" },\n});\n// result.text = \"Cleaned, structured transcript.\"\n// result.rawTranscript = \"Original unstructured transcript with um, uh...\"\n// result.gutterContent = [{ type: \"vine\", content: \"A tangent\", anchor: \"word\" }]\n```\n\n### Transcription Flow\n\n```\n┌────────────────────────────────────────────────────────────────────────┐\n│ AUDIO INPUT │\n│ (Uint8Array, max 25MB) │\n└─────────────────────────────────┬──────────────────────────────────────┘\n │\n ▼\n┌────────────────────────────────────────────────────────────────────────┐\n│ CLOUDFLARE WHISPER │\n│ │\n│ Primary: @cf/openai/whisper-large-v3-turbo │\n│ Fallback: @cf/openai/whisper → @cf/openai/whisper-tiny-en │\n│ │\n│ Output: { text, word_count, words[]?, duration? } │\n└─────────────────────────────────┬──────────────────────────────────────┘\n │\n ┌─────────────┴─────────────┐\n │ │\n mode=\"raw\" mode=\"draft\"\n │ │\n ▼ ▼\n ┌───────────────┐ ┌─────────────────────────┐\n │ PII Scrub │ │ LLM Structuring │\n │ Return text │ │ (DeepSeek v3.2) │\n └───────────────┘ │ │\n │ - Clean filler words │\n │ - Structure content │\n │ - Extract Vines │\n │ │\n └────────────┬────────────┘\n │\n ▼\n ┌─────────────────────────┐\n │ PII Scrub │\n │ Return structured text │\n │ + gutterContent[] │\n └─────────────────────────┘\n```\n\n### Quota Consumption\n\n- **Raw mode:** 1 transcription quota unit\n- **Draft mode:** 1 transcription + 1 generation quota unit (two-step process)\n\n### PII Scrubbing\n\nTranscription output passes through the same PII scrubber as other Lumen tasks. Email addresses, phone numbers, and other sensitive patterns are replaced with tokens before returning to the client.\n\n### Browser Integration (Flow Editor)\n\nThe VoiceInput component provides a microphone button for the Flow editor:\n\n- Press-and-hold recording (default)\n- Toggle mode for accessibility\n- Audio level visualization\n- Automatic codec detection (webm/opus preferred, mp4 for Safari)\n- Keyboard shortcut: `Cmd/Ctrl+Shift+U`\n\n---\n\n## Pre-Processing Pipeline\n\n### PII Scrubbing\n\nBefore any request leaves Grove:\n\n```typescript\nconst scrubbed = await scrubPII(input, {\n\tpatterns: [\n\t\t\"email\", // user@domain.com → [EMAIL]\n\t\t\"phone\", // +1-555-123-4567 → [PHONE]\n\t\t\"ssn\", // 123-45-6789 → [SSN]\n\t\t\"creditCard\", // 4111... → [CARD]\n\t\t\"ipAddress\", // 192.168.1.1 → [IP]\n\t],\n\tcustomPatterns: tenant.piiPatterns, // Per-tenant rules\n});\n```\n\n### Rate Limiting\n\nPer-tenant and per-task limits:\n\n```typescript\nconst limits = {\n\tmoderation: { rpm: 1000, daily: 50000 },\n\tgeneration: { rpm: 100, daily: 5000 },\n\tchat: { rpm: 60, daily: 2000 },\n\timage: { rpm: 10, daily: 100 },\n};\n\n// Checked before routing\nawait rateLimiter.check(tenant, task);\n```\n\n### Quota Management\n\nIntegrated with Grove's tier system:\n\n| Tier | Moderation | Generation | Chat | Image | Transcription |\n| --------- | ---------- | ---------- | --------- | --------- | ------------- |\n| Free | 100/day | 10/day | 5/day | 0 | 10/day |\n| Seedling | 1,000/day | 100/day | 50/day | 10/day | 100/day |\n| Sapling | 5,000/day | 500/day | 200/day | 50/day | 500/day |\n| Oak | 20,000/day | 2,000/day | 1,000/day | 200/day | 2,000/day |\n| Evergreen | Unlimited | 10,000/day | 5,000/day | 1,000/day | 10,000/day |\n\n---\n\n## Post-Processing Pipeline\n\n### Response Normalization\n\nAll responses follow a consistent format regardless of provider:\n\n```typescript\n// OpenRouter returns\n{ choices: [{ message: { content: \"...\" } }] }\n\n// Anthropic returns\n{ content: [{ type: \"text\", text: \"...\" }] }\n\n// Workers AI returns\n{ response: \"...\" }\n\n// Lumen normalizes to\n{ content: \"...\", metadata: { ... } }\n```\n\n### Usage Logging\n\nEvery request is logged (without content) for:\n\n- Tenant usage tracking\n- Cost attribution\n- Rate limit enforcement\n- Analytics (via Rings)\n\n```typescript\nawait logUsage({\n\ttenant,\n\ttask,\n\tmodel: response.metadata.model,\n\tinputTokens: response.metadata.inputTokens,\n\toutputTokens: response.metadata.outputTokens,\n\tlatencyMs: response.metadata.latencyMs,\n\tcached: response.metadata.cached,\n\ttimestamp: Date.now(),\n});\n```\n\n---\n\n## Cloudflare AI Gateway Integration\n\nLumen uses Cloudflare AI Gateway as its underlying infrastructure:\n\n```\n┌─────────────────────────────────────────────────────────────────┐\n│ Cloudflare AI Gateway │\n│ │\n│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │\n│ │ Caching │ │ Logging │ │ Analytics │ │\n│ │ (semantic) │ │ (requests) │ │ (costs) │ │\n│ └──────────────┘ └──────────────┘ └──────────────┘ │\n│ │\n│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │\n│ │Rate Limiting │ │ Fallback │ │ Retries │ │\n│ │ (provider) │ │ (chains) │ │ (auto) │ │\n│ └──────────────┘ └──────────────┘ └──────────────┘ │\n│ │\n└─────────────────────────────────────────────────────────────────┘\n```\n\n**Benefits:**\n\n- **Caching:** Semantic caching reduces redundant calls\n- **Logging:** Full request/response logging (redacted)\n- **Analytics:** Cost tracking per gateway\n- **Rate Limiting:** Provider-level limits (distinct from tenant limits)\n- **Fallback:** Automatic failover to backup providers\n- **Retries:** Exponential backoff on transient failures\n\n---\n\n## Provider Configuration\n\nLumen uses just two providers: OpenRouter as the universal gateway for all primary inference, and Cloudflare Workers AI as a fast, free fallback layer.\n\n### OpenRouter (Primary Provider)\n\n```typescript\nconst openRouter = {\n\tbaseUrl: \"https://openrouter.ai/api/v1\",\n\tapiKey: env.OPENROUTER_API_KEY,\n\tmodels: {\n\t\t// Generation/Chat/Summary\n\t\tgeneration: \"deepseek/deepseek-v3.2\",\n\t\tchat: \"deepseek/deepseek-v3.2\",\n\t\tsummary: \"deepseek/deepseek-v3.2\",\n\t\t// Moderation\n\t\tmoderation: \"openai/gpt-oss-safeguard-20b\",\n\t\t// Image\n\t\timage: \"google/gemini-2.5-flash\",\n\t\t// Code\n\t\tcode: \"deepseek/deepseek-v3.2\",\n\t\t// Embeddings\n\t\tembedding: \"baai/bge-m3\",\n\t\t// Fallback models\n\t\tfallback_chat: \"moonshotai/kimi-k2-0905\",\n\t\tfallback_vision: \"anthropic/claude-haiku-4.5\",\n\t\tfallback_embed: \"qwen/qwen3-embedding-8b\",\n\t},\n\theaders: {\n\t\t\"HTTP-Referer\": \"https://grove.place\",\n\t\t\"X-Title\": \"Grove\",\n\t},\n};\n```\n\n### Workers AI (Fallback Provider)\n\n```typescript\nconst workersAI = {\n\tbinding: env.AI, // Cloudflare Workers AI binding\n\tmodels: {\n\t\tmoderation: \"@cf/meta/llama-guard-3-8b\", // CF-local moderation (not in primary cascade)\n\t\tmoderation_alt: \"@hf/google/shieldgemma-2b\", // Legacy tertiary (not in primary cascade)\n\t\tembedding: \"@cf/baai/bge-base-en-v1.5\", // Fallback embeddings\n\t},\n};\n```\n\n---\n\n## Error Handling\n\n### Error Types\n\n```typescript\ntype LumenError =\n\t| { code: \"RATE_LIMITED\"; retryAfter: number }\n\t| { code: \"QUOTA_EXCEEDED\"; task: TaskType; limit: number }\n\t| { code: \"PROVIDER_ERROR\"; provider: string; message: string }\n\t| { code: \"INVALID_TASK\"; task: string }\n\t| { code: \"PII_DETECTED\"; fields: string[] }\n\t| { code: \"CONTENT_BLOCKED\"; reason: string };\n```\n\n### Fallback Chain\n\n```typescript\nconst fallbackChains = {\n\tgeneration: [\n\t\t{ provider: \"openrouter\", model: \"deepseek/deepseek-v3.2\" },\n\t\t{ provider: \"openrouter\", model: \"moonshotai/kimi-k2-0905\" },\n\t\t{ provider: \"openrouter\", model: \"meta-llama/llama-3.3-70b-instruct\" },\n\t],\n\tmoderation: [\n\t\t{ provider: \"openrouter\", model: \"openai/gpt-oss-safeguard-20b\" },\n\t\t{ provider: \"openrouter\", model: \"meta-llama/llama-guard-4-12b\" },\n\t\t{ provider: \"openrouter\", model: \"deepseek/deepseek-v3.2\" },\n\t],\n\timage: [\n\t\t{ provider: \"openrouter\", model: \"google/gemini-2.5-flash\" },\n\t\t{ provider: \"openrouter\", model: \"anthropic/claude-haiku-4.5\" },\n\t],\n\tembedding: [\n\t\t{ provider: \"openrouter\", model: \"baai/bge-m3\" },\n\t\t{ provider: \"openrouter\", model: \"qwen/qwen3-embedding-8b\" },\n\t\t{ provider: \"cloudflare-ai\", model: \"@cf/baai/bge-base-en-v1.5\" },\n\t],\n\ttranscription: [\n\t\t{ provider: \"cloudflare-ai\", model: \"@cf/openai/whisper-large-v3-turbo\" },\n\t\t{ provider: \"cloudflare-ai\", model: \"@cf/openai/whisper\" },\n\t\t{ provider: \"cloudflare-ai\", model: \"@cf/openai/whisper-tiny-en\" },\n\t],\n};\n\n// If primary fails, try fallbacks in order\nfor (const { provider, model } of fallbackChains[task]) {\n\ttry {\n\t\treturn await callProvider(provider, model, input);\n\t} catch (e) {\n\t\tif (isRetryable(e)) continue;\n\t\tthrow e;\n\t}\n}\n```\n\n---\n\n## Implementation\n\n### File Structure\n\n```\nlibs/engine/src/lib/lumen/\n├── index.ts # Public exports & factory\n├── types.ts # Type definitions (includes ScribeMode, GutterItem)\n├── client.ts # LumenClient class (includes transcribe())\n├── router.ts # Task → model routing\n├── config.ts # Task registry & model configs (includes Whisper)\n├── errors.ts # Custom error types\n├── providers/\n│ ├── index.ts # Provider factory & registry\n│ ├── types.ts # Provider interface\n│ ├── cloudflare-ai.ts # CF Workers AI (moderation, embeddings, transcription)\n│ └── openrouter.ts # OpenRouter (primary)\n├── prompts/\n│ └── scribe-draft.ts # Draft mode LLM structuring prompt\n├── pipeline/\n│ ├── preprocessor.ts # PII scrubbing, input validation\n│ └── postprocessor.ts # Response normalization, usage logging\n└── quota/\n ├── tracker.ts # D1-backed usage tracking\n └── limits.ts # Tier-based daily limits\n```\n\n### Package Exports\n\n```json\n{\n\t\"exports\": {\n\t\t\"./lumen\": {\n\t\t\t\"types\": \"./dist/lib/lumen/index.d.ts\",\n\t\t\t\"import\": \"./dist/lib/lumen/index.js\"\n\t\t}\n\t}\n}\n```\n\n---\n\n## Security Considerations\n\n1. **No content logging** — Request/response content never stored\n2. **PII scrubbing** — Sensitive data removed before external calls\n3. **API key isolation** — Keys stored as Cloudflare secrets\n4. **Tenant isolation** — Requests scoped to tenant, no cross-tenant access\n5. **Rate limiting** — Prevents abuse, protects quotas\n6. **Audit trail** — Usage logged (without content) for accountability\n\n---\n\n## Cost Analysis\n\n### Per-Million Token Costs (USD)\n\n| Task | Model | Input | Output |\n| ----------------------- | ---------------- | ----- | ------ |\n| Generation/Chat/Summary | DeepSeek v3.2 | $0.25 | $0.38 |\n| Fallback | Kimi K2 | $0.39 | $1.90 |\n| Tertiary | Llama 3.3 70B | $0.10 | $0.32 |\n| Image | Gemini 2.5 Flash | $0.15 | $0.60 |\n| Image Fallback | Claude Haiku 4.5 | $1.00 | $5.00 |\n| Moderation | GPT-oss Safeg. | $0.075| $0.30 |\n| Moderation Fallback | LlamaGuard 4 | $0.10 | $0.10 |\n| Embedding | BGE-M3 | $0.02 | - |\n| Embedding Fallback | Qwen3 Embed | $0.02 | - |\n| CF Workers AI | All models | Free | Free |\n\n### Cost Comparison: Old vs New\n\n| Task | Old (Claude Sonnet) | New (Primary) | Savings |\n| --------------- | ------------------- | ------------- | ------- |\n| Image Analysis | $3.00/$15.00 | $0.15/$0.60 | ~95% |\n| Code Generation | $3.00/$15.00 | $0.25/$0.38 | ~97% |\n| Generation | $0.30/$1.00 | $0.25/$0.38 | ~60% |\n\n### Monthly Estimates (Per Active User)\n\n| Usage Level | Moderation | Generation | Chat | Image | Total |\n| ----------- | ---------- | ---------- | --------- | -------- | ------ |\n| Light | 100 calls | 20 calls | 10 calls | 5 calls | ~$0.02 |\n| Medium | 500 calls | 100 calls | 50 calls | 20 calls | ~$0.10 |\n| Heavy | 2000 calls | 500 calls | 200 calls | 50 calls | ~$0.40 |\n\n---\n\n## Implementation Checklist\n\n### Phase 1: Foundation\n\n- [ ] Create `libs/engine/src/lib/lumen/` structure\n- [ ] Define types and interfaces\n- [ ] Implement task router\n- [ ] Add Workers AI provider (moderation, embeddings)\n- [ ] Add OpenRouter provider (generation, chat)\n- [ ] Basic pre-processing (rate limiting)\n\n### Phase 2: Integration\n\n- [ ] Migrate Thorn to use Lumen for moderation\n- [ ] Migrate Wisp to use Lumen for generation\n- [ ] Migrate Timeline curio to use Lumen for summaries\n- [ ] Add PII scrubbing pipeline\n- [ ] Add quota management\n\n### Phase 3: Production\n\n- [ ] Add Cloudflare AI Gateway integration\n- [ ] Implement fallback chains\n- [ ] Add comprehensive error handling\n- [ ] Usage logging to Rings\n- [ ] Performance monitoring via Vista\n\n### Phase 4: Advanced\n\n- [ ] Streaming support\n- [ ] Semantic caching\n- [ ] A/B testing for models\n- [ ] Custom model overrides per tenant\n- [ ] Cost attribution dashboard\n\n---\n\n## References\n\n- [Cloudflare Workers AI](https://developers.cloudflare.com/workers-ai/)\n- [Cloudflare AI Gateway](https://developers.cloudflare.com/ai-gateway/)\n- [OpenRouter API](https://openrouter.ai/docs)\n- [GPT-oss Safeguard](https://openai.com/index/introducing-gpt-oss-safeguard/)\n- [LlamaGuard 4](https://ai.meta.com/research/publications/llama-guard-llm-based-input-output-safeguard-for-human-ai-conversations/)\n- [Grove Naming Guide](/docs/philosophy/grove-naming.md)\n\n---\n\n_Light from the void._\n\n**Last updated:** January 2026\n**Status:** Implementation Complete (PR #415)\n**Author:** Autumn Brown\n"}}}}