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.
- package/README.md +241 -240
- package/README.zh.md +241 -240
- package/docs/internals.md +128 -122
- package/package.json +127 -117
- package/src/config.ts +739 -720
- package/src/context-management/messages.ts +123 -0
- package/src/context-management/tools.ts +11 -0
- package/src/context-management/window-bulk.ts +149 -0
- package/src/context-management/window-manager.ts +693 -570
- package/src/extension-runtime.ts +1329 -1208
- package/src/native-fallback.ts +218 -145
- package/src/types.ts +628 -619
package/src/native-fallback.ts
CHANGED
|
@@ -1,145 +1,218 @@
|
|
|
1
|
-
import {
|
|
2
|
-
compact,
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
type
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
import {
|
|
10
|
-
import
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
export
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
| "
|
|
18
|
-
| "model-
|
|
19
|
-
| "
|
|
20
|
-
| "
|
|
21
|
-
| "
|
|
22
|
-
| "
|
|
23
|
-
| "
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
*
|
|
64
|
-
*
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
*
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
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
|
+
}
|