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

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,13 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [18.2.11] - 2026-09-23
6
+
7
+ ### Fixed
8
+
9
+ - Fixed Claude Opus 5.5 not applying a mid-session switch to high-effort reasoning when the session started without an explicit effort setting.
10
+ - Fixed Alibaba Token Plan monthly quotas not appearing in usage reports or the status line.
11
+
5
12
  ## [18.2.9] - 2026-09-22
6
13
 
7
14
  ### Added
@@ -2274,17 +2281,4 @@
2274
2281
  - Preserved Anthropic `stop_details` on assistant messages so refusal and sensitive classifier stops remain structurally visible to callers. ([#2290](https://github.com/can1357/oh-my-pi/issues/2290))
2275
2282
  - Fixed OpenAI Responses, Azure OpenAI Responses, and OpenAI Completions streams hanging until the 120s idle watchdog errored the turn when a provider delivers the terminal frame but never sends `[DONE]` nor closes the connection. `processResponsesStream` now breaks out of the event loop on `response.completed`/`response.incomplete` (mirroring the Codex websocket/SSE terminal break), and the completions consumer breaks once `finish_reason` plus a usage payload arrived — or, for hosts that never send usage, ends the stream cleanly via a short post-finish grace window (`iterateWithTerminalGrace`) that aborts the transport to release the socket.
2276
2283
 
2277
- ## [15.11.0] - 2026-06-10
2278
-
2279
- ### Added
2280
-
2281
- - Added optional `ImageContent.detail` (`"auto" | "low" | "high" | "original"`): an OpenAI resolution hint forwarded by the `openai-responses` serializers (default stays `auto`) and by `openai-completions` for the values Chat Completions supports. `"original"` preserves native resolution — required for snapcompact frames, whose pixel-font glyphs do not survive the default downscale. Providers without a detail knob ignore the field.
2282
-
2283
- ### Fixed
2284
-
2285
- - Fixed OpenRouter DeepSeek V4 strict tool schemas nesting `anyOf` inside the nullable wrapper for optional unions, which produced a branch without `type` and triggered OpenRouter's `Invalid tool parameters schema : field anyOf: missing field type` 400. ([#2270](https://github.com/can1357/oh-my-pi/issues/2270))
2286
- - Hardened strict tool-schema handling beyond the optional-union case: `enforceStrictSchema` now splices natively nested pure unions into the parent `anyOf` (only when the inner node carries no constraining siblings, since sibling keywords are conjunctive with `anyOf`), so source schemas with nested unions no longer produce type-less `anyOf` branches that strict upstream validators reject. ([#2270](https://github.com/can1357/oh-my-pi/issues/2270))
2287
- - Made the openai-completions non-strict retry reachable for `"mixed"` strict mode (previously gated to `all_strict`, i.e. Cerebras only) and taught it to recognize upstream tool-schema validation 400s (`Invalid tool parameters schema …`, `Invalid schema for function …`). A matching rejection now retries the request with base (non-strict) schemas and persists `strictToolsDisabled` on the provider session, so later requests skip the doomed strict attempt instead of paying a 400 + retry round-trip each turn. ([#2270](https://github.com/can1357/oh-my-pi/issues/2270))
2288
- - Cross-model `anthropic-messages → anthropic-messages` continuations now preserve prior assistant turns' reasoning chains end-to-end: every prior `thinking`/`redactedThinking` block survives (not just the latest surviving assistant), and third-party ↔ third-party replays keep their signatures intact so the reasoning chain stays signed for the next turn. Signatures are stripped (and any `redacted_thinking` sibling without a native landing spot is dropped) only when an official Anthropic endpoint is on either end of the replay — official Anthropic cryptographically binds reasoning signatures to its key+session+model, while compatible reasoning endpoints (Z.AI, DeepSeek, custom anthropic-messages providers configured via `models.yaml`) treat them as opaque continuation hints. Source-side official detection uses the canonical catalog provider id `"anthropic"` (assistant messages carry no `baseUrl`); target-side detection reuses the baked `compat.officialEndpoint` flag. Latest-turn byte-for-byte behavior (Anthropic's "thinking blocks in the latest assistant message cannot be modified" rule) and existing aborted/errored last-block sanitization are unchanged. ([#2257](https://github.com/can1357/oh-my-pi/issues/2257), [#2265](https://github.com/can1357/oh-my-pi/issues/2265))
2289
-
2290
- Older entries are archived in [packages/ai/CHANGELOG.md@1f7329fc2c7c](https://github.com/can1357/oh-my-pi/blob/1f7329fc2c7c366b38731738e0db9c170f9bb348/packages/ai/CHANGELOG.md).
2284
+ Older entries are archived in [packages/ai/CHANGELOG.md@d58593a30902](https://github.com/can1357/oh-my-pi/blob/d58593a3090258473304608d68ffd1f620e6b695/packages/ai/CHANGELOG.md).
@@ -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.11",
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.11",
156
+ "@oh-my-pi/pi-catalog": "18.2.11",
157
+ "@oh-my-pi/pi-natives": "18.2.11",
158
+ "@oh-my-pi/pi-utils": "18.2.11",
159
+ "@oh-my-pi/pi-wire": "18.2.11"
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
  }
@@ -93,9 +93,9 @@ function usageStatus(usedFraction: number): UsageLimit["status"] {
93
93
  }
94
94
 
95
95
  function buildLimit(
96
- id: "5h" | "7d",
96
+ id: "5h" | "7d" | "monthly",
97
97
  label: string,
98
- durationMs: number,
98
+ durationMs: number | undefined,
99
99
  usedFraction: number | undefined,
100
100
  resetsAt: number | undefined,
101
101
  accountId: string | undefined,
@@ -105,7 +105,7 @@ function buildLimit(
105
105
  id: `credits:${id}`,
106
106
  label,
107
107
  scope: { provider: PROVIDER, ...(accountId ? { accountId } : {}), windowId: id },
108
- window: { id, label, durationMs, ...(resetsAt ? { resetsAt } : {}) },
108
+ window: { id, label, ...(durationMs !== undefined ? { durationMs } : {}), ...(resetsAt ? { resetsAt } : {}) },
109
109
  amount: { used: usedFraction * 100, usedFraction, unit: "percent" },
110
110
  status: usageStatus(usedFraction),
111
111
  };
@@ -245,6 +245,17 @@ async function fetchAlibabaTokenPlanUsage(
245
245
  parsePositiveTimestamp(responseData.per1WeekResetTime),
246
246
  accountId,
247
247
  ),
248
+ // Monthly-only plans report just this bucket, and the console never
249
+ // states its span (calendar months differ), so the window carries a
250
+ // reset deadline without a duration.
251
+ buildLimit(
252
+ "monthly",
253
+ "Monthly Credits",
254
+ undefined,
255
+ parseUsedFraction(responseData.per1MonthPercentage),
256
+ parsePositiveTimestamp(responseData.per1MonthResetTime),
257
+ accountId,
258
+ ),
248
259
  ].filter((limit): limit is UsageLimit => limit !== undefined);
249
260
  if (limits.length === 0) return null;
250
261
  return {
@@ -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
  /**