@oh-my-pi/pi-agent-core 18.18.0 → 18.18.2
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/CHANGELOG.md +12 -0
- package/dist/types/agent.d.ts +2 -0
- package/dist/types/compaction/compaction.d.ts +11 -0
- package/dist/types/compaction/openai.d.ts +8 -5
- package/package.json +8 -8
- package/src/agent.ts +22 -0
- package/src/compaction/compaction.ts +37 -8
- package/src/compaction/openai.ts +39 -10
- package/src/telemetry.ts +2 -1
- package/src/tokenizer.ts +18 -14
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,18 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [18.18.2] - 2026-11-08
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- Added `canReplayRemoteCompactionFrom()` to determine whether a model can replay provider-native compaction produced by another model.
|
|
10
|
+
|
|
11
|
+
### Fixed
|
|
12
|
+
|
|
13
|
+
- Fixed GenAI message-content capture configured in omp's own `.env` files so it continues to apply when those values are no longer exposed through the process environment.
|
|
14
|
+
- Fixed compaction preparation for remote OpenAI compaction that the active model cannot replay, ensuring the content is expanded and remains available for compaction.
|
|
15
|
+
- Fixed generic remote compaction requests to `/chat/completions` so gateway sessions retain the correct per-request model attribution.
|
|
16
|
+
|
|
5
17
|
## [18.17.0] - 2026-11-06
|
|
6
18
|
|
|
7
19
|
### Added
|
package/dist/types/agent.d.ts
CHANGED
|
@@ -460,6 +460,8 @@ export declare class Agent {
|
|
|
460
460
|
* recompute their own snapshot from `peekSteeringQueue()`/`peekFollowUpQueue()`
|
|
461
461
|
* (or a higher-level view) on notification. */
|
|
462
462
|
onQueueChange(listener: () => void): () => void;
|
|
463
|
+
/** Notify a session when this agent parks or resumes at a safe process-pause boundary. */
|
|
464
|
+
onPauseChange(listener: (paused: boolean) => void): () => void;
|
|
463
465
|
/** Register an independently removable hook that runs before queued messages are consumed. */
|
|
464
466
|
addBeforeQueuedMessageDequeueHook(hook: (signal?: AbortSignal) => Promise<void> | void): () => void;
|
|
465
467
|
/** Register an independently removable hook that runs immediately before each model call, with the
|
|
@@ -205,6 +205,8 @@ export declare const AUTO_HANDOFF_THRESHOLD_FOCUS: string;
|
|
|
205
205
|
export interface SummaryOptions {
|
|
206
206
|
promptOverride?: string;
|
|
207
207
|
extraContext?: string[];
|
|
208
|
+
/** Additional summary context fetched only when a compaction request consumes it. */
|
|
209
|
+
loadExtraContext?: () => Promise<string[] | undefined>;
|
|
208
210
|
remoteEndpoint?: string;
|
|
209
211
|
/** Stable system-prompt segments from the live turn, preserved for provider cache reuse. */
|
|
210
212
|
remoteSystemPrompt?: string[];
|
|
@@ -264,6 +266,8 @@ export interface SummaryOptions {
|
|
|
264
266
|
tools?: Tool[];
|
|
265
267
|
/** Optional fetch implementation threaded into remote compaction calls. */
|
|
266
268
|
fetch?: FetchImpl;
|
|
269
|
+
/** Inspect or replace a generic remote-compaction payload before it is sent. */
|
|
270
|
+
onPayload?: SimpleStreamOptions["onPayload"];
|
|
267
271
|
/**
|
|
268
272
|
* Optional completion transport override for host-level request wrappers
|
|
269
273
|
* (e.g. the coding-agent provider-concurrency limiter). When provided,
|
|
@@ -378,6 +382,13 @@ export interface CompactionPreparation {
|
|
|
378
382
|
* Responses-family replay. Local summaries have no provider restriction.
|
|
379
383
|
*/
|
|
380
384
|
export declare function canReplayRemoteCompaction(preserveData: Record<string, unknown> | undefined, activeModel: Model): boolean;
|
|
385
|
+
/**
|
|
386
|
+
* Whether `reader` can replay the provider-native compaction `compactor` would
|
|
387
|
+
* write. Only the OpenAI Responses lanes (the model's own API or its
|
|
388
|
+
* `remoteCompaction.api` override) store a provider-bound payload; the
|
|
389
|
+
* Anthropic lane and local methods leave a summary any model reads.
|
|
390
|
+
*/
|
|
391
|
+
export declare function canReplayRemoteCompactionFrom(compactor: Model, reader: Model): boolean;
|
|
381
392
|
/**
|
|
382
393
|
* Whether compaction preparation may reuse a native boundary instead of
|
|
383
394
|
* re-expanding its original messages. This is deliberately stricter than normal
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
* summarization endpoints that accept `{ systemPrompt, prompt }` and reply
|
|
15
15
|
* with `{ summary, shortSummary? }`.
|
|
16
16
|
*/
|
|
17
|
-
import type { CodexCompactionContext, FetchImpl, Message, Model, ProviderSessionState } from "@oh-my-pi/pi-ai/types";
|
|
17
|
+
import type { CodexCompactionContext, FetchImpl, Message, Model, ProviderSessionState, SimpleStreamOptions } from "@oh-my-pi/pi-ai/types";
|
|
18
18
|
import { Tokenizer } from "../tokenizer.js";
|
|
19
19
|
export * from "./compaction-v2-streaming.js";
|
|
20
20
|
export declare const OPENAI_REMOTE_COMPACTION_PRESERVE_KEY = "openaiRemoteCompaction";
|
|
@@ -153,14 +153,17 @@ export declare function requestOpenAiRemoteCompaction(model: Model, apiKey: stri
|
|
|
153
153
|
* HTTP 400 `"'messages' is required"`, compaction silently fell back to
|
|
154
154
|
* local summarization, and context grew unbounded.
|
|
155
155
|
*
|
|
156
|
-
*
|
|
157
|
-
*
|
|
158
|
-
* forwarded as `Authorization: Bearer`.
|
|
159
|
-
*
|
|
156
|
+
* On chat-completions endpoints the model's request headers are resolved for
|
|
157
|
+
* each call, its wire id is sent (llama.cpp requires it), and `opts.apiKey` is
|
|
158
|
+
* forwarded as `Authorization: Bearer`. Custom summarizers receive no model
|
|
159
|
+
* credentials. `opts.onPayload` can inspect or replace either request body.
|
|
160
|
+
* Callers wrap this in `withAuth` so 401s force-refresh through the standard
|
|
161
|
+
* credential rotation policy.
|
|
160
162
|
*/
|
|
161
163
|
export declare function requestRemoteCompaction(endpoint: string, request: RemoteCompactionRequest, signal?: AbortSignal, opts?: {
|
|
162
164
|
fetch?: FetchImpl;
|
|
163
165
|
timeoutMs?: number;
|
|
164
166
|
model?: Model;
|
|
165
167
|
apiKey?: string;
|
|
168
|
+
onPayload?: SimpleStreamOptions["onPayload"];
|
|
166
169
|
}): Promise<RemoteCompactionResponse>;
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@oh-my-pi/pi-agent-core",
|
|
4
|
-
"version": "18.18.
|
|
4
|
+
"version": "18.18.2",
|
|
5
5
|
"description": "General-purpose agent with transport abstraction, state management, and attachment support",
|
|
6
6
|
"homepage": "https://omp.sh",
|
|
7
7
|
"author": {
|
|
@@ -38,16 +38,16 @@
|
|
|
38
38
|
"fmt": "oxfmt --no-error-on-unmatched-pattern 'src/**/*.{ts,tsx}' '{test,bench,examples,scripts}/**/*.ts' '*.ts'"
|
|
39
39
|
},
|
|
40
40
|
"dependencies": {
|
|
41
|
-
"@oh-my-pi/pi-ai": "18.18.
|
|
42
|
-
"@oh-my-pi/pi-catalog": "18.18.
|
|
43
|
-
"@oh-my-pi/pi-natives": "18.18.
|
|
44
|
-
"@oh-my-pi/pi-utils": "18.18.
|
|
45
|
-
"@oh-my-pi/pi-wire": "18.18.
|
|
46
|
-
"@oh-my-pi/snapcompact": "18.18.
|
|
41
|
+
"@oh-my-pi/pi-ai": "18.18.2",
|
|
42
|
+
"@oh-my-pi/pi-catalog": "18.18.2",
|
|
43
|
+
"@oh-my-pi/pi-natives": "18.18.2",
|
|
44
|
+
"@oh-my-pi/pi-utils": "18.18.2",
|
|
45
|
+
"@oh-my-pi/pi-wire": "18.18.2",
|
|
46
|
+
"@oh-my-pi/snapcompact": "18.18.2",
|
|
47
47
|
"@opentelemetry/api": "^1.9.1"
|
|
48
48
|
},
|
|
49
49
|
"devDependencies": {
|
|
50
|
-
"@oh-my-pi/omptype": "18.18.
|
|
50
|
+
"@oh-my-pi/omptype": "18.18.2",
|
|
51
51
|
"@opentelemetry/context-async-hooks": "2.9.0",
|
|
52
52
|
"@opentelemetry/sdk-trace-base": "2.9.0",
|
|
53
53
|
"@types/bun": "^1.3.14"
|
package/src/agent.ts
CHANGED
|
@@ -423,6 +423,7 @@ export class Agent {
|
|
|
423
423
|
#tokenizer = new Tokenizer(this.#state.model);
|
|
424
424
|
#listeners = new Set<(e: AgentEvent) => void>();
|
|
425
425
|
#queueListeners = new Set<() => void>();
|
|
426
|
+
#pauseListeners = new Set<(paused: boolean) => void>();
|
|
426
427
|
#abortController?: AbortController;
|
|
427
428
|
#convertToLlm: (messages: AgentMessage[]) => Message[] | Promise<Message[]>;
|
|
428
429
|
#transformContext?: (messages: AgentMessage[], signal?: AbortSignal) => Promise<AgentMessage[]>;
|
|
@@ -1014,6 +1015,12 @@ export class Agent {
|
|
|
1014
1015
|
return () => this.#queueListeners.delete(listener);
|
|
1015
1016
|
}
|
|
1016
1017
|
|
|
1018
|
+
/** Notify a session when this agent parks or resumes at a safe process-pause boundary. */
|
|
1019
|
+
onPauseChange(listener: (paused: boolean) => void): () => void {
|
|
1020
|
+
this.#pauseListeners.add(listener);
|
|
1021
|
+
return () => this.#pauseListeners.delete(listener);
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1017
1024
|
/** Register an independently removable hook that runs before queued messages are consumed. */
|
|
1018
1025
|
addBeforeQueuedMessageDequeueHook(hook: (signal?: AbortSignal) => Promise<void> | void): () => void {
|
|
1019
1026
|
const registration = (signal?: AbortSignal) => hook(signal);
|
|
@@ -1378,6 +1385,19 @@ export class Agent {
|
|
|
1378
1385
|
}
|
|
1379
1386
|
}
|
|
1380
1387
|
|
|
1388
|
+
/** Report a safe-boundary park/resume transition; a throwing observer never disrupts the loop. */
|
|
1389
|
+
#emitPauseChanged(paused: boolean): void {
|
|
1390
|
+
for (const listener of this.#pauseListeners) {
|
|
1391
|
+
try {
|
|
1392
|
+
listener(paused);
|
|
1393
|
+
} catch (err) {
|
|
1394
|
+
logger.warn("Agent pause listener threw", {
|
|
1395
|
+
error: err instanceof Error ? err.message : String(err),
|
|
1396
|
+
});
|
|
1397
|
+
}
|
|
1398
|
+
}
|
|
1399
|
+
}
|
|
1400
|
+
|
|
1381
1401
|
replaceQueues(steering: AgentMessage[], followUp: AgentMessage[]) {
|
|
1382
1402
|
this.#steeringQueue = steering.slice();
|
|
1383
1403
|
this.#followUpQueue = followUp.slice();
|
|
@@ -2078,7 +2098,9 @@ export class Agent {
|
|
|
2078
2098
|
onHarmonyLeak: this.#onHarmonyLeak,
|
|
2079
2099
|
onTurnEnd: (messages, signal, context) => this.#onTurnEnd?.(messages, signal, context),
|
|
2080
2100
|
onPauseChange: paused => {
|
|
2101
|
+
if (this.#isPaused === paused) return;
|
|
2081
2102
|
this.#isPaused = paused;
|
|
2103
|
+
this.#emitPauseChanged(paused);
|
|
2082
2104
|
},
|
|
2083
2105
|
getToolChoice,
|
|
2084
2106
|
softToolRequirementState: this.#softToolRequirementState,
|
|
@@ -691,6 +691,8 @@ function shouldRetryHandoffWithAutoToolChoice(response: AssistantMessage): boole
|
|
|
691
691
|
export interface SummaryOptions {
|
|
692
692
|
promptOverride?: string;
|
|
693
693
|
extraContext?: string[];
|
|
694
|
+
/** Additional summary context fetched only when a compaction request consumes it. */
|
|
695
|
+
loadExtraContext?: () => Promise<string[] | undefined>;
|
|
694
696
|
remoteEndpoint?: string;
|
|
695
697
|
/** Stable system-prompt segments from the live turn, preserved for provider cache reuse. */
|
|
696
698
|
remoteSystemPrompt?: string[];
|
|
@@ -755,6 +757,8 @@ export interface SummaryOptions {
|
|
|
755
757
|
tools?: Tool[];
|
|
756
758
|
/** Optional fetch implementation threaded into remote compaction calls. */
|
|
757
759
|
fetch?: FetchImpl;
|
|
760
|
+
/** Inspect or replace a generic remote-compaction payload before it is sent. */
|
|
761
|
+
onPayload?: SimpleStreamOptions["onPayload"];
|
|
758
762
|
/**
|
|
759
763
|
* Optional completion transport override for host-level request wrappers
|
|
760
764
|
* (e.g. the coding-agent provider-concurrency limiter). When provided,
|
|
@@ -1038,7 +1042,7 @@ async function summarizeConversationWindow(
|
|
|
1038
1042
|
endpoint,
|
|
1039
1043
|
{ systemPrompt: SUMMARIZATION_SYSTEM_PROMPT, prompt: promptText, maxTokens },
|
|
1040
1044
|
signal,
|
|
1041
|
-
{ fetch: options.fetch, model, apiKey: key },
|
|
1045
|
+
{ fetch: options.fetch, model, apiKey: key, onPayload: options.onPayload },
|
|
1042
1046
|
),
|
|
1043
1047
|
{ signal, missingKeyMessage: "Remote compaction credentials unavailable" },
|
|
1044
1048
|
);
|
|
@@ -1269,7 +1273,7 @@ async function generateShortSummary(
|
|
|
1269
1273
|
endpoint,
|
|
1270
1274
|
{ systemPrompt: SUMMARIZATION_SYSTEM_PROMPT, prompt: promptText, maxTokens },
|
|
1271
1275
|
signal,
|
|
1272
|
-
{ fetch: options?.fetch, model, apiKey: key },
|
|
1276
|
+
{ fetch: options?.fetch, model, apiKey: key, onPayload: options?.onPayload },
|
|
1273
1277
|
),
|
|
1274
1278
|
{ signal, missingKeyMessage: "Remote compaction credentials unavailable" },
|
|
1275
1279
|
);
|
|
@@ -1398,6 +1402,19 @@ export function canReplayRemoteCompaction(
|
|
|
1398
1402
|
return !remote || (remote.provider === activeModel.provider && isOpenAiRemoteCompactionApi(activeModel.api));
|
|
1399
1403
|
}
|
|
1400
1404
|
|
|
1405
|
+
/**
|
|
1406
|
+
* Whether `reader` can replay the provider-native compaction `compactor` would
|
|
1407
|
+
* write. Only the OpenAI Responses lanes (the model's own API or its
|
|
1408
|
+
* `remoteCompaction.api` override) store a provider-bound payload; the
|
|
1409
|
+
* Anthropic lane and local methods leave a summary any model reads.
|
|
1410
|
+
*/
|
|
1411
|
+
export function canReplayRemoteCompactionFrom(compactor: Model, reader: Model): boolean {
|
|
1412
|
+
return (
|
|
1413
|
+
!isOpenAiRemoteCompactionApi(compactor.remoteCompaction?.api ?? compactor.api) ||
|
|
1414
|
+
(compactor.provider === reader.provider && isOpenAiRemoteCompactionApi(reader.api))
|
|
1415
|
+
);
|
|
1416
|
+
}
|
|
1417
|
+
|
|
1401
1418
|
/**
|
|
1402
1419
|
* Whether compaction preparation may reuse a native boundary instead of
|
|
1403
1420
|
* re-expanding its original messages. This is deliberately stricter than normal
|
|
@@ -1461,7 +1478,12 @@ export function prepareCompaction(
|
|
|
1461
1478
|
): CompactionPreparation | undefined {
|
|
1462
1479
|
const lastEntry = pathEntries[pathEntries.length - 1];
|
|
1463
1480
|
// A speculative native record may leave uncovered messages before the record.
|
|
1464
|
-
|
|
1481
|
+
// A trailing boundary this preparation re-expands covers nothing yet.
|
|
1482
|
+
if (
|
|
1483
|
+
lastEntry?.type === "compaction" &&
|
|
1484
|
+
!lastEntry.providerReplayThroughEntryId &&
|
|
1485
|
+
(!activeModel || remotePreserveReusable(lastEntry.preserveData, activeModel, settings))
|
|
1486
|
+
) {
|
|
1465
1487
|
return undefined;
|
|
1466
1488
|
}
|
|
1467
1489
|
|
|
@@ -1776,11 +1798,20 @@ export async function compact(
|
|
|
1776
1798
|
codexCompaction: options?.codexCompaction,
|
|
1777
1799
|
tools: options?.tools,
|
|
1778
1800
|
fetch: options?.fetch,
|
|
1801
|
+
onPayload: options?.onPayload,
|
|
1779
1802
|
completeImpl: options?.completeImpl,
|
|
1780
1803
|
// The caller's opt-out must reach every summarization oneshot, otherwise
|
|
1781
1804
|
// an outer retry loop multiplies with the inner one (see SummaryOptions).
|
|
1782
1805
|
oneshotRetry: options?.oneshotRetry,
|
|
1783
1806
|
};
|
|
1807
|
+
let extraContextLoaded = false;
|
|
1808
|
+
const ensureExtraContext = async (): Promise<void> => {
|
|
1809
|
+
if (extraContextLoaded || !options?.loadExtraContext) return;
|
|
1810
|
+
const context = await options.loadExtraContext();
|
|
1811
|
+
signal?.throwIfAborted();
|
|
1812
|
+
if (context?.length) summaryOptions.extraContext = [...(summaryOptions.extraContext ?? []), ...context];
|
|
1813
|
+
extraContextLoaded = true;
|
|
1814
|
+
};
|
|
1784
1815
|
|
|
1785
1816
|
// An archive keeps the reasoning policy of the model it was rendered for;
|
|
1786
1817
|
// Claude refuses inputs that reproduce reasoning (issue #6093), so a
|
|
@@ -2214,6 +2245,7 @@ export async function compact(
|
|
|
2214
2245
|
].filter(files => files.after >= filesDueBefore);
|
|
2215
2246
|
if (carried.length > 0) nativeRetainedFiles = carried;
|
|
2216
2247
|
}
|
|
2248
|
+
await ensureExtraContext();
|
|
2217
2249
|
try {
|
|
2218
2250
|
// The live turn's own system prompt, wire tools and transformed
|
|
2219
2251
|
// history when the host can build them: kept thinking remains valid
|
|
@@ -2274,6 +2306,7 @@ export async function compact(
|
|
|
2274
2306
|
if (!usedRemoteCompaction && nativeCompactionError !== undefined && !summaryOptions.remoteEndpoint) {
|
|
2275
2307
|
throw new NativeCompactionError(nativeCompactionError);
|
|
2276
2308
|
}
|
|
2309
|
+
if (!usedRemoteCompaction && nativeSummary === undefined) await ensureExtraContext();
|
|
2277
2310
|
|
|
2278
2311
|
// Generate summaries (can be parallel if both needed) and merge into one
|
|
2279
2312
|
let summary: string;
|
|
@@ -2332,11 +2365,7 @@ export async function compact(
|
|
|
2332
2365
|
|
|
2333
2366
|
const shortSummary = usedRemoteCompaction
|
|
2334
2367
|
? "Remote compaction"
|
|
2335
|
-
: await generateShortSummary(recentMessages, summary, model, reserveTokens, apiKey, signal,
|
|
2336
|
-
...summaryOptions,
|
|
2337
|
-
extraContext: options?.extraContext,
|
|
2338
|
-
thinkingLevel: options?.thinkingLevel,
|
|
2339
|
-
});
|
|
2368
|
+
: await generateShortSummary(recentMessages, summary, model, reserveTokens, apiKey, signal, summaryOptions);
|
|
2340
2369
|
|
|
2341
2370
|
// Compute file lists and append to summary
|
|
2342
2371
|
const { readFiles, modifiedFiles } = snapcompact.computeFileLists(fileOps);
|
package/src/compaction/openai.ts
CHANGED
|
@@ -39,6 +39,7 @@ import type {
|
|
|
39
39
|
Message,
|
|
40
40
|
Model,
|
|
41
41
|
ProviderSessionState,
|
|
42
|
+
SimpleStreamOptions,
|
|
42
43
|
} from "@oh-my-pi/pi-ai/types";
|
|
43
44
|
import {
|
|
44
45
|
dropMalformedOpenAIResponsesToolCalls,
|
|
@@ -59,7 +60,16 @@ import {
|
|
|
59
60
|
OPENAI_HEADER_VALUES,
|
|
60
61
|
OPENAI_HEADERS,
|
|
61
62
|
} from "@oh-my-pi/pi-catalog/wire/codex";
|
|
62
|
-
import {
|
|
63
|
+
import {
|
|
64
|
+
$env,
|
|
65
|
+
isRecord,
|
|
66
|
+
logger,
|
|
67
|
+
prompt,
|
|
68
|
+
ptree,
|
|
69
|
+
stringifyJson,
|
|
70
|
+
structuredCloneJSON,
|
|
71
|
+
untilAborted,
|
|
72
|
+
} from "@oh-my-pi/pi-utils";
|
|
63
73
|
import { dataUrlImageSize } from "../image-tokens";
|
|
64
74
|
import { Tokenizer } from "../tokenizer";
|
|
65
75
|
import { appendAzureApiVersion, resolveAzureOpenAiBaseUrl } from "./azure-openai-endpoint";
|
|
@@ -1058,16 +1068,24 @@ export async function requestOpenAiRemoteCompaction(
|
|
|
1058
1068
|
* HTTP 400 `"'messages' is required"`, compaction silently fell back to
|
|
1059
1069
|
* local summarization, and context grew unbounded.
|
|
1060
1070
|
*
|
|
1061
|
-
*
|
|
1062
|
-
*
|
|
1063
|
-
* forwarded as `Authorization: Bearer`.
|
|
1064
|
-
*
|
|
1071
|
+
* On chat-completions endpoints the model's request headers are resolved for
|
|
1072
|
+
* each call, its wire id is sent (llama.cpp requires it), and `opts.apiKey` is
|
|
1073
|
+
* forwarded as `Authorization: Bearer`. Custom summarizers receive no model
|
|
1074
|
+
* credentials. `opts.onPayload` can inspect or replace either request body.
|
|
1075
|
+
* Callers wrap this in `withAuth` so 401s force-refresh through the standard
|
|
1076
|
+
* credential rotation policy.
|
|
1065
1077
|
*/
|
|
1066
1078
|
export async function requestRemoteCompaction(
|
|
1067
1079
|
endpoint: string,
|
|
1068
1080
|
request: RemoteCompactionRequest,
|
|
1069
1081
|
signal?: AbortSignal,
|
|
1070
|
-
opts?: {
|
|
1082
|
+
opts?: {
|
|
1083
|
+
fetch?: FetchImpl;
|
|
1084
|
+
timeoutMs?: number;
|
|
1085
|
+
model?: Model;
|
|
1086
|
+
apiKey?: string;
|
|
1087
|
+
onPayload?: SimpleStreamOptions["onPayload"];
|
|
1088
|
+
},
|
|
1071
1089
|
): Promise<RemoteCompactionResponse> {
|
|
1072
1090
|
let endpointPath = endpoint;
|
|
1073
1091
|
try {
|
|
@@ -1076,13 +1094,22 @@ export async function requestRemoteCompaction(
|
|
|
1076
1094
|
// Keep the raw endpoint for relative/custom fetch implementations.
|
|
1077
1095
|
}
|
|
1078
1096
|
const isChatCompletions = /\/chat\/completions\/?$/.test(endpointPath);
|
|
1079
|
-
const headers
|
|
1097
|
+
const headers = new Headers({ "content-type": "application/json" });
|
|
1080
1098
|
if (isChatCompletions) {
|
|
1081
|
-
if (opts?.apiKey) headers.Authorization
|
|
1082
|
-
|
|
1099
|
+
if (opts?.apiKey) headers.set("Authorization", `Bearer ${opts.apiKey}`);
|
|
1100
|
+
const model = opts?.model;
|
|
1101
|
+
const resolveHeaders = model?.resolveHeaders;
|
|
1102
|
+
const configuredHeaders = resolveHeaders
|
|
1103
|
+
? await untilAborted(signal, () => resolveHeaders(signal))
|
|
1104
|
+
: model?.headers;
|
|
1105
|
+
signal?.throwIfAborted();
|
|
1106
|
+
// `set` replaces case-insensitively, so a configured `authorization` overrides the default bearer.
|
|
1107
|
+
if (configuredHeaders) {
|
|
1108
|
+
for (const name in configuredHeaders) headers.set(name, configuredHeaders[name]!);
|
|
1109
|
+
}
|
|
1083
1110
|
}
|
|
1084
1111
|
|
|
1085
|
-
|
|
1112
|
+
let body: unknown = isChatCompletions
|
|
1086
1113
|
? {
|
|
1087
1114
|
model: opts?.model ? resolveOpenAiCompactModel(opts.model) : undefined,
|
|
1088
1115
|
messages: [
|
|
@@ -1093,6 +1120,8 @@ export async function requestRemoteCompaction(
|
|
|
1093
1120
|
max_tokens: request.maxTokens,
|
|
1094
1121
|
}
|
|
1095
1122
|
: { systemPrompt: request.systemPrompt, prompt: request.prompt, maxTokens: request.maxTokens };
|
|
1123
|
+
const replacement = await opts?.onPayload?.(body, opts.model, signal);
|
|
1124
|
+
if (replacement !== undefined) body = replacement;
|
|
1096
1125
|
|
|
1097
1126
|
const response = await (opts?.fetch ?? fetch)(endpoint, {
|
|
1098
1127
|
method: "POST",
|
package/src/telemetry.ts
CHANGED
|
@@ -39,6 +39,7 @@ import {
|
|
|
39
39
|
type ToolChoice,
|
|
40
40
|
type Usage,
|
|
41
41
|
} from "@oh-my-pi/pi-ai";
|
|
42
|
+
import { $otelEnv } from "@oh-my-pi/pi-utils";
|
|
42
43
|
import {
|
|
43
44
|
type Attributes,
|
|
44
45
|
type AttributeValue,
|
|
@@ -453,7 +454,7 @@ export function resolveTelemetry(
|
|
|
453
454
|
let contentCaptureEnvCache: ResolvedTelemetryContentCapture | undefined;
|
|
454
455
|
function readContentCaptureEnv(): ResolvedTelemetryContentCapture {
|
|
455
456
|
if (contentCaptureEnvCache !== undefined) return contentCaptureEnvCache;
|
|
456
|
-
const raw =
|
|
457
|
+
const raw = $otelEnv(CONTENT_CAPTURE_ENV);
|
|
457
458
|
if (!raw) {
|
|
458
459
|
contentCaptureEnvCache = "none";
|
|
459
460
|
return "none";
|
package/src/tokenizer.ts
CHANGED
|
@@ -3,16 +3,16 @@ import { renderDemotedThinking } from "@oh-my-pi/pi-ai/dialect/demotion";
|
|
|
3
3
|
import { isFactoryDroidGeminiWire, keepsFactoryGeminiReasoning } from "@oh-my-pi/pi-ai/providers/factory-droid/gemini";
|
|
4
4
|
import { ollamaChatDropsOwnThinking } from "@oh-my-pi/pi-ai/providers/ollama";
|
|
5
5
|
import { completionsDropsOwnThinking, isLiteLLMThinkingSignature } from "@oh-my-pi/pi-ai/providers/openai-completions";
|
|
6
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
getNativeResponsesHistoryItems,
|
|
8
|
+
getReplayableNativeResponsesHistoryItems,
|
|
9
|
+
replaysNativeResponsesTurn,
|
|
10
|
+
} from "@oh-my-pi/pi-ai/providers/openai-shared";
|
|
7
11
|
import {
|
|
8
12
|
dropsForeignReasoningPayload,
|
|
9
13
|
isSameModelTurn,
|
|
10
14
|
shouldDropTruncatedThinkingOnlyAssistant,
|
|
11
15
|
} from "@oh-my-pi/pi-ai/providers/transform-messages";
|
|
12
|
-
import {
|
|
13
|
-
getOpenAIResponsesHistoryItems,
|
|
14
|
-
sanitizeOpenAIResponsesAssistantHistoryItemsForReplay,
|
|
15
|
-
} from "@oh-my-pi/pi-ai/utils";
|
|
16
16
|
import { resolveThinkingReplayTokens, type ThinkingReplayTokens } from "@oh-my-pi/pi-catalog/compat/thinking-replay";
|
|
17
17
|
import type { ModelTokenizer } from "@oh-my-pi/pi-catalog/types";
|
|
18
18
|
import * as natives from "@oh-my-pi/pi-natives";
|
|
@@ -595,15 +595,19 @@ export class Tokenizer {
|
|
|
595
595
|
* its reasoning, so those send no reasoning and no signature (0).
|
|
596
596
|
*/
|
|
597
597
|
#replayedReasoningTokens(message: AssistantMessage): number | null | undefined {
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
const replay =
|
|
604
|
-
if (!replay
|
|
605
|
-
//
|
|
606
|
-
|
|
598
|
+
// A payload this reader does not own is not native history at all: the turn
|
|
599
|
+
// is rebuilt from its blocks, and its signatures are priced below.
|
|
600
|
+
if (!getNativeResponsesHistoryItems(message, this.#reasoningReader)) return undefined;
|
|
601
|
+
// The request decides whether the payload goes on the wire; `/shake` reads
|
|
602
|
+
// that same decision, so a kept turn and a billed turn always agree.
|
|
603
|
+
const replay = getReplayableNativeResponsesHistoryItems(message, this.#reasoningReader);
|
|
604
|
+
if (!replay || !replaysNativeResponsesTurn(replay, this.nativeHistoryReplay)) return 0;
|
|
605
|
+
// Then the amount: nothing reasoning-shaped survives to price, or the turn is
|
|
606
|
+
// dropped whole as truncated thinking, so it sends no reasoning and no
|
|
607
|
+
// signature either. An answer-only payload replays with neither.
|
|
608
|
+
if (!replay.some(item => item.type === "reasoning") || shouldDropTruncatedThinkingOnlyAssistant(message)) {
|
|
609
|
+
return 0;
|
|
610
|
+
}
|
|
607
611
|
return message.usage?.reasoningTokens ?? null;
|
|
608
612
|
}
|
|
609
613
|
|