@fgv/ts-extras 5.1.0-54 → 5.1.0-56

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 (166) hide show
  1. package/CAPABILITIES.md +95 -0
  2. package/dist/packlets/ai-assist/cacheRequest.js +118 -0
  3. package/dist/packlets/ai-assist/cacheRequest.js.map +1 -0
  4. package/dist/packlets/ai-assist/chatRequestBuilders.js +70 -5
  5. package/dist/packlets/ai-assist/chatRequestBuilders.js.map +1 -1
  6. package/dist/packlets/ai-assist/completionClient.js +122 -45
  7. package/dist/packlets/ai-assist/completionClient.js.map +1 -1
  8. package/dist/packlets/ai-assist/imageGenerationClient.js +1 -1
  9. package/dist/packlets/ai-assist/imageGenerationClient.js.map +1 -1
  10. package/dist/packlets/ai-assist/index.js +3 -1
  11. package/dist/packlets/ai-assist/index.js.map +1 -1
  12. package/dist/packlets/ai-assist/model.js.map +1 -1
  13. package/dist/packlets/ai-assist/promptCacheAnthropicBreakpointHarness.js +200 -0
  14. package/dist/packlets/ai-assist/promptCacheAnthropicBreakpointHarness.js.map +1 -0
  15. package/dist/packlets/ai-assist/promptCacheObservabilityHarness.js +137 -0
  16. package/dist/packlets/ai-assist/promptCacheObservabilityHarness.js.map +1 -0
  17. package/dist/packlets/ai-assist/promptCacheRoutingAbHarness.js +156 -0
  18. package/dist/packlets/ai-assist/promptCacheRoutingAbHarness.js.map +1 -0
  19. package/dist/packlets/ai-assist/promptCacheUsageMath.js +124 -0
  20. package/dist/packlets/ai-assist/promptCacheUsageMath.js.map +1 -0
  21. package/dist/packlets/ai-assist/registry.js +9 -11
  22. package/dist/packlets/ai-assist/registry.js.map +1 -1
  23. package/dist/packlets/ai-assist/streamUsageCapability.js +124 -0
  24. package/dist/packlets/ai-assist/streamUsageCapability.js.map +1 -0
  25. package/dist/packlets/ai-assist/streamingAdapters/anthropic.js +24 -5
  26. package/dist/packlets/ai-assist/streamingAdapters/anthropic.js.map +1 -1
  27. package/dist/packlets/ai-assist/streamingAdapters/clientToolContinuationBuilder.js +64 -33
  28. package/dist/packlets/ai-assist/streamingAdapters/clientToolContinuationBuilder.js.map +1 -1
  29. package/dist/packlets/ai-assist/streamingAdapters/common.js +25 -1
  30. package/dist/packlets/ai-assist/streamingAdapters/common.js.map +1 -1
  31. package/dist/packlets/ai-assist/streamingAdapters/gemini.js +12 -5
  32. package/dist/packlets/ai-assist/streamingAdapters/gemini.js.map +1 -1
  33. package/dist/packlets/ai-assist/streamingAdapters/openaiChat.js +34 -8
  34. package/dist/packlets/ai-assist/streamingAdapters/openaiChat.js.map +1 -1
  35. package/dist/packlets/ai-assist/streamingAdapters/openaiResponses.js +22 -14
  36. package/dist/packlets/ai-assist/streamingAdapters/openaiResponses.js.map +1 -1
  37. package/dist/packlets/ai-assist/streamingClient.js +3 -2
  38. package/dist/packlets/ai-assist/streamingClient.js.map +1 -1
  39. package/dist/packlets/ai-assist/structuredOutput.js +84 -7
  40. package/dist/packlets/ai-assist/structuredOutput.js.map +1 -1
  41. package/dist/packlets/ai-assist/structuredOutputTypes.js.map +1 -1
  42. package/dist/packlets/ai-assist/thinkingOptionsResolver.js +18 -3
  43. package/dist/packlets/ai-assist/thinkingOptionsResolver.js.map +1 -1
  44. package/dist/packlets/ai-assist/toolFormats.js +80 -0
  45. package/dist/packlets/ai-assist/toolFormats.js.map +1 -1
  46. package/dist/packlets/ai-assist/toolTypes.js +21 -0
  47. package/dist/packlets/ai-assist/toolTypes.js.map +1 -0
  48. package/dist/packlets/ai-assist/usageNormalization.js +178 -0
  49. package/dist/packlets/ai-assist/usageNormalization.js.map +1 -0
  50. package/dist/packlets/ai-assist/usageTypes.js +21 -0
  51. package/dist/packlets/ai-assist/usageTypes.js.map +1 -0
  52. package/dist/packlets/mustache/index.js.map +1 -1
  53. package/dist/packlets/mustache/interfaces.js.map +1 -1
  54. package/dist/packlets/mustache/mustacheTemplate.js +91 -0
  55. package/dist/packlets/mustache/mustacheTemplate.js.map +1 -1
  56. package/dist/ts-extras.d.ts +577 -30
  57. package/lib/packlets/ai-assist/cacheRequest.d.ts +100 -0
  58. package/lib/packlets/ai-assist/cacheRequest.d.ts.map +1 -0
  59. package/lib/packlets/ai-assist/cacheRequest.js +123 -0
  60. package/lib/packlets/ai-assist/cacheRequest.js.map +1 -0
  61. package/lib/packlets/ai-assist/chatRequestBuilders.d.ts +34 -4
  62. package/lib/packlets/ai-assist/chatRequestBuilders.d.ts.map +1 -1
  63. package/lib/packlets/ai-assist/chatRequestBuilders.js +73 -5
  64. package/lib/packlets/ai-assist/chatRequestBuilders.js.map +1 -1
  65. package/lib/packlets/ai-assist/completionClient.d.ts +6 -0
  66. package/lib/packlets/ai-assist/completionClient.d.ts.map +1 -1
  67. package/lib/packlets/ai-assist/completionClient.js +121 -44
  68. package/lib/packlets/ai-assist/completionClient.js.map +1 -1
  69. package/lib/packlets/ai-assist/imageGenerationClient.d.ts.map +1 -1
  70. package/lib/packlets/ai-assist/imageGenerationClient.js +1 -1
  71. package/lib/packlets/ai-assist/imageGenerationClient.js.map +1 -1
  72. package/lib/packlets/ai-assist/index.d.ts +7 -2
  73. package/lib/packlets/ai-assist/index.d.ts.map +1 -1
  74. package/lib/packlets/ai-assist/index.js +11 -1
  75. package/lib/packlets/ai-assist/index.js.map +1 -1
  76. package/lib/packlets/ai-assist/model.d.ts +59 -288
  77. package/lib/packlets/ai-assist/model.d.ts.map +1 -1
  78. package/lib/packlets/ai-assist/model.js.map +1 -1
  79. package/lib/packlets/ai-assist/promptCacheAnthropicBreakpointHarness.d.ts +124 -0
  80. package/lib/packlets/ai-assist/promptCacheAnthropicBreakpointHarness.d.ts.map +1 -0
  81. package/lib/packlets/ai-assist/promptCacheAnthropicBreakpointHarness.js +208 -0
  82. package/lib/packlets/ai-assist/promptCacheAnthropicBreakpointHarness.js.map +1 -0
  83. package/lib/packlets/ai-assist/promptCacheObservabilityHarness.d.ts +77 -0
  84. package/lib/packlets/ai-assist/promptCacheObservabilityHarness.d.ts.map +1 -0
  85. package/lib/packlets/ai-assist/promptCacheObservabilityHarness.js +143 -0
  86. package/lib/packlets/ai-assist/promptCacheObservabilityHarness.js.map +1 -0
  87. package/lib/packlets/ai-assist/promptCacheRoutingAbHarness.d.ts +84 -0
  88. package/lib/packlets/ai-assist/promptCacheRoutingAbHarness.d.ts.map +1 -0
  89. package/lib/packlets/ai-assist/promptCacheRoutingAbHarness.js +163 -0
  90. package/lib/packlets/ai-assist/promptCacheRoutingAbHarness.js.map +1 -0
  91. package/lib/packlets/ai-assist/promptCacheUsageMath.d.ts +105 -0
  92. package/lib/packlets/ai-assist/promptCacheUsageMath.d.ts.map +1 -0
  93. package/lib/packlets/ai-assist/promptCacheUsageMath.js +130 -0
  94. package/lib/packlets/ai-assist/promptCacheUsageMath.js.map +1 -0
  95. package/lib/packlets/ai-assist/registry.d.ts.map +1 -1
  96. package/lib/packlets/ai-assist/registry.js +9 -11
  97. package/lib/packlets/ai-assist/registry.js.map +1 -1
  98. package/lib/packlets/ai-assist/streamUsageCapability.d.ts +119 -0
  99. package/lib/packlets/ai-assist/streamUsageCapability.d.ts.map +1 -0
  100. package/lib/packlets/ai-assist/streamUsageCapability.js +130 -0
  101. package/lib/packlets/ai-assist/streamUsageCapability.js.map +1 -0
  102. package/lib/packlets/ai-assist/streamingAdapters/anthropic.d.ts.map +1 -1
  103. package/lib/packlets/ai-assist/streamingAdapters/anthropic.js +23 -4
  104. package/lib/packlets/ai-assist/streamingAdapters/anthropic.js.map +1 -1
  105. package/lib/packlets/ai-assist/streamingAdapters/clientToolContinuationBuilder.d.ts +21 -12
  106. package/lib/packlets/ai-assist/streamingAdapters/clientToolContinuationBuilder.d.ts.map +1 -1
  107. package/lib/packlets/ai-assist/streamingAdapters/clientToolContinuationBuilder.js +64 -34
  108. package/lib/packlets/ai-assist/streamingAdapters/clientToolContinuationBuilder.js.map +1 -1
  109. package/lib/packlets/ai-assist/streamingAdapters/common.d.ts +21 -0
  110. package/lib/packlets/ai-assist/streamingAdapters/common.d.ts.map +1 -1
  111. package/lib/packlets/ai-assist/streamingAdapters/common.js +25 -1
  112. package/lib/packlets/ai-assist/streamingAdapters/common.js.map +1 -1
  113. package/lib/packlets/ai-assist/streamingAdapters/gemini.d.ts.map +1 -1
  114. package/lib/packlets/ai-assist/streamingAdapters/gemini.js +12 -5
  115. package/lib/packlets/ai-assist/streamingAdapters/gemini.js.map +1 -1
  116. package/lib/packlets/ai-assist/streamingAdapters/openaiChat.d.ts +1 -1
  117. package/lib/packlets/ai-assist/streamingAdapters/openaiChat.d.ts.map +1 -1
  118. package/lib/packlets/ai-assist/streamingAdapters/openaiChat.js +33 -7
  119. package/lib/packlets/ai-assist/streamingAdapters/openaiChat.js.map +1 -1
  120. package/lib/packlets/ai-assist/streamingAdapters/openaiResponses.d.ts +1 -1
  121. package/lib/packlets/ai-assist/streamingAdapters/openaiResponses.d.ts.map +1 -1
  122. package/lib/packlets/ai-assist/streamingAdapters/openaiResponses.js +21 -13
  123. package/lib/packlets/ai-assist/streamingAdapters/openaiResponses.js.map +1 -1
  124. package/lib/packlets/ai-assist/streamingClient.d.ts.map +1 -1
  125. package/lib/packlets/ai-assist/streamingClient.js +3 -2
  126. package/lib/packlets/ai-assist/streamingClient.js.map +1 -1
  127. package/lib/packlets/ai-assist/structuredOutput.d.ts +32 -0
  128. package/lib/packlets/ai-assist/structuredOutput.d.ts.map +1 -1
  129. package/lib/packlets/ai-assist/structuredOutput.js +85 -7
  130. package/lib/packlets/ai-assist/structuredOutput.js.map +1 -1
  131. package/lib/packlets/ai-assist/structuredOutputTypes.d.ts +30 -0
  132. package/lib/packlets/ai-assist/structuredOutputTypes.d.ts.map +1 -1
  133. package/lib/packlets/ai-assist/structuredOutputTypes.js.map +1 -1
  134. package/lib/packlets/ai-assist/thinkingOptionsResolver.d.ts.map +1 -1
  135. package/lib/packlets/ai-assist/thinkingOptionsResolver.js +18 -3
  136. package/lib/packlets/ai-assist/thinkingOptionsResolver.js.map +1 -1
  137. package/lib/packlets/ai-assist/toolFormats.d.ts +55 -1
  138. package/lib/packlets/ai-assist/toolFormats.d.ts.map +1 -1
  139. package/lib/packlets/ai-assist/toolFormats.js +82 -0
  140. package/lib/packlets/ai-assist/toolFormats.js.map +1 -1
  141. package/lib/packlets/ai-assist/toolTypes.d.ts +328 -0
  142. package/lib/packlets/ai-assist/toolTypes.d.ts.map +1 -0
  143. package/lib/packlets/ai-assist/toolTypes.js +22 -0
  144. package/lib/packlets/ai-assist/toolTypes.js.map +1 -0
  145. package/lib/packlets/ai-assist/usageNormalization.d.ts +58 -0
  146. package/lib/packlets/ai-assist/usageNormalization.d.ts.map +1 -0
  147. package/lib/packlets/ai-assist/usageNormalization.js +184 -0
  148. package/lib/packlets/ai-assist/usageNormalization.js.map +1 -0
  149. package/lib/packlets/ai-assist/usageTypes.d.ts +84 -0
  150. package/lib/packlets/ai-assist/usageTypes.d.ts.map +1 -0
  151. package/lib/packlets/ai-assist/usageTypes.js +22 -0
  152. package/lib/packlets/ai-assist/usageTypes.js.map +1 -0
  153. package/lib/packlets/mustache/index.d.ts +1 -1
  154. package/lib/packlets/mustache/index.d.ts.map +1 -1
  155. package/lib/packlets/mustache/index.js.map +1 -1
  156. package/lib/packlets/mustache/interfaces.d.ts +30 -0
  157. package/lib/packlets/mustache/interfaces.d.ts.map +1 -1
  158. package/lib/packlets/mustache/interfaces.js.map +1 -1
  159. package/lib/packlets/mustache/mustacheTemplate.d.ts +56 -1
  160. package/lib/packlets/mustache/mustacheTemplate.d.ts.map +1 -1
  161. package/lib/packlets/mustache/mustacheTemplate.js +91 -0
  162. package/lib/packlets/mustache/mustacheTemplate.js.map +1 -1
  163. package/package.json +16 -16
  164. package/perf/promptCacheAnthropicBreakpoint.js +135 -0
  165. package/perf/promptCacheObservability.js +107 -0
  166. package/perf/promptCacheRoutingAb.js +105 -0
@@ -0,0 +1,163 @@
1
+ "use strict";
2
+ // Copyright (c) 2026 Erik Fortune
3
+ //
4
+ // Permission is hereby granted, free of charge, to any person obtaining a copy
5
+ // of this software and associated documentation files (the "Software"), to deal
6
+ // in the Software without restriction, including without limitation the rights
7
+ // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8
+ // copies of the Software, and to permit persons to whom the Software is
9
+ // furnished to do so, subject to the following conditions:
10
+ //
11
+ // The above copyright notice and this permission notice shall be included in all
12
+ // copies or substantial portions of the Software.
13
+ //
14
+ // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15
+ // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16
+ // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17
+ // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18
+ // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19
+ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
20
+ // SOFTWARE.
21
+ Object.defineProperty(exports, "__esModule", { value: true });
22
+ exports.buildRoutingAbPrefix = buildRoutingAbPrefix;
23
+ exports.describeRoutingAbUsage = describeRoutingAbUsage;
24
+ exports.runPromptCacheRoutingAbArm = runPromptCacheRoutingAbArm;
25
+ exports.runPromptCacheRoutingAbHarness = runPromptCacheRoutingAbHarness;
26
+ exports.formatRoutingAbVerdict = formatRoutingAbVerdict;
27
+ /**
28
+ * Pure logic and injectable transport for `perf/promptCacheRoutingAb.js` — the A/B that measures
29
+ * whether `IAiCacheRequest.cacheKey` actually improves the xAI cache-hit rate (design.md §2's
30
+ * 2026-09-19 measurement note, and result.md's "gating defect in C3").
31
+ *
32
+ * @remarks
33
+ * Extracted for the same reason as `promptCacheObservabilityHarness.ts`: the verdict this harness
34
+ * prints (CONFIRMS / MISSES / AMBIGUOUS) "has already been used to justify a merge," per this
35
+ * stream's follow-up task, and had no test checking it classifies correctly.
36
+ * @packageDocumentation
37
+ */
38
+ const ts_utils_1 = require("@fgv/ts-utils");
39
+ const promptCacheUsageMath_1 = require("./promptCacheUsageMath");
40
+ /**
41
+ * Builds one arm's stable system prefix. Ported verbatim from the harness's `stablePrefix(arm)`.
42
+ * `salt` and `arm` both feed the text so the two arms — and two separate runs — never warm each
43
+ * other's cache.
44
+ * @internal
45
+ */
46
+ function buildRoutingAbPrefix(salt, arm) {
47
+ const lines = [];
48
+ for (let i = 0; i < 400; i++) {
49
+ lines.push(`Directive ${salt}-${arm}-${i}: when asked about topic ${i}, respond precisely and cite source ${i}-${i * 7}.`);
50
+ }
51
+ return `You are a careful assistant operating under the following fixed policy document.\n\n${lines.join('\n')}`;
52
+ }
53
+ /**
54
+ * One line describing a usage block. Ported from `describeUsage`, now computing the ratio via
55
+ * {@link computeCacheUsageRatio} instead of an inline calculation that coalesced a missing
56
+ * `cachedInputTokens` or `uncachedInputTokens` to `0` (see that function's remarks).
57
+ * @internal
58
+ */
59
+ function describeRoutingAbUsage(usage) {
60
+ if (usage === undefined) {
61
+ return 'usage absent from response';
62
+ }
63
+ const { cachedInputTokens, uncachedInputTokens } = usage;
64
+ const { totalInputTokens, cachedRatio } = (0, promptCacheUsageMath_1.computeCacheUsageRatio)(usage);
65
+ if (cachedRatio === undefined) {
66
+ return `reports=${usage.reports} cached=${cachedInputTokens} uncached=${uncachedInputTokens} (ratio not computable)`;
67
+ }
68
+ return `reports=${usage.reports} cached=${cachedInputTokens} of ${totalInputTokens} input (${(0, promptCacheUsageMath_1.formatCacheRatioPercent)(cachedRatio)})`;
69
+ }
70
+ /**
71
+ * Runs one arm: a cold call, then a warm call sharing the same {@link buildRoutingAbPrefix} system
72
+ * prompt and differing only in the final user turn — optionally supplying `cacheKey`. Ported from
73
+ * `runArm`, with the network call injected.
74
+ * @internal
75
+ */
76
+ async function runPromptCacheRoutingAbArm(deps, params) {
77
+ const { descriptor, apiKey, label, salt, arm, cacheKey } = params;
78
+ const system = buildRoutingAbPrefix(salt, arm);
79
+ const cache = cacheKey !== undefined ? { cacheKey } : undefined;
80
+ const call = (content) => deps.callCompletion({
81
+ descriptor,
82
+ apiKey,
83
+ system,
84
+ messages: [{ role: 'user', content }],
85
+ cache
86
+ });
87
+ const cold = await call('In one sentence, what is directive 12 about?');
88
+ if (cold.isFailure()) {
89
+ return (0, ts_utils_1.fail)(`${label}: cold call failed: ${cold.message}`);
90
+ }
91
+ // Only the tail differs from the cold call — the prefix is byte-identical.
92
+ const warm = await call('In one sentence, what is directive 99 about?');
93
+ if (warm.isFailure()) {
94
+ return (0, ts_utils_1.fail)(`${label}: warm call failed: ${warm.message}`);
95
+ }
96
+ return (0, ts_utils_1.succeed)({ label, coldUsage: cold.value.usage, warmUsage: warm.value.usage });
97
+ }
98
+ /**
99
+ * Runs both arms of the routing-key A/B and classifies the result.
100
+ *
101
+ * @remarks
102
+ * The verdict compares each arm's **warm** call against the other's — not an arm's own cold vs.
103
+ * warm call — because the question is whether the routing key changes the warm hit rate relative
104
+ * to the no-key baseline, exactly as `promptCacheRoutingAb.js`'s original `main()` computed it
105
+ * (`pct(withoutKey)` / `pct(withKey)`, both taken from each arm's warm response).
106
+ * @internal
107
+ */
108
+ async function runPromptCacheRoutingAbHarness(deps, params) {
109
+ const { descriptor, apiKey, salt } = params;
110
+ const withoutKeyResult = await runPromptCacheRoutingAbArm(deps, {
111
+ descriptor,
112
+ apiKey,
113
+ label: 'arm 1: WITHOUT cacheKey (behaviour before this change)',
114
+ salt,
115
+ arm: 'a'
116
+ });
117
+ if (withoutKeyResult.isFailure()) {
118
+ return (0, ts_utils_1.fail)(withoutKeyResult.message);
119
+ }
120
+ const withKeyResult = await runPromptCacheRoutingAbArm(deps, {
121
+ descriptor,
122
+ apiKey,
123
+ label: 'arm 2: WITH cacheKey (this change)',
124
+ salt,
125
+ arm: 'b',
126
+ cacheKey: `routing-ab-${salt}`
127
+ });
128
+ if (withKeyResult.isFailure()) {
129
+ return (0, ts_utils_1.fail)(withKeyResult.message);
130
+ }
131
+ const comparison = (0, promptCacheUsageMath_1.compareCacheHitRatios)(withoutKeyResult.value.warmUsage, withKeyResult.value.warmUsage);
132
+ return (0, ts_utils_1.succeed)({
133
+ withoutKey: withoutKeyResult.value,
134
+ withKey: withKeyResult.value,
135
+ verdict: comparison.verdict,
136
+ withoutKeyWarmRatio: comparison.baselineRatio,
137
+ withKeyWarmRatio: comparison.candidateRatio
138
+ });
139
+ }
140
+ /**
141
+ * Formats the A/B's closing verdict line(s), matching the original harness's console output.
142
+ * @internal
143
+ */
144
+ function formatRoutingAbVerdict(result) {
145
+ const a = (0, promptCacheUsageMath_1.formatCacheRatioPercent)(result.withoutKeyWarmRatio);
146
+ const b = (0, promptCacheUsageMath_1.formatCacheRatioPercent)(result.withKeyWarmRatio);
147
+ switch (result.verdict) {
148
+ case 'confirms':
149
+ return (`arm 1 warm ${a} -> arm 2 warm ${b}\n` +
150
+ 'CONFIRMS the prediction: the routing key materially improves the hit rate.');
151
+ case 'misses':
152
+ return (`arm 1 warm ${a} -> arm 2 warm ${b}\n` +
153
+ 'MISSES the prediction: arm 2 did not jump. The routing key is not buying what this ' +
154
+ 'change claims — revise the change or the claim, do not re-run for a better number. ' +
155
+ '(Re-run once with a fresh SALT first, to rule out a TTL-tainted reading.)');
156
+ case 'ambiguous':
157
+ return (`arm 1 warm ${a} -> arm 2 warm ${b}\n` +
158
+ 'AMBIGUOUS: arm 1 may have hit by routing luck. Re-run with a fresh SALT.');
159
+ case 'inconclusive':
160
+ return 'inconclusive: cache-hit ratio not computable for at least one arm.';
161
+ }
162
+ }
163
+ //# sourceMappingURL=promptCacheRoutingAbHarness.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"promptCacheRoutingAbHarness.js","sourceRoot":"","sources":["../../../src/packlets/ai-assist/promptCacheRoutingAbHarness.ts"],"names":[],"mappings":"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","sourcesContent":["// Copyright (c) 2026 Erik Fortune\n//\n// Permission is hereby granted, free of charge, to any person obtaining a copy\n// of this software and associated documentation files (the \"Software\"), to deal\n// in the Software without restriction, including without limitation the rights\n// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n// copies of the Software, and to permit persons to whom the Software is\n// furnished to do so, subject to the following conditions:\n//\n// The above copyright notice and this permission notice shall be included in all\n// copies or substantial portions of the Software.\n//\n// THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\n// SOFTWARE.\n\n/**\n * Pure logic and injectable transport for `perf/promptCacheRoutingAb.js` — the A/B that measures\n * whether `IAiCacheRequest.cacheKey` actually improves the xAI cache-hit rate (design.md §2's\n * 2026-09-19 measurement note, and result.md's \"gating defect in C3\").\n *\n * @remarks\n * Extracted for the same reason as `promptCacheObservabilityHarness.ts`: the verdict this harness\n * prints (CONFIRMS / MISSES / AMBIGUOUS) \"has already been used to justify a merge,\" per this\n * stream's follow-up task, and had no test checking it classifies correctly.\n * @packageDocumentation\n */\n\nimport { fail, succeed, type Result } from '@fgv/ts-utils';\n\nimport { type IAiCacheRequest } from './cacheRequest';\nimport { type IProviderCompletionParams } from './completionClient';\nimport { type IAiCompletionResponse, type IAiProviderDescriptor } from './model';\nimport {\n type CacheHitComparisonVerdict,\n compareCacheHitRatios,\n computeCacheUsageRatio,\n formatCacheRatioPercent\n} from './promptCacheUsageMath';\nimport { type IAiCompletionUsage } from './usageTypes';\n\n/** Injected transport — only the network call needs a live key. @internal */\nexport interface IPromptCacheRoutingAbDeps {\n readonly callCompletion: (params: IProviderCompletionParams) => Promise<Result<IAiCompletionResponse>>;\n}\n\n/**\n * Builds one arm's stable system prefix. Ported verbatim from the harness's `stablePrefix(arm)`.\n * `salt` and `arm` both feed the text so the two arms — and two separate runs — never warm each\n * other's cache.\n * @internal\n */\nexport function buildRoutingAbPrefix(salt: string, arm: string): string {\n const lines: string[] = [];\n for (let i = 0; i < 400; i++) {\n lines.push(\n `Directive ${salt}-${arm}-${i}: when asked about topic ${i}, respond precisely and cite source ${i}-${\n i * 7\n }.`\n );\n }\n return `You are a careful assistant operating under the following fixed policy document.\\n\\n${lines.join(\n '\\n'\n )}`;\n}\n\n/**\n * One line describing a usage block. Ported from `describeUsage`, now computing the ratio via\n * {@link computeCacheUsageRatio} instead of an inline calculation that coalesced a missing\n * `cachedInputTokens` or `uncachedInputTokens` to `0` (see that function's remarks).\n * @internal\n */\nexport function describeRoutingAbUsage(usage: IAiCompletionUsage | undefined): string {\n if (usage === undefined) {\n return 'usage absent from response';\n }\n const { cachedInputTokens, uncachedInputTokens } = usage;\n const { totalInputTokens, cachedRatio } = computeCacheUsageRatio(usage);\n if (cachedRatio === undefined) {\n return `reports=${usage.reports} cached=${cachedInputTokens} uncached=${uncachedInputTokens} (ratio not computable)`;\n }\n return `reports=${\n usage.reports\n } cached=${cachedInputTokens} of ${totalInputTokens} input (${formatCacheRatioPercent(cachedRatio)})`;\n}\n\n/** One arm's cold-then-warm outcome. @internal */\nexport interface IPromptCacheRoutingAbArmResult {\n readonly label: string;\n readonly coldUsage: IAiCompletionUsage | undefined;\n readonly warmUsage: IAiCompletionUsage | undefined;\n}\n\n/**\n * Runs one arm: a cold call, then a warm call sharing the same {@link buildRoutingAbPrefix} system\n * prompt and differing only in the final user turn — optionally supplying `cacheKey`. Ported from\n * `runArm`, with the network call injected.\n * @internal\n */\nexport async function runPromptCacheRoutingAbArm(\n deps: IPromptCacheRoutingAbDeps,\n params: {\n readonly descriptor: IAiProviderDescriptor;\n readonly apiKey: string;\n readonly label: string;\n readonly salt: string;\n readonly arm: string;\n readonly cacheKey?: string;\n }\n): Promise<Result<IPromptCacheRoutingAbArmResult>> {\n const { descriptor, apiKey, label, salt, arm, cacheKey } = params;\n const system = buildRoutingAbPrefix(salt, arm);\n const cache: IAiCacheRequest | undefined = cacheKey !== undefined ? { cacheKey } : undefined;\n const call = (content: string): Promise<Result<IAiCompletionResponse>> =>\n deps.callCompletion({\n descriptor,\n apiKey,\n system,\n messages: [{ role: 'user', content }],\n cache\n });\n\n const cold = await call('In one sentence, what is directive 12 about?');\n if (cold.isFailure()) {\n return fail(`${label}: cold call failed: ${cold.message}`);\n }\n\n // Only the tail differs from the cold call — the prefix is byte-identical.\n const warm = await call('In one sentence, what is directive 99 about?');\n if (warm.isFailure()) {\n return fail(`${label}: warm call failed: ${warm.message}`);\n }\n\n return succeed({ label, coldUsage: cold.value.usage, warmUsage: warm.value.usage });\n}\n\n/** The full A/B: both arms' results and the routing-key verdict. @internal */\nexport interface IPromptCacheRoutingAbResult {\n readonly withoutKey: IPromptCacheRoutingAbArmResult;\n readonly withKey: IPromptCacheRoutingAbArmResult;\n /** Baseline = `withoutKey`'s warm call; candidate = `withKey`'s warm call. */\n readonly verdict: CacheHitComparisonVerdict;\n readonly withoutKeyWarmRatio: number | undefined;\n readonly withKeyWarmRatio: number | undefined;\n}\n\n/**\n * Runs both arms of the routing-key A/B and classifies the result.\n *\n * @remarks\n * The verdict compares each arm's **warm** call against the other's — not an arm's own cold vs.\n * warm call — because the question is whether the routing key changes the warm hit rate relative\n * to the no-key baseline, exactly as `promptCacheRoutingAb.js`'s original `main()` computed it\n * (`pct(withoutKey)` / `pct(withKey)`, both taken from each arm's warm response).\n * @internal\n */\nexport async function runPromptCacheRoutingAbHarness(\n deps: IPromptCacheRoutingAbDeps,\n params: {\n readonly descriptor: IAiProviderDescriptor;\n readonly apiKey: string;\n readonly salt: string;\n }\n): Promise<Result<IPromptCacheRoutingAbResult>> {\n const { descriptor, apiKey, salt } = params;\n\n const withoutKeyResult = await runPromptCacheRoutingAbArm(deps, {\n descriptor,\n apiKey,\n label: 'arm 1: WITHOUT cacheKey (behaviour before this change)',\n salt,\n arm: 'a'\n });\n if (withoutKeyResult.isFailure()) {\n return fail(withoutKeyResult.message);\n }\n\n const withKeyResult = await runPromptCacheRoutingAbArm(deps, {\n descriptor,\n apiKey,\n label: 'arm 2: WITH cacheKey (this change)',\n salt,\n arm: 'b',\n cacheKey: `routing-ab-${salt}`\n });\n if (withKeyResult.isFailure()) {\n return fail(withKeyResult.message);\n }\n\n const comparison = compareCacheHitRatios(withoutKeyResult.value.warmUsage, withKeyResult.value.warmUsage);\n return succeed({\n withoutKey: withoutKeyResult.value,\n withKey: withKeyResult.value,\n verdict: comparison.verdict,\n withoutKeyWarmRatio: comparison.baselineRatio,\n withKeyWarmRatio: comparison.candidateRatio\n });\n}\n\n/**\n * Formats the A/B's closing verdict line(s), matching the original harness's console output.\n * @internal\n */\nexport function formatRoutingAbVerdict(result: IPromptCacheRoutingAbResult): string {\n const a = formatCacheRatioPercent(result.withoutKeyWarmRatio);\n const b = formatCacheRatioPercent(result.withKeyWarmRatio);\n switch (result.verdict) {\n case 'confirms':\n return (\n `arm 1 warm ${a} -> arm 2 warm ${b}\\n` +\n 'CONFIRMS the prediction: the routing key materially improves the hit rate.'\n );\n case 'misses':\n return (\n `arm 1 warm ${a} -> arm 2 warm ${b}\\n` +\n 'MISSES the prediction: arm 2 did not jump. The routing key is not buying what this ' +\n 'change claims — revise the change or the claim, do not re-run for a better number. ' +\n '(Re-run once with a fresh SALT first, to rule out a TTL-tainted reading.)'\n );\n case 'ambiguous':\n return (\n `arm 1 warm ${a} -> arm 2 warm ${b}\\n` +\n 'AMBIGUOUS: arm 1 may have hit by routing luck. Re-run with a fresh SALT.'\n );\n case 'inconclusive':\n return 'inconclusive: cache-hit ratio not computable for at least one arm.';\n }\n}\n"]}
@@ -0,0 +1,105 @@
1
+ /**
2
+ * Cache-hit ratio arithmetic and cold/warm verdict classification shared by the two prompt-cache
3
+ * measurement harnesses under `perf/` (`promptCacheObservability.js`,
4
+ * `promptCacheRoutingAb.js`).
5
+ *
6
+ * @remarks
7
+ * Extracted per design.md §8's addendum ("Preferred remedy: keep `perf/` placement, add the
8
+ * seam") and `TESTING_GUIDELINES.md` § "Measurement Harnesses" — before this module existed,
9
+ * both harnesses computed this math inline, disagreed with each other on how to treat missing or
10
+ * zero-valued fields, and had no test of either version. See {@link computeCacheUsageRatio} and
11
+ * {@link compareCacheHitRatios} for what each inline version did and what changed.
12
+ * @packageDocumentation
13
+ */
14
+ import { type IAiCompletionUsage } from './usageTypes';
15
+ /**
16
+ * A cache-hit ratio derived from one completion's usage block.
17
+ * @internal
18
+ */
19
+ export interface ICacheUsageRatio {
20
+ /** `cachedInputTokens + uncachedInputTokens`, when both are known. `0` is a valid value. */
21
+ readonly totalInputTokens: number | undefined;
22
+ /**
23
+ * `cachedInputTokens / totalInputTokens`, in `[0, 1]`.
24
+ *
25
+ * @remarks
26
+ * `undefined` whenever the ratio is not a meaningful number: the usage block itself is absent,
27
+ * either token count is unreported, or the total is `0` (there is no input to have a ratio
28
+ * over). Per design.md's governing rule R-c, unknown is reported as unknown, never defaulted to
29
+ * a number.
30
+ */
31
+ readonly cachedRatio: number | undefined;
32
+ }
33
+ /**
34
+ * Computes {@link ICacheUsageRatio} from a completion's usage block.
35
+ *
36
+ * @remarks
37
+ * The two harnesses this was extracted from each computed this ratio independently, inline, and
38
+ * disagreed with each other — and with this function — on edge cases that neither had a test for:
39
+ *
40
+ * - `promptCacheObservability.js`'s `summarizeUsage` divided by a `0` total without a guard,
41
+ * printing `NaN%` whenever a response reported `uncachedInputTokens: 0, cachedInputTokens: 0`.
42
+ * - `promptCacheRoutingAb.js`'s inline `pct()` coalesced a missing `cachedInputTokens` **or**
43
+ * `uncachedInputTokens` to `0` (`(u.cachedInputTokens ?? 0) + (u.uncachedInputTokens ?? 0)`,
44
+ * `100 * (u.cachedInputTokens ?? 0) / total`) — and which field was missing decided the
45
+ * direction of the fabrication: a missing `cachedInputTokens` fabricated a false low ratio
46
+ * (feeding a false `MISSES`), while a missing `uncachedInputTokens` fabricated a false **100%**
47
+ * (feeding a false `CONFIRMS`) — this is not a one-directional bug.
48
+ * - `promptCacheRoutingAb.js`'s `describeUsage` also special-cased a *genuinely reported*
49
+ * `cachedInputTokens: 0, uncachedInputTokens: 0` (both fields present) as `0.0%`, and the
50
+ * verdict's `pct()` scored that same input as a hard `0`, feeding `MISSES`. This function scores
51
+ * a real zero-input total as not computable, same as observability's zero-total case above —
52
+ * there were no bytes to have a ratio over, so `0.0%` claims more than the data supports.
53
+ *
54
+ * All three are fixed here rather than reproduced: this function returns `undefined` — never a
55
+ * fabricated number — whenever the ratio cannot honestly be computed, matching the same
56
+ * unknown-is-unknown rule the normalized {@link IAiCompletionUsage} type itself follows.
57
+ * @internal
58
+ */
59
+ export declare function computeCacheUsageRatio(usage: IAiCompletionUsage | undefined): ICacheUsageRatio;
60
+ /**
61
+ * Formats a cache-hit ratio as a percentage string, or `'n/a'` when {@link computeCacheUsageRatio}
62
+ * could not compute one.
63
+ * @internal
64
+ */
65
+ export declare function formatCacheRatioPercent(ratio: number | undefined): string;
66
+ /**
67
+ * Verdict for a baseline/candidate pair of cache-hit ratios.
68
+ *
69
+ * @remarks
70
+ * `'inconclusive'` has no equivalent in the original inline logic — see {@link compareCacheHitRatios}.
71
+ * @internal
72
+ */
73
+ export type CacheHitComparisonVerdict = 'confirms' | 'misses' | 'ambiguous' | 'inconclusive';
74
+ /** A candidate ratio at or below this percentage cannot count as a cache hit. @internal */
75
+ export declare const CACHE_HIT_FLOOR_PERCENT: number;
76
+ /**
77
+ * The candidate ratio must clear the baseline ratio by at least this multiple to be attributed to
78
+ * something other than routing luck.
79
+ * @internal
80
+ */
81
+ export declare const CACHE_HIT_MIN_IMPROVEMENT_FACTOR: number;
82
+ /** The outcome of comparing a baseline usage block against a candidate one. @internal */
83
+ export interface ICacheHitComparison {
84
+ readonly baselineRatio: number | undefined;
85
+ readonly candidateRatio: number | undefined;
86
+ readonly verdict: CacheHitComparisonVerdict;
87
+ }
88
+ /**
89
+ * Classifies a baseline/candidate pair of usage blocks by their cache-hit ratio.
90
+ *
91
+ * @remarks
92
+ * Ported from `promptCacheRoutingAb.js`'s inline verdict —
93
+ * `if (b > 50 && b > a * 5) CONFIRMS; else if (b <= 50) MISSES; else AMBIGUOUS` — now driven off
94
+ * {@link computeCacheUsageRatio} rather than a hand-rolled `pct()` that coalesced missing fields
95
+ * to `0`. `'inconclusive'` is new: it is what an unclassifiable ratio (baseline or candidate
96
+ * `undefined`) reports now, in place of the original's silent `0`, which could otherwise produce
97
+ * a false `MISSES` verdict from data that never said "no cache hit" — it said nothing at all.
98
+ *
99
+ * The same classification also drives `promptCacheObservability.js`'s cold/warm comparison
100
+ * (baseline = cold call, candidate = warm call), which previously had no computed verdict of any
101
+ * kind — only two printed summaries and a static reminder for a human to judge by eye.
102
+ * @internal
103
+ */
104
+ export declare function compareCacheHitRatios(baseline: IAiCompletionUsage | undefined, candidate: IAiCompletionUsage | undefined): ICacheHitComparison;
105
+ //# sourceMappingURL=promptCacheUsageMath.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"promptCacheUsageMath.d.ts","sourceRoot":"","sources":["../../../src/packlets/ai-assist/promptCacheUsageMath.ts"],"names":[],"mappings":"AAoBA;;;;;;;;;;;;GAYG;AAEH,OAAO,EAAE,KAAK,kBAAkB,EAAE,MAAM,cAAc,CAAC;AAEvD;;;GAGG;AACH,MAAM,WAAW,gBAAgB;IAC/B,4FAA4F;IAC5F,QAAQ,CAAC,gBAAgB,EAAE,MAAM,GAAG,SAAS,CAAC;IAC9C;;;;;;;;OAQG;IACH,QAAQ,CAAC,WAAW,EAAE,MAAM,GAAG,SAAS,CAAC;CAC1C;AAED;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,wBAAgB,sBAAsB,CAAC,KAAK,EAAE,kBAAkB,GAAG,SAAS,GAAG,gBAAgB,CA8B9F;AAED;;;;GAIG;AACH,wBAAgB,uBAAuB,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,CAEzE;AAED;;;;;;GAMG;AACH,MAAM,MAAM,yBAAyB,GAAG,UAAU,GAAG,QAAQ,GAAG,WAAW,GAAG,cAAc,CAAC;AAE7F,2FAA2F;AAC3F,eAAO,MAAM,uBAAuB,EAAE,MAAW,CAAC;AAElD;;;;GAIG;AACH,eAAO,MAAM,gCAAgC,EAAE,MAAU,CAAC;AAE1D,yFAAyF;AACzF,MAAM,WAAW,mBAAmB;IAClC,QAAQ,CAAC,aAAa,EAAE,MAAM,GAAG,SAAS,CAAC;IAC3C,QAAQ,CAAC,cAAc,EAAE,MAAM,GAAG,SAAS,CAAC;IAC5C,QAAQ,CAAC,OAAO,EAAE,yBAAyB,CAAC;CAC7C;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,qBAAqB,CACnC,QAAQ,EAAE,kBAAkB,GAAG,SAAS,EACxC,SAAS,EAAE,kBAAkB,GAAG,SAAS,GACxC,mBAAmB,CAkBrB"}
@@ -0,0 +1,130 @@
1
+ "use strict";
2
+ // Copyright (c) 2026 Erik Fortune
3
+ //
4
+ // Permission is hereby granted, free of charge, to any person obtaining a copy
5
+ // of this software and associated documentation files (the "Software"), to deal
6
+ // in the Software without restriction, including without limitation the rights
7
+ // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8
+ // copies of the Software, and to permit persons to whom the Software is
9
+ // furnished to do so, subject to the following conditions:
10
+ //
11
+ // The above copyright notice and this permission notice shall be included in all
12
+ // copies or substantial portions of the Software.
13
+ //
14
+ // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15
+ // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16
+ // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17
+ // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18
+ // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19
+ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
20
+ // SOFTWARE.
21
+ Object.defineProperty(exports, "__esModule", { value: true });
22
+ exports.CACHE_HIT_MIN_IMPROVEMENT_FACTOR = exports.CACHE_HIT_FLOOR_PERCENT = void 0;
23
+ exports.computeCacheUsageRatio = computeCacheUsageRatio;
24
+ exports.formatCacheRatioPercent = formatCacheRatioPercent;
25
+ exports.compareCacheHitRatios = compareCacheHitRatios;
26
+ /**
27
+ * Computes {@link ICacheUsageRatio} from a completion's usage block.
28
+ *
29
+ * @remarks
30
+ * The two harnesses this was extracted from each computed this ratio independently, inline, and
31
+ * disagreed with each other — and with this function — on edge cases that neither had a test for:
32
+ *
33
+ * - `promptCacheObservability.js`'s `summarizeUsage` divided by a `0` total without a guard,
34
+ * printing `NaN%` whenever a response reported `uncachedInputTokens: 0, cachedInputTokens: 0`.
35
+ * - `promptCacheRoutingAb.js`'s inline `pct()` coalesced a missing `cachedInputTokens` **or**
36
+ * `uncachedInputTokens` to `0` (`(u.cachedInputTokens ?? 0) + (u.uncachedInputTokens ?? 0)`,
37
+ * `100 * (u.cachedInputTokens ?? 0) / total`) — and which field was missing decided the
38
+ * direction of the fabrication: a missing `cachedInputTokens` fabricated a false low ratio
39
+ * (feeding a false `MISSES`), while a missing `uncachedInputTokens` fabricated a false **100%**
40
+ * (feeding a false `CONFIRMS`) — this is not a one-directional bug.
41
+ * - `promptCacheRoutingAb.js`'s `describeUsage` also special-cased a *genuinely reported*
42
+ * `cachedInputTokens: 0, uncachedInputTokens: 0` (both fields present) as `0.0%`, and the
43
+ * verdict's `pct()` scored that same input as a hard `0`, feeding `MISSES`. This function scores
44
+ * a real zero-input total as not computable, same as observability's zero-total case above —
45
+ * there were no bytes to have a ratio over, so `0.0%` claims more than the data supports.
46
+ *
47
+ * All three are fixed here rather than reproduced: this function returns `undefined` — never a
48
+ * fabricated number — whenever the ratio cannot honestly be computed, matching the same
49
+ * unknown-is-unknown rule the normalized {@link IAiCompletionUsage} type itself follows.
50
+ * @internal
51
+ */
52
+ function computeCacheUsageRatio(usage) {
53
+ if (usage === undefined) {
54
+ return { totalInputTokens: undefined, cachedRatio: undefined };
55
+ }
56
+ const { cachedInputTokens, uncachedInputTokens } = usage;
57
+ if (cachedInputTokens === undefined || uncachedInputTokens === undefined) {
58
+ return { totalInputTokens: undefined, cachedRatio: undefined };
59
+ }
60
+ // A *present but out-of-range* component is not the same hazard as an absent one, and the
61
+ // absent case above does not cover it. A token count cannot be negative or non-finite, so one
62
+ // that is means the usage block is malformed — and computing anyway yields a ratio outside
63
+ // [0, 1] that this function's own contract promises cannot happen. That ratio then reaches
64
+ // `compareCacheHitRatios`, where a skewed baseline can fabricate a favourable verdict from an
65
+ // unremarkable candidate. Reachable from real wire data, not just hand-built input:
66
+ // `usageNormalization.ts` derives `uncachedInputTokens` as `promptTokens - cachedInputTokens`
67
+ // on both the Chat Completions and Responses routes with no guard, so any provider reporting
68
+ // `cached_tokens > prompt_tokens` produces exactly this shape.
69
+ if (!Number.isFinite(cachedInputTokens) ||
70
+ !Number.isFinite(uncachedInputTokens) ||
71
+ cachedInputTokens < 0 ||
72
+ uncachedInputTokens < 0) {
73
+ return { totalInputTokens: undefined, cachedRatio: undefined };
74
+ }
75
+ const totalInputTokens = cachedInputTokens + uncachedInputTokens;
76
+ if (totalInputTokens <= 0) {
77
+ return { totalInputTokens, cachedRatio: undefined };
78
+ }
79
+ return { totalInputTokens, cachedRatio: cachedInputTokens / totalInputTokens };
80
+ }
81
+ /**
82
+ * Formats a cache-hit ratio as a percentage string, or `'n/a'` when {@link computeCacheUsageRatio}
83
+ * could not compute one.
84
+ * @internal
85
+ */
86
+ function formatCacheRatioPercent(ratio) {
87
+ return ratio === undefined ? 'n/a' : `${(ratio * 100).toFixed(1)}%`;
88
+ }
89
+ /** A candidate ratio at or below this percentage cannot count as a cache hit. @internal */
90
+ exports.CACHE_HIT_FLOOR_PERCENT = 50;
91
+ /**
92
+ * The candidate ratio must clear the baseline ratio by at least this multiple to be attributed to
93
+ * something other than routing luck.
94
+ * @internal
95
+ */
96
+ exports.CACHE_HIT_MIN_IMPROVEMENT_FACTOR = 5;
97
+ /**
98
+ * Classifies a baseline/candidate pair of usage blocks by their cache-hit ratio.
99
+ *
100
+ * @remarks
101
+ * Ported from `promptCacheRoutingAb.js`'s inline verdict —
102
+ * `if (b > 50 && b > a * 5) CONFIRMS; else if (b <= 50) MISSES; else AMBIGUOUS` — now driven off
103
+ * {@link computeCacheUsageRatio} rather than a hand-rolled `pct()` that coalesced missing fields
104
+ * to `0`. `'inconclusive'` is new: it is what an unclassifiable ratio (baseline or candidate
105
+ * `undefined`) reports now, in place of the original's silent `0`, which could otherwise produce
106
+ * a false `MISSES` verdict from data that never said "no cache hit" — it said nothing at all.
107
+ *
108
+ * The same classification also drives `promptCacheObservability.js`'s cold/warm comparison
109
+ * (baseline = cold call, candidate = warm call), which previously had no computed verdict of any
110
+ * kind — only two printed summaries and a static reminder for a human to judge by eye.
111
+ * @internal
112
+ */
113
+ function compareCacheHitRatios(baseline, candidate) {
114
+ const baselineRatio = computeCacheUsageRatio(baseline).cachedRatio;
115
+ const candidateRatio = computeCacheUsageRatio(candidate).cachedRatio;
116
+ if (baselineRatio === undefined || candidateRatio === undefined) {
117
+ return { baselineRatio, candidateRatio, verdict: 'inconclusive' };
118
+ }
119
+ const baselinePercent = baselineRatio * 100;
120
+ const candidatePercent = candidateRatio * 100;
121
+ if (candidatePercent > exports.CACHE_HIT_FLOOR_PERCENT &&
122
+ candidatePercent > baselinePercent * exports.CACHE_HIT_MIN_IMPROVEMENT_FACTOR) {
123
+ return { baselineRatio, candidateRatio, verdict: 'confirms' };
124
+ }
125
+ if (candidatePercent <= exports.CACHE_HIT_FLOOR_PERCENT) {
126
+ return { baselineRatio, candidateRatio, verdict: 'misses' };
127
+ }
128
+ return { baselineRatio, candidateRatio, verdict: 'ambiguous' };
129
+ }
130
+ //# sourceMappingURL=promptCacheUsageMath.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"promptCacheUsageMath.js","sourceRoot":"","sources":["../../../src/packlets/ai-assist/promptCacheUsageMath.ts"],"names":[],"mappings":"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","sourcesContent":["// Copyright (c) 2026 Erik Fortune\n//\n// Permission is hereby granted, free of charge, to any person obtaining a copy\n// of this software and associated documentation files (the \"Software\"), to deal\n// in the Software without restriction, including without limitation the rights\n// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n// copies of the Software, and to permit persons to whom the Software is\n// furnished to do so, subject to the following conditions:\n//\n// The above copyright notice and this permission notice shall be included in all\n// copies or substantial portions of the Software.\n//\n// THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\n// SOFTWARE.\n\n/**\n * Cache-hit ratio arithmetic and cold/warm verdict classification shared by the two prompt-cache\n * measurement harnesses under `perf/` (`promptCacheObservability.js`,\n * `promptCacheRoutingAb.js`).\n *\n * @remarks\n * Extracted per design.md §8's addendum (\"Preferred remedy: keep `perf/` placement, add the\n * seam\") and `TESTING_GUIDELINES.md` § \"Measurement Harnesses\" — before this module existed,\n * both harnesses computed this math inline, disagreed with each other on how to treat missing or\n * zero-valued fields, and had no test of either version. See {@link computeCacheUsageRatio} and\n * {@link compareCacheHitRatios} for what each inline version did and what changed.\n * @packageDocumentation\n */\n\nimport { type IAiCompletionUsage } from './usageTypes';\n\n/**\n * A cache-hit ratio derived from one completion's usage block.\n * @internal\n */\nexport interface ICacheUsageRatio {\n /** `cachedInputTokens + uncachedInputTokens`, when both are known. `0` is a valid value. */\n readonly totalInputTokens: number | undefined;\n /**\n * `cachedInputTokens / totalInputTokens`, in `[0, 1]`.\n *\n * @remarks\n * `undefined` whenever the ratio is not a meaningful number: the usage block itself is absent,\n * either token count is unreported, or the total is `0` (there is no input to have a ratio\n * over). Per design.md's governing rule R-c, unknown is reported as unknown, never defaulted to\n * a number.\n */\n readonly cachedRatio: number | undefined;\n}\n\n/**\n * Computes {@link ICacheUsageRatio} from a completion's usage block.\n *\n * @remarks\n * The two harnesses this was extracted from each computed this ratio independently, inline, and\n * disagreed with each other — and with this function — on edge cases that neither had a test for:\n *\n * - `promptCacheObservability.js`'s `summarizeUsage` divided by a `0` total without a guard,\n * printing `NaN%` whenever a response reported `uncachedInputTokens: 0, cachedInputTokens: 0`.\n * - `promptCacheRoutingAb.js`'s inline `pct()` coalesced a missing `cachedInputTokens` **or**\n * `uncachedInputTokens` to `0` (`(u.cachedInputTokens ?? 0) + (u.uncachedInputTokens ?? 0)`,\n * `100 * (u.cachedInputTokens ?? 0) / total`) — and which field was missing decided the\n * direction of the fabrication: a missing `cachedInputTokens` fabricated a false low ratio\n * (feeding a false `MISSES`), while a missing `uncachedInputTokens` fabricated a false **100%**\n * (feeding a false `CONFIRMS`) — this is not a one-directional bug.\n * - `promptCacheRoutingAb.js`'s `describeUsage` also special-cased a *genuinely reported*\n * `cachedInputTokens: 0, uncachedInputTokens: 0` (both fields present) as `0.0%`, and the\n * verdict's `pct()` scored that same input as a hard `0`, feeding `MISSES`. This function scores\n * a real zero-input total as not computable, same as observability's zero-total case above —\n * there were no bytes to have a ratio over, so `0.0%` claims more than the data supports.\n *\n * All three are fixed here rather than reproduced: this function returns `undefined` — never a\n * fabricated number — whenever the ratio cannot honestly be computed, matching the same\n * unknown-is-unknown rule the normalized {@link IAiCompletionUsage} type itself follows.\n * @internal\n */\nexport function computeCacheUsageRatio(usage: IAiCompletionUsage | undefined): ICacheUsageRatio {\n if (usage === undefined) {\n return { totalInputTokens: undefined, cachedRatio: undefined };\n }\n const { cachedInputTokens, uncachedInputTokens } = usage;\n if (cachedInputTokens === undefined || uncachedInputTokens === undefined) {\n return { totalInputTokens: undefined, cachedRatio: undefined };\n }\n // A *present but out-of-range* component is not the same hazard as an absent one, and the\n // absent case above does not cover it. A token count cannot be negative or non-finite, so one\n // that is means the usage block is malformed — and computing anyway yields a ratio outside\n // [0, 1] that this function's own contract promises cannot happen. That ratio then reaches\n // `compareCacheHitRatios`, where a skewed baseline can fabricate a favourable verdict from an\n // unremarkable candidate. Reachable from real wire data, not just hand-built input:\n // `usageNormalization.ts` derives `uncachedInputTokens` as `promptTokens - cachedInputTokens`\n // on both the Chat Completions and Responses routes with no guard, so any provider reporting\n // `cached_tokens > prompt_tokens` produces exactly this shape.\n if (\n !Number.isFinite(cachedInputTokens) ||\n !Number.isFinite(uncachedInputTokens) ||\n cachedInputTokens < 0 ||\n uncachedInputTokens < 0\n ) {\n return { totalInputTokens: undefined, cachedRatio: undefined };\n }\n const totalInputTokens = cachedInputTokens + uncachedInputTokens;\n if (totalInputTokens <= 0) {\n return { totalInputTokens, cachedRatio: undefined };\n }\n return { totalInputTokens, cachedRatio: cachedInputTokens / totalInputTokens };\n}\n\n/**\n * Formats a cache-hit ratio as a percentage string, or `'n/a'` when {@link computeCacheUsageRatio}\n * could not compute one.\n * @internal\n */\nexport function formatCacheRatioPercent(ratio: number | undefined): string {\n return ratio === undefined ? 'n/a' : `${(ratio * 100).toFixed(1)}%`;\n}\n\n/**\n * Verdict for a baseline/candidate pair of cache-hit ratios.\n *\n * @remarks\n * `'inconclusive'` has no equivalent in the original inline logic — see {@link compareCacheHitRatios}.\n * @internal\n */\nexport type CacheHitComparisonVerdict = 'confirms' | 'misses' | 'ambiguous' | 'inconclusive';\n\n/** A candidate ratio at or below this percentage cannot count as a cache hit. @internal */\nexport const CACHE_HIT_FLOOR_PERCENT: number = 50;\n\n/**\n * The candidate ratio must clear the baseline ratio by at least this multiple to be attributed to\n * something other than routing luck.\n * @internal\n */\nexport const CACHE_HIT_MIN_IMPROVEMENT_FACTOR: number = 5;\n\n/** The outcome of comparing a baseline usage block against a candidate one. @internal */\nexport interface ICacheHitComparison {\n readonly baselineRatio: number | undefined;\n readonly candidateRatio: number | undefined;\n readonly verdict: CacheHitComparisonVerdict;\n}\n\n/**\n * Classifies a baseline/candidate pair of usage blocks by their cache-hit ratio.\n *\n * @remarks\n * Ported from `promptCacheRoutingAb.js`'s inline verdict —\n * `if (b > 50 && b > a * 5) CONFIRMS; else if (b <= 50) MISSES; else AMBIGUOUS` — now driven off\n * {@link computeCacheUsageRatio} rather than a hand-rolled `pct()` that coalesced missing fields\n * to `0`. `'inconclusive'` is new: it is what an unclassifiable ratio (baseline or candidate\n * `undefined`) reports now, in place of the original's silent `0`, which could otherwise produce\n * a false `MISSES` verdict from data that never said \"no cache hit\" — it said nothing at all.\n *\n * The same classification also drives `promptCacheObservability.js`'s cold/warm comparison\n * (baseline = cold call, candidate = warm call), which previously had no computed verdict of any\n * kind — only two printed summaries and a static reminder for a human to judge by eye.\n * @internal\n */\nexport function compareCacheHitRatios(\n baseline: IAiCompletionUsage | undefined,\n candidate: IAiCompletionUsage | undefined\n): ICacheHitComparison {\n const baselineRatio = computeCacheUsageRatio(baseline).cachedRatio;\n const candidateRatio = computeCacheUsageRatio(candidate).cachedRatio;\n if (baselineRatio === undefined || candidateRatio === undefined) {\n return { baselineRatio, candidateRatio, verdict: 'inconclusive' };\n }\n const baselinePercent = baselineRatio * 100;\n const candidatePercent = candidateRatio * 100;\n if (\n candidatePercent > CACHE_HIT_FLOOR_PERCENT &&\n candidatePercent > baselinePercent * CACHE_HIT_MIN_IMPROVEMENT_FACTOR\n ) {\n return { baselineRatio, candidateRatio, verdict: 'confirms' };\n }\n if (candidatePercent <= CACHE_HIT_FLOOR_PERCENT) {\n return { baselineRatio, candidateRatio, verdict: 'misses' };\n }\n return { baselineRatio, candidateRatio, verdict: 'ambiguous' };\n}\n"]}
@@ -1 +1 @@
1
- {"version":3,"file":"registry.d.ts","sourceRoot":"","sources":["../../../src/packlets/ai-assist/registry.ts"],"names":[],"mappings":"AAoBA;;;GAGG;AAEH,OAAO,EAAQ,MAAM,EAAW,MAAM,eAAe,CAAC;AAEtD,OAAO,EACL,KAAK,YAAY,EACjB,KAAK,2BAA2B,EAChC,KAAK,uBAAuB,EAC5B,KAAK,wBAAwB,EAC7B,KAAK,qBAAqB,EAE3B,MAAM,SAAS,CAAC;AACjB,OAAO,KAAK,EAAE,6BAA6B,EAAE,MAAM,yBAAyB,CAAC;AAkT7E;;;GAGG;AACH,eAAO,MAAM,cAAc,EAAE,aAAa,CAAC,YAAY,CAAsC,CAAC;AAE9F;;;;GAIG;AACH,wBAAgB,sBAAsB,IAAI,aAAa,CAAC,qBAAqB,CAAC,CAE7E;AAED;;;;;GAKG;AACH,wBAAgB,qBAAqB,CAAC,EAAE,EAAE,MAAM,GAAG,MAAM,CAAC,qBAAqB,CAAC,CAM/E;AAED;;;;;;;GAOG;AACH,wBAAgB,uBAAuB,CAAC,UAAU,EAAE,qBAAqB,GAAG,OAAO,CAElF;AAsCD;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,wBAAgB,sBAAsB,CACpC,UAAU,EAAE,qBAAqB,EACjC,OAAO,EAAE,MAAM,GACd,uBAAuB,GAAG,SAAS,CAErC;AAED;;;;;;;GAOG;AACH,wBAAgB,iBAAiB,CAAC,UAAU,EAAE,qBAAqB,GAAG,OAAO,CAE5E;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,iCAAiC,CAC/C,UAAU,EAAE,qBAAqB,EACjC,OAAO,EAAE,MAAM,GACd,6BAA6B,GAAG,SAAS,CAE3C;AAED;;;GAGG;AACH,wBAAgB,wBAAwB,CAAC,UAAU,EAAE,qBAAqB,GAAG,OAAO,CAEnF;AAED;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,wBAAgB,0BAA0B,CACxC,UAAU,EAAE,qBAAqB,EACjC,OAAO,EAAE,MAAM,GACd,2BAA2B,GAAG,SAAS,CAEzC;AAMD;;;;;;;GAOG;AACH,eAAO,MAAM,+BAA+B,EAAE,wBAyD7C,CAAC"}
1
+ {"version":3,"file":"registry.d.ts","sourceRoot":"","sources":["../../../src/packlets/ai-assist/registry.ts"],"names":[],"mappings":"AAoBA;;;GAGG;AAEH,OAAO,EAAQ,MAAM,EAAW,MAAM,eAAe,CAAC;AAEtD,OAAO,EACL,KAAK,YAAY,EACjB,KAAK,2BAA2B,EAChC,KAAK,uBAAuB,EAC5B,KAAK,wBAAwB,EAC7B,KAAK,qBAAqB,EAE3B,MAAM,SAAS,CAAC;AACjB,OAAO,KAAK,EAAE,6BAA6B,EAAE,MAAM,yBAAyB,CAAC;AAgT7E;;;GAGG;AACH,eAAO,MAAM,cAAc,EAAE,aAAa,CAAC,YAAY,CAAsC,CAAC;AAE9F;;;;GAIG;AACH,wBAAgB,sBAAsB,IAAI,aAAa,CAAC,qBAAqB,CAAC,CAE7E;AAED;;;;;GAKG;AACH,wBAAgB,qBAAqB,CAAC,EAAE,EAAE,MAAM,GAAG,MAAM,CAAC,qBAAqB,CAAC,CAM/E;AAED;;;;;;;GAOG;AACH,wBAAgB,uBAAuB,CAAC,UAAU,EAAE,qBAAqB,GAAG,OAAO,CAElF;AAsCD;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,wBAAgB,sBAAsB,CACpC,UAAU,EAAE,qBAAqB,EACjC,OAAO,EAAE,MAAM,GACd,uBAAuB,GAAG,SAAS,CAErC;AAED;;;;;;;GAOG;AACH,wBAAgB,iBAAiB,CAAC,UAAU,EAAE,qBAAqB,GAAG,OAAO,CAE5E;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,iCAAiC,CAC/C,UAAU,EAAE,qBAAqB,EACjC,OAAO,EAAE,MAAM,GACd,6BAA6B,GAAG,SAAS,CAE3C;AAED;;;GAGG;AACH,wBAAgB,wBAAwB,CAAC,UAAU,EAAE,qBAAqB,GAAG,OAAO,CAEnF;AAED;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,wBAAgB,0BAA0B,CACxC,UAAU,EAAE,qBAAqB,EACjC,OAAO,EAAE,MAAM,GACd,2BAA2B,GAAG,SAAS,CAEzC;AAMD;;;;;;;GAOG;AACH,eAAO,MAAM,+BAA+B,EAAE,wBAyD7C,CAAC"}
@@ -53,8 +53,7 @@ const BUILTIN_PROVIDERS = [
53
53
  supportedTools: [],
54
54
  corsRestricted: false,
55
55
  streamingCorsRestricted: false,
56
- acceptsImageInput: false,
57
- thinkingMode: 'unsupported'
56
+ acceptsImageInput: false
58
57
  },
59
58
  {
60
59
  id: 'anthropic',
@@ -81,7 +80,6 @@ const BUILTIN_PROVIDERS = [
81
80
  corsRestricted: false,
82
81
  streamingCorsRestricted: false,
83
82
  acceptsImageInput: true,
84
- thinkingMode: 'optional',
85
83
  // Claude 5 family requires the adaptive thinking wire shape (thinking.type: 'adaptive' +
86
84
  // output_config.effort) and 400s on the legacy thinking.type: 'enabled' + budget_tokens
87
85
  // shape; see AiAssist.isAdaptiveThinkingModel.
@@ -116,10 +114,16 @@ const BUILTIN_PROVIDERS = [
116
114
  '@google-gemini:embedding': 'gemini-embedding-001' // NOT deprecated — aliased for uniformity only
117
115
  },
118
116
  supportedTools: ['web_search'],
117
+ // `generateContent` HTTP-400s (INVALID_ARGUMENT) when built-in grounding
118
+ // (`web_search` → `google_search`) and `function_declarations` are combined in
119
+ // one request. The only descriptor in the registry that declares an exclusion;
120
+ // keep it that way deliberately — a new entry here changes what a host gets
121
+ // back without the host changing anything, so it wants the same evidence a
122
+ // capability claim wants.
123
+ serverToolsExclusiveWithClientTools: ['web_search'],
119
124
  corsRestricted: false,
120
125
  streamingCorsRestricted: false,
121
126
  acceptsImageInput: true,
122
- thinkingMode: 'optional',
123
127
  // Gemini carries the constraint inside `generationConfig`
124
128
  // (responseMimeType + responseSchema), uniform across the family.
125
129
  structuredOutput: [{ modelPrefix: '', format: 'gemini-response-schema' }],
@@ -156,8 +160,7 @@ const BUILTIN_PROVIDERS = [
156
160
  supportedTools: [],
157
161
  corsRestricted: false,
158
162
  streamingCorsRestricted: false,
159
- acceptsImageInput: false,
160
- thinkingMode: 'unsupported'
163
+ acceptsImageInput: false
161
164
  },
162
165
  {
163
166
  id: 'mistral',
@@ -171,7 +174,6 @@ const BUILTIN_PROVIDERS = [
171
174
  corsRestricted: false,
172
175
  streamingCorsRestricted: false,
173
176
  acceptsImageInput: false,
174
- thinkingMode: 'unsupported',
175
177
  structuredOutput: [{ modelPrefix: '', format: 'openai-json-schema' }],
176
178
  embedding: [{ modelPrefix: '', format: 'openai-embeddings' }]
177
179
  },
@@ -187,7 +189,6 @@ const BUILTIN_PROVIDERS = [
187
189
  corsRestricted: false,
188
190
  streamingCorsRestricted: false,
189
191
  acceptsImageInput: false,
190
- thinkingMode: 'unsupported',
191
192
  embedding: [{ modelPrefix: '', format: 'openai-embeddings' }]
192
193
  },
193
194
  {
@@ -221,7 +222,6 @@ const BUILTIN_PROVIDERS = [
221
222
  corsRestricted: false,
222
223
  streamingCorsRestricted: false,
223
224
  acceptsImageInput: true,
224
- thinkingMode: 'optional',
225
225
  responsesOnlyModelPrefixes: ['gpt-5.5-pro'],
226
226
  // Declared once for the whole line. The Chat-Completions-vs-Responses split is
227
227
  // NOT declared here on purpose: the route depends on whether the call carries
@@ -273,7 +273,6 @@ const BUILTIN_PROVIDERS = [
273
273
  corsRestricted: false,
274
274
  streamingCorsRestricted: false,
275
275
  acceptsImageInput: false,
276
- thinkingMode: 'unsupported',
277
276
  embedding: [{ modelPrefix: '', format: 'openai-embeddings' }]
278
277
  },
279
278
  {
@@ -298,7 +297,6 @@ const BUILTIN_PROVIDERS = [
298
297
  corsRestricted: true,
299
298
  streamingCorsRestricted: true,
300
299
  acceptsImageInput: true,
301
- thinkingMode: 'optional',
302
300
  structuredOutput: [{ modelPrefix: '', format: 'openai-json-schema' }],
303
301
  imageGeneration: [
304
302
  {