pi-openai-toolkit 0.14.11 → 0.14.13

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.
@@ -1,145 +1,218 @@
1
- import {
2
- compact,
3
- type CompactionResult,
4
- type ExtensionContext,
5
- type SessionBeforeCompactEvent,
6
- } from "@earendil-works/pi-coding-agent";
7
- import type { ProviderHeaders } from "@earendil-works/pi-ai";
8
- import { mergeProviderHeaders } from "./provider-headers";
9
- import { parseModelSpec } from "./runtime";
10
- import type { CompactionConfig } from "./types";
11
-
12
- export { parseModelSpec } from "./runtime";
13
-
14
- export type NativeFallbackFailureReason =
15
- | "disabled"
16
- | "no-model-configured"
17
- | "invalid-model-spec"
18
- | "model-not-found"
19
- | "same-as-current-model"
20
- | "auth-failed"
21
- | "aborted"
22
- | "empty-summary"
23
- | "compact-failed";
24
-
25
- export type NativeFallbackResult =
26
- | {
27
- ok: true;
28
- result: CompactionResult;
29
- model: { provider: string; id: string };
30
- }
31
- | {
32
- ok: false;
33
- reason: NativeFallbackFailureReason;
34
- modelSpec?: string;
35
- errorMessage?: string;
36
- };
37
-
38
- /** pi's exported native compact(); injectable for tests. */
39
- export type NativeCompactFn = typeof compact;
40
-
41
- type ResolvedAuth =
42
- | { ok: true; apiKey?: string; headers?: ProviderHeaders; env?: Record<string, string> }
43
- | { ok: false; error: string };
44
-
45
- function isAbortError(error: unknown): boolean {
46
- return (
47
- (error instanceof DOMException && error.name === "AbortError") ||
48
- (error instanceof Error && (error.name === "AbortError" || error.name === "ABORT_ERR"))
49
- );
50
- }
51
-
52
- function toErrorMessage(error: unknown): string {
53
- return error instanceof Error ? error.message : String(error);
54
- }
55
-
56
- /**
57
- * Run pi's native compaction method with a caller-resolved summary model.
58
- *
59
- * The two compaction chains stay separate: the caller passes `remoteCompactModel` only when a
60
- * remote v2 request was actually attempted and failed, and `nativeFallback.model` only when the
61
- * active model cannot use remote v2 at all. This function never inspects `remoteCompactModel`
62
- * itself, so a model that cannot compact remotely can never inherit the remote producer.
63
- *
64
- * Only the "resolved model differs from the current one" case is handled here: when the feature
65
- * is disabled, nothing is resolved, or it equals the current model, the caller returns undefined
66
- * from session_before_compact so pi runs the same native path itself, keeping its internal
67
- * streamFn/thinkingLevel wiring.
68
- */
69
- export async function runNativeFallbackCompaction(args: {
70
- ctx: ExtensionContext;
71
- event: SessionBeforeCompactEvent;
72
- config: CompactionConfig;
73
- /** Model spec supplied by the caller for this specific fallback reason. */
74
- modelSpec?: string;
75
- compactFn?: NativeCompactFn;
76
- }): Promise<NativeFallbackResult> {
77
- const { ctx, event, config } = args;
78
- const compactFn = args.compactFn ?? compact;
79
- const nativeFallback = config.nativeFallback;
80
-
81
- if (!nativeFallback.enabled) {
82
- return { ok: false, reason: "disabled" };
83
- }
84
-
85
- const spec = args.modelSpec?.trim() ?? "";
86
- if (!spec) {
87
- return { ok: false, reason: "no-model-configured" };
88
- }
89
-
90
- const parsed = parseModelSpec(spec);
91
- if (!parsed) {
92
- return { ok: false, reason: "invalid-model-spec", modelSpec: spec };
93
- }
94
-
95
- const model = ctx.modelRegistry.find(parsed.provider, parsed.modelId);
96
- if (!model) {
97
- return { ok: false, reason: "model-not-found", modelSpec: spec };
98
- }
99
-
100
- if (ctx.model && ctx.model.provider === model.provider && ctx.model.id === model.id) {
101
- return { ok: false, reason: "same-as-current-model", modelSpec: spec };
102
- }
103
-
104
- let auth: ResolvedAuth;
105
- try {
106
- auth = (await ctx.modelRegistry.getApiKeyAndHeaders(model)) as ResolvedAuth;
107
- } catch (error) {
108
- return { ok: false, reason: "auth-failed", modelSpec: spec, errorMessage: toErrorMessage(error) };
109
- }
110
- if (!auth.ok) {
111
- return { ok: false, reason: "auth-failed", modelSpec: spec, errorMessage: auth.error };
112
- }
113
-
114
- try {
115
- const result = await compactFn(
116
- event.preparation,
117
- model,
118
- auth.apiKey,
119
- mergeProviderHeaders(auth.headers),
120
- event.customInstructions,
121
- event.signal,
122
- nativeFallback.thinkingLevel,
123
- undefined,
124
- auth.env,
125
- );
126
-
127
- if (event.signal.aborted) {
128
- return { ok: false, reason: "aborted", modelSpec: spec };
129
- }
130
- if (!result.summary || result.summary.trim().length === 0) {
131
- return { ok: false, reason: "empty-summary", modelSpec: spec };
132
- }
133
-
134
- return {
135
- ok: true,
136
- result,
137
- model: { provider: model.provider, id: model.id },
138
- };
139
- } catch (error) {
140
- if (event.signal.aborted || isAbortError(error)) {
141
- return { ok: false, reason: "aborted", modelSpec: spec };
142
- }
143
- return { ok: false, reason: "compact-failed", modelSpec: spec, errorMessage: toErrorMessage(error) };
144
- }
145
- }
1
+ import {
2
+ compact,
3
+ DEFAULT_COMPACTION_SETTINGS,
4
+ estimateTokens,
5
+ type CompactionResult,
6
+ type ExtensionContext,
7
+ type SessionBeforeCompactEvent,
8
+ } from "@earendil-works/pi-coding-agent";
9
+ import type { ProviderHeaders } from "@earendil-works/pi-ai";
10
+ import { mergeProviderHeaders } from "./provider-headers";
11
+ import { parseModelSpec } from "./runtime";
12
+ import type { CompactionConfig } from "./types";
13
+
14
+ export { parseModelSpec } from "./runtime";
15
+
16
+ export type NativeFallbackFailureReason =
17
+ | "disabled"
18
+ | "no-model-configured"
19
+ | "invalid-model-spec"
20
+ | "model-not-found"
21
+ | "same-as-current-model"
22
+ | "auth-failed"
23
+ | "aborted"
24
+ | "empty-summary"
25
+ | "compact-failed"
26
+ /** The configured summary model cannot fit the request it would have to send. */
27
+ | "model-window-too-small";
28
+
29
+ export type NativeFallbackResult =
30
+ | {
31
+ ok: true;
32
+ result: CompactionResult;
33
+ model: { provider: string; id: string };
34
+ }
35
+ | {
36
+ ok: false;
37
+ reason: NativeFallbackFailureReason;
38
+ modelSpec?: string;
39
+ errorMessage?: string;
40
+ estimatedTokens?: number;
41
+ contextWindow?: number;
42
+ };
43
+
44
+ /** pi's exported native compact(); injectable for tests. */
45
+ export type NativeCompactFn = typeof compact;
46
+
47
+ type ResolvedAuth =
48
+ | { ok: true; apiKey?: string; headers?: ProviderHeaders; env?: Record<string, string> }
49
+ | { ok: false; error: string };
50
+
51
+ function isAbortError(error: unknown): boolean {
52
+ return (
53
+ (error instanceof DOMException && error.name === "AbortError") ||
54
+ (error instanceof Error && (error.name === "AbortError" || error.name === "ABORT_ERR"))
55
+ );
56
+ }
57
+
58
+ function toErrorMessage(error: unknown): string {
59
+ return error instanceof Error ? error.message : String(error);
60
+ }
61
+
62
+ /**
63
+ * Output budget Pi gives the summary itself: `generateSummary` caps `maxTokens` at
64
+ * `min(floor(0.8 * reserveTokens), model.maxTokens)`.
65
+ */
66
+ const SUMMARIZATION_OUTPUT_RESERVE_TOKENS = Math.floor(
67
+ DEFAULT_COMPACTION_SETTINGS.reserveTokens * 0.8,
68
+ );
69
+
70
+ /** Summarization instructions plus the `<conversation>` framing around the transcript. */
71
+ const SUMMARIZATION_TEMPLATE_TOKENS = 1024;
72
+
73
+ export type SummarizationRequestSize = {
74
+ inputTokens: number;
75
+ outputTokens: number;
76
+ totalTokens: number;
77
+ };
78
+
79
+ /**
80
+ * Size of the request Pi's `compact()` is about to send, measured the same way Pi
81
+ * measures context (the exported `estimateTokens`, a conservative chars/4).
82
+ *
83
+ * A session driven by a wide-window model (400k) produces a preparation that a narrower
84
+ * summary model (272k) cannot accept at all: the provider terminates the stream and the
85
+ * first manual `/compact` fails. Callers must not attempt that request; the estimate is
86
+ * deliberately an under-declaration of certainty, so a missing `contextWindow` skips the
87
+ * check instead of guessing.
88
+ */
89
+ export function estimateSummarizationRequest(
90
+ preparation: SessionBeforeCompactEvent["preparation"],
91
+ model: { contextWindow?: number; maxTokens?: number },
92
+ ): SummarizationRequestSize {
93
+ const conversation = [
94
+ ...(preparation.messagesToSummarize ?? []),
95
+ ...(preparation.turnPrefixMessages ?? []),
96
+ ];
97
+ let inputTokens = SUMMARIZATION_TEMPLATE_TOKENS;
98
+ for (const message of conversation) inputTokens += estimateTokens(message as never);
99
+ if (preparation.previousSummary) {
100
+ inputTokens += Math.ceil(String(preparation.previousSummary).length / 4);
101
+ }
102
+ const outputTokens = model.maxTokens && model.maxTokens > 0
103
+ ? Math.min(SUMMARIZATION_OUTPUT_RESERVE_TOKENS, model.maxTokens)
104
+ : SUMMARIZATION_OUTPUT_RESERVE_TOKENS;
105
+ return { inputTokens, outputTokens, totalTokens: inputTokens + outputTokens };
106
+ }
107
+
108
+ /** Whether the summary model can accept this request at all; unknown windows always pass. */
109
+ export function fitsSummarizationRequest(
110
+ size: SummarizationRequestSize,
111
+ model: { contextWindow?: number },
112
+ ): boolean {
113
+ const contextWindow = model.contextWindow;
114
+ if (typeof contextWindow !== "number" || contextWindow <= 0) return true;
115
+ return size.totalTokens <= contextWindow;
116
+ }
117
+
118
+ /**
119
+ * Run pi's native compaction method with a caller-resolved summary model.
120
+ *
121
+ * The two compaction chains stay separate: the caller passes `remoteCompactModel` only when a
122
+ * remote v2 request was actually attempted and failed, and `nativeFallback.model` only when the
123
+ * active model cannot use remote v2 at all. This function never inspects `remoteCompactModel`
124
+ * itself, so a model that cannot compact remotely can never inherit the remote producer.
125
+ *
126
+ * Only the "resolved model differs from the current one" case is handled here: when the feature
127
+ * is disabled, nothing is resolved, or it equals the current model, the caller returns undefined
128
+ * from session_before_compact so pi runs the same native path itself, keeping its internal
129
+ * streamFn/thinkingLevel wiring.
130
+ */
131
+ export async function runNativeFallbackCompaction(args: {
132
+ ctx: ExtensionContext;
133
+ event: SessionBeforeCompactEvent;
134
+ config: CompactionConfig;
135
+ /** Model spec supplied by the caller for this specific fallback reason. */
136
+ modelSpec?: string;
137
+ compactFn?: NativeCompactFn;
138
+ }): Promise<NativeFallbackResult> {
139
+ const { ctx, event, config } = args;
140
+ const compactFn = args.compactFn ?? compact;
141
+ const nativeFallback = config.nativeFallback;
142
+
143
+ if (!nativeFallback.enabled) {
144
+ return { ok: false, reason: "disabled" };
145
+ }
146
+
147
+ const spec = args.modelSpec?.trim() ?? "";
148
+ if (!spec) {
149
+ return { ok: false, reason: "no-model-configured" };
150
+ }
151
+
152
+ const parsed = parseModelSpec(spec);
153
+ if (!parsed) {
154
+ return { ok: false, reason: "invalid-model-spec", modelSpec: spec };
155
+ }
156
+
157
+ const model = ctx.modelRegistry.find(parsed.provider, parsed.modelId);
158
+ if (!model) {
159
+ return { ok: false, reason: "model-not-found", modelSpec: spec };
160
+ }
161
+
162
+ if (ctx.model && ctx.model.provider === model.provider && ctx.model.id === model.id) {
163
+ return { ok: false, reason: "same-as-current-model", modelSpec: spec };
164
+ }
165
+
166
+ const size = estimateSummarizationRequest(event.preparation, model as { contextWindow?: number; maxTokens?: number });
167
+ if (!fitsSummarizationRequest(size, model as { contextWindow?: number })) {
168
+ return {
169
+ ok: false,
170
+ reason: "model-window-too-small",
171
+ modelSpec: spec,
172
+ estimatedTokens: size.totalTokens,
173
+ contextWindow: (model as { contextWindow?: number }).contextWindow,
174
+ };
175
+ }
176
+
177
+ let auth: ResolvedAuth;
178
+ try {
179
+ auth = (await ctx.modelRegistry.getApiKeyAndHeaders(model)) as ResolvedAuth;
180
+ } catch (error) {
181
+ return { ok: false, reason: "auth-failed", modelSpec: spec, errorMessage: toErrorMessage(error) };
182
+ }
183
+ if (!auth.ok) {
184
+ return { ok: false, reason: "auth-failed", modelSpec: spec, errorMessage: auth.error };
185
+ }
186
+
187
+ try {
188
+ const result = await compactFn(
189
+ event.preparation,
190
+ model,
191
+ auth.apiKey,
192
+ mergeProviderHeaders(auth.headers),
193
+ event.customInstructions,
194
+ event.signal,
195
+ nativeFallback.thinkingLevel,
196
+ undefined,
197
+ auth.env,
198
+ );
199
+
200
+ if (event.signal.aborted) {
201
+ return { ok: false, reason: "aborted", modelSpec: spec };
202
+ }
203
+ if (!result.summary || result.summary.trim().length === 0) {
204
+ return { ok: false, reason: "empty-summary", modelSpec: spec };
205
+ }
206
+
207
+ return {
208
+ ok: true,
209
+ result,
210
+ model: { provider: model.provider, id: model.id },
211
+ };
212
+ } catch (error) {
213
+ if (event.signal.aborted || isAbortError(error)) {
214
+ return { ok: false, reason: "aborted", modelSpec: spec };
215
+ }
216
+ return { ok: false, reason: "compact-failed", modelSpec: spec, errorMessage: toErrorMessage(error) };
217
+ }
218
+ }