@oh-my-pi/pi-ai 18.2.9 → 18.2.10

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,10 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ### Fixed
6
+
7
+ - Fixed Claude Opus 5.5 ignoring a mid-session switch to high effort when the session started without an explicit effort; the change is now sent as a cache-preserving per-message effort control ([#12909](https://github.com/can1357/oh-my-pi/pull/12909) by [@h4vc](https://github.com/h4vc)).
8
+
5
9
  ## [18.2.9] - 2026-09-22
6
10
 
7
11
  ### Added
@@ -25,9 +25,14 @@ export interface ClaudeResetConsumeResult {
25
25
  raw?: unknown;
26
26
  }
27
27
  /**
28
- * Reuse program blocks when a normal usage response actually includes them.
29
- * A lone non-redeemable Cedar block is inconclusive because Juniper requires
30
- * the separate at-wall read, so callers must still perform discovery.
28
+ * Reuse program blocks when a normal usage response actually evaluated them.
29
+ * Anthropic lists every promo program as a key on the plain `/usage` body and
30
+ * leaves the value `null` until the matching probe query (`?cedar_ember=1`,
31
+ * `?at_wall=1`) asks for an evaluation, so a `null` block is "unknown", never
32
+ * "no resets". Only an object block is an answer; anything else returns `null`
33
+ * so the caller falls through to {@link listClaudeResetCredits}. A lone
34
+ * non-redeemable Cedar block is likewise inconclusive because Juniper requires
35
+ * the separate at-wall read.
31
36
  *
32
37
  * @internal
33
38
  */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oh-my-pi/pi-ai",
3
- "version": "18.2.9",
3
+ "version": "18.2.10",
4
4
  "description": "Unified LLM API with automatic model discovery and provider configuration",
5
5
  "keywords": [
6
6
  "ai",
@@ -152,11 +152,11 @@
152
152
  "fmt": "oxfmt --no-error-on-unmatched-pattern 'src/**/*.{ts,tsx}' '{test,bench,examples,scripts}/**/*.ts' '*.ts'"
153
153
  },
154
154
  "dependencies": {
155
- "@oh-my-pi/omptype": "18.2.9",
156
- "@oh-my-pi/pi-catalog": "18.2.9",
157
- "@oh-my-pi/pi-natives": "18.2.9",
158
- "@oh-my-pi/pi-utils": "18.2.9",
159
- "@oh-my-pi/pi-wire": "18.2.9"
155
+ "@oh-my-pi/omptype": "18.2.10",
156
+ "@oh-my-pi/pi-catalog": "18.2.10",
157
+ "@oh-my-pi/pi-natives": "18.2.10",
158
+ "@oh-my-pi/pi-utils": "18.2.10",
159
+ "@oh-my-pi/pi-wire": "18.2.10"
160
160
  },
161
161
  "devDependencies": {
162
162
  "@types/bun": "^1.3.14"
@@ -466,8 +466,11 @@ type AnthropicControlState = {
466
466
  stableSystemBlocks: AnthropicSystemBlock[] | undefined;
467
467
  systemFingerprint: string | undefined;
468
468
  controlTransitions: AnthropicControlTransition[];
469
- baseEffort: AnthropicOutputEffort | undefined;
469
+ /** Whether the effort baseline was captured; `undefined` efforts are a valid baseline. */
470
+ effortBaselined: boolean;
471
+ /** Top-level `output_config.effort` of the baseline request; `undefined` = API default. */
470
472
  baseEffortWire: AnthropicOutputEffort | undefined;
473
+ /** Effort in force at the conversation tail; `undefined` = API default. */
471
474
  currentEffort: AnthropicOutputEffort | undefined;
472
475
  };
473
476
 
@@ -504,7 +507,7 @@ function createAnthropicControlState(): AnthropicControlState {
504
507
  stableSystemBlocks: undefined,
505
508
  systemFingerprint: undefined,
506
509
  controlTransitions: [],
507
- baseEffort: undefined,
510
+ effortBaselined: false,
508
511
  baseEffortWire: undefined,
509
512
  currentEffort: undefined,
510
513
  };
@@ -3999,7 +4002,7 @@ function resetAnthropicControlState(state: AnthropicControlState): void {
3999
4002
  state.stableSystemBlocks = undefined;
4000
4003
  state.systemFingerprint = undefined;
4001
4004
  state.controlTransitions = [];
4002
- state.baseEffort = undefined;
4005
+ state.effortBaselined = false;
4003
4006
  state.baseEffortWire = undefined;
4004
4007
  state.currentEffort = undefined;
4005
4008
  }
@@ -4202,6 +4205,13 @@ function planStableAnthropicTools(
4202
4205
  * later changes as per-message effort. Anthropic applies a system message's
4203
4206
  * `output_config.effort` from the next `user` turn on, so the control is
4204
4207
  * anchored before the latest user message to take effect on this response.
4208
+ *
4209
+ * An omitted effort means the API's per-model default (`medium` on Opus 5.5,
4210
+ * `high` elsewhere), so it is tracked as its own state rather than assumed to
4211
+ * be any concrete level: every later explicit level is sent as a control. A
4212
+ * per-message control cannot express "back to the API default", so a request
4213
+ * that drops its effort mid-session keeps the level already in force instead
4214
+ * of rewriting the top-level value and invalidating the cache.
4205
4215
  */
4206
4216
  function planStableAnthropicEffort(
4207
4217
  current: AnthropicOutputEffort | undefined,
@@ -4210,18 +4220,17 @@ function planStableAnthropicEffort(
4210
4220
  enabled: boolean,
4211
4221
  ): AnthropicOutputEffort | undefined {
4212
4222
  if (!state || !enabled) return current;
4213
- const effective = current ?? "high";
4214
- if (state.baseEffort === undefined) {
4215
- state.baseEffort = effective;
4223
+ if (!state.effortBaselined) {
4224
+ state.effortBaselined = true;
4216
4225
  state.baseEffortWire = current;
4217
- state.currentEffort = effective;
4226
+ state.currentEffort = current;
4218
4227
  return current;
4219
4228
  }
4220
- if (state.currentEffort !== effective) {
4229
+ if (current !== undefined && state.currentEffort !== current) {
4221
4230
  const lastUserIndex = messages.findLastIndex(message => message.role === "user");
4222
4231
  const messageCount = lastUserIndex >= 0 ? lastUserIndex : messages.length;
4223
- recordAnthropicControlTransition(state, messages, messageCount, [], effective);
4224
- state.currentEffort = effective;
4232
+ recordAnthropicControlTransition(state, messages, messageCount, [], current);
4233
+ state.currentEffort = current;
4225
4234
  }
4226
4235
  return state.baseEffortWire;
4227
4236
  }
@@ -448,9 +448,14 @@ async function withResolvedOrganization(
448
448
  }
449
449
 
450
450
  /**
451
- * Reuse program blocks when a normal usage response actually includes them.
452
- * A lone non-redeemable Cedar block is inconclusive because Juniper requires
453
- * the separate at-wall read, so callers must still perform discovery.
451
+ * Reuse program blocks when a normal usage response actually evaluated them.
452
+ * Anthropic lists every promo program as a key on the plain `/usage` body and
453
+ * leaves the value `null` until the matching probe query (`?cedar_ember=1`,
454
+ * `?at_wall=1`) asks for an evaluation, so a `null` block is "unknown", never
455
+ * "no resets". Only an object block is an answer; anything else returns `null`
456
+ * so the caller falls through to {@link listClaudeResetCredits}. A lone
457
+ * non-redeemable Cedar block is likewise inconclusive because Juniper requires
458
+ * the separate at-wall read.
454
459
  *
455
460
  * @internal
456
461
  */
@@ -459,26 +464,24 @@ export function parseClaudeResetCreditsFromUsagePayload(
459
464
  orgId?: string,
460
465
  baseUrl?: string,
461
466
  ): ClaudeResetCreditList | null {
462
- if (!isRecord(payload) || !Object.hasOwn(payload, CEDAR_PROGRAM)) return null;
463
- const parsedCedar = parseCedarStatus(payload[CEDAR_PROGRAM]);
464
- if (!parsedCedar.ok) return null;
467
+ if (!isRecord(payload)) return null;
468
+ const cedarBlock = payload[CEDAR_PROGRAM];
469
+ const juniperBlock = payload[JUNIPER_PROGRAM];
465
470
  const normalizedOrgId = orgId?.trim();
466
471
  const safeOrgId = normalizedOrgId && ORGANIZATION_ID.test(normalizedOrgId) ? normalizedOrgId : undefined;
467
- const cedarList = parsedCedar.status ? normalizeCedarList(parsedCedar.status, safeOrgId, baseUrl) : undefined;
468
- if (cedarList && (cedarList.availableCount > 0 || cedarList.nextCreditId)) return cedarList;
469
- if (!Object.hasOwn(payload, JUNIPER_PROGRAM)) return null;
470
- const parsedJuniper = parseJuniperStatus(payload[JUNIPER_PROGRAM]);
471
- if (!parsedJuniper.ok) return null;
472
- if (parsedJuniper.status) return normalizeJuniperList(parsedJuniper.status, safeOrgId, baseUrl);
473
- if (cedarList) return cedarList;
474
- return {
475
- availableCount: 0,
476
- redeemableCount: 0,
477
- eligible: false,
478
- credits: [],
479
- ...(safeOrgId ? { orgId: safeOrgId } : {}),
480
- ...(baseUrl ? { baseUrl } : {}),
481
- };
472
+ let cedarList: ClaudeResetCreditList | undefined;
473
+ if (isRecord(cedarBlock)) {
474
+ const parsedCedar = parseCedarStatus(cedarBlock);
475
+ if (!parsedCedar.ok || !parsedCedar.status) return null;
476
+ cedarList = normalizeCedarList(parsedCedar.status, safeOrgId, baseUrl);
477
+ if (cedarList.availableCount > 0 || cedarList.nextCreditId) return cedarList;
478
+ }
479
+ if (isRecord(juniperBlock)) {
480
+ const parsedJuniper = parseJuniperStatus(juniperBlock);
481
+ if (!parsedJuniper.ok || !parsedJuniper.status) return null;
482
+ return normalizeJuniperList(parsedJuniper.status, safeOrgId, baseUrl);
483
+ }
484
+ return cedarList ?? null;
482
485
  }
483
486
 
484
487
  /**