@oh-my-pi/pi-ai 18.2.5 → 18.2.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/CHANGELOG.md +7 -0
- package/package.json +6 -6
- package/src/auth-broker/discover.ts +5 -3
- package/src/providers/anthropic.ts +119 -31
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [18.2.6] - 2026-09-18
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
|
|
9
|
+
- Fixed Anthropic prompt-cache head re-baselining on every memory recall refresh: the system breakpoint now anchors on the last stable segment instead of the volatile recall suffix, and the stable-system fingerprint ignores recall blocks, so a recall refresh re-bills only the suffix instead of the whole tools+system head.
|
|
10
|
+
- Fixed auth-broker client config resolution failing silently on Windows when reading the token file or `config.yml`; reads now use `node:fs` instead of `Bun.file`.
|
|
11
|
+
|
|
5
12
|
## [18.2.5] - 2026-09-17
|
|
6
13
|
|
|
7
14
|
### Added
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@oh-my-pi/pi-ai",
|
|
3
|
-
"version": "18.2.
|
|
3
|
+
"version": "18.2.6",
|
|
4
4
|
"description": "Unified LLM API with automatic model discovery and provider configuration",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ai",
|
|
@@ -128,11 +128,11 @@
|
|
|
128
128
|
"fmt": "oxfmt --no-error-on-unmatched-pattern 'src/**/*.{ts,tsx}' '{test,bench,examples,scripts}/**/*.ts' '*.ts'"
|
|
129
129
|
},
|
|
130
130
|
"dependencies": {
|
|
131
|
-
"@oh-my-pi/omptype": "18.2.
|
|
132
|
-
"@oh-my-pi/pi-catalog": "18.2.
|
|
133
|
-
"@oh-my-pi/pi-natives": "18.2.
|
|
134
|
-
"@oh-my-pi/pi-utils": "18.2.
|
|
135
|
-
"@oh-my-pi/pi-wire": "18.2.
|
|
131
|
+
"@oh-my-pi/omptype": "18.2.6",
|
|
132
|
+
"@oh-my-pi/pi-catalog": "18.2.6",
|
|
133
|
+
"@oh-my-pi/pi-natives": "18.2.6",
|
|
134
|
+
"@oh-my-pi/pi-utils": "18.2.6",
|
|
135
|
+
"@oh-my-pi/pi-wire": "18.2.6"
|
|
136
136
|
},
|
|
137
137
|
"devDependencies": {
|
|
138
138
|
"@types/bun": "^1.3.14"
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
* token file → local SQLite) in one place so build-time tooling sees the same
|
|
5
5
|
* credentials as the TUI.
|
|
6
6
|
*/
|
|
7
|
+
import * as fs from "node:fs/promises";
|
|
7
8
|
import * as path from "node:path";
|
|
8
9
|
import {
|
|
9
10
|
$envExact,
|
|
@@ -60,7 +61,7 @@ async function defaultResolveConfigValue(config: string): Promise<string | undef
|
|
|
60
61
|
|
|
61
62
|
async function readTokenFile(): Promise<string | null> {
|
|
62
63
|
try {
|
|
63
|
-
const raw = await
|
|
64
|
+
const raw = await fs.readFile(getAuthBrokerTokenFilePath(), "utf8");
|
|
64
65
|
const trimmed = raw.trim();
|
|
65
66
|
return trimmed.length > 0 ? trimmed : null;
|
|
66
67
|
} catch (err) {
|
|
@@ -99,7 +100,7 @@ async function readConfigYaml(agentDir: string): Promise<ConfigSnapshot> {
|
|
|
99
100
|
for (const filename of MAIN_CONFIG_FILENAMES) {
|
|
100
101
|
const configPath = path.join(agentDir, filename);
|
|
101
102
|
try {
|
|
102
|
-
const raw = await
|
|
103
|
+
const raw = await fs.readFile(configPath, "utf8");
|
|
103
104
|
const parsed = YAML.parse(raw);
|
|
104
105
|
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return {};
|
|
105
106
|
const record = parsed as Record<string, unknown>;
|
|
@@ -121,7 +122,8 @@ export async function loadAuthBrokerAccountPool(): Promise<AuthBrokerAccountPool
|
|
|
121
122
|
|
|
122
123
|
let parsed: unknown;
|
|
123
124
|
try {
|
|
124
|
-
|
|
125
|
+
const raw = await fs.readFile(filePath, "utf8");
|
|
126
|
+
parsed = JSON.parse(raw.charCodeAt(0) === 0xfeff ? raw.slice(1) : raw);
|
|
125
127
|
} catch (error) {
|
|
126
128
|
throw new AIError.ConfigurationError(`Unable to read OMP_AUTH_BROKER_ACCOUNT_POOL_FILE at ${filePath}`, {
|
|
127
129
|
cause: error,
|
|
@@ -3954,8 +3954,17 @@ function applyPromptCaching(params: MessageCreateParamsStreaming, cacheControl?:
|
|
|
3954
3954
|
const messageEnd = hasTrailingAssistantPad ? trailingIndex - 1 : trailingIndex;
|
|
3955
3955
|
|
|
3956
3956
|
// A breakpoint caches every preceding byte, not only the decorated message.
|
|
3957
|
-
//
|
|
3958
|
-
//
|
|
3957
|
+
// A per-call or turn-scoped message is rebuilt next request, so a prefix
|
|
3958
|
+
// spanning it cannot match — but only at its own position. Messages after
|
|
3959
|
+
// the mark are ordinary persisted history with stable bytes, so a later
|
|
3960
|
+
// breakpoint still matches everything after the mark. The cost is bounded
|
|
3961
|
+
// to re-billing the marked bytes themselves, not the growing tail.
|
|
3962
|
+
// Hence two anchors, not a truncation: the newest candidate at or before
|
|
3963
|
+
// the first per-call/turn-scoped message (when one exists) pins the
|
|
3964
|
+
// reusable prefix behind the mark, and the rolling tail candidates pin
|
|
3965
|
+
// the suffix after it. Turn-scoped `clear_at` messages are absent next
|
|
3966
|
+
// request, so they still truncate the decimation range (ordinals would
|
|
3967
|
+
// shift), but per-call marks no longer freeze the tail.
|
|
3959
3968
|
let stableMessageEnd = messageEnd;
|
|
3960
3969
|
for (let index = 0; index <= messageEnd; index++) {
|
|
3961
3970
|
const message = params.messages[index];
|
|
@@ -3980,17 +3989,21 @@ function applyPromptCaching(params: MessageCreateParamsStreaming, cacheControl?:
|
|
|
3980
3989
|
// Stable historical decimation checkpoint every 15 user turns (15th, 30th, 45th...)
|
|
3981
3990
|
const decimationIndices = userIndices.filter((_, ordinal) => (ordinal + 1) % ANTHROPIC_DECIMATION_INTERVAL === 0);
|
|
3982
3991
|
|
|
3983
|
-
// Collect up to 2 trailing candidates from the
|
|
3984
|
-
//
|
|
3985
|
-
//
|
|
3986
|
-
//
|
|
3987
|
-
//
|
|
3988
|
-
//
|
|
3989
|
-
//
|
|
3992
|
+
// Collect up to 2 trailing candidates from the message tail, skipping
|
|
3993
|
+
// per-call messages, turn-scoped messages, and mid-conversation
|
|
3994
|
+
// tool-control messages. A per-call tail candidate is rebuilt next request
|
|
3995
|
+
// (fresh timestamps on appended probes, fresh redaction bytes), so a
|
|
3996
|
+
// breakpoint on it cannot match — it would spend the tail anchor on bytes
|
|
3997
|
+
// that never repeat while the persisted history behind it goes uncached.
|
|
3998
|
+
// Turn-scoped messages are absent next request for the same reason, and
|
|
3999
|
+
// tool controls reject cache_control outright. The walk starts at the
|
|
4000
|
+
// message tail (not the truncated prefix end) so the anchor advances every
|
|
4001
|
+
// turn; the sub-prefix candidate below covers the reusable region behind
|
|
4002
|
+
// a mark.
|
|
3990
4003
|
const trailingCandidates: number[] = [];
|
|
3991
|
-
for (let index =
|
|
4004
|
+
for (let index = messageEnd; index >= 0 && trailingCandidates.length < 2; index--) {
|
|
3992
4005
|
const message = params.messages[index];
|
|
3993
|
-
if (!message) continue;
|
|
4006
|
+
if (!message || message.clear_at === "next_user_message" || isPerCallContextMessage(message)) continue;
|
|
3994
4007
|
if (
|
|
3995
4008
|
message.role === "system" &&
|
|
3996
4009
|
typeof message.content !== "string" &&
|
|
@@ -4002,11 +4015,13 @@ function applyPromptCaching(params: MessageCreateParamsStreaming, cacheControl?:
|
|
|
4002
4015
|
}
|
|
4003
4016
|
trailingCandidates.push(index);
|
|
4004
4017
|
}
|
|
4005
|
-
|
|
4006
4018
|
// Prioritize:
|
|
4007
4019
|
// 1. Most recent trailing message
|
|
4008
4020
|
// 2. Latest decimation checkpoints (newest first) to maintain stable long-context anchors
|
|
4009
|
-
// 3.
|
|
4021
|
+
// 3. Newest message at or before the first per-call/turn-scoped mark, so a
|
|
4022
|
+
// volatile interior message costs only its own re-billed bytes instead
|
|
4023
|
+
// of invalidating the whole reusable prefix behind it
|
|
4024
|
+
// 4. Second trailing message
|
|
4010
4025
|
const candidateIndices: number[] = [];
|
|
4011
4026
|
if (trailingCandidates.length > 0) {
|
|
4012
4027
|
candidateIndices.push(trailingCandidates[0]);
|
|
@@ -4016,6 +4031,9 @@ function applyPromptCaching(params: MessageCreateParamsStreaming, cacheControl?:
|
|
|
4016
4031
|
candidateIndices.push(decimationIndices[i]);
|
|
4017
4032
|
}
|
|
4018
4033
|
}
|
|
4034
|
+
if (stableMessageEnd < messageEnd && stableMessageEnd >= 0 && !candidateIndices.includes(stableMessageEnd)) {
|
|
4035
|
+
candidateIndices.push(stableMessageEnd);
|
|
4036
|
+
}
|
|
4019
4037
|
for (const index of trailingCandidates) {
|
|
4020
4038
|
if (!candidateIndices.includes(index)) {
|
|
4021
4039
|
candidateIndices.push(index);
|
|
@@ -4034,17 +4052,57 @@ function applyPromptCaching(params: MessageCreateParamsStreaming, cacheControl?:
|
|
|
4034
4052
|
}
|
|
4035
4053
|
}
|
|
4036
4054
|
|
|
4055
|
+
/**
|
|
4056
|
+
* Trailing system-prompt segments carrying per-turn volatile content (memory
|
|
4057
|
+
* recall blocks). They are rendered by the coding agent as their own
|
|
4058
|
+
* `systemPrompt` array elements and appended last, so on the wire they
|
|
4059
|
+
* normally form a volatile suffix after the stable prefix. The system cache
|
|
4060
|
+
* breakpoint anchors on the last stable segment instead of the array tail, so
|
|
4061
|
+
* a recall refresh re-bills only the suffix and the message tail for one turn
|
|
4062
|
+
* while the tools+stable-system prefix stays a cache hit. The fingerprint in
|
|
4063
|
+
* `planStableAnthropicSystem` is scoped the same way, so a recall-only change
|
|
4064
|
+
* no longer resets the tool/control baselines either.
|
|
4065
|
+
*
|
|
4066
|
+
* Only a genuinely trailing volatile run counts: a `before_agent_start`
|
|
4067
|
+
* extension override may append a stable policy block after the staged recall
|
|
4068
|
+
* block, and that block must stay fingerprinted stable (a change to it has to
|
|
4069
|
+
* re-baseline). A volatile block stranded mid-array still poisons the prefix
|
|
4070
|
+
* at its position — prefix caching is positional, so no classification can
|
|
4071
|
+
* save the bytes after it — but the stable tail is at least fingerprinted
|
|
4072
|
+
* instead of silently excluded.
|
|
4073
|
+
*
|
|
4074
|
+
* Detection is by our own markup, not model identity: recall blocks always
|
|
4075
|
+
* open with `<memories>`. Stable segments containing recalled text elsewhere
|
|
4076
|
+
* (e.g. quoted in conversation) are unaffected — only a leading tag counts.
|
|
4077
|
+
*/
|
|
4078
|
+
const VOLATILE_SYSTEM_SEGMENT_MARKERS = ["<memories>"];
|
|
4079
|
+
|
|
4080
|
+
function stableSystemSuffixStart(systemBlocks: readonly AnthropicSystemBlock[]): number {
|
|
4081
|
+
let start = systemBlocks.length;
|
|
4082
|
+
while (start > 0) {
|
|
4083
|
+
const text = systemBlocks[start - 1]?.text ?? "";
|
|
4084
|
+
if (!VOLATILE_SYSTEM_SEGMENT_MARKERS.some(marker => text.startsWith(marker))) break;
|
|
4085
|
+
start--;
|
|
4086
|
+
}
|
|
4087
|
+
return start;
|
|
4088
|
+
}
|
|
4089
|
+
|
|
4037
4090
|
/**
|
|
4038
4091
|
* Anchor cache_control on the stable request head — the last (non-deferred)
|
|
4039
|
-
* tool definition and the last system block. The canonical cache order is
|
|
4040
|
-
* tools → system → messages, so a breakpoint on the final system block caches
|
|
4041
|
-
* the entire tools+system prefix, and the extra tool breakpoint keeps the tool
|
|
4092
|
+
* tool definition and the last stable system block. The canonical cache order is
|
|
4093
|
+
* tools → system → messages, so a breakpoint on the final stable system block caches
|
|
4094
|
+
* the entire tools+stable-system prefix, and the extra tool breakpoint keeps the tool
|
|
4042
4095
|
* definitions cached even when the system text changes. This guarantees the
|
|
4043
4096
|
* large, unchanging head is a cache hit on every turn regardless of how the
|
|
4044
4097
|
* message tail churns — the breakpoint placement first-party Anthropic clients
|
|
4045
4098
|
* (Claude Code, Pi) use. Without it, the general API-key path anchors only the
|
|
4046
4099
|
* moving message tail, so tail churn re-writes the whole head uncached.
|
|
4047
4100
|
*
|
|
4101
|
+
* Volatile trailing segments (memory recall) sit after the breakpoint, so a
|
|
4102
|
+
* recall refresh re-bills only the suffix and the tail for one turn instead of
|
|
4103
|
+
* the whole head. When every system block is volatile there is no stable
|
|
4104
|
+
* boundary and the breakpoint stays on the array tail (previous behavior).
|
|
4105
|
+
*
|
|
4048
4106
|
* Anthropic allows at most 4 cache breakpoints per request. At most one is
|
|
4049
4107
|
* spent on tools and one on system here, leaving the remaining budget for
|
|
4050
4108
|
* the message tail and historical decimation checkpoints in `applyPromptCaching`.
|
|
@@ -4080,9 +4138,28 @@ function applyHeadCaching(
|
|
|
4080
4138
|
}
|
|
4081
4139
|
}
|
|
4082
4140
|
|
|
4083
|
-
if (systemBlocks && systemBlocks.length > 0
|
|
4084
|
-
|
|
4085
|
-
|
|
4141
|
+
if (systemBlocks && systemBlocks.length > 0) {
|
|
4142
|
+
// Anchor on the last stable block so a volatile recall suffix refresh
|
|
4143
|
+
// re-bills only the suffix, not the whole head. The skip-if-decorated
|
|
4144
|
+
// check applies only when there is no volatile suffix (previous
|
|
4145
|
+
// behavior): with a suffix present the boundary anchor is added
|
|
4146
|
+
// whenever the anchor block itself lacks a breakpoint, even if the
|
|
4147
|
+
// OAuth path pre-decorated its identity block — otherwise the only
|
|
4148
|
+
// system breakpoint sits before the stable prompt and a recall
|
|
4149
|
+
// refresh re-bills it. The message budget in `applyPromptCaching`
|
|
4150
|
+
// shrinks accordingly (4 minus head breakpoints). All-volatile falls
|
|
4151
|
+
// back to tail anchoring (previous behavior).
|
|
4152
|
+
const suffixStart = stableSystemSuffixStart(systemBlocks);
|
|
4153
|
+
if (suffixStart === systemBlocks.length) {
|
|
4154
|
+
if (!systemBlocks.some(block => block.cache_control != null)) {
|
|
4155
|
+
const lastBlock = systemBlocks[systemBlocks.length - 1];
|
|
4156
|
+
if (lastBlock) lastBlock.cache_control = cloneAnthropicCacheControl(cacheControl);
|
|
4157
|
+
}
|
|
4158
|
+
} else {
|
|
4159
|
+
const anchorIndex = suffixStart === 0 ? systemBlocks.length - 1 : suffixStart - 1;
|
|
4160
|
+
const anchor = systemBlocks[anchorIndex];
|
|
4161
|
+
if (anchor && anchor.cache_control == null) anchor.cache_control = cloneAnthropicCacheControl(cacheControl);
|
|
4162
|
+
}
|
|
4086
4163
|
}
|
|
4087
4164
|
}
|
|
4088
4165
|
|
|
@@ -4166,11 +4243,15 @@ function getAnthropicControlState(
|
|
|
4166
4243
|
): AnthropicControlState | undefined {
|
|
4167
4244
|
if (!state) return undefined;
|
|
4168
4245
|
const root = messages[0];
|
|
4246
|
+
// Key on the stable system prefix, not the full array: a volatile recall
|
|
4247
|
+
// suffix refresh must resolve the same baseline or the declared-tool,
|
|
4248
|
+
// effort, and control-transition state it preserves is lost with it.
|
|
4249
|
+
const stablePrefix = system?.slice(0, stableSystemSuffixStart(system)) ?? null;
|
|
4169
4250
|
const fingerprint = String(
|
|
4170
4251
|
Bun.hash(
|
|
4171
4252
|
JSON.stringify([
|
|
4172
4253
|
sessionId ?? "",
|
|
4173
|
-
|
|
4254
|
+
stablePrefix?.map(block => block.text) ?? null,
|
|
4174
4255
|
root ? anthropicControlMessageProjection(root) : null,
|
|
4175
4256
|
]),
|
|
4176
4257
|
),
|
|
@@ -4216,14 +4297,16 @@ function syncAnthropicControlState(state: AnthropicControlState, messages: reado
|
|
|
4216
4297
|
}
|
|
4217
4298
|
|
|
4218
4299
|
/**
|
|
4219
|
-
* Keep the top-level `system` array byte-stable across a session. The
|
|
4220
|
-
* captured on the first request
|
|
4221
|
-
* request's cache breakpoints) while
|
|
4300
|
+
* Keep the top-level `system` array byte-stable across a session. The stable
|
|
4301
|
+
* prefix captured on the first request is replayed verbatim (with the current
|
|
4302
|
+
* request's cache breakpoints) while its text is unchanged; the volatile
|
|
4303
|
+
* recall suffix always passes through current-turn. A stable-prefix change
|
|
4222
4304
|
* re-baselines instead of duplicating the prompt as a mid-conversation
|
|
4223
4305
|
* system message: omp's system prompt is one rendered segment that embeds
|
|
4224
4306
|
* the tool roster, so replaying a second copy on every later request would
|
|
4225
4307
|
* cost the full prompt again per change. The prefix rewrite is absorbed by
|
|
4226
|
-
* `prefix_mismatch_behavior: "drop_block"` and one cache miss
|
|
4308
|
+
* `prefix_mismatch_behavior: "drop_block"` and one cache miss, while a
|
|
4309
|
+
* recall-only change keeps the tool/control baselines intact.
|
|
4227
4310
|
*/
|
|
4228
4311
|
function planStableAnthropicSystem(
|
|
4229
4312
|
current: AnthropicSystemBlock[] | undefined,
|
|
@@ -4231,16 +4314,21 @@ function planStableAnthropicSystem(
|
|
|
4231
4314
|
enabled: boolean,
|
|
4232
4315
|
): AnthropicSystemBlock[] | undefined {
|
|
4233
4316
|
if (!state || !enabled) return current;
|
|
4234
|
-
const
|
|
4317
|
+
const suffixStart = stableSystemSuffixStart(current ?? []);
|
|
4318
|
+
const fingerprint = JSON.stringify(current?.slice(0, suffixStart).map(block => block.text) ?? null);
|
|
4235
4319
|
if (state.systemFingerprint !== fingerprint) {
|
|
4236
4320
|
resetAnthropicControlState(state);
|
|
4237
4321
|
state.systemFingerprint = fingerprint;
|
|
4238
|
-
state.stableSystemBlocks = current?.map(block => ({ type: block.type, text: block.text }));
|
|
4239
|
-
}
|
|
4240
|
-
|
|
4241
|
-
|
|
4242
|
-
|
|
4243
|
-
|
|
4322
|
+
state.stableSystemBlocks = current?.slice(0, suffixStart).map(block => ({ type: block.type, text: block.text }));
|
|
4323
|
+
}
|
|
4324
|
+
const stableReplay =
|
|
4325
|
+
state.stableSystemBlocks?.map((block, index) => {
|
|
4326
|
+
const cacheControl = current?.[index]?.cache_control;
|
|
4327
|
+
return cacheControl ? { ...block, cache_control: cloneAnthropicCacheControl(cacheControl) } : { ...block };
|
|
4328
|
+
}) ?? [];
|
|
4329
|
+
const suffix = current?.slice(suffixStart).map(block => ({ ...block })) ?? [];
|
|
4330
|
+
const replayed = [...stableReplay, ...suffix];
|
|
4331
|
+
return replayed.length > 0 ? replayed : undefined;
|
|
4244
4332
|
}
|
|
4245
4333
|
|
|
4246
4334
|
function anthropicToolDefinitionKey(tool: AnthropicWireTool): string {
|