From 1ece62ce8cc7c5ab1b976eaf54493bd20c38486c Mon Sep 17 00:00:00 2001 From: Meleki Date: Wed, 13 May 2026 06:46:26 +0200 Subject: [PATCH] =?UTF-8?q?feat(cache):=201h-Prompt-Cache-TTL=20f=C3=BCr?= =?UTF-8?q?=20API=20und=20Claude=20CLI?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Anthropic hat Anfang März 2026 die Default-Cache-TTL stillschweigend von 1h auf 5min gesenkt. Bei Sessions mit Pausen >5min wird der Cache jedes Mal neu geschrieben (teuer). Dieser Commit aktiviert die extended 1h-TTL über beide Pfade. API-Pfad (AnthropicService): - Beta-Header "extended-cache-ttl-2025-04-11" hinzugefügt - Falsches Top-Level cache_control (Root-Level wurde ignoriert) entfernt - Neue Helper ApplyCacheControlToLast() setzt cache_control idempotent auf das letzte Element via JSON-Roundtrip zu Dictionary - 2 Cache-Breakpoints pro Iteration: * Letztes Tool (cached gesamten Tool-Block) * Letzter System-Prompt-Block (cached System-Prompt) - Sub-Agent-Loop (Preparer/Review/Finalizer): String-System-Prompt wird bei Bedarf in Block-Form mit cache_control konvertiert CLI-Pfad (ClaudeCliService): - Env-Var ENABLE_PROMPT_CACHING_1H=1 für jeden CLI-Child-Prozess gesetzt (offizieller Opt-in seit Claude CLI 2.1.108, gilt für API-Key, Bedrock, Vertex, Foundry) --- Services/AnthropicService.Agents.cs | 17 +++++++++++++- Services/AnthropicService.Streaming.cs | 6 ++++- Services/AnthropicService.cs | 31 +++++++++++++++++++++++++- Services/ClaudeCliService.cs | 5 +++++ 4 files changed, 56 insertions(+), 3 deletions(-) diff --git a/Services/AnthropicService.Agents.cs b/Services/AnthropicService.Agents.cs index d14acf8..48678d1 100644 --- a/Services/AnthropicService.Agents.cs +++ b/Services/AnthropicService.Agents.cs @@ -466,9 +466,24 @@ public partial class AnthropicService }; if (systemPrompt is List spBlocks && spBlocks.Count > 0) + { + // Cache-Breakpoint auf den letzten System-Prompt-Block (1h TTL). + ApplyCacheControlToLast(spBlocks); payload["system"] = spBlocks; + } else if (systemPrompt is string spText && !string.IsNullOrEmpty(spText)) - payload["system"] = spText; + { + // String-System-Prompt → in Block-Form mit cache_control konvertieren. + payload["system"] = new List + { + new Dictionary + { + ["type"] = "text", + ["text"] = spText, + ["cache_control"] = new { type = "ephemeral", ttl = "1h" } + } + }; + } AddToolsToPayload(payload, toolOpts with { Iteration = iterations }); diff --git a/Services/AnthropicService.Streaming.cs b/Services/AnthropicService.Streaming.cs index 0ae42ec..0a2a482 100644 --- a/Services/AnthropicService.Streaming.cs +++ b/Services/AnthropicService.Streaming.cs @@ -93,13 +93,17 @@ public partial class AnthropicService { ["model"] = modelId, ["max_tokens"] = 16384, - ["cache_control"] = new { type = "ephemeral" }, ["messages"] = messages, ["stream"] = true }; if (systemPrompt != null && systemPrompt.Count > 0) + { + // Cache-Breakpoint auf den letzten System-Prompt-Block (1h TTL). + // Idempotent über Iterationen. + ApplyCacheControlToLast(systemPrompt); payload["system"] = systemPrompt; + } var remoteConn = remoteMgr?.GetBySessionId(sessionId); var hasRemote = remoteConn != null; diff --git a/Services/AnthropicService.cs b/Services/AnthropicService.cs index 32e0ab7..b7ee941 100644 --- a/Services/AnthropicService.cs +++ b/Services/AnthropicService.cs @@ -318,6 +318,31 @@ public partial class AnthropicService private Task FinalizeChain(ChainRecord chain, TokenUsage usage, int iterations, string stopReason, System.Diagnostics.Stopwatch sw, string streamErrorInfo = null) => SseHelper.FinalizeChain(_db, chain, usage, iterations, stopReason, sw, streamErrorInfo); + /// + /// Versieht das letzte Element einer Block-Liste (Tools, System-Prompt-Blöcke, etc.) mit + /// einer cache_control-Annotation. Konvertiert das Element über JSON-Roundtrip in ein + /// Dictionary, damit cache_control auch bei anonymous types gesetzt werden kann. + /// Idempotent: Mehrfaches Aufrufen überschreibt nur das cache_control-Feld. + /// + private static void ApplyCacheControlToLast(List blocks, string ttl = "1h") + { + if (blocks == null || blocks.Count == 0) return; + var last = blocks[blocks.Count - 1]; + Dictionary dict; + if (last is Dictionary existing) + { + dict = existing; + } + else + { + var json = JsonSerializer.Serialize(last); + var parsed = JsonSerializer.Deserialize>(json); + dict = parsed ?? new Dictionary(); + } + dict["cache_control"] = new { type = "ephemeral", ttl }; + blocks[blocks.Count - 1] = dict; + } + /// /// Fügt Tool-Definitionen und WebSearch zum Payload hinzu. /// @@ -351,6 +376,10 @@ public partial class AnthropicService tools.Add(new { type = opts.WebSearchVersion, name = "web_search", max_uses = 5 }); // web_search_20260209: Code Execution wird von der API automatisch injected – NICHT manuell hinzufügen! } + // Cache-Breakpoint auf das letzte Tool → cached den gesamten Tool-Block (1h TTL). + // Da Tools pro Iteration neu generiert werden, ist das idempotent. + ApplyCacheControlToLast(tools); + payload["tools"] = tools; var effectiveChoice = (!opts.PlanMode && opts.ToolChoice == "any" && opts.Iteration == 1) ? "any" : "auto"; payload["tool_choice"] = new { type = effectiveChoice }; @@ -405,7 +434,7 @@ public partial class AnthropicService var client = _httpFactory.CreateClient(); client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", apiToken); client.DefaultRequestHeaders.Add("anthropic-version", "2023-06-01"); - client.DefaultRequestHeaders.Add("anthropic-beta", "claude-code-20250219,oauth-2025-04-20,code-execution-2025-08-25"); + client.DefaultRequestHeaders.Add("anthropic-beta", "claude-code-20250219,oauth-2025-04-20,code-execution-2025-08-25,extended-cache-ttl-2025-04-11"); client.DefaultRequestHeaders.Add("User-Agent", "claude-cli/2.1.2 (external, cli)"); client.DefaultRequestHeaders.Add("x-app", "cli"); client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); diff --git a/Services/ClaudeCliService.cs b/Services/ClaudeCliService.cs index 0a356f0..411a29f 100644 --- a/Services/ClaudeCliService.cs +++ b/Services/ClaudeCliService.cs @@ -70,6 +70,11 @@ public class ClaudeCliService StandardErrorEncoding = Encoding.UTF8 }; + // 1h-Prompt-Cache-TTL für Claude CLI aktivieren (seit CLI 2.1.108). + // Anthropic hat Anfang März 2026 den Default von 1h auf 5min gesenkt, + // dieser Flag holt die 1h-TTL zurück (gilt für API-Key, Bedrock, Vertex, Foundry). + psi.Environment["ENABLE_PROMPT_CACHING_1H"] = "1"; + foreach (var arg in args) psi.ArgumentList.Add(arg);