@fgv/ts-extras 5.1.0-55 → 5.1.0-57
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 +96 -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 +72 -7
- package/dist/packlets/ai-assist/chatRequestBuilders.js.map +1 -1
- package/dist/packlets/ai-assist/completionClient.js +195 -58
- package/dist/packlets/ai-assist/completionClient.js.map +1 -1
- package/dist/packlets/ai-assist/converters.js +54 -0
- package/dist/packlets/ai-assist/converters.js.map +1 -1
- package/dist/packlets/ai-assist/embeddingClient.js +16 -1
- package/dist/packlets/ai-assist/embeddingClient.js.map +1 -1
- package/dist/packlets/ai-assist/imageGenerationClient.js +17 -2
- 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/listModelsClient.js +19 -1
- package/dist/packlets/ai-assist/listModelsClient.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 +134 -0
- package/dist/packlets/ai-assist/streamUsageCapability.js.map +1 -0
- package/dist/packlets/ai-assist/streamingAdapters/anthropic.js +32 -9
- package/dist/packlets/ai-assist/streamingAdapters/anthropic.js.map +1 -1
- package/dist/packlets/ai-assist/streamingAdapters/clientToolContinuationBuilder.js +81 -35
- 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 +45 -11
- package/dist/packlets/ai-assist/streamingAdapters/openaiChat.js.map +1 -1
- package/dist/packlets/ai-assist/streamingAdapters/openaiResponses.js +33 -22
- package/dist/packlets/ai-assist/streamingAdapters/openaiResponses.js.map +1 -1
- package/dist/packlets/ai-assist/streamingAdapters/proxy.js +62 -4
- package/dist/packlets/ai-assist/streamingAdapters/proxy.js.map +1 -1
- package/dist/packlets/ai-assist/streamingClient.js +20 -5
- package/dist/packlets/ai-assist/streamingClient.js.map +1 -1
- package/dist/packlets/ai-assist/thinkingOptionsResolver.js +23 -5
- 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 +764 -48
- 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 +36 -6
- package/lib/packlets/ai-assist/chatRequestBuilders.d.ts.map +1 -1
- package/lib/packlets/ai-assist/chatRequestBuilders.js +75 -7
- package/lib/packlets/ai-assist/chatRequestBuilders.js.map +1 -1
- package/lib/packlets/ai-assist/completionClient.d.ts +19 -0
- package/lib/packlets/ai-assist/completionClient.d.ts.map +1 -1
- package/lib/packlets/ai-assist/completionClient.js +194 -57
- package/lib/packlets/ai-assist/completionClient.js.map +1 -1
- package/lib/packlets/ai-assist/converters.d.ts +46 -0
- package/lib/packlets/ai-assist/converters.d.ts.map +1 -1
- package/lib/packlets/ai-assist/converters.js +55 -1
- package/lib/packlets/ai-assist/converters.js.map +1 -1
- package/lib/packlets/ai-assist/embeddingClient.d.ts +9 -0
- package/lib/packlets/ai-assist/embeddingClient.d.ts.map +1 -1
- package/lib/packlets/ai-assist/embeddingClient.js +16 -1
- package/lib/packlets/ai-assist/embeddingClient.js.map +1 -1
- package/lib/packlets/ai-assist/imageGenerationClient.d.ts +9 -0
- package/lib/packlets/ai-assist/imageGenerationClient.d.ts.map +1 -1
- package/lib/packlets/ai-assist/imageGenerationClient.js +17 -2
- 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/listModelsClient.d.ts +12 -0
- package/lib/packlets/ai-assist/listModelsClient.d.ts.map +1 -1
- package/lib/packlets/ai-assist/listModelsClient.js +19 -1
- package/lib/packlets/ai-assist/listModelsClient.js.map +1 -1
- package/lib/packlets/ai-assist/model.d.ts +81 -295
- 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 +129 -0
- package/lib/packlets/ai-assist/streamUsageCapability.d.ts.map +1 -0
- package/lib/packlets/ai-assist/streamUsageCapability.js +140 -0
- package/lib/packlets/ai-assist/streamUsageCapability.js.map +1 -0
- package/lib/packlets/ai-assist/streamingAdapters/anthropic.d.ts +2 -1
- package/lib/packlets/ai-assist/streamingAdapters/anthropic.d.ts.map +1 -1
- package/lib/packlets/ai-assist/streamingAdapters/anthropic.js +29 -6
- package/lib/packlets/ai-assist/streamingAdapters/anthropic.js.map +1 -1
- package/lib/packlets/ai-assist/streamingAdapters/clientToolContinuationBuilder.d.ts +58 -14
- package/lib/packlets/ai-assist/streamingAdapters/clientToolContinuationBuilder.d.ts.map +1 -1
- package/lib/packlets/ai-assist/streamingAdapters/clientToolContinuationBuilder.js +81 -36
- package/lib/packlets/ai-assist/streamingAdapters/clientToolContinuationBuilder.js.map +1 -1
- package/lib/packlets/ai-assist/streamingAdapters/common.d.ts +38 -3
- 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 +2 -1
- package/lib/packlets/ai-assist/streamingAdapters/openaiChat.d.ts.map +1 -1
- package/lib/packlets/ai-assist/streamingAdapters/openaiChat.js +42 -8
- package/lib/packlets/ai-assist/streamingAdapters/openaiChat.js.map +1 -1
- package/lib/packlets/ai-assist/streamingAdapters/openaiResponses.d.ts +2 -1
- package/lib/packlets/ai-assist/streamingAdapters/openaiResponses.d.ts.map +1 -1
- package/lib/packlets/ai-assist/streamingAdapters/openaiResponses.js +30 -19
- package/lib/packlets/ai-assist/streamingAdapters/openaiResponses.js.map +1 -1
- package/lib/packlets/ai-assist/streamingAdapters/proxy.d.ts +22 -0
- package/lib/packlets/ai-assist/streamingAdapters/proxy.d.ts.map +1 -1
- package/lib/packlets/ai-assist/streamingAdapters/proxy.js +62 -4
- package/lib/packlets/ai-assist/streamingAdapters/proxy.js.map +1 -1
- package/lib/packlets/ai-assist/streamingClient.d.ts +5 -0
- package/lib/packlets/ai-assist/streamingClient.d.ts.map +1 -1
- package/lib/packlets/ai-assist/streamingClient.js +20 -5
- package/lib/packlets/ai-assist/streamingClient.js.map +1 -1
- package/lib/packlets/ai-assist/thinkingOptionsResolver.d.ts +11 -3
- package/lib/packlets/ai-assist/thinkingOptionsResolver.d.ts.map +1 -1
- package/lib/packlets/ai-assist/thinkingOptionsResolver.js +23 -5
- 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 +386 -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,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Request-level prompt-caching plan (design.md §6.1) and the shared validation + splitting logic
|
|
3
|
+
* every adapter that emits cache breakpoints (Anthropic, OpenAI) uses, so the ascending/range/cap
|
|
4
|
+
* rules live in exactly one place rather than being re-derived per adapter.
|
|
5
|
+
*
|
|
6
|
+
* @packageDocumentation
|
|
7
|
+
*/
|
|
8
|
+
import { Result } from '@fgv/ts-utils';
|
|
9
|
+
/**
|
|
10
|
+
* A request-level prompt-caching plan.
|
|
11
|
+
*
|
|
12
|
+
* @remarks
|
|
13
|
+
* Offsets rather than content blocks — see design.md §6.1 for why: `AiPrompt`/`IChatRequest`
|
|
14
|
+
* carry `system` as a plain string, offsets are usable without `@fgv/ts-prompt-assist` (a
|
|
15
|
+
* caller can say "the first 4,000 characters of my system prompt are stable" directly), and
|
|
16
|
+
* they are already the currency `IPromptSection.start`/`.chars` report.
|
|
17
|
+
* @public
|
|
18
|
+
*/
|
|
19
|
+
export interface IAiCacheRequest {
|
|
20
|
+
/**
|
|
21
|
+
* Strictly ascending UTF-16 offsets into `AiPrompt.system` (equivalently
|
|
22
|
+
* `IProviderCompletionParams.system`), each marking the end of a cacheable prefix. Each must
|
|
23
|
+
* be an integer `> 0` and `< system.length`. Validated where the offsets are actually used
|
|
24
|
+
* (against the request's own `system.length`, and optionally against a cap) — never clamped;
|
|
25
|
+
* an invalid plan fails the request rather than silently caching less than declared. Sent where
|
|
26
|
+
* the provider has a breakpoint mechanism (Anthropic `cache_control`, OpenAI
|
|
27
|
+
* `prompt_cache_breakpoint`). Ignored by adapters that have no such concept (Gemini).
|
|
28
|
+
*/
|
|
29
|
+
readonly systemBreakpoints?: ReadonlyArray<number>;
|
|
30
|
+
/**
|
|
31
|
+
* Opaque cache-routing key, sent where the provider has one and by that provider's transport:
|
|
32
|
+
* `prompt_cache_key` as a body field on OpenAI (both routes) and on xAI Responses, and an
|
|
33
|
+
* `x-grok-conv-id` header on xAI Chat Completions. See `supportsPromptCacheRouting`.
|
|
34
|
+
*
|
|
35
|
+
* @remarks
|
|
36
|
+
* On xAI this is not a nicety. Its prompt cache is per-server and evictable, so two requests
|
|
37
|
+
* sharing a prefix can still miss if they are routed to different servers; the key pins them
|
|
38
|
+
* together. Providers with no routing concept ignore it and receive a request byte-identical
|
|
39
|
+
* to one built without a `cache` at all.
|
|
40
|
+
*/
|
|
41
|
+
readonly cacheKey?: string;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Validates an {@link IAiCacheRequest}'s `systemBreakpoints` against the length of the `system`
|
|
45
|
+
* string they index into, and optionally against a per-model write cap.
|
|
46
|
+
*
|
|
47
|
+
* @remarks
|
|
48
|
+
* **Fails loudly, never clamps** (design.md §6.1): a non-ascending, out-of-range, or over-cap
|
|
49
|
+
* offset returns `fail()`. A silently-clamped breakpoint is precisely the silent, non-caching
|
|
50
|
+
* failure this stream exists to remove. `systemLength` and `maxBreakpointWrites` are themselves
|
|
51
|
+
* validated as non-negative integers — checked before `systemBreakpoints` even, so a malformed
|
|
52
|
+
* value (`NaN`, a negative number, a fraction) fails loudly rather than silently passing every
|
|
53
|
+
* offset check it should have rejected (`offset >= NaN` is always `false`) or, for the cap,
|
|
54
|
+
* passing unnoticed on an empty offset list. `maxBreakpointWrites` is left to the caller to supply —
|
|
55
|
+
* there is no model-keyed cap table in this package yet (design.md §7's `IAiCacheCapability` is
|
|
56
|
+
* out of this slice's scope), and design.md §5.2 establishes that a caller producing breakpoints
|
|
57
|
+
* from the three-level `PromptCacheStability` vocabulary never emits more than two, so an
|
|
58
|
+
* omitted cap is not a soundness gap for that caller — only a direct caller supplying more than a
|
|
59
|
+
* provider's cap would ever need one enforced here.
|
|
60
|
+
*
|
|
61
|
+
* Takes a length rather than {@link validateAiCacheRequest}'s `system` string because a caller
|
|
62
|
+
* building a plan from an `@fgv/ts-prompt-assist` `IPromptComposition` (`totalChars`) has the
|
|
63
|
+
* length without holding the resolved body text itself.
|
|
64
|
+
* @public
|
|
65
|
+
*/
|
|
66
|
+
export declare function validateCacheBreakpoints(systemLength: number, cache: IAiCacheRequest, maxBreakpointWrites?: number): Result<IAiCacheRequest>;
|
|
67
|
+
/**
|
|
68
|
+
* Validates an {@link IAiCacheRequest}'s `systemBreakpoints` against the `system` string they
|
|
69
|
+
* index into. Thin wrapper over {@link validateCacheBreakpoints} for a caller that holds the
|
|
70
|
+
* actual string (every `ts-extras` adapter) rather than just its length.
|
|
71
|
+
* @public
|
|
72
|
+
*/
|
|
73
|
+
export declare function validateAiCacheRequest(system: string, cache: IAiCacheRequest, maxBreakpointWrites?: number): Result<IAiCacheRequest>;
|
|
74
|
+
/**
|
|
75
|
+
* One piece of `system` after splitting at its cache breakpoints. Concatenating every segment's
|
|
76
|
+
* `text`, in order, reproduces `system` exactly.
|
|
77
|
+
* @public
|
|
78
|
+
*/
|
|
79
|
+
export interface ICacheSystemSegment {
|
|
80
|
+
/** This segment's text, a contiguous slice of the original `system` string. */
|
|
81
|
+
readonly text: string;
|
|
82
|
+
/**
|
|
83
|
+
* True for the segment that ends a cacheable prefix — the one a provider's adapter should mark
|
|
84
|
+
* with its native cache directive (Anthropic `cache_control`, OpenAI `prompt_cache_breakpoint`).
|
|
85
|
+
* False for every other segment, including the final (volatile) tail after the last breakpoint.
|
|
86
|
+
*/
|
|
87
|
+
readonly cacheBreakpoint: boolean;
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Splits `system` into cache-annotated segments per `cache.systemBreakpoints`, validating first.
|
|
91
|
+
*
|
|
92
|
+
* @remarks
|
|
93
|
+
* When `cache` is `undefined` or declares no breakpoints, returns the whole string as a single
|
|
94
|
+
* non-breakpointed segment — an un-annotated caller's request body is byte-identical to before
|
|
95
|
+
* this feature existed (design.md §11). Shared by the Anthropic and OpenAI adapters so the
|
|
96
|
+
* splitting logic (and its validation) is written once.
|
|
97
|
+
* @internal
|
|
98
|
+
*/
|
|
99
|
+
export declare function splitSystemForCache(system: string, cache: IAiCacheRequest | undefined, maxBreakpointWrites?: number): Result<ReadonlyArray<ICacheSystemSegment>>;
|
|
100
|
+
//# sourceMappingURL=cacheRequest.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cacheRequest.d.ts","sourceRoot":"","sources":["../../../src/packlets/ai-assist/cacheRequest.ts"],"names":[],"mappings":"AAoBA;;;;;;GAMG;AAEH,OAAO,EAAQ,MAAM,EAAW,MAAM,eAAe,CAAC;AAEtD;;;;;;;;;GASG;AACH,MAAM,WAAW,eAAe;IAC9B;;;;;;;;OAQG;IACH,QAAQ,CAAC,iBAAiB,CAAC,EAAE,aAAa,CAAC,MAAM,CAAC,CAAC;IACnD;;;;;;;;;;OAUG;IACH,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;CAC5B;AAED;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,wBAAgB,wBAAwB,CACtC,YAAY,EAAE,MAAM,EACpB,KAAK,EAAE,eAAe,EACtB,mBAAmB,CAAC,EAAE,MAAM,GAC3B,MAAM,CAAC,eAAe,CAAC,CAsCzB;AAED;;;;;GAKG;AACH,wBAAgB,sBAAsB,CACpC,MAAM,EAAE,MAAM,EACd,KAAK,EAAE,eAAe,EACtB,mBAAmB,CAAC,EAAE,MAAM,GAC3B,MAAM,CAAC,eAAe,CAAC,CAEzB;AAED;;;;GAIG;AACH,MAAM,WAAW,mBAAmB;IAClC,+EAA+E;IAC/E,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB;;;;OAIG;IACH,QAAQ,CAAC,eAAe,EAAE,OAAO,CAAC;CACnC;AAED;;;;;;;;;GASG;AACH,wBAAgB,mBAAmB,CACjC,MAAM,EAAE,MAAM,EACd,KAAK,EAAE,eAAe,GAAG,SAAS,EAClC,mBAAmB,CAAC,EAAE,MAAM,GAC3B,MAAM,CAAC,aAAa,CAAC,mBAAmB,CAAC,CAAC,CAgB5C"}
|
|
@@ -0,0 +1,123 @@
|
|
|
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.validateCacheBreakpoints = validateCacheBreakpoints;
|
|
23
|
+
exports.validateAiCacheRequest = validateAiCacheRequest;
|
|
24
|
+
exports.splitSystemForCache = splitSystemForCache;
|
|
25
|
+
/**
|
|
26
|
+
* Request-level prompt-caching plan (design.md §6.1) and the shared validation + splitting logic
|
|
27
|
+
* every adapter that emits cache breakpoints (Anthropic, OpenAI) uses, so the ascending/range/cap
|
|
28
|
+
* rules live in exactly one place rather than being re-derived per adapter.
|
|
29
|
+
*
|
|
30
|
+
* @packageDocumentation
|
|
31
|
+
*/
|
|
32
|
+
const ts_utils_1 = require("@fgv/ts-utils");
|
|
33
|
+
/**
|
|
34
|
+
* Validates an {@link IAiCacheRequest}'s `systemBreakpoints` against the length of the `system`
|
|
35
|
+
* string they index into, and optionally against a per-model write cap.
|
|
36
|
+
*
|
|
37
|
+
* @remarks
|
|
38
|
+
* **Fails loudly, never clamps** (design.md §6.1): a non-ascending, out-of-range, or over-cap
|
|
39
|
+
* offset returns `fail()`. A silently-clamped breakpoint is precisely the silent, non-caching
|
|
40
|
+
* failure this stream exists to remove. `systemLength` and `maxBreakpointWrites` are themselves
|
|
41
|
+
* validated as non-negative integers — checked before `systemBreakpoints` even, so a malformed
|
|
42
|
+
* value (`NaN`, a negative number, a fraction) fails loudly rather than silently passing every
|
|
43
|
+
* offset check it should have rejected (`offset >= NaN` is always `false`) or, for the cap,
|
|
44
|
+
* passing unnoticed on an empty offset list. `maxBreakpointWrites` is left to the caller to supply —
|
|
45
|
+
* there is no model-keyed cap table in this package yet (design.md §7's `IAiCacheCapability` is
|
|
46
|
+
* out of this slice's scope), and design.md §5.2 establishes that a caller producing breakpoints
|
|
47
|
+
* from the three-level `PromptCacheStability` vocabulary never emits more than two, so an
|
|
48
|
+
* omitted cap is not a soundness gap for that caller — only a direct caller supplying more than a
|
|
49
|
+
* provider's cap would ever need one enforced here.
|
|
50
|
+
*
|
|
51
|
+
* Takes a length rather than {@link validateAiCacheRequest}'s `system` string because a caller
|
|
52
|
+
* building a plan from an `@fgv/ts-prompt-assist` `IPromptComposition` (`totalChars`) has the
|
|
53
|
+
* length without holding the resolved body text itself.
|
|
54
|
+
* @public
|
|
55
|
+
*/
|
|
56
|
+
function validateCacheBreakpoints(systemLength, cache, maxBreakpointWrites) {
|
|
57
|
+
if (!Number.isInteger(systemLength) || systemLength < 0) {
|
|
58
|
+
return (0, ts_utils_1.fail)(`systemLength (${systemLength}) must be a non-negative integer`);
|
|
59
|
+
}
|
|
60
|
+
if (maxBreakpointWrites !== undefined &&
|
|
61
|
+
(!Number.isInteger(maxBreakpointWrites) || maxBreakpointWrites < 0)) {
|
|
62
|
+
return (0, ts_utils_1.fail)(`maxBreakpointWrites (${maxBreakpointWrites}) must be a non-negative integer`);
|
|
63
|
+
}
|
|
64
|
+
const offsets = cache.systemBreakpoints;
|
|
65
|
+
if (offsets === undefined || offsets.length === 0) {
|
|
66
|
+
return (0, ts_utils_1.succeed)(cache);
|
|
67
|
+
}
|
|
68
|
+
if (maxBreakpointWrites !== undefined && offsets.length > maxBreakpointWrites) {
|
|
69
|
+
return (0, ts_utils_1.fail)(`cache.systemBreakpoints has ${offsets.length} entries, exceeding the cap of ` +
|
|
70
|
+
`${maxBreakpointWrites} for this request`);
|
|
71
|
+
}
|
|
72
|
+
let previous = 0;
|
|
73
|
+
for (let i = 0; i < offsets.length; i++) {
|
|
74
|
+
const offset = offsets[i];
|
|
75
|
+
if (!Number.isInteger(offset) || offset <= 0 || offset >= systemLength) {
|
|
76
|
+
return (0, ts_utils_1.fail)(`cache.systemBreakpoints[${i}] (${offset}) must be an integer strictly between 0 and ` +
|
|
77
|
+
`system.length (${systemLength})`);
|
|
78
|
+
}
|
|
79
|
+
if (offset <= previous) {
|
|
80
|
+
return (0, ts_utils_1.fail)(`cache.systemBreakpoints must be strictly ascending; [${i}] (${offset}) does not exceed ` +
|
|
81
|
+
`the preceding offset (${previous})`);
|
|
82
|
+
}
|
|
83
|
+
previous = offset;
|
|
84
|
+
}
|
|
85
|
+
return (0, ts_utils_1.succeed)(cache);
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Validates an {@link IAiCacheRequest}'s `systemBreakpoints` against the `system` string they
|
|
89
|
+
* index into. Thin wrapper over {@link validateCacheBreakpoints} for a caller that holds the
|
|
90
|
+
* actual string (every `ts-extras` adapter) rather than just its length.
|
|
91
|
+
* @public
|
|
92
|
+
*/
|
|
93
|
+
function validateAiCacheRequest(system, cache, maxBreakpointWrites) {
|
|
94
|
+
return validateCacheBreakpoints(system.length, cache, maxBreakpointWrites);
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Splits `system` into cache-annotated segments per `cache.systemBreakpoints`, validating first.
|
|
98
|
+
*
|
|
99
|
+
* @remarks
|
|
100
|
+
* When `cache` is `undefined` or declares no breakpoints, returns the whole string as a single
|
|
101
|
+
* non-breakpointed segment — an un-annotated caller's request body is byte-identical to before
|
|
102
|
+
* this feature existed (design.md §11). Shared by the Anthropic and OpenAI adapters so the
|
|
103
|
+
* splitting logic (and its validation) is written once.
|
|
104
|
+
* @internal
|
|
105
|
+
*/
|
|
106
|
+
function splitSystemForCache(system, cache, maxBreakpointWrites) {
|
|
107
|
+
if ((cache === null || cache === void 0 ? void 0 : cache.systemBreakpoints) === undefined || cache.systemBreakpoints.length === 0) {
|
|
108
|
+
return (0, ts_utils_1.succeed)([{ text: system, cacheBreakpoint: false }]);
|
|
109
|
+
}
|
|
110
|
+
return validateAiCacheRequest(system, cache, maxBreakpointWrites).onSuccess((validated) => {
|
|
111
|
+
const segments = [];
|
|
112
|
+
let start = 0;
|
|
113
|
+
// `validateAiCacheRequest` already guarantees `systemBreakpoints` is defined, non-empty,
|
|
114
|
+
// strictly ascending, and every offset is `> 0` and `< system.length`.
|
|
115
|
+
for (const offset of validated.systemBreakpoints) {
|
|
116
|
+
segments.push({ text: system.slice(start, offset), cacheBreakpoint: true });
|
|
117
|
+
start = offset;
|
|
118
|
+
}
|
|
119
|
+
segments.push({ text: system.slice(start), cacheBreakpoint: false });
|
|
120
|
+
return (0, ts_utils_1.succeed)(segments);
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
//# sourceMappingURL=cacheRequest.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cacheRequest.js","sourceRoot":"","sources":["../../../src/packlets/ai-assist/cacheRequest.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 * Request-level prompt-caching plan (design.md §6.1) and the shared validation + splitting logic\n * every adapter that emits cache breakpoints (Anthropic, OpenAI) uses, so the ascending/range/cap\n * rules live in exactly one place rather than being re-derived per adapter.\n *\n * @packageDocumentation\n */\n\nimport { fail, Result, succeed } from '@fgv/ts-utils';\n\n/**\n * A request-level prompt-caching plan.\n *\n * @remarks\n * Offsets rather than content blocks — see design.md §6.1 for why: `AiPrompt`/`IChatRequest`\n * carry `system` as a plain string, offsets are usable without `@fgv/ts-prompt-assist` (a\n * caller can say \"the first 4,000 characters of my system prompt are stable\" directly), and\n * they are already the currency `IPromptSection.start`/`.chars` report.\n * @public\n */\nexport interface IAiCacheRequest {\n /**\n * Strictly ascending UTF-16 offsets into `AiPrompt.system` (equivalently\n * `IProviderCompletionParams.system`), each marking the end of a cacheable prefix. Each must\n * be an integer `> 0` and `< system.length`. Validated where the offsets are actually used\n * (against the request's own `system.length`, and optionally against a cap) — never clamped;\n * an invalid plan fails the request rather than silently caching less than declared. Sent where\n * the provider has a breakpoint mechanism (Anthropic `cache_control`, OpenAI\n * `prompt_cache_breakpoint`). Ignored by adapters that have no such concept (Gemini).\n */\n readonly systemBreakpoints?: ReadonlyArray<number>;\n /**\n * Opaque cache-routing key, sent where the provider has one and by that provider's transport:\n * `prompt_cache_key` as a body field on OpenAI (both routes) and on xAI Responses, and an\n * `x-grok-conv-id` header on xAI Chat Completions. See `supportsPromptCacheRouting`.\n *\n * @remarks\n * On xAI this is not a nicety. Its prompt cache is per-server and evictable, so two requests\n * sharing a prefix can still miss if they are routed to different servers; the key pins them\n * together. Providers with no routing concept ignore it and receive a request byte-identical\n * to one built without a `cache` at all.\n */\n readonly cacheKey?: string;\n}\n\n/**\n * Validates an {@link IAiCacheRequest}'s `systemBreakpoints` against the length of the `system`\n * string they index into, and optionally against a per-model write cap.\n *\n * @remarks\n * **Fails loudly, never clamps** (design.md §6.1): a non-ascending, out-of-range, or over-cap\n * offset returns `fail()`. A silently-clamped breakpoint is precisely the silent, non-caching\n * failure this stream exists to remove. `systemLength` and `maxBreakpointWrites` are themselves\n * validated as non-negative integers — checked before `systemBreakpoints` even, so a malformed\n * value (`NaN`, a negative number, a fraction) fails loudly rather than silently passing every\n * offset check it should have rejected (`offset >= NaN` is always `false`) or, for the cap,\n * passing unnoticed on an empty offset list. `maxBreakpointWrites` is left to the caller to supply —\n * there is no model-keyed cap table in this package yet (design.md §7's `IAiCacheCapability` is\n * out of this slice's scope), and design.md §5.2 establishes that a caller producing breakpoints\n * from the three-level `PromptCacheStability` vocabulary never emits more than two, so an\n * omitted cap is not a soundness gap for that caller — only a direct caller supplying more than a\n * provider's cap would ever need one enforced here.\n *\n * Takes a length rather than {@link validateAiCacheRequest}'s `system` string because a caller\n * building a plan from an `@fgv/ts-prompt-assist` `IPromptComposition` (`totalChars`) has the\n * length without holding the resolved body text itself.\n * @public\n */\nexport function validateCacheBreakpoints(\n systemLength: number,\n cache: IAiCacheRequest,\n maxBreakpointWrites?: number\n): Result<IAiCacheRequest> {\n if (!Number.isInteger(systemLength) || systemLength < 0) {\n return fail(`systemLength (${systemLength}) must be a non-negative integer`);\n }\n if (\n maxBreakpointWrites !== undefined &&\n (!Number.isInteger(maxBreakpointWrites) || maxBreakpointWrites < 0)\n ) {\n return fail(`maxBreakpointWrites (${maxBreakpointWrites}) must be a non-negative integer`);\n }\n const offsets = cache.systemBreakpoints;\n if (offsets === undefined || offsets.length === 0) {\n return succeed(cache);\n }\n if (maxBreakpointWrites !== undefined && offsets.length > maxBreakpointWrites) {\n return fail(\n `cache.systemBreakpoints has ${offsets.length} entries, exceeding the cap of ` +\n `${maxBreakpointWrites} for this request`\n );\n }\n let previous = 0;\n for (let i = 0; i < offsets.length; i++) {\n const offset = offsets[i];\n if (!Number.isInteger(offset) || offset <= 0 || offset >= systemLength) {\n return fail(\n `cache.systemBreakpoints[${i}] (${offset}) must be an integer strictly between 0 and ` +\n `system.length (${systemLength})`\n );\n }\n if (offset <= previous) {\n return fail(\n `cache.systemBreakpoints must be strictly ascending; [${i}] (${offset}) does not exceed ` +\n `the preceding offset (${previous})`\n );\n }\n previous = offset;\n }\n return succeed(cache);\n}\n\n/**\n * Validates an {@link IAiCacheRequest}'s `systemBreakpoints` against the `system` string they\n * index into. Thin wrapper over {@link validateCacheBreakpoints} for a caller that holds the\n * actual string (every `ts-extras` adapter) rather than just its length.\n * @public\n */\nexport function validateAiCacheRequest(\n system: string,\n cache: IAiCacheRequest,\n maxBreakpointWrites?: number\n): Result<IAiCacheRequest> {\n return validateCacheBreakpoints(system.length, cache, maxBreakpointWrites);\n}\n\n/**\n * One piece of `system` after splitting at its cache breakpoints. Concatenating every segment's\n * `text`, in order, reproduces `system` exactly.\n * @public\n */\nexport interface ICacheSystemSegment {\n /** This segment's text, a contiguous slice of the original `system` string. */\n readonly text: string;\n /**\n * True for the segment that ends a cacheable prefix — the one a provider's adapter should mark\n * with its native cache directive (Anthropic `cache_control`, OpenAI `prompt_cache_breakpoint`).\n * False for every other segment, including the final (volatile) tail after the last breakpoint.\n */\n readonly cacheBreakpoint: boolean;\n}\n\n/**\n * Splits `system` into cache-annotated segments per `cache.systemBreakpoints`, validating first.\n *\n * @remarks\n * When `cache` is `undefined` or declares no breakpoints, returns the whole string as a single\n * non-breakpointed segment — an un-annotated caller's request body is byte-identical to before\n * this feature existed (design.md §11). Shared by the Anthropic and OpenAI adapters so the\n * splitting logic (and its validation) is written once.\n * @internal\n */\nexport function splitSystemForCache(\n system: string,\n cache: IAiCacheRequest | undefined,\n maxBreakpointWrites?: number\n): Result<ReadonlyArray<ICacheSystemSegment>> {\n if (cache?.systemBreakpoints === undefined || cache.systemBreakpoints.length === 0) {\n return succeed([{ text: system, cacheBreakpoint: false }]);\n }\n return validateAiCacheRequest(system, cache, maxBreakpointWrites).onSuccess((validated) => {\n const segments: ICacheSystemSegment[] = [];\n let start = 0;\n // `validateAiCacheRequest` already guarantees `systemBreakpoints` is defined, non-empty,\n // strictly ascending, and every offset is `> 0` and `< system.length`.\n for (const offset of validated.systemBreakpoints as ReadonlyArray<number>) {\n segments.push({ text: system.slice(start, offset), cacheBreakpoint: true });\n start = offset;\n }\n segments.push({ text: system.slice(start), cacheBreakpoint: false });\n return succeed(segments);\n });\n}\n"]}
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Per-format chat request shape builders. Shared between the synchronous
|
|
3
|
-
* (`
|
|
4
|
-
* shapes stay consistent.
|
|
3
|
+
* (`completionClient.ts`) and streaming (`streamingClient.ts`) paths so the
|
|
4
|
+
* wire shapes stay consistent.
|
|
5
5
|
*
|
|
6
6
|
* @packageDocumentation
|
|
7
7
|
*/
|
|
8
8
|
import { type JsonObject } from '@fgv/ts-json-base';
|
|
9
9
|
import { Result } from '@fgv/ts-utils';
|
|
10
|
+
import { type IAiCacheRequest } from './cacheRequest';
|
|
10
11
|
import { AiPrompt, type IChatMessage } from './model';
|
|
11
12
|
/**
|
|
12
13
|
* The result of splitting an {@link AiAssist.IChatRequest} into the per-builder
|
|
@@ -80,9 +81,11 @@ export interface IBuildMessagesOptions {
|
|
|
80
81
|
}
|
|
81
82
|
/**
|
|
82
83
|
* Builds the messages array from prompt + optional history (`head`) and raw
|
|
83
|
-
* continuation (`rawTail`) messages. The caller supplies the
|
|
84
|
-
* (string
|
|
85
|
-
*
|
|
84
|
+
* continuation (`rawTail`) messages. The caller supplies the system content
|
|
85
|
+
* (string when no cache breakpoints are requested, parts array when they are
|
|
86
|
+
* — see {@link buildOpenAiChatSystemContent} / {@link buildOpenAiResponsesSystemContent})
|
|
87
|
+
* and the user content (string for text-only, parts array for vision prompts),
|
|
88
|
+
* since both shapes differ by format.
|
|
86
89
|
*
|
|
87
90
|
* `rawTail` items (OpenAI / xAI Responses `function_call` /
|
|
88
91
|
* `function_call_output` continuation items) are appended verbatim after the
|
|
@@ -93,7 +96,34 @@ export interface IBuildMessagesOptions {
|
|
|
93
96
|
*
|
|
94
97
|
* @internal
|
|
95
98
|
*/
|
|
96
|
-
export declare function buildMessages(
|
|
99
|
+
export declare function buildMessages(systemContent: string | unknown[], userContent: string | unknown[], options?: IBuildMessagesOptions): Array<Record<string, unknown>>;
|
|
100
|
+
/**
|
|
101
|
+
* Builds the system content for OpenAI Chat Completions, splitting `system` at `cache`'s
|
|
102
|
+
* breakpoints (design.md §6.3) into content parts each carrying
|
|
103
|
+
* `prompt_cache_breakpoint: { mode: 'explicit' }` where a cacheable prefix ends.
|
|
104
|
+
* `prompt_cache_options` is never touched — OpenAI keeps its own implicit breakpoint and writes
|
|
105
|
+
* up to the latest three of ours on top of it. Returns the plain `system` string when `cache` is
|
|
106
|
+
* `undefined` or declares no breakpoints, byte-identical to a request built before cache support
|
|
107
|
+
* existed.
|
|
108
|
+
* @internal
|
|
109
|
+
*/
|
|
110
|
+
export declare function buildOpenAiChatSystemContent(system: string, cache: IAiCacheRequest | undefined): Result<string | unknown[]>;
|
|
111
|
+
/**
|
|
112
|
+
* Builds the system content for the OpenAI / xAI Responses API. Identical to
|
|
113
|
+
* {@link buildOpenAiChatSystemContent} except for the content-part type (`'input_text'` instead
|
|
114
|
+
* of `'text'`) — see research.md §1.2, "Where the breakpoint may be attached."
|
|
115
|
+
* @internal
|
|
116
|
+
*/
|
|
117
|
+
export declare function buildOpenAiResponsesSystemContent(system: string, cache: IAiCacheRequest | undefined): Result<string | unknown[]>;
|
|
118
|
+
/**
|
|
119
|
+
* Builds the Anthropic `system` field, splitting `system` at `cache`'s breakpoints into content
|
|
120
|
+
* blocks carrying `cache_control: { type: 'ephemeral' }` on the block that ends each cacheable
|
|
121
|
+
* prefix (design.md §6.1). Returns the plain `system` string when `cache` is `undefined` or
|
|
122
|
+
* declares no breakpoints — byte-identical to `system: prompt.system` from a request built
|
|
123
|
+
* before cache support existed.
|
|
124
|
+
* @internal
|
|
125
|
+
*/
|
|
126
|
+
export declare function buildAnthropicSystem(system: string, cache: IAiCacheRequest | undefined): Result<string | unknown[]>;
|
|
97
127
|
/**
|
|
98
128
|
* Builds the user content for OpenAI Chat Completions when attachments are
|
|
99
129
|
* present. Returns a string when there are no attachments.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"chatRequestBuilders.d.ts","sourceRoot":"","sources":["../../../src/packlets/ai-assist/chatRequestBuilders.ts"],"names":[],"mappings":"AAoBA;;;;;;GAMG;AAEH,OAAO,EAAgB,KAAK,UAAU,EAAE,MAAM,mBAAmB,CAAC;AAClE,OAAO,EAAoC,MAAM,EAAW,MAAM,eAAe,CAAC;AAElF,OAAO,EAAE,QAAQ,EAA2B,KAAK,YAAY,EAAa,MAAM,SAAS,CAAC;AAE1F;;;;;GAKG;AACH,MAAM,WAAW,iBAAiB;IAChC,qFAAqF;IACrF,QAAQ,CAAC,MAAM,EAAE,QAAQ,CAAC;IAC1B,0EAA0E;IAC1E,QAAQ,CAAC,IAAI,EAAE,aAAa,CAAC,YAAY,CAAC,CAAC;CAC5C;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,gBAAgB,CAC9B,MAAM,EAAE,MAAM,GAAG,SAAS,EAC1B,QAAQ,EAAE,aAAa,CAAC,YAAY,CAAC,GACpC,MAAM,CAAC,iBAAiB,CAAC,CAW3B;AAED;;;;;;;;;GASG;AACH,wBAAgB,yBAAyB,CAAC,KAAK,EAAE,iBAAiB,GAAG,YAAY,EAAE,CAKlF;AAgED;;;;;GAKG;AACH,MAAM,WAAW,qBAAqB;IACpC;;;;OAIG;IACH,QAAQ,CAAC,IAAI,CAAC,EAAE,aAAa,CAAC,YAAY,CAAC,CAAC;IAC5C;;;;;;;;;;;;;;;;;OAiBG;IACH,QAAQ,CAAC,OAAO,CAAC,EAAE,aAAa,CAAC,UAAU,CAAC,CAAC;CAC9C;AAED
|
|
1
|
+
{"version":3,"file":"chatRequestBuilders.d.ts","sourceRoot":"","sources":["../../../src/packlets/ai-assist/chatRequestBuilders.ts"],"names":[],"mappings":"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"}
|
|
@@ -22,6 +22,9 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
22
22
|
exports.splitChatRequest = splitChatRequest;
|
|
23
23
|
exports.normalizeOutboundMessages = normalizeOutboundMessages;
|
|
24
24
|
exports.buildMessages = buildMessages;
|
|
25
|
+
exports.buildOpenAiChatSystemContent = buildOpenAiChatSystemContent;
|
|
26
|
+
exports.buildOpenAiResponsesSystemContent = buildOpenAiResponsesSystemContent;
|
|
27
|
+
exports.buildAnthropicSystem = buildAnthropicSystem;
|
|
25
28
|
exports.buildOpenAiChatUserContent = buildOpenAiChatUserContent;
|
|
26
29
|
exports.buildOpenAiResponsesUserContent = buildOpenAiResponsesUserContent;
|
|
27
30
|
exports.buildAnthropicUserContent = buildAnthropicUserContent;
|
|
@@ -30,13 +33,14 @@ exports.buildAnthropicMessages = buildAnthropicMessages;
|
|
|
30
33
|
exports.buildGeminiContents = buildGeminiContents;
|
|
31
34
|
/**
|
|
32
35
|
* Per-format chat request shape builders. Shared between the synchronous
|
|
33
|
-
* (`
|
|
34
|
-
* shapes stay consistent.
|
|
36
|
+
* (`completionClient.ts`) and streaming (`streamingClient.ts`) paths so the
|
|
37
|
+
* wire shapes stay consistent.
|
|
35
38
|
*
|
|
36
39
|
* @packageDocumentation
|
|
37
40
|
*/
|
|
38
41
|
const ts_json_base_1 = require("@fgv/ts-json-base");
|
|
39
42
|
const ts_utils_1 = require("@fgv/ts-utils");
|
|
43
|
+
const cacheRequest_1 = require("./cacheRequest");
|
|
40
44
|
const model_1 = require("./model");
|
|
41
45
|
/**
|
|
42
46
|
* Splits a unified {@link AiAssist.IChatRequest} into `{ prompt, head }` for the
|
|
@@ -127,9 +131,11 @@ const geminiRawTailMessageConverter = ts_utils_1.Converters.object({
|
|
|
127
131
|
}, { strict: false });
|
|
128
132
|
/**
|
|
129
133
|
* Builds the messages array from prompt + optional history (`head`) and raw
|
|
130
|
-
* continuation (`rawTail`) messages. The caller supplies the
|
|
131
|
-
* (string
|
|
132
|
-
*
|
|
134
|
+
* continuation (`rawTail`) messages. The caller supplies the system content
|
|
135
|
+
* (string when no cache breakpoints are requested, parts array when they are
|
|
136
|
+
* — see {@link buildOpenAiChatSystemContent} / {@link buildOpenAiResponsesSystemContent})
|
|
137
|
+
* and the user content (string for text-only, parts array for vision prompts),
|
|
138
|
+
* since both shapes differ by format.
|
|
133
139
|
*
|
|
134
140
|
* `rawTail` items (OpenAI / xAI Responses `function_call` /
|
|
135
141
|
* `function_call_output` continuation items) are appended verbatim after the
|
|
@@ -140,8 +146,8 @@ const geminiRawTailMessageConverter = ts_utils_1.Converters.object({
|
|
|
140
146
|
*
|
|
141
147
|
* @internal
|
|
142
148
|
*/
|
|
143
|
-
function buildMessages(
|
|
144
|
-
const messages = [{ role: 'system', content:
|
|
149
|
+
function buildMessages(systemContent, userContent, options) {
|
|
150
|
+
const messages = [{ role: 'system', content: systemContent }];
|
|
145
151
|
if (options === null || options === void 0 ? void 0 : options.head) {
|
|
146
152
|
for (const msg of options.head) {
|
|
147
153
|
messages.push({ role: msg.role, content: msg.content });
|
|
@@ -161,6 +167,68 @@ function buildMessages(systemPrompt, userContent, options) {
|
|
|
161
167
|
}
|
|
162
168
|
return messages;
|
|
163
169
|
}
|
|
170
|
+
/**
|
|
171
|
+
* Splits `segments` (from {@link splitSystemForCache}) into a single string when nothing carries
|
|
172
|
+
* a breakpoint, or a content-part array in `partType`'s shape (with `prompt_cache_breakpoint` on
|
|
173
|
+
* the parts that end a cacheable prefix) otherwise.
|
|
174
|
+
*
|
|
175
|
+
* @remarks
|
|
176
|
+
* A single non-breakpointed segment (the common case: `cache` omitted or empty) returns the
|
|
177
|
+
* plain string, byte-identical to a request built before cache support existed. Shared by
|
|
178
|
+
* {@link buildOpenAiChatSystemContent} and {@link buildOpenAiResponsesSystemContent}, whose only
|
|
179
|
+
* difference is the content-part `type` OpenAI's two APIs use (`'text'` vs `'input_text'`,
|
|
180
|
+
* research.md §1.2).
|
|
181
|
+
* @internal
|
|
182
|
+
*/
|
|
183
|
+
function toOpenAiSystemContent(segments, partType) {
|
|
184
|
+
if (segments.length === 1 && !segments[0].cacheBreakpoint) {
|
|
185
|
+
return segments[0].text;
|
|
186
|
+
}
|
|
187
|
+
return segments.map((seg) => (Object.assign({ type: partType, text: seg.text }, (seg.cacheBreakpoint
|
|
188
|
+
? {
|
|
189
|
+
// eslint-disable-next-line @typescript-eslint/naming-convention -- wire field name
|
|
190
|
+
prompt_cache_breakpoint: { mode: 'explicit' }
|
|
191
|
+
}
|
|
192
|
+
: {}))));
|
|
193
|
+
}
|
|
194
|
+
/**
|
|
195
|
+
* Builds the system content for OpenAI Chat Completions, splitting `system` at `cache`'s
|
|
196
|
+
* breakpoints (design.md §6.3) into content parts each carrying
|
|
197
|
+
* `prompt_cache_breakpoint: { mode: 'explicit' }` where a cacheable prefix ends.
|
|
198
|
+
* `prompt_cache_options` is never touched — OpenAI keeps its own implicit breakpoint and writes
|
|
199
|
+
* up to the latest three of ours on top of it. Returns the plain `system` string when `cache` is
|
|
200
|
+
* `undefined` or declares no breakpoints, byte-identical to a request built before cache support
|
|
201
|
+
* existed.
|
|
202
|
+
* @internal
|
|
203
|
+
*/
|
|
204
|
+
function buildOpenAiChatSystemContent(system, cache) {
|
|
205
|
+
return (0, cacheRequest_1.splitSystemForCache)(system, cache).onSuccess((segments) => (0, ts_utils_1.succeed)(toOpenAiSystemContent(segments, 'text')));
|
|
206
|
+
}
|
|
207
|
+
/**
|
|
208
|
+
* Builds the system content for the OpenAI / xAI Responses API. Identical to
|
|
209
|
+
* {@link buildOpenAiChatSystemContent} except for the content-part type (`'input_text'` instead
|
|
210
|
+
* of `'text'`) — see research.md §1.2, "Where the breakpoint may be attached."
|
|
211
|
+
* @internal
|
|
212
|
+
*/
|
|
213
|
+
function buildOpenAiResponsesSystemContent(system, cache) {
|
|
214
|
+
return (0, cacheRequest_1.splitSystemForCache)(system, cache).onSuccess((segments) => (0, ts_utils_1.succeed)(toOpenAiSystemContent(segments, 'input_text')));
|
|
215
|
+
}
|
|
216
|
+
/**
|
|
217
|
+
* Builds the Anthropic `system` field, splitting `system` at `cache`'s breakpoints into content
|
|
218
|
+
* blocks carrying `cache_control: { type: 'ephemeral' }` on the block that ends each cacheable
|
|
219
|
+
* prefix (design.md §6.1). Returns the plain `system` string when `cache` is `undefined` or
|
|
220
|
+
* declares no breakpoints — byte-identical to `system: prompt.system` from a request built
|
|
221
|
+
* before cache support existed.
|
|
222
|
+
* @internal
|
|
223
|
+
*/
|
|
224
|
+
function buildAnthropicSystem(system, cache) {
|
|
225
|
+
return (0, cacheRequest_1.splitSystemForCache)(system, cache).onSuccess((segments) => {
|
|
226
|
+
if (segments.length === 1 && !segments[0].cacheBreakpoint) {
|
|
227
|
+
return (0, ts_utils_1.succeed)(segments[0].text);
|
|
228
|
+
}
|
|
229
|
+
return (0, ts_utils_1.succeed)(segments.map((seg) => (Object.assign({ type: 'text', text: seg.text }, (seg.cacheBreakpoint ? { cache_control: { type: 'ephemeral' } } : {})))));
|
|
230
|
+
});
|
|
231
|
+
}
|
|
164
232
|
/**
|
|
165
233
|
* Builds the user content for OpenAI Chat Completions when attachments are
|
|
166
234
|
* present. Returns a string when there are no attachments.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"chatRequestBuilders.js","sourceRoot":"","sources":["../../../src/packlets/ai-assist/chatRequestBuilders.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 * Per-format chat request shape builders. Shared between the synchronous\n * (`apiClient.ts`) and streaming (`streamingClient.ts`) paths so the wire\n * shapes stay consistent.\n *\n * @packageDocumentation\n */\n\nimport { isJsonObject, type JsonObject } from '@fgv/ts-json-base';\nimport { type Converter, Converters, fail, Result, succeed } from '@fgv/ts-utils';\n\nimport { AiPrompt, type IAiImageAttachment, type IChatMessage, toDataUrl } from './model';\n\n/**\n * The result of splitting an {@link AiAssist.IChatRequest} into the per-builder\n * inputs: the current turn (as an {@link AiPrompt}, carrying system + the final\n * user message + any attachments) and the preceding conversation history.\n * @internal\n */\nexport interface ISplitChatRequest {\n /** The current turn lowered to an {@link AiPrompt} (system + user + attachments). */\n readonly prompt: AiPrompt;\n /** Prior conversation history — every message before the current turn. */\n readonly head: ReadonlyArray<IChatMessage>;\n}\n\n/**\n * Splits a unified {@link AiAssist.IChatRequest} into `{ prompt, head }` for the\n * per-provider builders. The **last** message is the current `user` turn; the\n * preceding messages are history (`head`). This is the single linearization\n * shared by every turn entry point, so the completion path and the client-tool\n * turn path place history at the identical position relative to the current turn.\n *\n * Fails when `messages` is empty (no current turn) or when the last message is\n * not a `user` turn — relabelling a trailing assistant message as the user turn\n * would be a silent footgun, so it is rejected loudly instead.\n *\n * @internal\n */\nexport function splitChatRequest(\n system: string | undefined,\n messages: ReadonlyArray<IChatMessage>\n): Result<ISplitChatRequest> {\n if (messages.length === 0) {\n return fail('messages must contain at least one entry (the current user turn)');\n }\n const current = messages[messages.length - 1];\n if (current.role !== 'user') {\n return fail(`the last message must be the current user turn (role 'user'); got '${current.role}'`);\n }\n const head = messages.slice(0, messages.length - 1);\n const prompt = new AiPrompt(current.content, system ?? '', current.attachments);\n return succeed({ prompt, head });\n}\n\n/**\n * Rebuilds the ordered messages for a proxy wire body so that only the current\n * turn can carry attachments — history (non-current) messages are reduced to\n * `{ role, content }`. The direct per-provider builders already drop attachments\n * on history turns (only the current user turn's attachments are honored), so\n * normalizing here keeps the proxy wire shape consistent with the direct paths\n * and avoids transmitting attachment payloads the upstream provider would ignore.\n *\n * @internal\n */\nexport function normalizeOutboundMessages(split: ISplitChatRequest): IChatMessage[] {\n return [\n ...split.head.map((m) => ({ role: m.role, content: m.content })),\n ...split.prompt.toRequest().messages\n ];\n}\n\n/**\n * Converter for a rawTail message entry. Narrows a `JsonObject` to\n * `{ role: string; content: string | unknown[] }` at runtime using the\n * Converter pattern. Entries that fail validation are silently skipped — the\n * surrounding function is infallible, and a malformed continuation message is\n * better omitted than transmitted verbatim.\n * @internal\n */\nconst rawTailMessageConverter: Converter<{ role: 'user' | 'assistant'; content: string | unknown[] }> =\n Converters.object<{ role: 'user' | 'assistant'; content: string | unknown[] }>(\n {\n role: Converters.enumeratedValue<'user' | 'assistant'>(['user', 'assistant']),\n content: Converters.oneOf<string | unknown[]>([\n Converters.string,\n Converters.isA('array', (v): v is unknown[] => Array.isArray(v))\n ])\n },\n { strict: false }\n );\n\n/**\n * Converter for an OpenAI / xAI Responses API `rawTail` item. These are\n * provider-native input items (`function_call`, `function_call_output`) whose\n * fields differ per item type, so — unlike the Anthropic `{ role, content }`\n * projection — the whole object is preserved verbatim.\n *\n * The static input is already typed `JsonObject`, so the `isJsonObject` guard\n * is a runtime backstop, not a compile-time narrowing: continuation messages\n * originate from a prior turn's `IAiClientToolContinuation.messages` and a\n * consumer may persist and reload them through untyped JSON before passing them\n * back. The guard preserves the same \"a malformed continuation message is\n * better omitted than transmitted verbatim\" posture as the Anthropic path —\n * non-object entries fail conversion and are skipped by the caller.\n * @internal\n */\nconst openAiRawTailItemConverter: Converter<JsonObject> = Converters.isA<JsonObject>(\n 'JsonObject',\n (v): v is JsonObject => isJsonObject(v)\n);\n\n/**\n * Converter for a Gemini `rawTail` item. Gemini continuation messages are\n * `{ role, parts }` turns (a model turn with `functionCall` parts followed by a\n * user turn with `functionResponse` parts). Narrows a `JsonObject` to\n * `{ role: 'user' | 'model'; parts: Array<Record<string, unknown>> }`; entries\n * that fail validation are skipped by the caller.\n * @internal\n */\nconst geminiRawTailMessageConverter: Converter<{\n role: 'user' | 'model';\n parts: unknown[];\n}> = Converters.object<{ role: 'user' | 'model'; parts: unknown[] }>(\n {\n role: Converters.enumeratedValue<'user' | 'model'>(['user', 'model']),\n // `parts` is preserved verbatim and serialized into the request body, so the\n // element shape is not narrowed here — `Array.isArray` soundly guarantees\n // `unknown[]` (narrowing to `Record<string, unknown>[]` would be an unchecked cast).\n parts: Converters.isA('array', (v): v is unknown[] => Array.isArray(v))\n },\n { strict: false }\n);\n\n/**\n * Optional history (`head`) and raw continuation (`rawTail`) messages to weave\n * around the prompt's current user message.\n *\n * @internal\n */\nexport interface IBuildMessagesOptions {\n /**\n * Prior conversation history inserted between the system prompt and the\n * prompt's current user message (multi-turn chat / correction retries). The\n * single ordered linearization is `[system, ...head, user, ...rawTail]`.\n */\n readonly head?: ReadonlyArray<IChatMessage>;\n /**\n * Raw JSON objects appended after the prompt's user message. Used to\n * inject provider-specific continuation messages (e.g. Anthropic assistant\n * turns with thinking blocks, OpenAI Responses `function_call` /\n * `function_call_output` items, Gemini `functionCall` / `functionResponse`\n * turns) that cannot be expressed as plain {@link IChatMessage} objects.\n *\n * Each builder applies its own provider-specific shape guard:\n * - {@link buildAnthropicMessages} projects each entry to `{ role, content }`.\n * - {@link buildMessages} (OpenAI / xAI Responses) preserves each item\n * verbatim (item fields differ per `type`), guarding only that it is a\n * JSON object.\n * - {@link buildGeminiContents} projects each entry to `{ role, parts }`.\n *\n * Entries that fail their builder's shape check are silently skipped (the\n * caller is responsible for supplying well-formed continuation messages).\n * Appended after the current user message.\n */\n readonly rawTail?: ReadonlyArray<JsonObject>;\n}\n\n/**\n * Builds the messages array from prompt + optional history (`head`) and raw\n * continuation (`rawTail`) messages. The caller supplies the user content\n * (string for text-only, parts array for vision prompts) since the parts shape\n * differs by format.\n *\n * `rawTail` items (OpenAI / xAI Responses `function_call` /\n * `function_call_output` continuation items) are appended verbatim after the\n * user message — their fields differ per item `type`, so they are preserved\n * rather than projected. The return type is `Array<Record<string, unknown>>`\n * to accommodate both `{ role, content }` messages and these heterogeneous\n * input items.\n *\n * @internal\n */\nexport function buildMessages(\n systemPrompt: string,\n userContent: string | unknown[],\n options?: IBuildMessagesOptions\n): Array<Record<string, unknown>> {\n const messages: Array<Record<string, unknown>> = [{ role: 'system', content: systemPrompt }];\n if (options?.head) {\n for (const msg of options.head) {\n messages.push({ role: msg.role, content: msg.content });\n }\n }\n messages.push({ role: 'user', content: userContent });\n // OpenAI / xAI Responses continuation items (function_call /\n // function_call_output) are appended verbatim — their field set differs per\n // item type, so the whole object is preserved rather than projected.\n if (options?.rawTail) {\n for (const item of options.rawTail) {\n const converted = openAiRawTailItemConverter.convert(item);\n if (converted.isSuccess()) {\n messages.push(converted.value);\n }\n }\n }\n return messages;\n}\n\n/**\n * Builds the user content for OpenAI Chat Completions when attachments are\n * present. Returns a string when there are no attachments.\n *\n * @internal\n */\nexport function buildOpenAiChatUserContent(prompt: AiPrompt): string | unknown[] {\n if (prompt.attachments.length === 0) {\n return prompt.user;\n }\n return [\n { type: 'text', text: prompt.user },\n ...prompt.attachments.map((att: IAiImageAttachment) => ({\n type: 'image_url',\n image_url: {\n url: toDataUrl(att),\n ...(att.detail !== undefined ? { detail: att.detail } : {})\n }\n }))\n ];\n}\n\n/**\n * Builds the user content for OpenAI / xAI Responses API when attachments\n * are present. Responses API uses `input_text` / `input_image` part types,\n * distinct from Chat Completions' `text` / `image_url`.\n *\n * @internal\n */\nexport function buildOpenAiResponsesUserContent(prompt: AiPrompt): string | unknown[] {\n if (prompt.attachments.length === 0) {\n return prompt.user;\n }\n return [\n { type: 'input_text', text: prompt.user },\n ...prompt.attachments.map((att: IAiImageAttachment) => ({\n type: 'input_image',\n image_url: toDataUrl(att),\n ...(att.detail !== undefined ? { detail: att.detail } : {})\n }))\n ];\n}\n\n/**\n * Builds the user-message content for Anthropic when attachments are present.\n *\n * @internal\n */\nexport function buildAnthropicUserContent(prompt: AiPrompt): string | unknown[] {\n if (prompt.attachments.length === 0) {\n return prompt.user;\n }\n return [\n { type: 'text', text: prompt.user },\n ...prompt.attachments.map((att: IAiImageAttachment) => ({\n type: 'image',\n source: {\n type: 'base64',\n media_type: att.mimeType,\n data: att.base64\n }\n }))\n ];\n}\n\n/**\n * Builds the Gemini `parts` array for the user turn, including any image\n * attachments as `inlineData` parts.\n *\n * @internal\n */\nexport function buildGeminiUserParts(prompt: AiPrompt): Array<Record<string, unknown>> {\n const parts: Array<Record<string, unknown>> = [{ text: prompt.user }];\n for (const att of prompt.attachments) {\n parts.push({ inlineData: { mimeType: att.mimeType, data: att.base64 } });\n }\n return parts;\n}\n\n/**\n * Builds the Anthropic messages array, weaving any `head` history messages\n * between implicit system + the prompt's user message and appending `rawTail`\n * continuation messages after. System messages are filtered out (Anthropic uses\n * a top-level system field).\n *\n * @internal\n */\nexport function buildAnthropicMessages(\n prompt: AiPrompt,\n options?: IBuildMessagesOptions\n): Array<{ role: string; content: string | unknown[] }> {\n const messages: Array<{ role: string; content: string | unknown[] }> = [];\n if (options?.head) {\n for (const msg of options.head) {\n if (msg.role !== 'system') {\n messages.push({ role: msg.role, content: msg.content });\n }\n }\n }\n messages.push({ role: 'user', content: buildAnthropicUserContent(prompt) });\n if (options?.rawTail) {\n for (const msg of options.rawTail) {\n const converted = rawTailMessageConverter.convert(msg);\n if (converted.isSuccess()) {\n messages.push(converted.value);\n }\n }\n }\n return messages;\n}\n\n/**\n * Builds the Gemini `contents` array, weaving any `head` history messages before\n * the prompt's user parts and appending `rawTail` continuation messages after.\n * System messages are filtered out (Gemini uses a top-level systemInstruction\n * field) and assistant roles are mapped to Gemini's `model` role.\n *\n * @internal\n */\nexport function buildGeminiContents(\n prompt: AiPrompt,\n options?: IBuildMessagesOptions\n): Array<{ role: string; parts: unknown[] }> {\n const contents: Array<{ role: string; parts: unknown[] }> = [];\n if (options?.head) {\n for (const msg of options.head) {\n if (msg.role !== 'system') {\n contents.push({\n role: msg.role === 'assistant' ? 'model' : msg.role,\n parts: [{ text: msg.content }]\n });\n }\n }\n }\n contents.push({ role: 'user', parts: buildGeminiUserParts(prompt) });\n // Gemini continuation turns (model `functionCall` parts + user\n // `functionResponse` parts) are projected to `{ role, parts }`.\n if (options?.rawTail) {\n for (const item of options.rawTail) {\n const converted = geminiRawTailMessageConverter.convert(item);\n if (converted.isSuccess()) {\n contents.push(converted.value);\n }\n }\n }\n return contents;\n}\n"]}
|
|
1
|
+
{"version":3,"file":"chatRequestBuilders.js","sourceRoot":"","sources":["../../../src/packlets/ai-assist/chatRequestBuilders.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 * Per-format chat request shape builders. Shared between the synchronous\n * (`completionClient.ts`) and streaming (`streamingClient.ts`) paths so the\n * wire shapes stay consistent.\n *\n * @packageDocumentation\n */\n\nimport { isJsonObject, type JsonObject } from '@fgv/ts-json-base';\nimport { type Converter, Converters, fail, Result, succeed } from '@fgv/ts-utils';\n\nimport { type IAiCacheRequest, splitSystemForCache } from './cacheRequest';\nimport { AiPrompt, type IAiImageAttachment, type IChatMessage, toDataUrl } from './model';\n\n/**\n * The result of splitting an {@link AiAssist.IChatRequest} into the per-builder\n * inputs: the current turn (as an {@link AiPrompt}, carrying system + the final\n * user message + any attachments) and the preceding conversation history.\n * @internal\n */\nexport interface ISplitChatRequest {\n /** The current turn lowered to an {@link AiPrompt} (system + user + attachments). */\n readonly prompt: AiPrompt;\n /** Prior conversation history — every message before the current turn. */\n readonly head: ReadonlyArray<IChatMessage>;\n}\n\n/**\n * Splits a unified {@link AiAssist.IChatRequest} into `{ prompt, head }` for the\n * per-provider builders. The **last** message is the current `user` turn; the\n * preceding messages are history (`head`). This is the single linearization\n * shared by every turn entry point, so the completion path and the client-tool\n * turn path place history at the identical position relative to the current turn.\n *\n * Fails when `messages` is empty (no current turn) or when the last message is\n * not a `user` turn — relabelling a trailing assistant message as the user turn\n * would be a silent footgun, so it is rejected loudly instead.\n *\n * @internal\n */\nexport function splitChatRequest(\n system: string | undefined,\n messages: ReadonlyArray<IChatMessage>\n): Result<ISplitChatRequest> {\n if (messages.length === 0) {\n return fail('messages must contain at least one entry (the current user turn)');\n }\n const current = messages[messages.length - 1];\n if (current.role !== 'user') {\n return fail(`the last message must be the current user turn (role 'user'); got '${current.role}'`);\n }\n const head = messages.slice(0, messages.length - 1);\n const prompt = new AiPrompt(current.content, system ?? '', current.attachments);\n return succeed({ prompt, head });\n}\n\n/**\n * Rebuilds the ordered messages for a proxy wire body so that only the current\n * turn can carry attachments — history (non-current) messages are reduced to\n * `{ role, content }`. The direct per-provider builders already drop attachments\n * on history turns (only the current user turn's attachments are honored), so\n * normalizing here keeps the proxy wire shape consistent with the direct paths\n * and avoids transmitting attachment payloads the upstream provider would ignore.\n *\n * @internal\n */\nexport function normalizeOutboundMessages(split: ISplitChatRequest): IChatMessage[] {\n return [\n ...split.head.map((m) => ({ role: m.role, content: m.content })),\n ...split.prompt.toRequest().messages\n ];\n}\n\n/**\n * Converter for a rawTail message entry. Narrows a `JsonObject` to\n * `{ role: string; content: string | unknown[] }` at runtime using the\n * Converter pattern. Entries that fail validation are silently skipped — the\n * surrounding function is infallible, and a malformed continuation message is\n * better omitted than transmitted verbatim.\n * @internal\n */\nconst rawTailMessageConverter: Converter<{ role: 'user' | 'assistant'; content: string | unknown[] }> =\n Converters.object<{ role: 'user' | 'assistant'; content: string | unknown[] }>(\n {\n role: Converters.enumeratedValue<'user' | 'assistant'>(['user', 'assistant']),\n content: Converters.oneOf<string | unknown[]>([\n Converters.string,\n Converters.isA('array', (v): v is unknown[] => Array.isArray(v))\n ])\n },\n { strict: false }\n );\n\n/**\n * Converter for an OpenAI / xAI Responses API `rawTail` item. These are\n * provider-native input items (`function_call`, `function_call_output`) whose\n * fields differ per item type, so — unlike the Anthropic `{ role, content }`\n * projection — the whole object is preserved verbatim.\n *\n * The static input is already typed `JsonObject`, so the `isJsonObject` guard\n * is a runtime backstop, not a compile-time narrowing: continuation messages\n * originate from a prior turn's `IAiClientToolContinuation.messages` and a\n * consumer may persist and reload them through untyped JSON before passing them\n * back. The guard preserves the same \"a malformed continuation message is\n * better omitted than transmitted verbatim\" posture as the Anthropic path —\n * non-object entries fail conversion and are skipped by the caller.\n * @internal\n */\nconst openAiRawTailItemConverter: Converter<JsonObject> = Converters.isA<JsonObject>(\n 'JsonObject',\n (v): v is JsonObject => isJsonObject(v)\n);\n\n/**\n * Converter for a Gemini `rawTail` item. Gemini continuation messages are\n * `{ role, parts }` turns (a model turn with `functionCall` parts followed by a\n * user turn with `functionResponse` parts). Narrows a `JsonObject` to\n * `{ role: 'user' | 'model'; parts: Array<Record<string, unknown>> }`; entries\n * that fail validation are skipped by the caller.\n * @internal\n */\nconst geminiRawTailMessageConverter: Converter<{\n role: 'user' | 'model';\n parts: unknown[];\n}> = Converters.object<{ role: 'user' | 'model'; parts: unknown[] }>(\n {\n role: Converters.enumeratedValue<'user' | 'model'>(['user', 'model']),\n // `parts` is preserved verbatim and serialized into the request body, so the\n // element shape is not narrowed here — `Array.isArray` soundly guarantees\n // `unknown[]` (narrowing to `Record<string, unknown>[]` would be an unchecked cast).\n parts: Converters.isA('array', (v): v is unknown[] => Array.isArray(v))\n },\n { strict: false }\n);\n\n/**\n * Optional history (`head`) and raw continuation (`rawTail`) messages to weave\n * around the prompt's current user message.\n *\n * @internal\n */\nexport interface IBuildMessagesOptions {\n /**\n * Prior conversation history inserted between the system prompt and the\n * prompt's current user message (multi-turn chat / correction retries). The\n * single ordered linearization is `[system, ...head, user, ...rawTail]`.\n */\n readonly head?: ReadonlyArray<IChatMessage>;\n /**\n * Raw JSON objects appended after the prompt's user message. Used to\n * inject provider-specific continuation messages (e.g. Anthropic assistant\n * turns with thinking blocks, OpenAI Responses `function_call` /\n * `function_call_output` items, Gemini `functionCall` / `functionResponse`\n * turns) that cannot be expressed as plain {@link IChatMessage} objects.\n *\n * Each builder applies its own provider-specific shape guard:\n * - {@link buildAnthropicMessages} projects each entry to `{ role, content }`.\n * - {@link buildMessages} (OpenAI / xAI Responses) preserves each item\n * verbatim (item fields differ per `type`), guarding only that it is a\n * JSON object.\n * - {@link buildGeminiContents} projects each entry to `{ role, parts }`.\n *\n * Entries that fail their builder's shape check are silently skipped (the\n * caller is responsible for supplying well-formed continuation messages).\n * Appended after the current user message.\n */\n readonly rawTail?: ReadonlyArray<JsonObject>;\n}\n\n/**\n * Builds the messages array from prompt + optional history (`head`) and raw\n * continuation (`rawTail`) messages. The caller supplies the system content\n * (string when no cache breakpoints are requested, parts array when they are\n * — see {@link buildOpenAiChatSystemContent} / {@link buildOpenAiResponsesSystemContent})\n * and the user content (string for text-only, parts array for vision prompts),\n * since both shapes differ by format.\n *\n * `rawTail` items (OpenAI / xAI Responses `function_call` /\n * `function_call_output` continuation items) are appended verbatim after the\n * user message — their fields differ per item `type`, so they are preserved\n * rather than projected. The return type is `Array<Record<string, unknown>>`\n * to accommodate both `{ role, content }` messages and these heterogeneous\n * input items.\n *\n * @internal\n */\nexport function buildMessages(\n systemContent: string | unknown[],\n userContent: string | unknown[],\n options?: IBuildMessagesOptions\n): Array<Record<string, unknown>> {\n const messages: Array<Record<string, unknown>> = [{ role: 'system', content: systemContent }];\n if (options?.head) {\n for (const msg of options.head) {\n messages.push({ role: msg.role, content: msg.content });\n }\n }\n messages.push({ role: 'user', content: userContent });\n // OpenAI / xAI Responses continuation items (function_call /\n // function_call_output) are appended verbatim — their field set differs per\n // item type, so the whole object is preserved rather than projected.\n if (options?.rawTail) {\n for (const item of options.rawTail) {\n const converted = openAiRawTailItemConverter.convert(item);\n if (converted.isSuccess()) {\n messages.push(converted.value);\n }\n }\n }\n return messages;\n}\n\n/**\n * Splits `segments` (from {@link splitSystemForCache}) into a single string when nothing carries\n * a breakpoint, or a content-part array in `partType`'s shape (with `prompt_cache_breakpoint` on\n * the parts that end a cacheable prefix) otherwise.\n *\n * @remarks\n * A single non-breakpointed segment (the common case: `cache` omitted or empty) returns the\n * plain string, byte-identical to a request built before cache support existed. Shared by\n * {@link buildOpenAiChatSystemContent} and {@link buildOpenAiResponsesSystemContent}, whose only\n * difference is the content-part `type` OpenAI's two APIs use (`'text'` vs `'input_text'`,\n * research.md §1.2).\n * @internal\n */\nfunction toOpenAiSystemContent(\n segments: ReadonlyArray<{ readonly text: string; readonly cacheBreakpoint: boolean }>,\n partType: 'text' | 'input_text'\n): string | unknown[] {\n if (segments.length === 1 && !segments[0].cacheBreakpoint) {\n return segments[0].text;\n }\n return segments.map((seg) => ({\n type: partType,\n text: seg.text,\n ...(seg.cacheBreakpoint\n ? {\n // eslint-disable-next-line @typescript-eslint/naming-convention -- wire field name\n prompt_cache_breakpoint: { mode: 'explicit' }\n }\n : {})\n }));\n}\n\n/**\n * Builds the system content for OpenAI Chat Completions, splitting `system` at `cache`'s\n * breakpoints (design.md §6.3) into content parts each carrying\n * `prompt_cache_breakpoint: { mode: 'explicit' }` where a cacheable prefix ends.\n * `prompt_cache_options` is never touched — OpenAI keeps its own implicit breakpoint and writes\n * up to the latest three of ours on top of it. Returns the plain `system` string when `cache` is\n * `undefined` or declares no breakpoints, byte-identical to a request built before cache support\n * existed.\n * @internal\n */\nexport function buildOpenAiChatSystemContent(\n system: string,\n cache: IAiCacheRequest | undefined\n): Result<string | unknown[]> {\n return splitSystemForCache(system, cache).onSuccess((segments) =>\n succeed(toOpenAiSystemContent(segments, 'text'))\n );\n}\n\n/**\n * Builds the system content for the OpenAI / xAI Responses API. Identical to\n * {@link buildOpenAiChatSystemContent} except for the content-part type (`'input_text'` instead\n * of `'text'`) — see research.md §1.2, \"Where the breakpoint may be attached.\"\n * @internal\n */\nexport function buildOpenAiResponsesSystemContent(\n system: string,\n cache: IAiCacheRequest | undefined\n): Result<string | unknown[]> {\n return splitSystemForCache(system, cache).onSuccess((segments) =>\n succeed(toOpenAiSystemContent(segments, 'input_text'))\n );\n}\n\n/**\n * Builds the Anthropic `system` field, splitting `system` at `cache`'s breakpoints into content\n * blocks carrying `cache_control: { type: 'ephemeral' }` on the block that ends each cacheable\n * prefix (design.md §6.1). Returns the plain `system` string when `cache` is `undefined` or\n * declares no breakpoints — byte-identical to `system: prompt.system` from a request built\n * before cache support existed.\n * @internal\n */\nexport function buildAnthropicSystem(\n system: string,\n cache: IAiCacheRequest | undefined\n): Result<string | unknown[]> {\n return splitSystemForCache(system, cache).onSuccess((segments) => {\n if (segments.length === 1 && !segments[0].cacheBreakpoint) {\n return succeed<string | unknown[]>(segments[0].text);\n }\n return succeed<string | unknown[]>(\n segments.map((seg) => ({\n type: 'text',\n text: seg.text,\n ...(seg.cacheBreakpoint ? { cache_control: { type: 'ephemeral' } } : {})\n }))\n );\n });\n}\n\n/**\n * Builds the user content for OpenAI Chat Completions when attachments are\n * present. Returns a string when there are no attachments.\n *\n * @internal\n */\nexport function buildOpenAiChatUserContent(prompt: AiPrompt): string | unknown[] {\n if (prompt.attachments.length === 0) {\n return prompt.user;\n }\n return [\n { type: 'text', text: prompt.user },\n ...prompt.attachments.map((att: IAiImageAttachment) => ({\n type: 'image_url',\n image_url: {\n url: toDataUrl(att),\n ...(att.detail !== undefined ? { detail: att.detail } : {})\n }\n }))\n ];\n}\n\n/**\n * Builds the user content for OpenAI / xAI Responses API when attachments\n * are present. Responses API uses `input_text` / `input_image` part types,\n * distinct from Chat Completions' `text` / `image_url`.\n *\n * @internal\n */\nexport function buildOpenAiResponsesUserContent(prompt: AiPrompt): string | unknown[] {\n if (prompt.attachments.length === 0) {\n return prompt.user;\n }\n return [\n { type: 'input_text', text: prompt.user },\n ...prompt.attachments.map((att: IAiImageAttachment) => ({\n type: 'input_image',\n image_url: toDataUrl(att),\n ...(att.detail !== undefined ? { detail: att.detail } : {})\n }))\n ];\n}\n\n/**\n * Builds the user-message content for Anthropic when attachments are present.\n *\n * @internal\n */\nexport function buildAnthropicUserContent(prompt: AiPrompt): string | unknown[] {\n if (prompt.attachments.length === 0) {\n return prompt.user;\n }\n return [\n { type: 'text', text: prompt.user },\n ...prompt.attachments.map((att: IAiImageAttachment) => ({\n type: 'image',\n source: {\n type: 'base64',\n media_type: att.mimeType,\n data: att.base64\n }\n }))\n ];\n}\n\n/**\n * Builds the Gemini `parts` array for the user turn, including any image\n * attachments as `inlineData` parts.\n *\n * @internal\n */\nexport function buildGeminiUserParts(prompt: AiPrompt): Array<Record<string, unknown>> {\n const parts: Array<Record<string, unknown>> = [{ text: prompt.user }];\n for (const att of prompt.attachments) {\n parts.push({ inlineData: { mimeType: att.mimeType, data: att.base64 } });\n }\n return parts;\n}\n\n/**\n * Builds the Anthropic messages array, weaving any `head` history messages\n * between implicit system + the prompt's user message and appending `rawTail`\n * continuation messages after. System messages are filtered out (Anthropic uses\n * a top-level system field).\n *\n * @internal\n */\nexport function buildAnthropicMessages(\n prompt: AiPrompt,\n options?: IBuildMessagesOptions\n): Array<{ role: string; content: string | unknown[] }> {\n const messages: Array<{ role: string; content: string | unknown[] }> = [];\n if (options?.head) {\n for (const msg of options.head) {\n if (msg.role !== 'system') {\n messages.push({ role: msg.role, content: msg.content });\n }\n }\n }\n messages.push({ role: 'user', content: buildAnthropicUserContent(prompt) });\n if (options?.rawTail) {\n for (const msg of options.rawTail) {\n const converted = rawTailMessageConverter.convert(msg);\n if (converted.isSuccess()) {\n messages.push(converted.value);\n }\n }\n }\n return messages;\n}\n\n/**\n * Builds the Gemini `contents` array, weaving any `head` history messages before\n * the prompt's user parts and appending `rawTail` continuation messages after.\n * System messages are filtered out (Gemini uses a top-level systemInstruction\n * field) and assistant roles are mapped to Gemini's `model` role.\n *\n * @internal\n */\nexport function buildGeminiContents(\n prompt: AiPrompt,\n options?: IBuildMessagesOptions\n): Array<{ role: string; parts: unknown[] }> {\n const contents: Array<{ role: string; parts: unknown[] }> = [];\n if (options?.head) {\n for (const msg of options.head) {\n if (msg.role !== 'system') {\n contents.push({\n role: msg.role === 'assistant' ? 'model' : msg.role,\n parts: [{ text: msg.content }]\n });\n }\n }\n }\n contents.push({ role: 'user', parts: buildGeminiUserParts(prompt) });\n // Gemini continuation turns (model `functionCall` parts + user\n // `functionResponse` parts) are projected to `{ role, parts }`.\n if (options?.rawTail) {\n for (const item of options.rawTail) {\n const converted = geminiRawTailMessageConverter.convert(item);\n if (converted.isSuccess()) {\n contents.push(converted.value);\n }\n }\n }\n return contents;\n}\n"]}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { type Logging, Result } from '@fgv/ts-utils';
|
|
2
2
|
import { type AiServerToolConfig, type IAiCompletionResponse, type IAiProviderDescriptor, type IChatRequest, type IThinkingConfig, type ModelSpec } from './model';
|
|
3
|
+
import { type IAiCacheRequest } from './cacheRequest';
|
|
3
4
|
import type { StructuredOutputRequest } from './structuredOutputTypes';
|
|
4
5
|
/**
|
|
5
6
|
* Parameters for a provider completion request. Carries the unified
|
|
@@ -65,6 +66,11 @@ export interface IProviderCompletionParams extends IChatRequest {
|
|
|
65
66
|
* `IAiCompletionResponse.structuredOutput`.
|
|
66
67
|
*/
|
|
67
68
|
readonly structuredOutput?: StructuredOutputRequest;
|
|
69
|
+
/**
|
|
70
|
+
* Prompt-caching plan for this request. Omitted, nothing cache-related is sent — the request
|
|
71
|
+
* body is byte-identical to a build predating this feature. See {@link AiAssist.IAiCacheRequest}.
|
|
72
|
+
*/
|
|
73
|
+
readonly cache?: IAiCacheRequest;
|
|
68
74
|
}
|
|
69
75
|
/**
|
|
70
76
|
* Calls the appropriate chat completion API for a given provider. Routes by
|
|
@@ -79,6 +85,19 @@ export declare function callProviderCompletion(params: IProviderCompletionParams
|
|
|
79
85
|
* CORS, and API key forwarding. The request body serializes the unified
|
|
80
86
|
* {@link AiAssist.IChatRequest} shape (`system?` + `messages`). Enforces the same
|
|
81
87
|
* non-empty / trailing-user-turn and image-input invariants as the direct path.
|
|
88
|
+
*
|
|
89
|
+
* @remarks
|
|
90
|
+
* Two parameters are handled before the body is composed, rather than forwarded.
|
|
91
|
+
* `tier` is resolved here and sent as a concrete `modelOverride`, because both
|
|
92
|
+
* halves of the resolution live on the descriptor the caller already holds — so a
|
|
93
|
+
* proxy needs no `tier` vocabulary, where a `tier` body field it did not understand
|
|
94
|
+
* would be ignored and silently serve a frontier request from the base model.
|
|
95
|
+
* `endpoint` is **refused**: it names where the prompt must go, the proxy is what
|
|
96
|
+
* makes that call, and the field has never been sent to one — so honoring it is
|
|
97
|
+
* unverifiable and ignoring it would reach the provider's default upstream instead
|
|
98
|
+
* of the host the caller pinned. Use `callProviderCompletion`, or point the
|
|
99
|
+
* proxy itself at the intended upstream.
|
|
100
|
+
*
|
|
82
101
|
* @param proxyUrl - Base URL of the proxy server
|
|
83
102
|
* @param params - Same parameters as {@link callProviderCompletion}
|
|
84
103
|
* @public
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"completionClient.d.ts","sourceRoot":"","sources":["../../../src/packlets/ai-assist/completionClient.ts"],"names":[],"mappings":"AA8BA,OAAO,EAGL,KAAK,OAAO,EACZ,MAAM,EAIP,MAAM,eAAe,CAAC;AAEvB,OAAO,EAEL,KAAK,kBAAkB,EAEvB,KAAK,qBAAqB,EAC1B,KAAK,qBAAqB,EAE1B,KAAK,YAAY,EACjB,KAAK,eAAe,EACpB,KAAK,SAAS,EAMf,MAAM,SAAS,CAAC;
|
|
1
|
+
{"version":3,"file":"completionClient.d.ts","sourceRoot":"","sources":["../../../src/packlets/ai-assist/completionClient.ts"],"names":[],"mappings":"AA8BA,OAAO,EAGL,KAAK,OAAO,EACZ,MAAM,EAIP,MAAM,eAAe,CAAC;AAEvB,OAAO,EAEL,KAAK,kBAAkB,EAEvB,KAAK,qBAAqB,EAC1B,KAAK,qBAAqB,EAE1B,KAAK,YAAY,EACjB,KAAK,eAAe,EACpB,KAAK,SAAS,EAMf,MAAM,SAAS,CAAC;AACjB,OAAO,EAAE,KAAK,eAAe,EAAE,MAAM,gBAAgB,CAAC;AA2CtD,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,yBAAyB,CAAC;AAYvE;;;;;GAKG;AACH,MAAM,WAAW,yBAA0B,SAAQ,YAAY;IAC7D,8BAA8B;IAC9B,QAAQ,CAAC,UAAU,EAAE,qBAAqB,CAAC;IAC3C,iCAAiC;IACjC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB;;;OAGG;IACH,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC;IAC9B,qGAAqG;IACrG,QAAQ,CAAC,aAAa,CAAC,EAAE,SAAS,CAAC;IACnC;;;;;OAKG;IACH,QAAQ,CAAC,IAAI,CAAC,EAAE,UAAU,GAAG,UAAU,CAAC;IACxC,0DAA0D;IAC1D,QAAQ,CAAC,MAAM,CAAC,EAAE,OAAO,CAAC,OAAO,CAAC;IAClC,uGAAuG;IACvG,QAAQ,CAAC,KAAK,CAAC,EAAE,aAAa,CAAC,kBAAkB,CAAC,CAAC;IACnD,kEAAkE;IAClE,QAAQ,CAAC,MAAM,CAAC,EAAE,WAAW,CAAC;IAC9B;;;;;OAKG;IACH,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAC3B;;;OAGG;IACH,QAAQ,CAAC,QAAQ,CAAC,EAAE,eAAe,CAAC;IACpC;;;;;;;OAOG;IACH,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;IAC5B;;;;;;;;;;OAUG;IACH,QAAQ,CAAC,gBAAgB,CAAC,EAAE,uBAAuB,CAAC;IACpD;;;OAGG;IACH,QAAQ,CAAC,KAAK,CAAC,EAAE,eAAe,CAAC;CAClC;AA0lBD;;;;;GAKG;AACH,wBAAsB,sBAAsB,CAC1C,MAAM,EAAE,yBAAyB,GAChC,OAAO,CAAC,MAAM,CAAC,qBAAqB,CAAC,CAAC,CAwMxC;AAMD;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,wBAAsB,qBAAqB,CACzC,QAAQ,EAAE,MAAM,EAChB,MAAM,EAAE,yBAAyB,GAChC,OAAO,CAAC,MAAM,CAAC,qBAAqB,CAAC,CAAC,CAwLxC"}
|