pi-openai-toolkit 0.20.5 → 0.20.6

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-openai-toolkit",
3
- "version": "0.20.5",
3
+ "version": "0.20.6",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "engines": {
@@ -882,7 +882,9 @@ async function handleSessionBeforeCompact(
882
882
  config,
883
883
  ctx,
884
884
  );
885
- if (config.remoteCompactModel) {
885
+ const expectedConsumerCapabilityMiss =
886
+ resolution.reason === "unsupported-api" && resolution.modelSpec === undefined;
887
+ if (config.remoteCompactModel && !expectedConsumerCapabilityMiss) {
886
888
  notifyWarning(
887
889
  ctx,
888
890
  `remote compaction model "${config.remoteCompactModel}" unusable (${resolution.reason}); using the native fallback chain`,
@@ -1,7 +1,8 @@
1
1
  import {
2
2
  compact,
3
+ convertToLlm,
3
4
  DEFAULT_COMPACTION_SETTINGS,
4
- estimateTokens,
5
+ serializeConversation,
5
6
  type CompactionResult,
6
7
  type ExtensionContext,
7
8
  type SessionBeforeCompactEvent,
@@ -59,16 +60,10 @@ function toErrorMessage(error: unknown): string {
59
60
  return error instanceof Error ? error.message : String(error);
60
61
  }
61
62
 
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. */
63
+ /** Summarization instructions plus the framing around the serialized transcript. */
71
64
  const SUMMARIZATION_TEMPLATE_TOKENS = 1024;
65
+ const HISTORY_OUTPUT_RESERVE_RATIO = 0.8;
66
+ const TURN_PREFIX_OUTPUT_RESERVE_RATIO = 0.5;
72
67
 
73
68
  export type SummarizationRequestSize = {
74
69
  inputTokens: number;
@@ -76,33 +71,93 @@ export type SummarizationRequestSize = {
76
71
  totalTokens: number;
77
72
  };
78
73
 
74
+ function estimateTextTokens(text: string | undefined): number {
75
+ return text ? Math.ceil(String(text).length / 4) : 0;
76
+ }
77
+
78
+ function estimateSerializedConversationTokens(
79
+ messages: SessionBeforeCompactEvent["preparation"]["messagesToSummarize"],
80
+ ): number {
81
+ const serialized = serializeConversation(convertToLlm(messages));
82
+ return estimateTextTokens(serialized);
83
+ }
84
+
85
+ function getEffectiveReserveTokens(preparation: SessionBeforeCompactEvent["preparation"]): number {
86
+ const reserveTokens = preparation.settings?.reserveTokens;
87
+ return typeof reserveTokens === "number" && Number.isFinite(reserveTokens) && reserveTokens >= 0
88
+ ? reserveTokens
89
+ : DEFAULT_COMPACTION_SETTINGS.reserveTokens;
90
+ }
91
+
92
+ function getOutputReserveTokens(
93
+ reserveTokens: number,
94
+ model: { maxTokens?: number },
95
+ ratio: number,
96
+ ): number {
97
+ const outputTokens = Math.floor(ratio * reserveTokens);
98
+ return typeof model.maxTokens === "number" && model.maxTokens > 0
99
+ ? Math.min(outputTokens, model.maxTokens)
100
+ : outputTokens;
101
+ }
102
+
103
+ function estimateSummaryRequest(
104
+ messages: SessionBeforeCompactEvent["preparation"]["messagesToSummarize"],
105
+ outputTokens: number,
106
+ previousSummary?: string,
107
+ customInstructions?: string,
108
+ ): SummarizationRequestSize {
109
+ const inputTokens =
110
+ SUMMARIZATION_TEMPLATE_TOKENS
111
+ + estimateSerializedConversationTokens(messages)
112
+ + estimateTextTokens(previousSummary)
113
+ + estimateTextTokens(customInstructions);
114
+ return { inputTokens, outputTokens, totalTokens: inputTokens + outputTokens };
115
+ }
116
+
79
117
  /**
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).
118
+ * Size of the individual request(s) Pi's `compact()` is about to send.
82
119
  *
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.
120
+ * Pi first converts messages with `convertToLlm()` and then applies
121
+ * `serializeConversation()`, which truncates tool results before they enter the
122
+ * summarization prompt. Split-turn compaction sends history and turn-prefix
123
+ * summaries as separate requests, so this returns the largest individual request
124
+ * rather than adding their inputs together. The fixed allowance remains
125
+ * deliberately conservative for Pi's system prompt, tags, and instructions.
126
+ *
127
+ * A session driven by a wide-window model (400k) can produce a preparation that a
128
+ * narrower summary model (272k) cannot accept. Callers must not attempt that request;
129
+ * a missing `contextWindow` skips the check instead of guessing.
88
130
  */
89
131
  export function estimateSummarizationRequest(
90
132
  preparation: SessionBeforeCompactEvent["preparation"],
91
133
  model: { contextWindow?: number; maxTokens?: number },
134
+ customInstructions?: string,
92
135
  ): 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);
136
+ const reserveTokens = getEffectiveReserveTokens(preparation);
137
+ const historyMessages = preparation.messagesToSummarize ?? [];
138
+ const turnPrefixMessages = preparation.turnPrefixMessages ?? [];
139
+ const historyOutputTokens = getOutputReserveTokens(reserveTokens, model, HISTORY_OUTPUT_RESERVE_RATIO);
140
+ const historyRequest = estimateSummaryRequest(
141
+ historyMessages,
142
+ historyOutputTokens,
143
+ preparation.previousSummary,
144
+ customInstructions,
145
+ );
146
+
147
+ if (!preparation.isSplitTurn || turnPrefixMessages.length === 0) {
148
+ return historyRequest;
101
149
  }
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 };
150
+
151
+ const requests = [
152
+ ...(historyMessages.length > 0 ? [historyRequest] : []),
153
+ estimateSummaryRequest(
154
+ turnPrefixMessages,
155
+ getOutputReserveTokens(reserveTokens, model, TURN_PREFIX_OUTPUT_RESERVE_RATIO),
156
+ ),
157
+ ];
158
+ return requests.reduce((largest, request) =>
159
+ request.totalTokens > largest.totalTokens ? request : largest,
160
+ );
106
161
  }
107
162
 
108
163
  /** Whether the summary model can accept this request at all; unknown windows always pass. */
@@ -163,7 +218,11 @@ export async function runNativeFallbackCompaction(args: {
163
218
  return { ok: false, reason: "same-as-current-model", modelSpec: spec };
164
219
  }
165
220
 
166
- const size = estimateSummarizationRequest(event.preparation, model as { contextWindow?: number; maxTokens?: number });
221
+ const size = estimateSummarizationRequest(
222
+ event.preparation,
223
+ model as { contextWindow?: number; maxTokens?: number },
224
+ event.customInstructions,
225
+ );
167
226
  if (!fitsSummarizationRequest(size, model as { contextWindow?: number })) {
168
227
  return {
169
228
  ok: false,