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);