@oh-my-pi/pi-ai 15.10.6 → 15.10.7
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 +8 -0
- package/package.json +2 -2
- package/src/auth-storage.ts +12 -5
- package/src/providers/transform-messages.ts +31 -17
- package/src/usage/google-antigravity.ts +64 -17
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [15.10.7] - 2026-06-08
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
|
|
9
|
+
- Fixed first-party Anthropic requests returning HTTP 400 "Invalid `signature` in `thinking` block" after interrupting the model during its visible output. `transformMessages` stripped the signature from every `thinking` block of an `aborted`/`error` turn, including blocks that had already finished streaming — Anthropic delivers a block's signature at `content_block_stop` before the next block starts, so a thinking block followed by `text`/`tool_use` is fully signed. The valid signature was then replayed empty (`signature: ""`), which signature-enforcing Anthropic rejects, including when the provider is routed through an LLM gateway baseUrl. Only the single mid-stream block at the abort point is now treated as untrustworthy; completed thinking blocks keep their replayable signatures ([#2144](https://github.com/can1357/oh-my-pi/issues/2144)).
|
|
10
|
+
- Pinned a regression test against issue [#2123](https://github.com/can1357/oh-my-pi/issues/2123): OAuth requests to adaptive-thinking Claude Opus models (4.6+) ship a `context_management.edits[clear_thinking_20251015]` block paired with the `thinking` field, but the eager-todo prelude (and other paths that force `tool_choice` to `tool`/`any` on the first user turn) route through `disableThinkingIfToolChoiceForced`, which would strip `params.thinking` while leaving the orphan `context_management` behind. The Anthropic API then rejected the request with `400 ... clear_thinking_20251015 strategy requires thinking to be enabled or adaptive`. The fix that lands in [15.10.5] now drops both fields together; the new test locks the contract so the strategy can never outlive its enabling `thinking` payload again.
|
|
11
|
+
- Fixed Antigravity usage counters so exhausted Google/Gemini quota renders as `0% free` while separate Anthropic/OpenAI-backed Antigravity model counters remain visible independently, without replaying stale pre-fix cached usage reports.
|
|
12
|
+
|
|
5
13
|
## [15.10.6] - 2026-06-08
|
|
6
14
|
|
|
7
15
|
### Added
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@oh-my-pi/pi-ai",
|
|
4
|
-
"version": "15.10.
|
|
4
|
+
"version": "15.10.7",
|
|
5
5
|
"description": "Unified LLM API with automatic model discovery and provider configuration",
|
|
6
6
|
"homepage": "https://omp.sh",
|
|
7
7
|
"author": "Can Boluk",
|
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
},
|
|
40
40
|
"dependencies": {
|
|
41
41
|
"@bufbuild/protobuf": "^2.12.0",
|
|
42
|
-
"@oh-my-pi/pi-utils": "15.10.
|
|
42
|
+
"@oh-my-pi/pi-utils": "15.10.7",
|
|
43
43
|
"openai": "^6.39.0",
|
|
44
44
|
"partial-json": "^0.1.7",
|
|
45
45
|
"zod": "4.4.3"
|
package/src/auth-storage.ts
CHANGED
|
@@ -492,6 +492,9 @@ const USAGE_FAILURE_BACKOFF_MS = 10_000;
|
|
|
492
492
|
// Bumped from 3s — Claude usage retries up to 3 times with exponential backoff
|
|
493
493
|
// (~3.5s total worst case); a tight per-request budget aborts retries mid-cycle.
|
|
494
494
|
const DEFAULT_USAGE_REQUEST_TIMEOUT_MS = 10_000;
|
|
495
|
+
const USAGE_REPORT_CACHE_KEY_VERSION_OVERRIDES: Partial<Record<Provider, number>> = {
|
|
496
|
+
"google-antigravity": 2,
|
|
497
|
+
};
|
|
495
498
|
const DEFAULT_OAUTH_REFRESH_TIMEOUT_MS = 10_000;
|
|
496
499
|
/**
|
|
497
500
|
* Refresh OAuth access tokens this many ms before their stated expiry. The
|
|
@@ -1638,15 +1641,19 @@ export class AuthStorage {
|
|
|
1638
1641
|
#buildUsageReportCacheKey(request: UsageRequestDescriptor): string {
|
|
1639
1642
|
const baseUrl = this.#normalizeUsageBaseUrl(request.baseUrl) || "default";
|
|
1640
1643
|
const identity = this.#buildUsageCacheIdentity(request.credential);
|
|
1641
|
-
|
|
1644
|
+
const versionOverride = USAGE_REPORT_CACHE_KEY_VERSION_OVERRIDES[request.provider];
|
|
1645
|
+
const providerKey = versionOverride === undefined ? request.provider : `${versionOverride}:${request.provider}`;
|
|
1646
|
+
return `report:${providerKey}:${baseUrl}:${identity}`;
|
|
1642
1647
|
}
|
|
1643
1648
|
|
|
1644
1649
|
#buildUsageReportsCacheKey(requests: ReadonlyArray<UsageRequestDescriptor>): string {
|
|
1645
1650
|
const snapshot = requests
|
|
1646
|
-
.map(
|
|
1647
|
-
request
|
|
1648
|
-
|
|
1649
|
-
|
|
1651
|
+
.map(request => {
|
|
1652
|
+
const versionOverride = USAGE_REPORT_CACHE_KEY_VERSION_OVERRIDES[request.provider];
|
|
1653
|
+
const providerKey =
|
|
1654
|
+
versionOverride === undefined ? request.provider : `${versionOverride}:${request.provider}`;
|
|
1655
|
+
return `${providerKey}:${this.#normalizeUsageBaseUrl(request.baseUrl) || "default"}:${this.#buildUsageCacheIdentity(request.credential)}`;
|
|
1656
|
+
})
|
|
1650
1657
|
.sort()
|
|
1651
1658
|
.join("\n");
|
|
1652
1659
|
return `reports:${Bun.hash(snapshot).toString(16)}`;
|
|
@@ -173,32 +173,46 @@ export function transformMessages<TApi extends Api>(
|
|
|
173
173
|
index === latestSurvivingAssistantIndex &&
|
|
174
174
|
model.api === "anthropic-messages" &&
|
|
175
175
|
assistantMsg.api === "anthropic-messages";
|
|
176
|
-
//
|
|
177
|
-
//
|
|
178
|
-
// strip signatures from thinking blocks in these messages.
|
|
176
|
+
// Thinking signatures can be untrustworthy for two distinct reasons with very
|
|
177
|
+
// different blast radii:
|
|
179
178
|
//
|
|
180
|
-
//
|
|
181
|
-
//
|
|
182
|
-
//
|
|
183
|
-
//
|
|
184
|
-
//
|
|
185
|
-
//
|
|
186
|
-
//
|
|
187
|
-
//
|
|
188
|
-
//
|
|
189
|
-
//
|
|
179
|
+
// 1. Aborted/errored turns: the stream stopped mid-block, so only the block
|
|
180
|
+
// that was streaming at the abort point — always the FINAL content block —
|
|
181
|
+
// can carry a partially-streamed (invalid) signature. Every earlier block
|
|
182
|
+
// completed: Anthropic delivers a block's signature at its
|
|
183
|
+
// `content_block_stop`, which necessarily fired before the next block began,
|
|
184
|
+
// so those signatures are whole and valid. Stripping them would needlessly
|
|
185
|
+
// discard a replayable thinking chain — e.g. interrupting during the visible
|
|
186
|
+
// text output after thinking already finished leaves a fully-signed thinking
|
|
187
|
+
// block that must be kept, or Anthropic rejects the replay with HTTP 400
|
|
188
|
+
// "Invalid `signature` in `thinking` block".
|
|
189
|
+
//
|
|
190
|
+
// 2. Abandoned tool-use turns: a turn that carries toolCall blocks but did NOT
|
|
191
|
+
// request tool execution (stopReason !== "toolUse" — e.g. adaptive-thinking
|
|
192
|
+
// Opus emitting tool calls and then ending on `end_turn`/`stop`). The agent
|
|
193
|
+
// loop pairs those calls with placeholder tool_results to keep the
|
|
194
|
+
// tool_use/tool_result contract valid. The turn completed cleanly, but its
|
|
195
|
+
// signatures are end_turn-bound and cannot be replayed in that synthesized
|
|
196
|
+
// continuation, so EVERY thinking signature is stripped.
|
|
197
|
+
//
|
|
198
|
+
// Latest abandoned turns are exempt because Anthropic requires thinking blocks
|
|
199
|
+
// from its most recent response to remain byte-for-byte unmodified.
|
|
190
200
|
const invalidStopReason = assistantMsg.stopReason === "aborted" || assistantMsg.stopReason === "error";
|
|
191
201
|
const abandonedToolUse =
|
|
192
202
|
!invalidStopReason &&
|
|
193
203
|
assistantMsg.stopReason !== "toolUse" &&
|
|
194
204
|
assistantMsg.content.some(b => b.type === "toolCall");
|
|
195
|
-
const
|
|
205
|
+
const lastBlockIndex = assistantMsg.content.length - 1;
|
|
196
206
|
|
|
197
|
-
const transformedContent = assistantMsg.content.flatMap(block => {
|
|
207
|
+
const transformedContent = assistantMsg.content.flatMap((block, blockIndex) => {
|
|
198
208
|
if (block.type === "thinking") {
|
|
199
|
-
//
|
|
209
|
+
// Only an aborted/errored turn's final (mid-stream) block can hold a
|
|
210
|
+
// partial signature; abandoned tool-use turns strip all. Drop the
|
|
211
|
+
// untrustworthy signature so the encoder can downgrade the block to text.
|
|
212
|
+
const signatureUntrustworthy =
|
|
213
|
+
abandonedToolUse || (invalidStopReason && blockIndex === lastBlockIndex);
|
|
200
214
|
const sanitized =
|
|
201
|
-
|
|
215
|
+
signatureUntrustworthy && block.thinkingSignature
|
|
202
216
|
? { ...block, thinkingSignature: undefined }
|
|
203
217
|
: block;
|
|
204
218
|
if (mustPreserveLatestAnthropicThinking) return abandonedToolUse ? block : sanitized;
|
|
@@ -18,6 +18,8 @@ interface AntigravityQuotaInfo {
|
|
|
18
18
|
tier?: string;
|
|
19
19
|
windowId?: string;
|
|
20
20
|
windowLabel?: string;
|
|
21
|
+
apiProvider?: string;
|
|
22
|
+
modelProvider?: string;
|
|
21
23
|
}
|
|
22
24
|
|
|
23
25
|
interface AntigravityModelInfo {
|
|
@@ -25,6 +27,8 @@ interface AntigravityModelInfo {
|
|
|
25
27
|
quotaInfo?: AntigravityQuotaInfo | AntigravityQuotaInfo[];
|
|
26
28
|
quotaInfos?: AntigravityQuotaInfo[];
|
|
27
29
|
quotaInfoByTier?: Record<string, AntigravityQuotaInfo | AntigravityQuotaInfo[]>;
|
|
30
|
+
apiProvider?: string;
|
|
31
|
+
modelProvider?: string;
|
|
28
32
|
}
|
|
29
33
|
|
|
30
34
|
interface AntigravityUsageResponse {
|
|
@@ -60,22 +64,47 @@ function parseWindow(info: AntigravityQuotaInfo): UsageWindow | undefined {
|
|
|
60
64
|
}
|
|
61
65
|
|
|
62
66
|
function buildAmount(info: AntigravityQuotaInfo): UsageAmount {
|
|
63
|
-
const
|
|
67
|
+
const apiRemainingFraction = clampFraction(info.remainingFraction);
|
|
68
|
+
// Observed Antigravity responses omit remainingFraction for exhausted
|
|
69
|
+
// Google/Gemini counters and keep only resetTime. Treat that shape as
|
|
70
|
+
// "blocked until reset" rather than unknown so a healthy sibling backend
|
|
71
|
+
// counter cannot mask it during dedupe.
|
|
72
|
+
const remainingFraction = apiRemainingFraction ?? (info.resetTime ? 0 : undefined);
|
|
64
73
|
const amount: UsageAmount = { unit: "percent" };
|
|
65
74
|
if (remainingFraction === undefined) return amount;
|
|
66
|
-
const usedFraction =
|
|
75
|
+
const usedFraction = 1 - remainingFraction;
|
|
67
76
|
amount.remainingFraction = remainingFraction;
|
|
68
77
|
amount.usedFraction = usedFraction;
|
|
69
78
|
amount.remaining = remainingFraction * 100;
|
|
70
|
-
amount.used = usedFraction
|
|
79
|
+
amount.used = usedFraction * 100;
|
|
71
80
|
amount.limit = 100;
|
|
72
81
|
return amount;
|
|
73
82
|
}
|
|
74
83
|
|
|
84
|
+
function formatCounterName(info: AntigravityQuotaInfo): string | undefined {
|
|
85
|
+
switch (info.modelProvider ?? info.apiProvider) {
|
|
86
|
+
case "MODEL_PROVIDER_ANTHROPIC":
|
|
87
|
+
case "API_PROVIDER_ANTHROPIC_VERTEX":
|
|
88
|
+
return "Anthropic";
|
|
89
|
+
case "MODEL_PROVIDER_GOOGLE":
|
|
90
|
+
case "API_PROVIDER_GOOGLE_GEMINI":
|
|
91
|
+
return "Google";
|
|
92
|
+
case "MODEL_PROVIDER_OPENAI":
|
|
93
|
+
case "API_PROVIDER_OPENAI_VERTEX":
|
|
94
|
+
return "OpenAI";
|
|
95
|
+
default:
|
|
96
|
+
return undefined;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
75
100
|
function normalizeQuotaInfos(info: AntigravityModelInfo): AntigravityQuotaInfo[] {
|
|
76
101
|
const results: AntigravityQuotaInfo[] = [];
|
|
102
|
+
const source = {
|
|
103
|
+
...(info.apiProvider ? { apiProvider: info.apiProvider } : {}),
|
|
104
|
+
...(info.modelProvider ? { modelProvider: info.modelProvider } : {}),
|
|
105
|
+
};
|
|
77
106
|
const addInfo = (value: AntigravityQuotaInfo, tier?: string) => {
|
|
78
|
-
results.push({ ...value, ...(tier ? { tier } : {}) });
|
|
107
|
+
results.push({ ...source, ...value, ...(tier ? { tier } : {}) });
|
|
79
108
|
};
|
|
80
109
|
const addArray = (values?: AntigravityQuotaInfo[]) => {
|
|
81
110
|
if (!values) return;
|
|
@@ -150,11 +179,20 @@ async function fetchAntigravityUsage(params: UsageFetchParams, ctx: UsageFetchCo
|
|
|
150
179
|
const data = (await response.json()) as AntigravityUsageResponse;
|
|
151
180
|
|
|
152
181
|
// The API returns per-model quota entries, but quota is shared across
|
|
153
|
-
// models within the same
|
|
154
|
-
//
|
|
182
|
+
// models within the same backend counter, tier, and reset window. Keep
|
|
183
|
+
// Google and Anthropic-backed Antigravity models separate so a healthy
|
|
184
|
+
// Claude counter cannot mask an exhausted Gemini counter.
|
|
155
185
|
const deduped = new Map<
|
|
156
186
|
string,
|
|
157
|
-
{
|
|
187
|
+
{
|
|
188
|
+
amount: UsageAmount;
|
|
189
|
+
window: UsageWindow | undefined;
|
|
190
|
+
tier: string | undefined;
|
|
191
|
+
tierKey: string;
|
|
192
|
+
windowId: string;
|
|
193
|
+
counterName: string | undefined;
|
|
194
|
+
counterKey: string;
|
|
195
|
+
}
|
|
158
196
|
>();
|
|
159
197
|
let earliestReset: number | undefined;
|
|
160
198
|
|
|
@@ -166,14 +204,16 @@ async function fetchAntigravityUsage(params: UsageFetchParams, ctx: UsageFetchCo
|
|
|
166
204
|
if (window?.resetsAt) {
|
|
167
205
|
earliestReset = earliestReset ? Math.min(earliestReset, window.resetsAt) : window.resetsAt;
|
|
168
206
|
}
|
|
169
|
-
const
|
|
207
|
+
const tierKey = (quotaInfo.tier ?? "default").toLowerCase();
|
|
208
|
+
const counterName = formatCounterName(quotaInfo);
|
|
209
|
+
const counterKey = counterName?.toLowerCase() ?? "default";
|
|
170
210
|
// Use quotaInfo.windowId even when parseWindow returns undefined
|
|
171
211
|
// (no resetTime) — separate windows must not collapse to "default".
|
|
172
212
|
const windowId = quotaInfo.windowId ?? window?.id ?? "default";
|
|
173
|
-
const key = `${
|
|
213
|
+
const key = `${counterKey}|${tierKey}|${windowId}`;
|
|
174
214
|
const existing = deduped.get(key);
|
|
175
215
|
if (!existing) {
|
|
176
|
-
deduped.set(key, { amount, window, tier: quotaInfo.tier });
|
|
216
|
+
deduped.set(key, { amount, window, tier: quotaInfo.tier, tierKey, windowId, counterName, counterKey });
|
|
177
217
|
continue;
|
|
178
218
|
}
|
|
179
219
|
// Merge: keep the entry with fraction data for the bar, but
|
|
@@ -190,7 +230,7 @@ async function fetchAntigravityUsage(params: UsageFetchParams, ctx: UsageFetchCo
|
|
|
190
230
|
if (!eHasFrac && cHasFrac) {
|
|
191
231
|
bestAmount = amount;
|
|
192
232
|
bestTier = quotaInfo.tier ?? existing.tier;
|
|
193
|
-
} else if (
|
|
233
|
+
} else if (eFrac !== undefined && cFrac !== undefined && cFrac < eFrac) {
|
|
194
234
|
bestAmount = amount;
|
|
195
235
|
bestTier = quotaInfo.tier ?? existing.tier;
|
|
196
236
|
}
|
|
@@ -199,23 +239,30 @@ async function fetchAntigravityUsage(params: UsageFetchParams, ctx: UsageFetchCo
|
|
|
199
239
|
if (!bestWindow?.resetsAt && window?.resetsAt) {
|
|
200
240
|
bestWindow = window;
|
|
201
241
|
}
|
|
202
|
-
deduped.set(key, {
|
|
242
|
+
deduped.set(key, {
|
|
243
|
+
amount: bestAmount,
|
|
244
|
+
window: bestWindow,
|
|
245
|
+
tier: bestTier,
|
|
246
|
+
tierKey: existing.tierKey,
|
|
247
|
+
windowId: existing.windowId,
|
|
248
|
+
counterName: existing.counterName,
|
|
249
|
+
counterKey: existing.counterKey,
|
|
250
|
+
});
|
|
203
251
|
}
|
|
204
252
|
}
|
|
205
253
|
|
|
206
254
|
const limits: UsageLimit[] = [];
|
|
207
|
-
for (const
|
|
208
|
-
const
|
|
209
|
-
const label = "Usage";
|
|
255
|
+
for (const entry of deduped.values()) {
|
|
256
|
+
const label = entry.counterName ? `Usage (${entry.counterName})` : "Usage";
|
|
210
257
|
limits.push({
|
|
211
|
-
id: `${params.provider}:${
|
|
258
|
+
id: `${params.provider}:${entry.counterKey}:${entry.tierKey}:${entry.windowId}`,
|
|
212
259
|
label,
|
|
213
260
|
scope: {
|
|
214
261
|
provider: params.provider,
|
|
215
262
|
accountId: credential.accountId,
|
|
216
263
|
projectId: credential.projectId,
|
|
217
264
|
tier: entry.tier,
|
|
218
|
-
windowId,
|
|
265
|
+
windowId: entry.windowId,
|
|
219
266
|
},
|
|
220
267
|
window: entry.window,
|
|
221
268
|
amount: entry.amount,
|