🪟 Windows ServerSchnellladesäulen: Wenn der Ladestecker zickt - Golem.de(17.09.2026 um 23:29 Uhr)
🕵️ HackingPOL-MK: Betrug beim Online-Kauf - Presseportal(17.09.2026 um 17:57 Uhr)
🔧 ProgrammierungAgentic CLI customizations now in the usage metrics API(17.09.2026 um 23:08 Uhr)
🔧 ProgrammierungCopilot impact dashboard now shows feature engagement(17.09.2026 um 23:47 Uhr)
🪟 Windows ServerSchnellladesäulen: Wenn der Ladestecker zickt - Golem.de(17.09.2026 um 23:29 Uhr)
🕵️ HackingPOL-MK: Betrug beim Online-Kauf - Presseportal(17.09.2026 um 17:57 Uhr)
🔧 ProgrammierungAgentic CLI customizations now in the usage metrics API(17.09.2026 um 23:08 Uhr)
🔧 ProgrammierungCopilot impact dashboard now shows feature engagement(17.09.2026 um 23:47 Uhr)
🔧 Programmierung 🕛 vor 4 Monaten 6 Min Lesezeit
0

How I built a shared Claude Haiku client with system-prompt caching for batch ETL

↗ Quelle (dev.to)
🗣️ Stimme:
📑 Inhaltsübersicht

One of the quieter architectural decisions in this project is how the three directory sites share a single Claude Haiku client. It lives in packages/shared/src/claude/index.ts and every ETL job — model summaries, game recommendations, open-source comparisons — goes through it. The interesting part isn't the singleton itself. It's the prompt caching setup and the failure path.






Why a shared library at all



Each app has its own ETL folder with its own generate-content.ts. But they all need the same two things: a consistent way to call claude-haiku-4-5-20251001, and a consistent way to handle the case where the API key isn't present (local dev, CI runs that don't need content).



Copying a new Anthropic({ apiKey }) call into three places would work, but it also means three places to update model names, three places to handle response errors, and three places where the caching setup could drift. So I extracted it.



The shared function signature is simple on purpose:




CODE
export async function generate(opts: GenerateOptions): Promise<GenerateResult> {






GenerateOptions has five fields: systemPrompt, userPrompt, model, maxTokens, and cacheSystem. The caller decides whether to cache. The library handles the mechanics.






The cacheSystem pattern



Claude's prompt caching works by marking message blocks with cache_control: { type: "ephemeral" }. When you send the same cached block in subsequent requests within a session (or across requests within the 5-minute TTL), Anthropic charges you the cached-read rate instead of the full input rate.



For batch generation — 100 models in a loop, all sharing the same system prompt — this can cut input token costs significantly. I don't have 30-day cost data yet, so I won't throw out a percentage. What I can say is that the implementation itself is two lines:




CODE
const systemBlock = opts.cacheSystem
? [{ type: "text" as const, text: opts.systemPrompt, cache_control: { type: "ephemeral" as const } }]
: opts.systemPrompt;






When cacheSystem is false, system gets a plain string and the Anthropic SDK handles it normally. When it's true, the system gets a single-element array with the cache marker. The rest of the messages.create call is identical.



The callers that loop over many entries — generate-content.ts and compare.ts — both pass cacheSystem: true. One-off calls or places where the system prompt varies per request wouldn't benefit, so they'd pass false or omit it.



One thing I haven't wired up yet: logging cache_creation_input_tokens and cache_read_input_tokens from res.usage. Those numbers come back in every response and would let me see the actual hit rate. Right now I surface the usage object in GenerateResult but nothing downstream reads it. That's on the list.






Defensive JSON parsing



Every content-generation prompt asks Claude to return a JSON object with specific keys. This works well most of the time. It doesn't work 100% of the time.



The Haiku model occasionally wraps the JSON in markdown code fences, adds a leading explanation sentence, or — rarely — returns structurally valid JSON that's missing one of the expected keys. So I wrote parseOrFallback:




CODE
function parseOrFallback(text: string, fb: GeneratedContent): GeneratedContent {
try {
const jsonMatch = text.match(/\{[\s\S]*\}/);
if (!jsonMatch) return fb;
const parsed = JSON.parse(jsonMatch[0]);
return {
summary: parsed.summary ?? fb.summary,
use_cases: Array.isArray(parsed.use_cases) ? parsed.use_cases : fb.use_cases,
pros: Array.isArray(parsed.pros) ? parsed.pros : fb.pros,
cons: Array.isArray(parsed.cons) ? parsed.cons : fb.cons,
};
} catch {
return fb;
}
}






The regex \{[\s\S]*\} extracts the first JSON object from the response text, ignoring any surrounding prose or fence markers. Then each field is validated individually: if pros is present but isn't an array, I use the fallback value for that field specifically rather than discarding the whole response.



The fallback content (fallbackContent()) generates generic but syntactically valid entries using the model name and pipeline tag. These get stored in the database with model_used = 'fallback-template' so I can query later and re-generate them when the API key is available or when the prompt improves.






No API key, no crash



Local development and CI jobs that don't touch content generation don't have ANTHROPIC_API_KEY set. The ETL scripts detect this with !!process.env.ANTHROPIC_API_KEY and route all rows to the fallback path instead of attempting the API call. The database still gets populated. The build still runs. No keys need to be mocked or stubbed.



This means a fresh clone of the repo can run pnpm etl and get a working site — it'll just have generic entry text instead of generated content. For prototyping new page layouts or testing the Turso connection, that's exactly the right trade-off.



The model_used column made this pattern easy to operationalize. After the first real content-generation run, I could query:




CODE
SELECT model_used, COUNT(*) FROM model_content GROUP BY model_used;






and immediately see how many entries were actually generated versus fell back. That count drives whether to re-run with a higher ETL_LIMIT.






What I'd do differently



The biggest gap is the missing usage logging. I return res.usage in GenerateResult, but none of the calling scripts do anything with it. Adding a single console.log of cache_read_input_tokens per batch run would give me real data to show. I'll fix that before the month-1 cost report.



I'd also consider adding a generateBatch function that accepts an array of user prompts and fires them sequentially in a single function call. Right now generate-content.ts loops externally. Moving the loop inside would make it easier to add rate limiting and retry logic in one place rather than per-caller.



The singleton Anthropic client is fine for the current scale — one ETL run at a time, no concurrency. If I ever run two ETL scripts in the same process simultaneously, the singleton is still safe because the Anthropic SDK is stateless between requests. But it's worth keeping in mind.



Finally: the system prompts are hardcoded strings inside each generate-content.ts. If I were building this again, I'd probably move them to a prompts/ directory as plain .txt files. Easier to diff, easier for non-TypeScript people to read, easier to add versioning later.



The pattern works well for what it is — a lightweight, shared abstraction that keeps caching consistent and keeps local dev functional. Month 1 numbers will tell whether the caching is actually saving meaningful cost, but the defensive parsing has already prevented at least a few bad database rows on the first content run.



Part of an ongoing 6-month experiment running three AI-curated directory sites. The technical claims here are real; this article was AI-assisted.

Vollständiger Original-Artikel
Den kompletten Beitrag mit allen Details direkt auf dev.to lesen.
↗ Original-Artikel auf dev.to lesen
Wie bewertest du diesen Beitrag?
1 Klick Feedback
Teilen mit Netzwerk & Team:

Community-Analysen & Experten-Meinungen 0

Verfasse deine eigene Analyse, teile Workarounds oder diskutiere diesen Vorfall im Blog.
Noch keine Community-Analyse verfasst. Markiere einen Textabschnitt oder klicke oben auf Eigene Analyse verfassen“!
Community Pulse: Relevanz-Einschätzung
1 Klick Experten-Votum
🔴 Akute Relevanz 0%
🟡 In Evaluierung 0%
🟢 Keine Auswirkung 0%
Spannende Innovation 0%
Verwandte Story-Cluster & Quellen (Vektor-KI)
Port 8095 Engine
1 Quelle
Small AI models let drones autonomously identify and attack battlefield targets
1 Quelle
Here’s What the AI Apocalypse Could Look Like
1 Quelle
The FAA’s plan to fix air traffic? $875 million worth of AI
Ähnliche Beiträge
🔍 Verwandte News

Auch interessante Nachrichten How I built a shared Claude Haiku client with system-prompt caching for batch ETL

Thematisch verwandte Begriffe: built, shared, Claude, Haiku · 6 Treffer

Laden...

Videos werden geladen ...

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...