@juspay/neurolink 12.40.3 → 12.41.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. package/CHANGELOG.md +3 -3
  2. package/dist/browser/neurolink.min.js +447 -443
  3. package/dist/cli/commands/proxy.js +5 -1
  4. package/dist/proxy/accountLedger.js +3 -0
  5. package/dist/proxy/codexOutboundCache.d.ts +52 -0
  6. package/dist/proxy/codexOutboundCache.js +147 -0
  7. package/dist/proxy/codexOutboundFallback.d.ts +27 -2
  8. package/dist/proxy/codexOutboundFallback.js +147 -11
  9. package/dist/proxy/codexResponsesFormat.js +7 -1
  10. package/dist/proxy/codexToAnthropicFallback.js +6 -0
  11. package/dist/proxy/proxyAnalysis.js +4 -0
  12. package/dist/proxy/proxyConfig.js +120 -0
  13. package/dist/proxy/proxyTokenUsage.d.ts +15 -0
  14. package/dist/proxy/proxyTokenUsage.js +33 -0
  15. package/dist/proxy/proxyTracer.d.ts +16 -1
  16. package/dist/proxy/proxyTracer.js +26 -5
  17. package/dist/proxy/requestLogger.js +9 -0
  18. package/dist/proxy/runtimeConfig.js +20 -0
  19. package/dist/proxy/sseInterceptor.js +9 -0
  20. package/dist/proxy/vertexAnthropicFallback.js +9 -0
  21. package/dist/server/routes/claudeProxyRoutes.js +65 -1
  22. package/dist/server/routes/codexProxyRoutes.d.ts +39 -3
  23. package/dist/server/routes/codexProxyRoutes.js +792 -8
  24. package/dist/server/routes/index.d.ts +8 -0
  25. package/dist/server/routes/index.js +9 -1
  26. package/dist/types/analytics.d.ts +25 -0
  27. package/dist/types/codex.d.ts +45 -1
  28. package/dist/types/proxy.d.ts +40 -2
  29. package/dist/types/proxyClient.d.ts +2 -0
  30. package/dist/types/server.d.ts +15 -0
  31. package/dist/types/subscription.d.ts +22 -0
  32. package/dist/utils/anthropicCacheBreakpoints.d.ts +2 -2
  33. package/dist/utils/anthropicCacheBreakpoints.js +14 -3
  34. package/dist/utils/pricing.js +101 -24
  35. package/docs-site/static/search-index.json +17 -6
  36. package/package.json +1 -1
@@ -16,6 +16,14 @@ export { createToolRoutes } from "./toolRoutes.js";
16
16
  /**
17
17
  * Create all standard routes
18
18
  * Convenience method that combines all route groups
19
+ *
20
+ * NOTE on the Codex outbound fallback's `anthropic` target: this function
21
+ * does not thread `loopbackPort`/`internalDispatch` into
22
+ * `createCodexProxyRoutes`, so that target cannot dispatch when the feature
23
+ * is enabled through `options.runtimeConfigProvider` here — only the CLI
24
+ * proxy command wires the in-process loopback directly. Route creation logs
25
+ * one warning when this gap applies; see `CreateRoutesOptions.runtimeConfigProvider`
26
+ * and docs/features/codex-proxy-support.md.
19
27
  */
20
28
  export declare function createAllRoutes(basePath?: string, options?: CreateRoutesOptions): RouteGroup[];
21
29
  /**
@@ -28,6 +28,14 @@ export { createToolRoutes } from "./toolRoutes.js";
28
28
  /**
29
29
  * Create all standard routes
30
30
  * Convenience method that combines all route groups
31
+ *
32
+ * NOTE on the Codex outbound fallback's `anthropic` target: this function
33
+ * does not thread `loopbackPort`/`internalDispatch` into
34
+ * `createCodexProxyRoutes`, so that target cannot dispatch when the feature
35
+ * is enabled through `options.runtimeConfigProvider` here — only the CLI
36
+ * proxy command wires the in-process loopback directly. Route creation logs
37
+ * one warning when this gap applies; see `CreateRoutesOptions.runtimeConfigProvider`
38
+ * and docs/features/codex-proxy-support.md.
31
39
  */
32
40
  export function createAllRoutes(basePath = "/api", options) {
33
41
  const routes = [
@@ -56,7 +64,7 @@ export function createAllRoutes(basePath = "/api", options) {
56
64
  routes.push(createOpenAIProxyRoutes(undefined, basePath));
57
65
  }
58
66
  if (enableCodexProxy) {
59
- routes.push(createCodexProxyRoutes(basePath));
67
+ routes.push(createCodexProxyRoutes(basePath, options?.runtimeConfigProvider));
60
68
  }
61
69
  if (enableGeminiProxy) {
62
70
  routes.push(createGeminiProxyRoutes(undefined, basePath));
@@ -12,10 +12,35 @@ export type TokenUsage = {
12
12
  output: number;
13
13
  total: number;
14
14
  cacheCreationTokens?: number;
15
+ /**
16
+ * The 1-hour-TTL share of `cacheCreationTokens` (a subset, not additive —
17
+ * Anthropic's `cache_creation.ephemeral_1h_input_tokens` is reported inside
18
+ * the same `cache_creation_input_tokens` total). Undefined/0 keeps today's
19
+ * pricing exactly, since `calculateCost` treats every write as 5-minute-TTL
20
+ * unless told otherwise.
21
+ */
22
+ cacheCreation1hTokens?: number;
15
23
  cacheReadTokens?: number;
16
24
  reasoning?: number;
17
25
  cacheSavingsPercent?: number;
18
26
  };
27
+ /** Per-token dollar rates for one model in the pricing table. */
28
+ export type ModelPricingRates = {
29
+ input: number;
30
+ output: number;
31
+ cacheRead?: number;
32
+ cacheCreation?: number;
33
+ /**
34
+ * 1-hour TTL cache-write rate (Anthropic's 2x-of-input multiplier),
35
+ * separate from `cacheCreation`'s 5-minute (1.25x) rate so a 1h-TTL
36
+ * write is never silently priced at the 5m rate. `calculateCost()`
37
+ * applies it to `TokenUsage.cacheCreation1hTokens`, the 1h share of the
38
+ * cache-write total that the Claude route, the Vertex passthrough and
39
+ * the Codex outbound fallback capture. Set on every Claude model; a
40
+ * model without it prices that share at `cacheCreation`.
41
+ */
42
+ cacheCreation1h?: number;
43
+ };
19
44
  /**
20
45
  * Error info type for analytics
21
46
  */
@@ -362,10 +362,13 @@ export type CodexResponseUsage = {
362
362
  input_tokens: number;
363
363
  output_tokens: number;
364
364
  total_tokens: number;
365
- /** Omitted entirely when neither cache field was observed on the Anthropic side. */
365
+ /** Omitted entirely when no cache field was observed on the Anthropic side. */
366
366
  input_tokens_details?: {
367
367
  cached_tokens?: number;
368
368
  cache_write_tokens?: number;
369
+ /** The 1-hour-TTL share of `cache_write_tokens` (a subset, not additive);
370
+ * sourced from `ClaudeUsage.cacheCreation1hTokens`. Omitted when not observed. */
371
+ cache_write_1h_tokens?: number;
369
372
  };
370
373
  /** Always omitted — ClaudeUsage carries no reasoning-token count to source it from. */
371
374
  output_tokens_details?: {
@@ -442,3 +445,44 @@ export type CodexResponseStream = {
442
445
  };
443
446
  /** Which output item kind (if any) `CodexResponsesStreamSerializer` currently has open. */
444
447
  export type CodexResponseOpenItemKind = "message" | "function_call" | "custom_tool_call" | null;
448
+ /** The four call sites in `codexProxyRoutes.ts`'s `dispatch()` that can trigger
449
+ * an outbound fallback attempt (stage-c-trigger.md §0's corrected line map). */
450
+ export type CodexOutboundFailureClass = "no_accounts" | "pool_exhausted" | "non_retryable_transport" | "loop_fallthrough";
451
+ /** What a single insertion point in `codexProxyRoutes.ts` observed. */
452
+ export type CodexOutboundFailureInput = {
453
+ failureClass: CodexOutboundFailureClass;
454
+ /** Present only for `non_retryable_transport`: the transport error code
455
+ * (e.g. `ECONNREFUSED`) that was NOT in the retryable allow-list. */
456
+ transportErrorCode?: string;
457
+ /** Present only for `loop_fallthrough`: the raw HTTP status the last
458
+ * exhausted account attempt ended on (`lastErrorStatus` at the account-loop
459
+ * call site). A 401/403 there is a Codex credential failure and stays
460
+ * eligible; any other 4xx except 429 rejects the request itself. */
461
+ lastStatus?: number;
462
+ /** Present only for `loop_fallthrough`: the error code the last exhausted
463
+ * account attempt reported, when it reported one. A code
464
+ * `classifyProxyFailureCode` marks non-retryable (content policy, invalid
465
+ * request) makes the failure ineligible whatever the status. */
466
+ lastErrorCode?: string;
467
+ };
468
+ /** `classifyCodexOutboundFailure`'s verdict: whether this failure is eligible
469
+ * to attempt an outbound fallback, and why (for tracing/logging only — the
470
+ * caller still applies its own config/loop-prevention/depth gates). */
471
+ export type CodexOutboundFallbackDecision = {
472
+ eligible: boolean;
473
+ reason: string;
474
+ };
475
+ /** Outcome of one `attemptCodexOutboundFallback` call. `not_attempted` covers
476
+ * every gate failure and every target exhausting without success — the
477
+ * caller falls through to its own existing (unchanged) error response in
478
+ * every one of those cases, so the flag-off / all-targets-failed paths stay
479
+ * byte-identical to today. */
480
+ export type CodexOutboundFallbackOutcome = {
481
+ kind: "not_attempted";
482
+ } | {
483
+ kind: "request_too_large";
484
+ message: string;
485
+ } | {
486
+ kind: "success";
487
+ response: Response;
488
+ };
@@ -26,7 +26,7 @@ import type { Ora } from "ora";
26
26
  import type { MCPToolRegistry } from "../mcp/toolRegistry.js";
27
27
  import type { ProxyTracer } from "../proxy/proxyTracer.js";
28
28
  import type { ACCOUNT_COOLING_REASONS, PROXY_ACCOUNT_TYPES, PROXY_ACCOUNT_ROUTING_MODES, PROXY_ACCOUNT_ROUTING_REASONS, PROXY_ACCOUNT_ROUTING_STRATEGIES } from "../proxy/routingEvidence.js";
29
- import type { FallbackEntry, ModelMapping, ProxyRoutingConfig, CloakingConfig } from "./subscription.js";
29
+ import type { CodexFallbackTarget, FallbackEntry, ModelMapping, ProxyRoutingConfig, CloakingConfig } from "./subscription.js";
30
30
  import type { RouteDeprecation } from "./server.js";
31
31
  import type { StoredOAuthTokens } from "./auth.js";
32
32
  /**
@@ -58,6 +58,14 @@ export type ClaudeCacheControl = {
58
58
  */
59
59
  ttl?: "5m" | "1h";
60
60
  };
61
+ /** TTL override for applyClaudeRequestCacheBreakpoints's breakpoint 1 only. */
62
+ export type ClaudeCacheBreakpointOptions = {
63
+ ttl?: "5m" | "1h";
64
+ };
65
+ /** Where a Claude-shaped request originated, for cache/cost attribution. */
66
+ export type ProxyRequestOrigin = "native" | "codex-fallback";
67
+ /** Session-affinity routing key for Codex-origin fallback requests. */
68
+ export type CodexAnthropicAffinityKey = `codex-thread:${string}` | `codex-session:${string}` | `codex-prefix:${string}`;
61
69
  /** A single text block in a Claude content array. */
62
70
  export type ClaudeTextBlock = {
63
71
  type: "text";
@@ -147,6 +155,12 @@ export type ClaudeUsage = {
147
155
  output_tokens: number;
148
156
  cache_creation_input_tokens?: number;
149
157
  cache_read_input_tokens?: number;
158
+ /** The 1-hour-TTL share of `cache_creation_input_tokens` (a subset, not
159
+ * additive), read from Anthropic's `usage.cache_creation.ephemeral_1h_input_tokens`.
160
+ * Omitted means "none known to be 1h", matching `UsageContext.cacheCreation1hTokens`'s
161
+ * own contract — every path that does not observe the breakdown prices exactly
162
+ * as before. */
163
+ cacheCreation1hTokens?: number;
150
164
  };
151
165
  /** Non-streaming response matching the Claude Messages API. */
152
166
  export type ClaudeResponse = {
@@ -679,6 +693,8 @@ export type RequestLogEntry = {
679
693
  inputTokens?: number;
680
694
  outputTokens?: number;
681
695
  cacheCreationTokens?: number;
696
+ /** 1h-TTL share of `cacheCreationTokens` (subset); unset when not reported. */
697
+ cacheCreation1hTokens?: number;
682
698
  cacheReadTokens?: number;
683
699
  /**
684
700
  * Whether the provider reported each cache count. Omitted means observed,
@@ -1010,7 +1026,11 @@ export type RequestAttemptLogEntry = {
1010
1026
  traceFlags?: number;
1011
1027
  };
1012
1028
  /** Additional fields recorded when a Codex response becomes client-final. */
1013
- export type CodexFinalLogExtra = Partial<Pick<RequestLogEntry, "errorType" | "errorMessage" | "errorCode" | "transportScope" | "inputTokens" | "outputTokens" | "cacheReadTokens" | "cacheCreationTokens" | "reasoningTokens" | "terminalOutcome" | "firstUsefulOutputMs" | "firstUsefulOutputStatus" | "firstUsefulOutputEvent" | "retryable">>;
1029
+ export type CodexFinalLogExtra = Partial<Pick<RequestLogEntry, "errorType" | "errorMessage" | "errorCode" | "transportScope" | "inputTokens" | "outputTokens" | "cacheReadTokens" | "cacheCreationTokens" | "cacheCreation1hTokens" | "cacheReadTokensObserved" | "cacheCreationTokensObserved" | "reasoningTokens" | "terminalOutcome" | "firstUsefulOutputMs" | "firstUsefulOutputStatus" | "firstUsefulOutputEvent" | "retryable" | "accountingScope" | "usageOwnerRequestId" | "model" | "provider" | "account" | "accountKey" | "accountType">>;
1030
+ /** Who served a Codex turn the outbound fallback handed to a Claude target.
1031
+ * The outer Codex entry records it, as the OpenAI bridge's does, so the
1032
+ * final log names the served engine instead of the Codex pool. */
1033
+ export type CodexOutboundServedAttribution = Required<Pick<RequestLogEntry, "account" | "accountType" | "provider" | "model">> & Pick<RequestLogEntry, "accountKey">;
1014
1034
  /** Additional fields recorded for each upstream Codex account attempt. */
1015
1035
  export type CodexAttemptLogExtra = Partial<Pick<RequestAttemptLogEntry, "inputTokens" | "outputTokens" | "cacheReadTokens" | "cacheCreationTokens" | "reasoningTokens" | "inputIncludesCachedTokens" | "errorType" | "errorMessage" | "errorCode" | "transportScope" | "retryable" | "rateLimitKind" | "cooldownReason" | "quotaResetAt" | "quotaScope">>;
1016
1036
  /** Minimal persistence contract needed by Codex rotating-token refreshes. */
@@ -1044,6 +1064,8 @@ export type ClaudeFinalRequestLogger = (status: number, accountLabel: string, ac
1044
1064
  inputTokens?: number;
1045
1065
  outputTokens?: number;
1046
1066
  cacheCreationTokens?: number;
1067
+ /** 1h-TTL share of `cacheCreationTokens` (subset); unset when not reported. */
1068
+ cacheCreation1hTokens?: number;
1047
1069
  cacheReadTokens?: number;
1048
1070
  /** False when the provider reported no cache breakdown; see RequestLogEntry. */
1049
1071
  cacheReadTokensObserved?: boolean;
@@ -1075,6 +1097,8 @@ export type AnthropicAttemptLogger = (status: number, errorType?: string, errorM
1075
1097
  inputTokens?: number;
1076
1098
  outputTokens?: number;
1077
1099
  cacheCreationTokens?: number;
1100
+ /** 1h-TTL share of `cacheCreationTokens` (subset); unset when not reported. */
1101
+ cacheCreation1hTokens?: number;
1078
1102
  cacheReadTokens?: number;
1079
1103
  retryable?: boolean;
1080
1104
  /** The transport failure happened before any request byte was sent. */
@@ -1930,6 +1954,12 @@ export type UsageContext = {
1930
1954
  inputTokens: number;
1931
1955
  outputTokens: number;
1932
1956
  cacheCreationTokens: number;
1957
+ /**
1958
+ * The 1-hour-TTL share of `cacheCreationTokens` (a subset, not additive).
1959
+ * Omitted means "none known to be 1h", so every path that does not read the
1960
+ * upstream `cache_creation` breakdown prices exactly as it did before.
1961
+ */
1962
+ cacheCreation1hTokens?: number;
1933
1963
  cacheReadTokens: number;
1934
1964
  /**
1935
1965
  * Whether the provider actually reported each cache count. Omitted means
@@ -2341,6 +2371,8 @@ export type ProxyAnalysisFinalRequestRecord = {
2341
2371
  outputTokens: number | null;
2342
2372
  cacheReadTokens: number | null;
2343
2373
  cacheCreationTokens: number | null;
2374
+ /** 1h-TTL share of `cacheCreationTokens` (subset); null when not reported. */
2375
+ cacheCreation1hTokens: number | null;
2344
2376
  /**
2345
2377
  * False when the provider reported no cache breakdown. Such a turn is not a
2346
2378
  * cache miss, so it is excluded from the hit-rate denominator rather than
@@ -2667,6 +2699,8 @@ export type SSETelemetry = {
2667
2699
  inputTokens: number;
2668
2700
  outputTokens: number;
2669
2701
  cacheCreationInputTokens: number;
2702
+ /** 1h-TTL share of `cacheCreationInputTokens`; absent when not reported. */
2703
+ cacheCreation1hInputTokens?: number;
2670
2704
  cacheReadInputTokens: number;
2671
2705
  totalTokens: number;
2672
2706
  };
@@ -2713,6 +2747,7 @@ export type TelemetryAccumulator = {
2713
2747
  inputTokens: number;
2714
2748
  outputTokens: number;
2715
2749
  cacheCreationInputTokens: number;
2750
+ cacheCreation1hInputTokens?: number;
2716
2751
  cacheReadInputTokens: number;
2717
2752
  contentBlocks: SSEContentBlock[];
2718
2753
  blockByteCounts: Map<number, number>;
@@ -3239,6 +3274,9 @@ export type ProxyRequestRoutingSnapshot = {
3239
3274
  sessionAffinity: boolean;
3240
3275
  sessionAffinityIdleTtlMs: number;
3241
3276
  spillInflight: number;
3277
+ codexOutboundFallbackEnabled: boolean;
3278
+ codexOutboundFallbackTargets: CodexFallbackTarget[];
3279
+ codexOutboundFallbackModelMappings: ModelMapping[];
3242
3280
  };
3243
3281
  /** Operator policy for paid extra usage. */
3244
3282
  export type ProxyOveragePolicy = "auto" | "always" | "never";
@@ -271,6 +271,8 @@ export type ProxyLedgerEntry = {
271
271
  outputTokens: number;
272
272
  cacheReadTokens: number;
273
273
  cacheCreationTokens: number;
274
+ /** 1h-TTL share of `cacheCreationTokens` (subset); 0 when not reported. */
275
+ cacheCreation1hTokens: number;
274
276
  };
275
277
  /** Incremental read position and accumulated entries for one request-log file. */
276
278
  export type ProxyLedgerFileCursor = {
@@ -7,6 +7,7 @@ import type { MCPToolRegistry } from "../mcp/toolRegistry.js";
7
7
  import type { NeuroLink } from "../neurolink.js";
8
8
  import type { JsonObject, JsonValue } from "./common.js";
9
9
  import type { ExternalMCPServerStatus } from "./externalMcp.js";
10
+ import type { ProxyRuntimeConfigProvider } from "./proxy.js";
10
11
  /**
11
12
  * Server adapter configuration
12
13
  */
@@ -1080,6 +1081,20 @@ export type CreateRoutesOptions = {
1080
1081
  codexProxy?: boolean;
1081
1082
  /** Enable the Gemini door on its own, for the same reason. */
1082
1083
  geminiProxy?: boolean;
1084
+ /**
1085
+ * Runtime config provider for engines that read it (currently: Codex
1086
+ * outbound fallback). NOTE: this option alone cannot make the Codex
1087
+ * outbound fallback's `anthropic` target reachable — that leg dispatches
1088
+ * through an in-process loopback (`loopbackPort` / `internalDispatch`)
1089
+ * that only `createCodexProxyRoutes`' direct, non-`CreateRoutesOptions`
1090
+ * parameters carry (the CLI's `neurolink proxy start` wires them; this
1091
+ * generic SDK route surface deliberately does not gain a new option for
1092
+ * it). Enabling the feature with an `anthropic` target here logs one
1093
+ * startup warning and then fails every such request at attempt time. A
1094
+ * `vertex`-only configuration is unaffected. See
1095
+ * docs/features/codex-proxy-support.md.
1096
+ */
1097
+ runtimeConfigProvider?: ProxyRuntimeConfigProvider;
1083
1098
  };
1084
1099
  /** Data stream finish event. */
1085
1100
  export type FinishEvent = DataStreamEvent & {
@@ -984,6 +984,14 @@ export type FallbackEntry = {
984
984
  /** Explicit Codex fallback effort. Omit to use the upstream default. */
985
985
  reasoningEffort?: CodexReasoningEffort;
986
986
  };
987
+ /** One outbound-fallback hop for a native Codex request whose own account pool
988
+ * is exhausted. No `reasoningEffort` (unlike FallbackEntry): that knob configures
989
+ * a Codex *inbound* leg and has no meaning once the request has left Codex's
990
+ * Responses format and become an Anthropic Messages request. */
991
+ export type CodexFallbackTarget = {
992
+ provider: "anthropic" | "vertex";
993
+ model: string;
994
+ };
987
995
  /** Full proxy routing config */
988
996
  export type ProxyRoutingConfig = {
989
997
  strategy: "round-robin" | "fill-first";
@@ -1040,6 +1048,20 @@ export type ProxyRoutingConfig = {
1040
1048
  sessionAffinityIdleTtlMs?: number;
1041
1049
  /** For a request without a binding: spill off an account already at N in-flight. 0-100, 0 = off. */
1042
1050
  spillInflight?: number;
1051
+ /** Master flag for native-Codex-request outbound fallback. Independent of
1052
+ * codexOutboundFallbackTargets so the list can be staged ahead of turning the
1053
+ * feature on. Default false — the only new key whose absence must reproduce
1054
+ * byte-identical current behavior. */
1055
+ codexOutboundFallbackEnabled?: boolean;
1056
+ /** Ordered targets tried after Codex's own account pool is exhausted.
1057
+ * Try-order = array order. Empty/absent with enabled:true is a no-op. */
1058
+ codexOutboundFallbackTargets?: CodexFallbackTarget[];
1059
+ /** Per-incoming-Codex-model override, layered on codexOutboundFallbackTargets.
1060
+ * Reuses ModelMapping's {from,to,provider}: from = requested Codex model,
1061
+ * provider = which target this overrides, to = model to send instead of that
1062
+ * target's plain `model`. Absent for a (from,provider) pair = use the target's
1063
+ * plain model. */
1064
+ codexOutboundFallbackModelMappings?: ModelMapping[];
1043
1065
  };
1044
1066
  /** Cloaking plugin config */
1045
1067
  export type CloakingConfig = {
@@ -1,4 +1,4 @@
1
- import type { ClaudeRequest, VertexAnthropicCacheInput, VertexAnthropicCacheOutput, VertexAnthropicMessage } from "../types/index.js";
1
+ import type { ClaudeCacheBreakpointOptions, ClaudeRequest, VertexAnthropicCacheInput, VertexAnthropicCacheOutput, VertexAnthropicMessage } from "../types/index.js";
2
2
  /** Anthropic allows at most four `cache_control` breakpoints per request. */
3
3
  export declare const ANTHROPIC_MAX_CACHE_BREAKPOINTS = 4;
4
4
  /**
@@ -61,4 +61,4 @@ export declare function applyAnthropicHistoryCacheBreakpoints(input: ReadonlyArr
61
61
  *
62
62
  * Pure: inputs are cloned, never mutated.
63
63
  */
64
- export declare function applyClaudeRequestCacheBreakpoints(request: ClaudeRequest): ClaudeRequest;
64
+ export declare function applyClaudeRequestCacheBreakpoints(request: ClaudeRequest, options?: ClaudeCacheBreakpointOptions): ClaudeRequest;
@@ -168,7 +168,7 @@ function markLastContentBlock(messages, i) {
168
168
  *
169
169
  * Pure: inputs are cloned, never mutated.
170
170
  */
171
- export function applyClaudeRequestCacheBreakpoints(request) {
171
+ export function applyClaudeRequestCacheBreakpoints(request, options) {
172
172
  const existing = countAnthropicCacheMarkers({
173
173
  system: request.system,
174
174
  tools: request.tools,
@@ -184,6 +184,7 @@ export function applyClaudeRequestCacheBreakpoints(request) {
184
184
  return request;
185
185
  }
186
186
  const out = { ...request };
187
+ const breakpointOneMarker = ephemeral(options?.ttl);
187
188
  // One marker on the stable head. The last system block is the better anchor
188
189
  // than the last tool, because system renders after tools and so a single
189
190
  // marker there caches both.
@@ -191,7 +192,7 @@ export function applyClaudeRequestCacheBreakpoints(request) {
191
192
  const system = out.system.map((b) => ({ ...b }));
192
193
  system[system.length - 1] = {
193
194
  ...system[system.length - 1],
194
- cache_control: CLAUDE_EPHEMERAL,
195
+ cache_control: breakpointOneMarker,
195
196
  };
196
197
  out.system = system;
197
198
  budget--;
@@ -203,7 +204,7 @@ export function applyClaudeRequestCacheBreakpoints(request) {
203
204
  const tools = out.tools.map((t) => ({ ...t }));
204
205
  tools[tools.length - 1] = {
205
206
  ...tools[tools.length - 1],
206
- cache_control: CLAUDE_EPHEMERAL,
207
+ cache_control: breakpointOneMarker,
207
208
  };
208
209
  out.tools = tools;
209
210
  budget--;
@@ -214,6 +215,16 @@ export function applyClaudeRequestCacheBreakpoints(request) {
214
215
  return out;
215
216
  }
216
217
  const CLAUDE_EPHEMERAL = { type: "ephemeral" };
218
+ /**
219
+ * Builds the breakpoint-1 marker, honoring a caller-supplied TTL override.
220
+ * Breakpoints 2-4 (`applyClaudeHistoryBreakpoints`) always use the bare
221
+ * `CLAUDE_EPHEMERAL` constant instead of this — they roll forward every turn
222
+ * or two, so paying the 1-hour (2x) rate on a marker about to be superseded
223
+ * is pure waste; only the stable head benefits from the longer window.
224
+ */
225
+ function ephemeral(ttl) {
226
+ return ttl ? { type: "ephemeral", ttl } : CLAUDE_EPHEMERAL;
227
+ }
217
228
  /**
218
229
  * Roll `budget` markers backwards along the history, marking the last content
219
230
  * block of each message that can carry one. Only text and tool_result blocks