@oh-my-pi/pi-ai 18.2.4 → 18.2.5

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 CHANGED
@@ -2,6 +2,16 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [18.2.5] - 2026-09-17
6
+
7
+ ### Added
8
+
9
+ - Added support for templating and custom base and authentication URLs in OAuth flows.
10
+
11
+ ### Fixed
12
+
13
+ - Fixed Anthropic prompt-cache breakpoints stalling when conversations include mid-conversation tool changes, preventing growing message tails from being unnecessarily re-billed as uncached input.
14
+
5
15
  ## [18.2.4] - 2026-09-17
6
16
 
7
17
  ### Added
@@ -48,7 +48,7 @@ export declare function postTokenRequest(request: CompiledOAuthRequest, standard
48
48
  response: Response;
49
49
  }>;
50
50
  /** Bearer GET declared by `userinfo`; failures leave identity fields unset. */
51
- export declare function applyUserinfo(userinfo: CompiledUserinfo | undefined, credentials: OAuthCredentials, context: RequestContext): Promise<OAuthCredentials>;
51
+ export declare function applyUserinfo(userinfo: CompiledUserinfo | undefined, credentials: OAuthCredentials, context: RequestContext, vars?: TemplateVars): Promise<OAuthCredentials>;
52
52
  /** Runs the rule's after-exchange / after-refresh hook, if declared. */
53
53
  export declare function applyAfterExchange(hook: string | undefined, credentials: OAuthCredentials, context: ExchangeContext): Promise<OAuthCredentials>;
54
54
  export declare function describeError(error: unknown): string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oh-my-pi/pi-ai",
3
- "version": "18.2.4",
3
+ "version": "18.2.5",
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.4",
132
- "@oh-my-pi/pi-catalog": "18.2.4",
133
- "@oh-my-pi/pi-natives": "18.2.4",
134
- "@oh-my-pi/pi-utils": "18.2.4",
135
- "@oh-my-pi/pi-wire": "18.2.4"
131
+ "@oh-my-pi/omptype": "18.2.5",
132
+ "@oh-my-pi/pi-catalog": "18.2.5",
133
+ "@oh-my-pi/pi-natives": "18.2.5",
134
+ "@oh-my-pi/pi-utils": "18.2.5",
135
+ "@oh-my-pi/pi-wire": "18.2.5"
136
136
  },
137
137
  "devDependencies": {
138
138
  "@types/bun": "^1.3.14"
@@ -1563,7 +1563,9 @@ async function* iterateAnthropicEvents(
1563
1563
  let sawMessageStart = false;
1564
1564
  let sawMessageEnd = false;
1565
1565
 
1566
- for await (const sse of readSseEvents(response.body, signal)) {
1566
+ // Capture `raw` only when the diagnostic observer exists; otherwise the
1567
+ // per-frame wire-line array is pure token-path garbage.
1568
+ for await (const sse of readSseEvents(response.body, signal, onSseEvent ? { captureRaw: true } : undefined)) {
1567
1569
  notifyRawSseEvent(onSseEvent, sse);
1568
1570
  if (sse.event === "error") {
1569
1571
  throw createAnthropicSseStreamError(sse.data);
@@ -3978,11 +3980,26 @@ function applyPromptCaching(params: MessageCreateParamsStreaming, cacheControl?:
3978
3980
  // Stable historical decimation checkpoint every 15 user turns (15th, 30th, 45th...)
3979
3981
  const decimationIndices = userIndices.filter((_, ordinal) => (ordinal + 1) % ANTHROPIC_DECIMATION_INTERVAL === 0);
3980
3982
 
3981
- // Collect up to 2 trailing candidates from the reusable prefix.
3983
+ // Collect up to 2 trailing candidates from the reusable prefix, skipping
3984
+ // mid-conversation tool-control messages. They contain only tool_addition /
3985
+ // tool_removal blocks, so cache_control is always rejected there; parking
3986
+ // the rolling window on one spends the tail breakpoint on a decoration
3987
+ // that always fails, and with decimation checkpoints present the remaining
3988
+ // breakpoints land on already-cached history while the growing tail is
3989
+ // re-billed as uncached input every turn.
3982
3990
  const trailingCandidates: number[] = [];
3983
3991
  for (let index = stableMessageEnd; index >= 0 && trailingCandidates.length < 2; index--) {
3984
3992
  const message = params.messages[index];
3985
3993
  if (!message) continue;
3994
+ if (
3995
+ message.role === "system" &&
3996
+ typeof message.content !== "string" &&
3997
+ Array.isArray(message.content) &&
3998
+ message.content.length > 0 &&
3999
+ message.content.every(block => block.type === "tool_addition" || block.type === "tool_removal")
4000
+ ) {
4001
+ continue;
4002
+ }
3986
4003
  trailingCandidates.push(index);
3987
4004
  }
3988
4005
 
@@ -267,10 +267,11 @@ export async function applyUserinfo(
267
267
  userinfo: CompiledUserinfo | undefined,
268
268
  credentials: OAuthCredentials,
269
269
  context: RequestContext,
270
+ vars: TemplateVars = {},
270
271
  ): Promise<OAuthCredentials> {
271
272
  if (!userinfo) return credentials;
272
273
  try {
273
- const response = await context.fetch(userinfo.url, {
274
+ const response = await context.fetch(template(userinfo.url, vars), {
274
275
  headers: { ...context.headers, Authorization: `Bearer ${credentials.access}` },
275
276
  signal: context.signal,
276
277
  });
@@ -93,6 +93,8 @@ export class DeclarativeOAuthCodeFlow extends OAuthCallbackFlow {
93
93
  #verifier = "";
94
94
  #clientId?: string;
95
95
  #clientSecret?: string;
96
+ #base?: string;
97
+ #auth?: string;
96
98
 
97
99
  constructor(
98
100
  ctrl: OAuthController,
@@ -123,6 +125,8 @@ export class DeclarativeOAuthCodeFlow extends OAuthCallbackFlow {
123
125
  const signal = this.ctrl.signal;
124
126
  this.#clientId = rule.clientId ? await resolveValue(rule.clientId, signal) : undefined;
125
127
  this.#clientSecret = rule.clientSecret ? await resolveValue(rule.clientSecret, signal) : undefined;
128
+ this.#base = rule.baseUrl ? (await resolveValue(rule.baseUrl, signal)).replace(/\/+$/, "") : undefined;
129
+ this.#auth = rule.authUrl ? (await resolveValue(rule.authUrl, signal)).replace(/\/+$/, "") : undefined;
126
130
  let challenge: string | undefined;
127
131
  if (rule.pkce) {
128
132
  const pkce = await generatePKCE();
@@ -136,6 +140,8 @@ export class DeclarativeOAuthCodeFlow extends OAuthCallbackFlow {
136
140
  scope,
137
141
  state,
138
142
  code_challenge: challenge,
143
+ base: this.#base,
144
+ auth: this.#auth,
139
145
  };
140
146
  const params = new URLSearchParams();
141
147
  if (rule.standardAuthorizeParams) {
@@ -150,7 +156,7 @@ export class DeclarativeOAuthCodeFlow extends OAuthCallbackFlow {
150
156
  if (state) params.set("state", state);
151
157
  }
152
158
  for (const key in rule.authorizeParams) params.set(key, template(rule.authorizeParams[key], vars));
153
- const authorizeUrl = await resolveValue(rule.authorizeUrl, signal);
159
+ const authorizeUrl = template(await resolveValue(rule.authorizeUrl, signal), vars);
154
160
  return { url: `${authorizeUrl}?${params.toString()}`, instructions: rule.instructions };
155
161
  }
156
162
 
@@ -185,6 +191,8 @@ export class DeclarativeOAuthCodeFlow extends OAuthCallbackFlow {
185
191
  code_verifier: rule.pkce ? this.#verifier : undefined,
186
192
  client_id: this.#clientId,
187
193
  client_secret: this.#clientSecret,
194
+ base: this.#base,
195
+ auth: this.#auth,
188
196
  };
189
197
  const context = { provider: this.#provider, fetch: this.#fetch, signal };
190
198
  const { body } = await postTokenRequest(
@@ -203,7 +211,7 @@ export class DeclarativeOAuthCodeFlow extends OAuthCallbackFlow {
203
211
  );
204
212
  throwIfCancelled(signal);
205
213
  let credentials = mapCredentials(rule.credential, body, this.#provider);
206
- credentials = await applyUserinfo(rule.userinfo, credentials, context);
214
+ credentials = await applyUserinfo(rule.userinfo, credentials, context, vars);
207
215
  throwIfCancelled(signal);
208
216
  return applyAfterExchange(rule.afterExchange, credentials, {
209
217
  provider: this.#provider,
@@ -31,7 +31,8 @@ async function loginClient(policy: CompiledAuthProvider, signal?: AbortSignal) {
31
31
  redirectUri: login.callback.redirectUri
32
32
  ? await resolveValue(login.callback.redirectUri, signal)
33
33
  : `http://${login.callback.hostname}:${login.callback.port}${login.callback.path}`,
34
- base: undefined,
34
+ base: login.baseUrl ? (await resolveValue(login.baseUrl, signal)).replace(/\/+$/, "") : undefined,
35
+ auth: login.authUrl ? (await resolveValue(login.authUrl, signal)).replace(/\/+$/, "") : undefined,
35
36
  };
36
37
  }
37
38
  if (login?.kind === "device-code") {
@@ -40,9 +41,10 @@ async function loginClient(policy: CompiledAuthProvider, signal?: AbortSignal) {
40
41
  clientSecret: undefined,
41
42
  redirectUri: undefined,
42
43
  base: login.baseUrl ? await resolveValue(login.baseUrl, signal) : undefined,
44
+ auth: undefined,
43
45
  };
44
46
  }
45
- return { clientId: undefined, clientSecret: undefined, redirectUri: undefined, base: undefined };
47
+ return { clientId: undefined, clientSecret: undefined, redirectUri: undefined, base: undefined, auth: undefined };
46
48
  }
47
49
 
48
50
  function createRequestRefresh(rule: RequestRefresh, policy: CompiledAuthProvider): Refresher {
@@ -66,6 +68,7 @@ function createRequestRefresh(rule: RequestRefresh, policy: CompiledAuthProvider
66
68
  client_secret: client.clientSecret,
67
69
  redirect_uri: client.redirectUri,
68
70
  base: client.base,
71
+ auth: client.auth,
69
72
  claude_code_sdk_version: claudeCodeSdkVersion,
70
73
  };
71
74
  const { body } = await postTokenRequest(