feat(cache): 1h-Prompt-Cache-TTL für API und Claude CLI

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)
This commit is contained in:
Meleki committed 2026-05-13 06:46:26 +02:00
1 parent e87d7c4eec
commit 1ece62ce8c
4 files changed
+56 -3

No files matched your search

+16 -1
View File
@@ -466,9 +466,24 @@ public partial class AnthropicService
};
if (systemPrompt is List<object> 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<object>
{
new Dictionary<string, object>
{
["type"] = "text",
["text"] = spText,
["cache_control"] = new { type = "ephemeral", ttl = "1h" }
}
};
}
AddToolsToPayload(payload, toolOpts with { Iteration = iterations });
+5 -1
View File
@@ -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;
+30 -1
View File
@@ -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);
/// <summary>
/// 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.
/// </summary>
private static void ApplyCacheControlToLast(List<object> blocks, string ttl = "1h")
{
if (blocks == null || blocks.Count == 0) return;
var last = blocks[blocks.Count - 1];
Dictionary<string, object> dict;
if (last is Dictionary<string, object> existing)
{
dict = existing;
}
else
{
var json = JsonSerializer.Serialize(last);
var parsed = JsonSerializer.Deserialize<Dictionary<string, object>>(json);
dict = parsed ?? new Dictionary<string, object>();
}
dict["cache_control"] = new { type = "ephemeral", ttl };
blocks[blocks.Count - 1] = dict;
}
/// <summary>
/// Fügt Tool-Definitionen und WebSearch zum Payload hinzu.
/// </summary>
@@ -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"));
+5
View File
@@ -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);