@oh-my-pi/pi-agent-core 18.18.1 → 18.18.3
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 +9 -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 +24 -3
- 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
|
|
@@ -266,6 +266,8 @@ export interface SummaryOptions {
|
|
|
266
266
|
tools?: Tool[];
|
|
267
267
|
/** Optional fetch implementation threaded into remote compaction calls. */
|
|
268
268
|
fetch?: FetchImpl;
|
|
269
|
+
/** Inspect or replace a generic remote-compaction payload before it is sent. */
|
|
270
|
+
onPayload?: SimpleStreamOptions["onPayload"];
|
|
269
271
|
/**
|
|
270
272
|
* Optional completion transport override for host-level request wrappers
|
|
271
273
|
* (e.g. the coding-agent provider-concurrency limiter). When provided,
|
|
@@ -380,6 +382,13 @@ export interface CompactionPreparation {
|
|
|
380
382
|
* Responses-family replay. Local summaries have no provider restriction.
|
|
381
383
|
*/
|
|
382
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;
|
|
383
392
|
/**
|
|
384
393
|
* Whether compaction preparation may reuse a native boundary instead of
|
|
385
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.3",
|
|
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.3",
|
|
42
|
+
"@oh-my-pi/pi-catalog": "18.18.3",
|
|
43
|
+
"@oh-my-pi/pi-natives": "18.18.3",
|
|
44
|
+
"@oh-my-pi/pi-utils": "18.18.3",
|
|
45
|
+
"@oh-my-pi/pi-wire": "18.18.3",
|
|
46
|
+
"@oh-my-pi/snapcompact": "18.18.3",
|
|
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.3",
|
|
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,
|
|
@@ -757,6 +757,8 @@ export interface SummaryOptions {
|
|
|
757
757
|
tools?: Tool[];
|
|
758
758
|
/** Optional fetch implementation threaded into remote compaction calls. */
|
|
759
759
|
fetch?: FetchImpl;
|
|
760
|
+
/** Inspect or replace a generic remote-compaction payload before it is sent. */
|
|
761
|
+
onPayload?: SimpleStreamOptions["onPayload"];
|
|
760
762
|
/**
|
|
761
763
|
* Optional completion transport override for host-level request wrappers
|
|
762
764
|
* (e.g. the coding-agent provider-concurrency limiter). When provided,
|
|
@@ -1040,7 +1042,7 @@ async function summarizeConversationWindow(
|
|
|
1040
1042
|
endpoint,
|
|
1041
1043
|
{ systemPrompt: SUMMARIZATION_SYSTEM_PROMPT, prompt: promptText, maxTokens },
|
|
1042
1044
|
signal,
|
|
1043
|
-
{ fetch: options.fetch, model, apiKey: key },
|
|
1045
|
+
{ fetch: options.fetch, model, apiKey: key, onPayload: options.onPayload },
|
|
1044
1046
|
),
|
|
1045
1047
|
{ signal, missingKeyMessage: "Remote compaction credentials unavailable" },
|
|
1046
1048
|
);
|
|
@@ -1271,7 +1273,7 @@ async function generateShortSummary(
|
|
|
1271
1273
|
endpoint,
|
|
1272
1274
|
{ systemPrompt: SUMMARIZATION_SYSTEM_PROMPT, prompt: promptText, maxTokens },
|
|
1273
1275
|
signal,
|
|
1274
|
-
{ fetch: options?.fetch, model, apiKey: key },
|
|
1276
|
+
{ fetch: options?.fetch, model, apiKey: key, onPayload: options?.onPayload },
|
|
1275
1277
|
),
|
|
1276
1278
|
{ signal, missingKeyMessage: "Remote compaction credentials unavailable" },
|
|
1277
1279
|
);
|
|
@@ -1400,6 +1402,19 @@ export function canReplayRemoteCompaction(
|
|
|
1400
1402
|
return !remote || (remote.provider === activeModel.provider && isOpenAiRemoteCompactionApi(activeModel.api));
|
|
1401
1403
|
}
|
|
1402
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
|
+
|
|
1403
1418
|
/**
|
|
1404
1419
|
* Whether compaction preparation may reuse a native boundary instead of
|
|
1405
1420
|
* re-expanding its original messages. This is deliberately stricter than normal
|
|
@@ -1463,7 +1478,12 @@ export function prepareCompaction(
|
|
|
1463
1478
|
): CompactionPreparation | undefined {
|
|
1464
1479
|
const lastEntry = pathEntries[pathEntries.length - 1];
|
|
1465
1480
|
// A speculative native record may leave uncovered messages before the record.
|
|
1466
|
-
|
|
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
|
+
) {
|
|
1467
1487
|
return undefined;
|
|
1468
1488
|
}
|
|
1469
1489
|
|
|
@@ -1778,6 +1798,7 @@ export async function compact(
|
|
|
1778
1798
|
codexCompaction: options?.codexCompaction,
|
|
1779
1799
|
tools: options?.tools,
|
|
1780
1800
|
fetch: options?.fetch,
|
|
1801
|
+
onPayload: options?.onPayload,
|
|
1781
1802
|
completeImpl: options?.completeImpl,
|
|
1782
1803
|
// The caller's opt-out must reach every summarization oneshot, otherwise
|
|
1783
1804
|
// an outer retry loop multiplies with the inner one (see SummaryOptions).
|
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
|
|