@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.
- package/CAPABILITIES.md +95 -0
- package/dist/packlets/ai-assist/cacheRequest.js +118 -0
- package/dist/packlets/ai-assist/cacheRequest.js.map +1 -0
- package/dist/packlets/ai-assist/chatRequestBuilders.js +70 -5
- package/dist/packlets/ai-assist/chatRequestBuilders.js.map +1 -1
- package/dist/packlets/ai-assist/completionClient.js +122 -45
- package/dist/packlets/ai-assist/completionClient.js.map +1 -1
- package/dist/packlets/ai-assist/imageGenerationClient.js +1 -1
- package/dist/packlets/ai-assist/imageGenerationClient.js.map +1 -1
- package/dist/packlets/ai-assist/index.js +3 -1
- package/dist/packlets/ai-assist/index.js.map +1 -1
- package/dist/packlets/ai-assist/model.js.map +1 -1
- package/dist/packlets/ai-assist/promptCacheAnthropicBreakpointHarness.js +200 -0
- package/dist/packlets/ai-assist/promptCacheAnthropicBreakpointHarness.js.map +1 -0
- package/dist/packlets/ai-assist/promptCacheObservabilityHarness.js +137 -0
- package/dist/packlets/ai-assist/promptCacheObservabilityHarness.js.map +1 -0
- package/dist/packlets/ai-assist/promptCacheRoutingAbHarness.js +156 -0
- package/dist/packlets/ai-assist/promptCacheRoutingAbHarness.js.map +1 -0
- package/dist/packlets/ai-assist/promptCacheUsageMath.js +124 -0
- package/dist/packlets/ai-assist/promptCacheUsageMath.js.map +1 -0
- package/dist/packlets/ai-assist/registry.js +9 -11
- package/dist/packlets/ai-assist/registry.js.map +1 -1
- package/dist/packlets/ai-assist/streamUsageCapability.js +124 -0
- package/dist/packlets/ai-assist/streamUsageCapability.js.map +1 -0
- package/dist/packlets/ai-assist/streamingAdapters/anthropic.js +24 -5
- package/dist/packlets/ai-assist/streamingAdapters/anthropic.js.map +1 -1
- package/dist/packlets/ai-assist/streamingAdapters/clientToolContinuationBuilder.js +64 -33
- package/dist/packlets/ai-assist/streamingAdapters/clientToolContinuationBuilder.js.map +1 -1
- package/dist/packlets/ai-assist/streamingAdapters/common.js +25 -1
- package/dist/packlets/ai-assist/streamingAdapters/common.js.map +1 -1
- package/dist/packlets/ai-assist/streamingAdapters/gemini.js +12 -5
- package/dist/packlets/ai-assist/streamingAdapters/gemini.js.map +1 -1
- package/dist/packlets/ai-assist/streamingAdapters/openaiChat.js +34 -8
- package/dist/packlets/ai-assist/streamingAdapters/openaiChat.js.map +1 -1
- package/dist/packlets/ai-assist/streamingAdapters/openaiResponses.js +22 -14
- package/dist/packlets/ai-assist/streamingAdapters/openaiResponses.js.map +1 -1
- package/dist/packlets/ai-assist/streamingClient.js +3 -2
- package/dist/packlets/ai-assist/streamingClient.js.map +1 -1
- package/dist/packlets/ai-assist/structuredOutput.js +84 -7
- package/dist/packlets/ai-assist/structuredOutput.js.map +1 -1
- package/dist/packlets/ai-assist/structuredOutputTypes.js.map +1 -1
- package/dist/packlets/ai-assist/thinkingOptionsResolver.js +18 -3
- package/dist/packlets/ai-assist/thinkingOptionsResolver.js.map +1 -1
- package/dist/packlets/ai-assist/toolFormats.js +80 -0
- package/dist/packlets/ai-assist/toolFormats.js.map +1 -1
- package/dist/packlets/ai-assist/toolTypes.js +21 -0
- package/dist/packlets/ai-assist/toolTypes.js.map +1 -0
- package/dist/packlets/ai-assist/usageNormalization.js +178 -0
- package/dist/packlets/ai-assist/usageNormalization.js.map +1 -0
- package/dist/packlets/ai-assist/usageTypes.js +21 -0
- package/dist/packlets/ai-assist/usageTypes.js.map +1 -0
- package/dist/packlets/mustache/index.js.map +1 -1
- package/dist/packlets/mustache/interfaces.js.map +1 -1
- package/dist/packlets/mustache/mustacheTemplate.js +91 -0
- package/dist/packlets/mustache/mustacheTemplate.js.map +1 -1
- package/dist/ts-extras.d.ts +577 -30
- package/lib/packlets/ai-assist/cacheRequest.d.ts +100 -0
- package/lib/packlets/ai-assist/cacheRequest.d.ts.map +1 -0
- package/lib/packlets/ai-assist/cacheRequest.js +123 -0
- package/lib/packlets/ai-assist/cacheRequest.js.map +1 -0
- package/lib/packlets/ai-assist/chatRequestBuilders.d.ts +34 -4
- package/lib/packlets/ai-assist/chatRequestBuilders.d.ts.map +1 -1
- package/lib/packlets/ai-assist/chatRequestBuilders.js +73 -5
- package/lib/packlets/ai-assist/chatRequestBuilders.js.map +1 -1
- package/lib/packlets/ai-assist/completionClient.d.ts +6 -0
- package/lib/packlets/ai-assist/completionClient.d.ts.map +1 -1
- package/lib/packlets/ai-assist/completionClient.js +121 -44
- package/lib/packlets/ai-assist/completionClient.js.map +1 -1
- package/lib/packlets/ai-assist/imageGenerationClient.d.ts.map +1 -1
- package/lib/packlets/ai-assist/imageGenerationClient.js +1 -1
- package/lib/packlets/ai-assist/imageGenerationClient.js.map +1 -1
- package/lib/packlets/ai-assist/index.d.ts +7 -2
- package/lib/packlets/ai-assist/index.d.ts.map +1 -1
- package/lib/packlets/ai-assist/index.js +11 -1
- package/lib/packlets/ai-assist/index.js.map +1 -1
- package/lib/packlets/ai-assist/model.d.ts +59 -288
- package/lib/packlets/ai-assist/model.d.ts.map +1 -1
- package/lib/packlets/ai-assist/model.js.map +1 -1
- package/lib/packlets/ai-assist/promptCacheAnthropicBreakpointHarness.d.ts +124 -0
- package/lib/packlets/ai-assist/promptCacheAnthropicBreakpointHarness.d.ts.map +1 -0
- package/lib/packlets/ai-assist/promptCacheAnthropicBreakpointHarness.js +208 -0
- package/lib/packlets/ai-assist/promptCacheAnthropicBreakpointHarness.js.map +1 -0
- package/lib/packlets/ai-assist/promptCacheObservabilityHarness.d.ts +77 -0
- package/lib/packlets/ai-assist/promptCacheObservabilityHarness.d.ts.map +1 -0
- package/lib/packlets/ai-assist/promptCacheObservabilityHarness.js +143 -0
- package/lib/packlets/ai-assist/promptCacheObservabilityHarness.js.map +1 -0
- package/lib/packlets/ai-assist/promptCacheRoutingAbHarness.d.ts +84 -0
- package/lib/packlets/ai-assist/promptCacheRoutingAbHarness.d.ts.map +1 -0
- package/lib/packlets/ai-assist/promptCacheRoutingAbHarness.js +163 -0
- package/lib/packlets/ai-assist/promptCacheRoutingAbHarness.js.map +1 -0
- package/lib/packlets/ai-assist/promptCacheUsageMath.d.ts +105 -0
- package/lib/packlets/ai-assist/promptCacheUsageMath.d.ts.map +1 -0
- package/lib/packlets/ai-assist/promptCacheUsageMath.js +130 -0
- package/lib/packlets/ai-assist/promptCacheUsageMath.js.map +1 -0
- package/lib/packlets/ai-assist/registry.d.ts.map +1 -1
- package/lib/packlets/ai-assist/registry.js +9 -11
- package/lib/packlets/ai-assist/registry.js.map +1 -1
- package/lib/packlets/ai-assist/streamUsageCapability.d.ts +119 -0
- package/lib/packlets/ai-assist/streamUsageCapability.d.ts.map +1 -0
- package/lib/packlets/ai-assist/streamUsageCapability.js +130 -0
- package/lib/packlets/ai-assist/streamUsageCapability.js.map +1 -0
- package/lib/packlets/ai-assist/streamingAdapters/anthropic.d.ts.map +1 -1
- package/lib/packlets/ai-assist/streamingAdapters/anthropic.js +23 -4
- package/lib/packlets/ai-assist/streamingAdapters/anthropic.js.map +1 -1
- package/lib/packlets/ai-assist/streamingAdapters/clientToolContinuationBuilder.d.ts +21 -12
- package/lib/packlets/ai-assist/streamingAdapters/clientToolContinuationBuilder.d.ts.map +1 -1
- package/lib/packlets/ai-assist/streamingAdapters/clientToolContinuationBuilder.js +64 -34
- package/lib/packlets/ai-assist/streamingAdapters/clientToolContinuationBuilder.js.map +1 -1
- package/lib/packlets/ai-assist/streamingAdapters/common.d.ts +21 -0
- package/lib/packlets/ai-assist/streamingAdapters/common.d.ts.map +1 -1
- package/lib/packlets/ai-assist/streamingAdapters/common.js +25 -1
- package/lib/packlets/ai-assist/streamingAdapters/common.js.map +1 -1
- package/lib/packlets/ai-assist/streamingAdapters/gemini.d.ts.map +1 -1
- package/lib/packlets/ai-assist/streamingAdapters/gemini.js +12 -5
- package/lib/packlets/ai-assist/streamingAdapters/gemini.js.map +1 -1
- package/lib/packlets/ai-assist/streamingAdapters/openaiChat.d.ts +1 -1
- package/lib/packlets/ai-assist/streamingAdapters/openaiChat.d.ts.map +1 -1
- package/lib/packlets/ai-assist/streamingAdapters/openaiChat.js +33 -7
- package/lib/packlets/ai-assist/streamingAdapters/openaiChat.js.map +1 -1
- package/lib/packlets/ai-assist/streamingAdapters/openaiResponses.d.ts +1 -1
- package/lib/packlets/ai-assist/streamingAdapters/openaiResponses.d.ts.map +1 -1
- package/lib/packlets/ai-assist/streamingAdapters/openaiResponses.js +21 -13
- package/lib/packlets/ai-assist/streamingAdapters/openaiResponses.js.map +1 -1
- package/lib/packlets/ai-assist/streamingClient.d.ts.map +1 -1
- package/lib/packlets/ai-assist/streamingClient.js +3 -2
- package/lib/packlets/ai-assist/streamingClient.js.map +1 -1
- package/lib/packlets/ai-assist/structuredOutput.d.ts +32 -0
- package/lib/packlets/ai-assist/structuredOutput.d.ts.map +1 -1
- package/lib/packlets/ai-assist/structuredOutput.js +85 -7
- package/lib/packlets/ai-assist/structuredOutput.js.map +1 -1
- package/lib/packlets/ai-assist/structuredOutputTypes.d.ts +30 -0
- package/lib/packlets/ai-assist/structuredOutputTypes.d.ts.map +1 -1
- package/lib/packlets/ai-assist/structuredOutputTypes.js.map +1 -1
- package/lib/packlets/ai-assist/thinkingOptionsResolver.d.ts.map +1 -1
- package/lib/packlets/ai-assist/thinkingOptionsResolver.js +18 -3
- package/lib/packlets/ai-assist/thinkingOptionsResolver.js.map +1 -1
- package/lib/packlets/ai-assist/toolFormats.d.ts +55 -1
- package/lib/packlets/ai-assist/toolFormats.d.ts.map +1 -1
- package/lib/packlets/ai-assist/toolFormats.js +82 -0
- package/lib/packlets/ai-assist/toolFormats.js.map +1 -1
- package/lib/packlets/ai-assist/toolTypes.d.ts +328 -0
- package/lib/packlets/ai-assist/toolTypes.d.ts.map +1 -0
- package/lib/packlets/ai-assist/toolTypes.js +22 -0
- package/lib/packlets/ai-assist/toolTypes.js.map +1 -0
- package/lib/packlets/ai-assist/usageNormalization.d.ts +58 -0
- package/lib/packlets/ai-assist/usageNormalization.d.ts.map +1 -0
- package/lib/packlets/ai-assist/usageNormalization.js +184 -0
- package/lib/packlets/ai-assist/usageNormalization.js.map +1 -0
- package/lib/packlets/ai-assist/usageTypes.d.ts +84 -0
- package/lib/packlets/ai-assist/usageTypes.d.ts.map +1 -0
- package/lib/packlets/ai-assist/usageTypes.js +22 -0
- package/lib/packlets/ai-assist/usageTypes.js.map +1 -0
- package/lib/packlets/mustache/index.d.ts +1 -1
- package/lib/packlets/mustache/index.d.ts.map +1 -1
- package/lib/packlets/mustache/index.js.map +1 -1
- package/lib/packlets/mustache/interfaces.d.ts +30 -0
- package/lib/packlets/mustache/interfaces.d.ts.map +1 -1
- package/lib/packlets/mustache/interfaces.js.map +1 -1
- package/lib/packlets/mustache/mustacheTemplate.d.ts +56 -1
- package/lib/packlets/mustache/mustacheTemplate.d.ts.map +1 -1
- package/lib/packlets/mustache/mustacheTemplate.js +91 -0
- package/lib/packlets/mustache/mustacheTemplate.js.map +1 -1
- package/package.json +16 -16
- package/perf/promptCacheAnthropicBreakpoint.js +135 -0
- package/perf/promptCacheObservability.js +107 -0
- 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;
|
|
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
|
{
|