Spaces:
Runtime error
Runtime error
File size: 11,224 Bytes
cd8bd0a | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 | /**
* Cache Control Policy
*
* Determines when to preserve client-side prompt caching headers (cache_control)
* vs. applying OmniRoute's own caching strategy.
*
* Client-side caching (e.g., Claude Code) should be preserved when:
* 1. Client is Claude Code or similar caching-aware client
* 2. Request will hit a deterministic target (single model or deterministic combo strategy)
* 3. Provider supports prompt caching (Anthropic, Alibaba Qwen, etc.)
*/
import type { RoutingStrategyValue } from "../../src/shared/constants/routingStrategies";
/**
* Cache control preservation modes
*/
export type CacheControlMode = "auto" | "always" | "never";
/**
* Cache control settings from the database
*/
export interface CacheControlSettings {
alwaysPreserveClientCache?: CacheControlMode;
}
/**
* Cache metrics for tracking effectiveness
*/
export interface CacheControlMetrics {
// Totals
totalRequests: number;
requestsWithCacheControl: number;
// Token counts
totalInputTokens: number;
totalCachedTokens: number;
totalCacheCreationTokens: number;
// Savings
tokensSaved: number;
estimatedCostSaved: number;
// Breakdowns
byProvider: Record<
string,
{
requests: number;
inputTokens: number;
cachedTokens: number;
cacheCreationTokens: number;
}
>;
byStrategy: Record<
string,
{
requests: number;
inputTokens: number;
cachedTokens: number;
cacheCreationTokens: number;
}
>;
lastUpdated: string;
}
/**
* Routing strategies that are deterministic (same request → same provider)
*/
const DETERMINISTIC_STRATEGIES: Set<RoutingStrategyValue> = new Set(["priority", "cost-optimized"]);
/**
* Providers that support prompt caching
*/
const CACHING_PROVIDERS = new Set([
"claude",
"anthropic",
"zai",
"qwen",
"deepseek",
// #3088 — Xiaomi MiMo honors OpenAI-format cache_control breakpoints. Without
// this entry, shouldPreserveCacheControl() returns false for Claude Code
// clients and filterToOpenAIFormat() strips cache_control, so Xiaomi never
// sees the cache hints and every request is a cache miss.
"xiaomi-mimo",
// #3955 — OpenAI / Codex / Azure-OpenAI use AUTOMATIC prefix caching: the longest
// matching prefix of a request is cached upstream WITHOUT any explicit cache_control
// markers. They must count as caching providers so the cache-aware compression guard
// preserves the cacheable prefix (system prompt / earliest messages) instead of
// rewriting it and forcing a cache miss. This also activates the intended
// `prompt_cache_key` cache-routing hint for OpenAI in chatCore.
"openai",
"codex",
"azure",
// #2069 — Alibaba DashScope's OpenAI-compatible endpoints (alibaba /
// alibaba-cn, upstream "alicode"/"alicode-intl") natively honor
// `cache_control: {type:"ephemeral"}` breakpoints. Without these entries
// shouldPreserveCacheControl() returns false for Claude Code clients and the
// OpenAI-format translator strips cache_control, so DashScope never sees the
// hints and every request is a cache miss.
"alibaba",
"alibaba-cn",
]);
/**
* Providers that honor EXPLICIT `cache_control` breakpoints carried inside an
* OpenAI-format request body (i.e. the markers must be passed THROUGH the
* Claude→OpenAI translation instead of stripped).
*
* This is a strict subset of CACHING_PROVIDERS and deliberately excludes
* `openai` / `codex` / `azure`: those use AUTOMATIC prefix caching (#3955) and
* do NOT accept explicit `cache_control` fields in the request — forwarding the
* markers there is meaningless at best and a 400 "unknown field" at worst, and
* it broke the chatCore "strips cache markers for non-Claude providers" test.
* Claude-format providers re-inject markers via prepareClaudeRequest, so they
* are not listed here either.
*/
const OPENAI_FORMAT_CACHE_CONTROL_PROVIDERS = new Set([
// #2069 — DashScope OpenAI-compatible endpoints accept ephemeral breakpoints.
"alibaba",
"alibaba-cn",
// #3088 — Xiaomi MiMo honors OpenAI-format cache_control breakpoints.
"xiaomi-mimo",
]);
/**
* Whether `cache_control` markers should be PASSED THROUGH the OpenAI-format
* translation for this provider (vs. stripped). Used to gate the request-side
* passthrough so generic / implicit-cache OpenAI providers keep getting cleaned.
*/
export function providerHonorsOpenAIFormatCacheControl(
provider: string | null | undefined
): boolean {
if (!provider) return false;
return OPENAI_FORMAT_CACHE_CONTROL_PROVIDERS.has(provider.toLowerCase());
}
/**
* Detect if the client is Claude Code or another caching-aware client
*/
export function isClaudeCodeClient(userAgent: string | null | undefined): boolean {
if (!userAgent) return false;
const ua = userAgent.toLowerCase();
// Claude Code user agents
if (ua.includes("claude-code") || ua.includes("claude_code")) return true;
if (ua.includes("claude-cli/")) return true;
if (ua.includes("sdk-cli")) return true;
if (ua.includes("anthropic") && ua.includes("cli")) return true;
return false;
}
/**
* Check if a provider supports prompt caching
* Supports caching if:
* 1. Provider is in the known caching providers list, OR
* 2. Provider uses Claude protocol (detected via targetFormat)
*/
export function providerSupportsCaching(
provider: string | null | undefined,
targetFormat?: string | null
): boolean {
if (!provider) return false;
if (CACHING_PROVIDERS.has(provider.toLowerCase())) return true;
// All Claude-protocol providers support prompt caching
if (targetFormat === "claude") return true;
return false;
}
/**
* Check if a routing strategy is deterministic
*/
export function isDeterministicStrategy(
strategy: RoutingStrategyValue | null | undefined
): boolean {
if (!strategy) return false;
return DETERMINISTIC_STRATEGIES.has(strategy);
}
/**
* Determine if client-side cache_control headers should be preserved
*
* @param userAgent - User-Agent header from the request
* @param isCombo - Whether this is a combo model
* @param comboStrategy - The combo's routing strategy (if applicable)
* @param targetProvider - The target provider for the request
* @param settings - Cache control settings from database (optional)
* @returns true if cache_control should be preserved, false if OmniRoute should manage it
*/
export function shouldPreserveCacheControl({
userAgent,
isCombo,
comboStrategy,
targetProvider,
targetFormat,
settings,
}: {
userAgent: string | null | undefined;
isCombo: boolean;
comboStrategy?: RoutingStrategyValue | null;
targetProvider: string | null | undefined;
targetFormat?: string | null;
settings?: CacheControlSettings;
}): boolean {
// User override takes precedence
if (settings?.alwaysPreserveClientCache === "always") {
return true;
}
if (settings?.alwaysPreserveClientCache === "never") {
return false;
}
// Auto mode: use automatic detection (existing logic)
// Must be a caching-aware client
if (!isClaudeCodeClient(userAgent)) {
return false;
}
// Target provider must support caching
if (!providerSupportsCaching(targetProvider, targetFormat)) {
return false;
}
// Single model: always preserve (deterministic)
if (!isCombo) {
return true;
}
// Combo: only preserve if strategy is deterministic
return isDeterministicStrategy(comboStrategy);
}
/**
* Track cache control metrics for a request
*/
export function trackCacheMetrics({
preserved,
provider,
strategy,
metrics,
inputTokens,
cachedTokens,
cacheCreationTokens,
}: {
preserved: boolean;
provider: string;
strategy: string | null | undefined;
metrics: CacheControlMetrics;
inputTokens?: number;
cachedTokens?: number;
cacheCreationTokens?: number;
}): CacheControlMetrics {
const now = new Date().toISOString();
// Initialize metrics if empty
if (!metrics) {
metrics = {
totalRequests: 0,
requestsWithCacheControl: 0,
totalInputTokens: 0,
totalCachedTokens: 0,
totalCacheCreationTokens: 0,
tokensSaved: 0,
estimatedCostSaved: 0,
byProvider: {},
byStrategy: {},
lastUpdated: now,
};
}
// Increment total requests
metrics.totalRequests++;
// Track token counts
const input = inputTokens || 0;
const cached = cachedTokens || 0;
const creation = cacheCreationTokens || 0;
metrics.totalInputTokens += input;
metrics.totalCachedTokens += cached;
metrics.totalCacheCreationTokens += creation;
// Calculate tokens saved (cached tokens are reused, not charged)
if (cached > 0) {
metrics.tokensSaved += cached;
}
// Only track requests where cache_control was preserved
if (preserved) {
metrics.requestsWithCacheControl++;
// Initialize provider tracking
if (!metrics.byProvider[provider]) {
metrics.byProvider[provider] = {
requests: 0,
inputTokens: 0,
cachedTokens: 0,
cacheCreationTokens: 0,
};
}
metrics.byProvider[provider].requests++;
metrics.byProvider[provider].inputTokens += input;
metrics.byProvider[provider].cachedTokens += cached;
metrics.byProvider[provider].cacheCreationTokens += creation;
// Initialize strategy tracking
if (strategy && !metrics.byStrategy[strategy]) {
metrics.byStrategy[strategy] = {
requests: 0,
inputTokens: 0,
cachedTokens: 0,
cacheCreationTokens: 0,
};
}
if (strategy) {
metrics.byStrategy[strategy].requests++;
metrics.byStrategy[strategy].inputTokens += input;
metrics.byStrategy[strategy].cachedTokens += cached;
metrics.byStrategy[strategy].cacheCreationTokens += creation;
}
}
metrics.lastUpdated = now;
return metrics;
}
/**
* Record cache token usage and update metrics
*/
export function updateCacheTokenMetrics({
metrics,
provider,
strategy,
inputTokens,
cachedTokens,
cacheCreationTokens,
costSaved,
}: {
metrics: CacheControlMetrics;
provider: string;
strategy: string | null | undefined;
inputTokens: number;
cachedTokens: number;
cacheCreationTokens: number;
costSaved?: number;
}): CacheControlMetrics {
metrics.totalCachedTokens += cachedTokens;
metrics.totalCacheCreationTokens += cacheCreationTokens;
metrics.totalInputTokens += inputTokens;
// Cached tokens are reused (saved), creation tokens are new cache writes
metrics.tokensSaved += cachedTokens;
if (costSaved !== undefined) {
metrics.estimatedCostSaved += costSaved;
}
// Update provider tracking
if (metrics.byProvider[provider]) {
metrics.byProvider[provider].cachedTokens += cachedTokens;
metrics.byProvider[provider].cacheCreationTokens += cacheCreationTokens;
metrics.byProvider[provider].inputTokens += inputTokens;
}
// Update strategy tracking
if (strategy && metrics.byStrategy[strategy]) {
metrics.byStrategy[strategy].cachedTokens += cachedTokens;
metrics.byStrategy[strategy].cacheCreationTokens += cacheCreationTokens;
metrics.byStrategy[strategy].inputTokens += inputTokens;
}
metrics.lastUpdated = new Date().toISOString();
return metrics;
}
|