@oh-my-pi/pi-ai 18.3.4 → 18.4.0
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 +35 -48
- package/dist/types/auth/blocks.d.ts +10 -19
- package/dist/types/auth/cascade.d.ts +1 -1
- package/dist/types/auth/rotation.d.ts +6 -3
- package/dist/types/auth/types.d.ts +23 -3
- package/dist/types/auth/usage-cache.d.ts +14 -2
- package/dist/types/auth/usage.d.ts +2 -2
- package/dist/types/auth-broker/client.d.ts +2 -1
- package/dist/types/auth-broker/remote-store.d.ts +5 -6
- package/dist/types/auth-broker/types.d.ts +3 -1
- package/dist/types/auth-broker/wire-schemas.d.ts +2 -1
- package/dist/types/auth-retry.d.ts +22 -13
- package/dist/types/images/types.d.ts +9 -0
- package/dist/types/index.d.ts +1 -0
- package/dist/types/judgment/types.d.ts +3 -2
- package/dist/types/providers/cursor/exec-modern.d.ts +1 -1
- package/dist/types/providers/cursor-pi-args.d.ts +10 -0
- package/dist/types/providers/cursor.d.ts +6 -2
- package/dist/types/providers/google-shared.d.ts +13 -2
- package/dist/types/types.d.ts +0 -10
- package/dist/types/usage/claude-reset.d.ts +6 -0
- package/dist/types/usage/claude.d.ts +6 -0
- package/dist/types/usage.d.ts +2 -0
- package/dist/types/utils/proxy.d.ts +2 -2
- package/package.json +6 -6
- package/src/auth/blocks.ts +79 -37
- package/src/auth/cascade.ts +5 -4
- package/src/auth/resets.ts +104 -40
- package/src/auth/rotation.ts +62 -21
- package/src/auth/select.ts +9 -2
- package/src/auth/types.ts +28 -3
- package/src/auth/usage-cache.ts +45 -2
- package/src/auth/usage.ts +52 -25
- package/src/auth-broker/client.ts +13 -0
- package/src/auth-broker/remote-store.ts +128 -27
- package/src/auth-broker/server.ts +20 -0
- package/src/auth-broker/types.ts +4 -1
- package/src/auth-broker/wire-schemas.ts +7 -0
- package/src/auth-retry.ts +50 -28
- package/src/images/openai-hosted.ts +18 -3
- package/src/images/types.ts +9 -0
- package/src/index.ts +1 -0
- package/src/judgment/types.ts +10 -6
- package/src/judgment/typesafe.ts +2 -2
- package/src/providers/anthropic.ts +54 -123
- package/src/providers/cursor/exec-modern.ts +1 -0
- package/src/providers/cursor/proto/agent.proto +5 -0
- package/src/providers/cursor-pi-args.ts +14 -0
- package/src/providers/cursor.ts +66 -28
- package/src/providers/google-gemini-cli.ts +2 -19
- package/src/providers/google-shared.ts +30 -22
- package/src/providers/ollama.ts +2 -0
- package/src/registry/oauth/oauth.html +206 -185
- package/src/stream.ts +2 -284
- package/src/types.ts +0 -10
- package/src/usage/claude-reset.ts +45 -11
- package/src/usage/claude.ts +79 -49
- package/src/usage.ts +2 -0
- package/src/utils/proxy.ts +3 -3
- package/src/utils/schema/CONSTRAINTS.md +30 -28
- package/src/utils/schema/normalize.ts +1 -1
- package/src/utils/schema/wire.ts +3 -3
package/src/judgment/typesafe.ts
CHANGED
|
@@ -86,8 +86,8 @@ const BACKOFF_MAX_MS = 5_000;
|
|
|
86
86
|
interface SystemOneResponse {
|
|
87
87
|
model: string;
|
|
88
88
|
answers: Record<string, Answer>;
|
|
89
|
-
/** OpenRouter adds the billed `cost` in USD;
|
|
90
|
-
usage: { input_tokens
|
|
89
|
+
/** OpenRouter adds the billed `cost` in USD; some routes omit token counts. */
|
|
90
|
+
usage: { input_tokens?: number; output_tokens?: number; cost?: number };
|
|
91
91
|
}
|
|
92
92
|
|
|
93
93
|
/** Server hint wins (capped); otherwise exponential backoff from {@link BACKOFF_BASE_MS}. */
|
|
@@ -1590,31 +1590,6 @@ export function applyAnthropicUsageExtras(usage: Usage, source: AnthropicUsageLi
|
|
|
1590
1590
|
}
|
|
1591
1591
|
}
|
|
1592
1592
|
|
|
1593
|
-
function parseAnthropicWireUsage(value: unknown): AnthropicWireUsage | undefined {
|
|
1594
|
-
if (!isRecord(value)) return undefined;
|
|
1595
|
-
const cacheCreation = isRecord(value.cache_creation)
|
|
1596
|
-
? {
|
|
1597
|
-
...(typeof value.cache_creation.ephemeral_5m_input_tokens === "number"
|
|
1598
|
-
? { ephemeral_5m_input_tokens: value.cache_creation.ephemeral_5m_input_tokens }
|
|
1599
|
-
: {}),
|
|
1600
|
-
...(typeof value.cache_creation.ephemeral_1h_input_tokens === "number"
|
|
1601
|
-
? { ephemeral_1h_input_tokens: value.cache_creation.ephemeral_1h_input_tokens }
|
|
1602
|
-
: {}),
|
|
1603
|
-
}
|
|
1604
|
-
: undefined;
|
|
1605
|
-
return {
|
|
1606
|
-
...(typeof value.input_tokens === "number" ? { input_tokens: value.input_tokens } : {}),
|
|
1607
|
-
...(typeof value.output_tokens === "number" ? { output_tokens: value.output_tokens } : {}),
|
|
1608
|
-
...(typeof value.cache_read_input_tokens === "number"
|
|
1609
|
-
? { cache_read_input_tokens: value.cache_read_input_tokens }
|
|
1610
|
-
: {}),
|
|
1611
|
-
...(typeof value.cache_creation_input_tokens === "number"
|
|
1612
|
-
? { cache_creation_input_tokens: value.cache_creation_input_tokens }
|
|
1613
|
-
: {}),
|
|
1614
|
-
...(cacheCreation === undefined ? {} : { cache_creation: cacheCreation }),
|
|
1615
|
-
};
|
|
1616
|
-
}
|
|
1617
|
-
|
|
1618
1593
|
function parseAnthropicFallbackWireBlock(value: unknown): AnthropicFallbackContent | undefined {
|
|
1619
1594
|
if (!isRecord(value) || value.type !== "fallback") return undefined;
|
|
1620
1595
|
const from = isRecord(value.from) && typeof value.from.model === "string" ? value.from.model : undefined;
|
|
@@ -1636,6 +1611,28 @@ function isReplayableAnthropicCompaction(
|
|
|
1636
1611
|
return payload?.type === "anthropicCompaction" && payload.provider === model.provider && payload.content.length > 0;
|
|
1637
1612
|
}
|
|
1638
1613
|
|
|
1614
|
+
/** Which persisted compaction summaries a request replays as native blocks. */
|
|
1615
|
+
interface AnthropicCompactionReplay {
|
|
1616
|
+
model: Model<"anthropic-messages">;
|
|
1617
|
+
/** Replay persisted legacy threshold blocks (encrypted content) too. */
|
|
1618
|
+
legacy: boolean;
|
|
1619
|
+
}
|
|
1620
|
+
|
|
1621
|
+
/**
|
|
1622
|
+
* Whether `payload` goes on the wire as a `compaction` block under `replay`:
|
|
1623
|
+
* replayable for the model, and carrying a signature or (when legacy replay is
|
|
1624
|
+
* on) the legacy ciphertext. Anything else is sent as the summary's text.
|
|
1625
|
+
*/
|
|
1626
|
+
function replaysAnthropicCompactionBlock(
|
|
1627
|
+
payload: ProviderPayload | undefined,
|
|
1628
|
+
replay: AnthropicCompactionReplay,
|
|
1629
|
+
): payload is AnthropicCompactionPayload {
|
|
1630
|
+
return (
|
|
1631
|
+
isReplayableAnthropicCompaction(payload, replay.model) &&
|
|
1632
|
+
(payload.signature !== undefined || (replay.legacy && payload.encryptedContent !== undefined))
|
|
1633
|
+
);
|
|
1634
|
+
}
|
|
1635
|
+
|
|
1639
1636
|
/** The wire block for a replayed compaction payload, opaque state included. */
|
|
1640
1637
|
function compactionBlockParam(payload: AnthropicCompactionPayload): CompactionBlockParam {
|
|
1641
1638
|
const { content, signature, encryptedContent } = payload;
|
|
@@ -2071,7 +2068,6 @@ const streamAnthropicOnce = (
|
|
|
2071
2068
|
});
|
|
2072
2069
|
}
|
|
2073
2070
|
|
|
2074
|
-
const zeroOutputCacheRefresh = options?.anthropicCacheRefreshRequest === true;
|
|
2075
2071
|
let client: AnthropicMessagesClientLike;
|
|
2076
2072
|
let isOAuthToken: boolean;
|
|
2077
2073
|
// Retained so a Claude Code version bump can rebuild the client's fingerprint headers.
|
|
@@ -2207,7 +2203,7 @@ const streamAnthropicOnce = (
|
|
|
2207
2203
|
model,
|
|
2208
2204
|
apiKey,
|
|
2209
2205
|
extraBetas,
|
|
2210
|
-
stream:
|
|
2206
|
+
stream: true,
|
|
2211
2207
|
interleavedThinking: options?.interleavedThinking ?? true,
|
|
2212
2208
|
headers: options?.headers,
|
|
2213
2209
|
dynamicHeaders: copilotDynamicHeaders?.headers,
|
|
@@ -2321,89 +2317,6 @@ const streamAnthropicOnce = (
|
|
|
2321
2317
|
const requestTimeoutMs =
|
|
2322
2318
|
firstEventTimeoutMs !== undefined && firstEventTimeoutMs > 0 ? firstEventTimeoutMs : undefined;
|
|
2323
2319
|
|
|
2324
|
-
if (zeroOutputCacheRefresh) {
|
|
2325
|
-
const refreshParams: MessageCreateParams = { ...params, max_tokens: 0, stream: false };
|
|
2326
|
-
// Anthropic rejects `tool_choice: {type:"tool"|"any"}` with `max_tokens: 0`
|
|
2327
|
-
// ("tool_choice ... cannot be used when max_tokens is 0", #12597). A refresh
|
|
2328
|
-
// replays the captured turn's payload, which can carry a forced selector
|
|
2329
|
-
// (e.g. a forced yield). A zero-output keep-alive produces no tokens, so the
|
|
2330
|
-
// forced choice is meaningless here — drop it so the request is accepted.
|
|
2331
|
-
const refreshChoiceType = refreshParams.tool_choice?.type;
|
|
2332
|
-
if (refreshChoiceType === "tool" || refreshChoiceType === "any") {
|
|
2333
|
-
delete refreshParams.tool_choice;
|
|
2334
|
-
}
|
|
2335
|
-
rawRequestDump = {
|
|
2336
|
-
provider: model.provider,
|
|
2337
|
-
api: output.api,
|
|
2338
|
-
model: model.id,
|
|
2339
|
-
method: "POST",
|
|
2340
|
-
url: `${baseUrl}/v1/messages${isOAuthToken ? "?beta=true" : ""}`,
|
|
2341
|
-
body: refreshParams,
|
|
2342
|
-
};
|
|
2343
|
-
const { requestSignal } = activeAbortTracker;
|
|
2344
|
-
// A replayed compaction block needs the beta on injected clients too.
|
|
2345
|
-
// Route by the client's own endpoint when it exposes one.
|
|
2346
|
-
const refreshBetaRouteUrl =
|
|
2347
|
-
options?.client !== undefined ? (injectedClientBaseUrl(options.client) ?? baseUrl) : baseUrl;
|
|
2348
|
-
let refreshHeaders: Record<string, string> | undefined;
|
|
2349
|
-
if (options?.client !== undefined && !isVertexRawPredictUrl(refreshBetaRouteUrl)) {
|
|
2350
|
-
if (carriesSignedCompaction(refreshParams)) {
|
|
2351
|
-
refreshHeaders = mergeAnthropicBetaHeader(refreshHeaders ?? mergedCallerHeaders, COMPACTION_BETA);
|
|
2352
|
-
}
|
|
2353
|
-
if (carriesLegacyCompactionEdit(refreshParams)) {
|
|
2354
|
-
refreshHeaders = mergeAnthropicBetaHeader(
|
|
2355
|
-
refreshHeaders ?? mergedCallerHeaders,
|
|
2356
|
-
LEGACY_COMPACTION_BETA,
|
|
2357
|
-
);
|
|
2358
|
-
}
|
|
2359
|
-
}
|
|
2360
|
-
const requestOptions = {
|
|
2361
|
-
...createSdkStreamRequestOptions(requestSignal, requestTimeoutMs),
|
|
2362
|
-
maxRetries: 0,
|
|
2363
|
-
...(refreshHeaders ? { headers: refreshHeaders } : {}),
|
|
2364
|
-
};
|
|
2365
|
-
const request: unknown =
|
|
2366
|
-
isOAuthToken && client.beta
|
|
2367
|
-
? client.beta.messages.create(refreshParams, requestOptions)
|
|
2368
|
-
: client.messages.create(refreshParams, requestOptions);
|
|
2369
|
-
if (!hasAnthropicRawResponseRequest(request)) {
|
|
2370
|
-
throw new AIError.AnthropicStreamEnvelopeError(
|
|
2371
|
-
"Anthropic cache refresh request did not expose a raw response",
|
|
2372
|
-
);
|
|
2373
|
-
}
|
|
2374
|
-
const response = await request.asResponse();
|
|
2375
|
-
await notifyProviderResponse(options, response, model, response.headers.get("request-id"));
|
|
2376
|
-
const body: unknown = await response.json();
|
|
2377
|
-
if (!isRecord(body)) {
|
|
2378
|
-
throw new AIError.AnthropicStreamEnvelopeError("Anthropic cache refresh returned a malformed response");
|
|
2379
|
-
}
|
|
2380
|
-
const wireUsage = parseAnthropicWireUsage(body.usage);
|
|
2381
|
-
if (!wireUsage) {
|
|
2382
|
-
throw new AIError.AnthropicStreamEnvelopeError("Anthropic cache refresh response omitted usage");
|
|
2383
|
-
}
|
|
2384
|
-
if (typeof body.id === "string") output.responseId = body.id;
|
|
2385
|
-
applyReportedInputTransformations(
|
|
2386
|
-
output,
|
|
2387
|
-
params,
|
|
2388
|
-
providerSessionState,
|
|
2389
|
-
body.input_transformations,
|
|
2390
|
-
seenInputTransformations,
|
|
2391
|
-
);
|
|
2392
|
-
output.usage.input = wireUsage.input_tokens ?? 0;
|
|
2393
|
-
output.usage.output = wireUsage.output_tokens ?? 0;
|
|
2394
|
-
output.usage.cacheRead = wireUsage.cache_read_input_tokens ?? 0;
|
|
2395
|
-
output.usage.cacheWrite = wireUsage.cache_creation_input_tokens ?? 0;
|
|
2396
|
-
applyAnthropicUsageExtras(output.usage, wireUsage);
|
|
2397
|
-
output.usage.totalTokens =
|
|
2398
|
-
output.usage.input + output.usage.output + output.usage.cacheRead + output.usage.cacheWrite;
|
|
2399
|
-
calculateCost(model, output.usage, output.timestamp);
|
|
2400
|
-
output.duration = performance.now() - startTime;
|
|
2401
|
-
stream.push({ type: "start", partial: output });
|
|
2402
|
-
stream.push({ type: "done", reason: "stop", message: output });
|
|
2403
|
-
stream.end();
|
|
2404
|
-
return;
|
|
2405
|
-
}
|
|
2406
|
-
|
|
2407
2320
|
// Opt-in flag: the response parser only honors `fallback` content
|
|
2408
2321
|
// blocks and `usage.iterations` when the current request opted into
|
|
2409
2322
|
// server-side-fallback beta chain. Leaving `fallbacks` unset preserves
|
|
@@ -4302,12 +4215,26 @@ function collectAnthropicControlRecords(messages: readonly Message[]): Anthropic
|
|
|
4302
4215
|
* lands between a `tool_use` and its `tool_result`, where `transformMessages`
|
|
4303
4216
|
* would flush synthetic aborted results; a mid-turn change therefore takes
|
|
4304
4217
|
* effect from the next step.
|
|
4218
|
+
*
|
|
4219
|
+
* A summary replayed as a `compaction` block is never a slot: the block must
|
|
4220
|
+
* open the request, so nothing may precede it. A response opened by the block
|
|
4221
|
+
* (no real user turn between them) takes the change from its next step. A
|
|
4222
|
+
* summary sent as text is an ordinary user turn.
|
|
4305
4223
|
*/
|
|
4306
|
-
function anthropicEffortInsertIndex(
|
|
4224
|
+
function anthropicEffortInsertIndex(
|
|
4225
|
+
messages: readonly Message[],
|
|
4226
|
+
end: number,
|
|
4227
|
+
compactionReplay: AnthropicCompactionReplay | undefined,
|
|
4228
|
+
): number {
|
|
4307
4229
|
for (let i = end - 1; i >= 0; i--) {
|
|
4308
|
-
const
|
|
4309
|
-
if (role === "
|
|
4310
|
-
if (role
|
|
4230
|
+
const message = messages[i];
|
|
4231
|
+
if (message?.role === "assistant") return end;
|
|
4232
|
+
if (message?.role !== "user") continue;
|
|
4233
|
+
if (!compactionReplay || !replaysAnthropicCompactionBlock(message.providerPayload, compactionReplay)) return i;
|
|
4234
|
+
let next = end;
|
|
4235
|
+
if (messages[next]?.role === "assistant") next++;
|
|
4236
|
+
while (messages[next]?.role === "toolResult") next++;
|
|
4237
|
+
return next;
|
|
4311
4238
|
}
|
|
4312
4239
|
return end;
|
|
4313
4240
|
}
|
|
@@ -4420,6 +4347,7 @@ function planAnthropicEffortControls(
|
|
|
4420
4347
|
messages: readonly Message[],
|
|
4421
4348
|
records: readonly AnthropicControlRecord[],
|
|
4422
4349
|
enabled: boolean,
|
|
4350
|
+
compactionReplay: AnthropicCompactionReplay | undefined,
|
|
4423
4351
|
): {
|
|
4424
4352
|
topLevel: AnthropicOutputEffort | undefined;
|
|
4425
4353
|
inserts: AnthropicControlInsert[];
|
|
@@ -4444,7 +4372,7 @@ function planAnthropicEffortControls(
|
|
|
4444
4372
|
const recorded = record.effort.tail;
|
|
4445
4373
|
if (recorded !== null && recorded !== tail) {
|
|
4446
4374
|
inserts.push({
|
|
4447
|
-
index: anthropicEffortInsertIndex(messages, record.index),
|
|
4375
|
+
index: anthropicEffortInsertIndex(messages, record.index, compactionReplay),
|
|
4448
4376
|
spec: { toolChanges: [], effort: recorded },
|
|
4449
4377
|
});
|
|
4450
4378
|
}
|
|
@@ -4452,7 +4380,7 @@ function planAnthropicEffortControls(
|
|
|
4452
4380
|
}
|
|
4453
4381
|
if (current !== undefined && current !== tail) {
|
|
4454
4382
|
inserts.push({
|
|
4455
|
-
index: anthropicEffortInsertIndex(messages, messages.length),
|
|
4383
|
+
index: anthropicEffortInsertIndex(messages, messages.length, compactionReplay),
|
|
4456
4384
|
spec: { toolChanges: [], effort: current },
|
|
4457
4385
|
});
|
|
4458
4386
|
tail = current;
|
|
@@ -4696,11 +4624,15 @@ function buildParams(
|
|
|
4696
4624
|
// the `effort-2025-11-24` beta, which that adapter can only accept in the body
|
|
4697
4625
|
// (`anthropic_beta`), never as the `anthropic-beta` HTTP header this path sets
|
|
4698
4626
|
// — so the field is dropped alongside the beta to avoid a 400 (#5614).
|
|
4627
|
+
const compactionReplay: AnthropicCompactionReplay | undefined = compactionSupported
|
|
4628
|
+
? { model: effectiveModel, legacy: !compactionRequest && !signedReplay }
|
|
4629
|
+
: undefined;
|
|
4699
4630
|
const effortPlan = planAnthropicEffortControls(
|
|
4700
4631
|
outputConfigEffort,
|
|
4701
4632
|
context.messages,
|
|
4702
4633
|
records,
|
|
4703
4634
|
model.compat.supportsPerMessageEffort === true,
|
|
4635
|
+
compactionReplay,
|
|
4704
4636
|
);
|
|
4705
4637
|
const wireMessages = convertAnthropicMessages(
|
|
4706
4638
|
insertAnthropicControlMarkers(context.messages, [...toolPlan.inserts, ...effortPlan.inserts]),
|
|
@@ -4709,7 +4641,7 @@ function buildParams(
|
|
|
4709
4641
|
{
|
|
4710
4642
|
serverSideFallbackEnabled: !!fallbacks?.length,
|
|
4711
4643
|
replayCompaction: compactionSupported,
|
|
4712
|
-
replayLegacyCompaction:
|
|
4644
|
+
replayLegacyCompaction: compactionReplay?.legacy,
|
|
4713
4645
|
dropAllThinking,
|
|
4714
4646
|
droppedThinkingBlocks,
|
|
4715
4647
|
credentialId: options?.credentialId,
|
|
@@ -5027,9 +4959,7 @@ export function convertAnthropicMessages(
|
|
|
5027
4959
|
if (
|
|
5028
4960
|
opts?.replayCompaction &&
|
|
5029
4961
|
(msg.role === "user" || msg.role === "developer") &&
|
|
5030
|
-
|
|
5031
|
-
(msg.providerPayload.signature !== undefined ||
|
|
5032
|
-
(opts.replayLegacyCompaction !== false && msg.providerPayload.encryptedContent !== undefined))
|
|
4962
|
+
replaysAnthropicCompactionBlock(msg.providerPayload, { model, legacy: opts.replayLegacyCompaction !== false })
|
|
5033
4963
|
) {
|
|
5034
4964
|
const compactionParam: AnthropicMessageParam = {
|
|
5035
4965
|
role: "assistant",
|
|
@@ -5113,9 +5043,10 @@ export function convertAnthropicMessages(
|
|
|
5113
5043
|
// replayed turn.
|
|
5114
5044
|
if (
|
|
5115
5045
|
opts?.replayCompaction &&
|
|
5116
|
-
|
|
5117
|
-
|
|
5118
|
-
|
|
5046
|
+
replaysAnthropicCompactionBlock(msg.providerPayload, {
|
|
5047
|
+
model,
|
|
5048
|
+
legacy: opts.replayLegacyCompaction !== false,
|
|
5049
|
+
})
|
|
5119
5050
|
) {
|
|
5120
5051
|
blocks.push(compactionBlockParam(msg.providerPayload));
|
|
5121
5052
|
}
|
|
@@ -836,6 +836,11 @@ message ShellOutputDeltaUpdate {
|
|
|
836
836
|
}
|
|
837
837
|
|
|
838
838
|
message TurnEndedUpdate {
|
|
839
|
+
optional int64 input_tokens = 1;
|
|
840
|
+
optional int64 output_tokens = 2;
|
|
841
|
+
optional int64 cache_read_tokens = 3;
|
|
842
|
+
optional int64 cache_write_tokens = 4;
|
|
843
|
+
optional int64 reasoning_tokens = 5;
|
|
839
844
|
}
|
|
840
845
|
|
|
841
846
|
// Only: user message appended update
|
|
@@ -200,6 +200,20 @@ export function piTimeout(timeout: number | undefined): number | undefined {
|
|
|
200
200
|
return timeout !== undefined && timeout >= 0 ? timeout : undefined;
|
|
201
201
|
}
|
|
202
202
|
|
|
203
|
+
/**
|
|
204
|
+
* Convert a legacy `ShellArgs`/`ShellStreamArgs` timeout into bash-tool seconds.
|
|
205
|
+
*
|
|
206
|
+
* Cursor states that budget in milliseconds — its own `ShellTimeout` result
|
|
207
|
+
* echoes it as `timeout_ms`, and `hard_timeout` documents the same unit — while
|
|
208
|
+
* the bash tool takes seconds and rejects anything past 3600, so forwarding the
|
|
209
|
+
* raw value turned a model-requested 15 s into `requested 15000s`. Sub-second
|
|
210
|
+
* budgets round up: 0 seconds means "no deadline" to the bash tool.
|
|
211
|
+
*/
|
|
212
|
+
export function shellTimeoutSeconds(timeoutMs: number | undefined): number | undefined {
|
|
213
|
+
if (!timeoutMs || timeoutMs <= 0) return undefined;
|
|
214
|
+
return Math.max(1, Math.round(timeoutMs / 1000));
|
|
215
|
+
}
|
|
216
|
+
|
|
203
217
|
/**
|
|
204
218
|
* Drop keys whose value is `undefined` so optional local-tool kwargs stay
|
|
205
219
|
* absent rather than present-as-undefined.
|
package/src/providers/cursor.ts
CHANGED
|
@@ -139,6 +139,7 @@ import {
|
|
|
139
139
|
SubagentResultSchema,
|
|
140
140
|
ThinkingMessageSchema,
|
|
141
141
|
ToolCallSchema,
|
|
142
|
+
type TurnEndedUpdate,
|
|
142
143
|
UserMessageActionSchema,
|
|
143
144
|
UserMessageSchema,
|
|
144
145
|
WebFetchAllowlistPrecheckResultSchema,
|
|
@@ -193,6 +194,7 @@ import type {
|
|
|
193
194
|
Tool,
|
|
194
195
|
ToolCall,
|
|
195
196
|
ToolResultMessage,
|
|
197
|
+
Usage,
|
|
196
198
|
} from "../types";
|
|
197
199
|
import { normalizeSystemPrompts, normalizeToolCallId } from "../utils";
|
|
198
200
|
import {
|
|
@@ -241,6 +243,7 @@ import {
|
|
|
241
243
|
piReadDisplayPath,
|
|
242
244
|
piReadPathHasRange,
|
|
243
245
|
piTimeout,
|
|
246
|
+
shellTimeoutSeconds,
|
|
244
247
|
} from "./cursor/exec-modern";
|
|
245
248
|
import { handleInteractionQuery } from "./cursor/interaction-query";
|
|
246
249
|
|
|
@@ -1199,7 +1202,7 @@ export async function handleServerMessage(
|
|
|
1199
1202
|
// permission prompt).
|
|
1200
1203
|
handleInteractionQuery(msg.message.value, h2Request);
|
|
1201
1204
|
} else if (msgCase === "conversationCheckpointUpdate") {
|
|
1202
|
-
handleConversationCheckpointUpdate(msg.message.value, output,
|
|
1205
|
+
handleConversationCheckpointUpdate(msg.message.value, output, onConversationCheckpoint);
|
|
1203
1206
|
}
|
|
1204
1207
|
}
|
|
1205
1208
|
|
|
@@ -1804,12 +1807,11 @@ async function handleExecServerMessage(
|
|
|
1804
1807
|
if (!args.toolCallId) args.toolCallId = crypto.randomUUID();
|
|
1805
1808
|
const normalizedArgs: ShellArgs = { ...args, workingDirectory: args.workingDirectory || process.cwd() };
|
|
1806
1809
|
// Match the bridge (`CursorExecHandlers.shell`): map `workingDirectory`
|
|
1807
|
-
// → `cwd`,
|
|
1808
|
-
const shellTimeout = args.timeout && args.timeout > 0 ? args.timeout : undefined;
|
|
1810
|
+
// → `cwd`, convert the millisecond budget to bash-tool seconds.
|
|
1809
1811
|
synthesizeCursorExecToolCall(output, stream, state, args.toolCallId, "bash", {
|
|
1810
1812
|
command: args.command,
|
|
1811
1813
|
cwd: args.workingDirectory || undefined,
|
|
1812
|
-
timeout:
|
|
1814
|
+
timeout: shellTimeoutSeconds(args.timeout),
|
|
1813
1815
|
});
|
|
1814
1816
|
const { execResult } = await resolveExecHandler(
|
|
1815
1817
|
args,
|
|
@@ -1827,11 +1829,10 @@ async function handleExecServerMessage(
|
|
|
1827
1829
|
case "shellStreamArgs": {
|
|
1828
1830
|
const args = execMsg.message.value;
|
|
1829
1831
|
if (!args.toolCallId) args.toolCallId = crypto.randomUUID();
|
|
1830
|
-
const shellStreamTimeout = args.timeout && args.timeout > 0 ? args.timeout : undefined;
|
|
1831
1832
|
synthesizeCursorExecToolCall(output, stream, state, args.toolCallId, "bash", {
|
|
1832
1833
|
command: args.command,
|
|
1833
1834
|
cwd: args.workingDirectory || undefined,
|
|
1834
|
-
timeout:
|
|
1835
|
+
timeout: shellTimeoutSeconds(args.timeout),
|
|
1835
1836
|
});
|
|
1836
1837
|
await handleShellStreamArgs(args, execMsg, h2Request, execHandlers, onToolResult);
|
|
1837
1838
|
return;
|
|
@@ -1932,12 +1933,21 @@ async function handleExecServerMessage(
|
|
|
1932
1933
|
);
|
|
1933
1934
|
return;
|
|
1934
1935
|
}
|
|
1935
|
-
|
|
1936
|
+
// Without a local MCP handler an external executor — an auth-gateway
|
|
1937
|
+
// client whose own tools Cursor sees as MCP tools — is the one that
|
|
1938
|
+
// runs the call, so the block must reach that client unresolved:
|
|
1939
|
+
// `isClientToolUse` (anthropic-messages-server) and the OpenAI chat
|
|
1940
|
+
// finish-reason mapper both report a handoff only for a toolCall that
|
|
1941
|
+
// carries no `kCursorExecResolved` marker. Answering the frame while
|
|
1942
|
+
// emitting nothing made the turn look like plain text that ended on
|
|
1943
|
+
// `stop`, so the client never saw the call it was meant to execute.
|
|
1944
|
+
const externalHandoff = externalToolExecutor && !execHandlers?.mcp;
|
|
1945
|
+
if (execHandlers?.mcp || externalHandoff) {
|
|
1936
1946
|
const existingBlock = output.content.find(
|
|
1937
1947
|
block => block.type === "toolCall" && block.id === mcpCall.toolCallId,
|
|
1938
1948
|
);
|
|
1939
1949
|
if (existingBlock) {
|
|
1940
|
-
markCursorExecResolved(existingBlock);
|
|
1950
|
+
if (!externalHandoff) markCursorExecResolved(existingBlock);
|
|
1941
1951
|
} else {
|
|
1942
1952
|
synthesizeCursorExecToolCall(
|
|
1943
1953
|
output,
|
|
@@ -1946,8 +1956,9 @@ async function handleExecServerMessage(
|
|
|
1946
1956
|
mcpCall.toolCallId,
|
|
1947
1957
|
mcpCall.toolName || mcpCall.name,
|
|
1948
1958
|
mcpCall.args,
|
|
1959
|
+
{ executed: !externalHandoff },
|
|
1949
1960
|
);
|
|
1950
|
-
state.resolvedMcpToolCallIds.add(mcpCall.toolCallId);
|
|
1961
|
+
if (!externalHandoff) state.resolvedMcpToolCallIds.add(mcpCall.toolCallId);
|
|
1951
1962
|
}
|
|
1952
1963
|
}
|
|
1953
1964
|
const { execResult } = await resolveExecHandler(
|
|
@@ -1955,10 +1966,7 @@ async function handleExecServerMessage(
|
|
|
1955
1966
|
execHandlers?.mcp?.bind(execHandlers),
|
|
1956
1967
|
onToolResult,
|
|
1957
1968
|
toolResult => buildMcpResultFromToolResult(mcpCall, toolResult),
|
|
1958
|
-
_reason =>
|
|
1959
|
-
externalToolExecutor && !execHandlers?.mcp
|
|
1960
|
-
? buildMcpExternalHandoffResult()
|
|
1961
|
-
: buildMcpToolNotFoundResult(mcpCall),
|
|
1969
|
+
_reason => (externalHandoff ? buildMcpExternalHandoffResult() : buildMcpToolNotFoundResult(mcpCall)),
|
|
1962
1970
|
error => buildMcpErrorResult(error),
|
|
1963
1971
|
execHandlers?.mcp ? { toolCallId: mcpCall.toolCallId, toolName: mcpCall.toolName } : null,
|
|
1964
1972
|
);
|
|
@@ -2302,7 +2310,7 @@ async function handleExecServerMessage(
|
|
|
2302
2310
|
synthesizeCursorExecToolCall(output, stream, state, args.toolCallId, "bash", {
|
|
2303
2311
|
command: args.command,
|
|
2304
2312
|
cwd: args.workingDirectory || undefined,
|
|
2305
|
-
timeout: args.timeout
|
|
2313
|
+
timeout: shellTimeoutSeconds(args.timeout),
|
|
2306
2314
|
});
|
|
2307
2315
|
const { execResult } = await resolveExecHandler(
|
|
2308
2316
|
normalizedArgs,
|
|
@@ -4135,7 +4143,9 @@ function endCurrentThinkingBlock(
|
|
|
4135
4143
|
* `agent-loop.ts` execution pass skips it — Cursor's server-driven exec
|
|
4136
4144
|
* channel already ran the tool via the bridge and buffered the result, so
|
|
4137
4145
|
* treating this block as runnable would re-execute the same side-effecting
|
|
4138
|
-
* tool a second time.
|
|
4146
|
+
* tool a second time. Pass `{ executed: false }` for a frame nothing ran: a
|
|
4147
|
+
* call handed to an external executor is only a handoff while the marker is
|
|
4148
|
+
* absent, and the consumer that runs it needs a runnable block.
|
|
4139
4149
|
*
|
|
4140
4150
|
* Exported for tests to exercise ordering with adjacent text/thinking blocks.
|
|
4141
4151
|
*/
|
|
@@ -4146,6 +4156,7 @@ export function synthesizeCursorExecToolCall(
|
|
|
4146
4156
|
toolCallId: string,
|
|
4147
4157
|
toolName: string,
|
|
4148
4158
|
args: Record<string, unknown>,
|
|
4159
|
+
{ executed = true }: { executed?: boolean } = {},
|
|
4149
4160
|
): void {
|
|
4150
4161
|
endCurrentTextBlock(output, stream, state);
|
|
4151
4162
|
endCurrentThinkingBlock(output, stream, state);
|
|
@@ -4159,7 +4170,7 @@ export function synthesizeCursorExecToolCall(
|
|
|
4159
4170
|
arguments: omitUndefinedArgs(args),
|
|
4160
4171
|
[kStreamingBlockIndex]: output.content.length,
|
|
4161
4172
|
[kStreamingBlockKind]: "cursor-exec",
|
|
4162
|
-
[kCursorExecResolved]: true,
|
|
4173
|
+
...(executed ? { [kCursorExecResolved]: true as const } : {}),
|
|
4163
4174
|
};
|
|
4164
4175
|
output.content.push(block);
|
|
4165
4176
|
const idx = output.content.length - 1;
|
|
@@ -4295,8 +4306,12 @@ export function processInteractionUpdate(
|
|
|
4295
4306
|
if (mcpCall) {
|
|
4296
4307
|
const args = mcpCall.args || {};
|
|
4297
4308
|
const id = args.toolCallId || crypto.randomUUID();
|
|
4298
|
-
|
|
4299
|
-
|
|
4309
|
+
state.resolvedMcpToolCallIds.delete(id);
|
|
4310
|
+
// The exec channel may have emitted this block first — executed
|
|
4311
|
+
// (marked resolved) or handed to an external executor (deliberately
|
|
4312
|
+
// unmarked). Either way the call is already in the transcript, so a
|
|
4313
|
+
// second block for the same id would duplicate it.
|
|
4314
|
+
if (output.content.some(block => block.type === "toolCall" && block.id === id)) {
|
|
4300
4315
|
return;
|
|
4301
4316
|
}
|
|
4302
4317
|
const block: ToolCallState = {
|
|
@@ -4311,9 +4326,6 @@ export function processInteractionUpdate(
|
|
|
4311
4326
|
[kStreamingBlockKind]: "mcp",
|
|
4312
4327
|
[kStreamingEnvelopeId]: update.message.value.callId || undefined,
|
|
4313
4328
|
};
|
|
4314
|
-
if (resolvedByExec) {
|
|
4315
|
-
markCursorExecResolved(block);
|
|
4316
|
-
}
|
|
4317
4329
|
output.content.push(block);
|
|
4318
4330
|
retainStreamedCall(state, block, update.message.value.callId);
|
|
4319
4331
|
stream.push({ type: "toolcall_start", contentIndex: output.content.length - 1, partial: output });
|
|
@@ -4549,6 +4561,7 @@ export function processInteractionUpdate(
|
|
|
4549
4561
|
}
|
|
4550
4562
|
} else if (updateCase === "turnEnded") {
|
|
4551
4563
|
output.stopReason = "stop";
|
|
4564
|
+
applyTurnEndedUsage(output.usage, update.message.value);
|
|
4552
4565
|
if (
|
|
4553
4566
|
classifyModel("cursor", output.model).family === "k3" &&
|
|
4554
4567
|
!output.content.some(item => item.type === "thinking" && item.thinking.length > 0)
|
|
@@ -4566,23 +4579,48 @@ export function processInteractionUpdate(
|
|
|
4566
4579
|
}
|
|
4567
4580
|
}
|
|
4568
4581
|
|
|
4582
|
+
/**
|
|
4583
|
+
* Adopt the authoritative per-turn counters Cursor sends on `TurnEndedUpdate`.
|
|
4584
|
+
*
|
|
4585
|
+
* `tokenDelta` frames carry a running output estimate only — a captured turn
|
|
4586
|
+
* summed 22 against a final 36 — and never report input, cache, or reasoning
|
|
4587
|
+
* tokens, so every bucket the final frame reports replaces the streamed
|
|
4588
|
+
* estimate. Unreported counters decode as `undefined`; a frame that reports
|
|
4589
|
+
* nothing at all leaves the streamed totals untouched.
|
|
4590
|
+
*/
|
|
4591
|
+
function applyTurnEndedUsage(usage: Usage, update: TurnEndedUpdate): void {
|
|
4592
|
+
const input = Number(update.inputTokens ?? 0n);
|
|
4593
|
+
const output = Number(update.outputTokens ?? 0n);
|
|
4594
|
+
const cacheRead = Number(update.cacheReadTokens ?? 0n);
|
|
4595
|
+
const cacheWrite = Number(update.cacheWriteTokens ?? 0n);
|
|
4596
|
+
const reasoning = Number(update.reasoningTokens ?? 0n);
|
|
4597
|
+
if (input <= 0 && output <= 0 && cacheRead <= 0 && cacheWrite <= 0) return;
|
|
4598
|
+
if (input > 0) usage.input = input;
|
|
4599
|
+
if (output > 0) usage.output = output;
|
|
4600
|
+
if (cacheRead > 0) usage.cacheRead = cacheRead;
|
|
4601
|
+
if (cacheWrite > 0) usage.cacheWrite = cacheWrite;
|
|
4602
|
+
if (reasoning > 0) usage.reasoningTokens = reasoning;
|
|
4603
|
+
usage.totalTokens = usage.input + usage.output + usage.cacheRead + usage.cacheWrite;
|
|
4604
|
+
}
|
|
4605
|
+
|
|
4606
|
+
/**
|
|
4607
|
+
* Record the context occupancy Cursor reports on every conversation checkpoint.
|
|
4608
|
+
*
|
|
4609
|
+
* `tokenDetails.usedTokens` counts the whole conversation, not this turn's
|
|
4610
|
+
* output, so it is independent of the `tokenDelta` estimate: compaction and
|
|
4611
|
+
* handoff read `usage.contextTokens` and otherwise see an output-only turn.
|
|
4612
|
+
*/
|
|
4569
4613
|
function handleConversationCheckpointUpdate(
|
|
4570
4614
|
checkpoint: ConversationStateStructure,
|
|
4571
4615
|
output: AssistantMessage,
|
|
4572
|
-
usageState: UsageState,
|
|
4573
4616
|
onConversationCheckpoint?: (checkpoint: ConversationStateStructure) => void,
|
|
4574
4617
|
): void {
|
|
4575
4618
|
onConversationCheckpoint?.(checkpoint);
|
|
4576
|
-
if (usageState.sawTokenDelta) {
|
|
4577
|
-
return;
|
|
4578
|
-
}
|
|
4579
4619
|
const usedTokens = checkpoint.tokenDetails?.usedTokens ?? 0;
|
|
4580
4620
|
if (usedTokens <= 0) {
|
|
4581
4621
|
return;
|
|
4582
4622
|
}
|
|
4583
|
-
|
|
4584
|
-
output.usage.contextTokens = usedTokens;
|
|
4585
|
-
}
|
|
4623
|
+
output.usage.contextTokens = usedTokens;
|
|
4586
4624
|
}
|
|
4587
4625
|
|
|
4588
4626
|
function createBlobId(data: Uint8Array): Uint8Array {
|
|
@@ -45,6 +45,7 @@ import {
|
|
|
45
45
|
hasMeaningfulGoogleContent,
|
|
46
46
|
isThinkingPart,
|
|
47
47
|
MAX_EMPTY_STREAM_RETRIES,
|
|
48
|
+
mapGoogleUsage,
|
|
48
49
|
mapStopReasonString,
|
|
49
50
|
mapToolChoice,
|
|
50
51
|
nextToolCallId,
|
|
@@ -873,25 +874,7 @@ export const streamGoogleGeminiCli: StreamFunction<"google-gemini-cli"> = (
|
|
|
873
874
|
}
|
|
874
875
|
|
|
875
876
|
if (responseData.usageMetadata) {
|
|
876
|
-
|
|
877
|
-
const promptTokens = responseData.usageMetadata.promptTokenCount || 0;
|
|
878
|
-
const cacheReadTokens = responseData.usageMetadata.cachedContentTokenCount || 0;
|
|
879
|
-
const thinkingTokens = responseData.usageMetadata.thoughtsTokenCount || 0;
|
|
880
|
-
output.usage = {
|
|
881
|
-
input: promptTokens - cacheReadTokens,
|
|
882
|
-
output: (responseData.usageMetadata.candidatesTokenCount || 0) + thinkingTokens,
|
|
883
|
-
cacheRead: cacheReadTokens,
|
|
884
|
-
cacheWrite: 0,
|
|
885
|
-
totalTokens: responseData.usageMetadata.totalTokenCount || 0,
|
|
886
|
-
...(thinkingTokens > 0 ? { reasoningTokens: thinkingTokens } : {}),
|
|
887
|
-
cost: {
|
|
888
|
-
input: 0,
|
|
889
|
-
output: 0,
|
|
890
|
-
cacheRead: 0,
|
|
891
|
-
cacheWrite: 0,
|
|
892
|
-
total: 0,
|
|
893
|
-
},
|
|
894
|
-
};
|
|
877
|
+
output.usage = mapGoogleUsage(responseData.usageMetadata);
|
|
895
878
|
calculateCost(model, output.usage, output.timestamp);
|
|
896
879
|
}
|
|
897
880
|
}
|
|
@@ -22,6 +22,7 @@ import type {
|
|
|
22
22
|
ThinkingContent,
|
|
23
23
|
Tool,
|
|
24
24
|
ToolCall,
|
|
25
|
+
Usage,
|
|
25
26
|
} from "../types";
|
|
26
27
|
import { shouldSendServiceTier } from "../types";
|
|
27
28
|
import { normalizeSystemPrompts } from "../utils";
|
|
@@ -38,6 +39,7 @@ import type {
|
|
|
38
39
|
Part,
|
|
39
40
|
ThinkingConfig,
|
|
40
41
|
ThinkingLevel,
|
|
42
|
+
UsageMetadata,
|
|
41
43
|
} from "./google-types";
|
|
42
44
|
import { transformMessages } from "./transform-messages";
|
|
43
45
|
import { NON_VISION_IMAGE_PLACEHOLDER } from "./vision-guard";
|
|
@@ -473,6 +475,33 @@ function resetGoogleStreamOutputForRetry(output: AssistantMessage): void {
|
|
|
473
475
|
output.timestamp = Date.now();
|
|
474
476
|
}
|
|
475
477
|
|
|
478
|
+
/**
|
|
479
|
+
* Maps Gemini `usageMetadata` onto {@link Usage} (cost zeroed for `calculateCost`).
|
|
480
|
+
*
|
|
481
|
+
* `promptTokenCount` includes `cachedContentTokenCount`, so input = prompt − cached,
|
|
482
|
+
* matching the OpenAI convention where input + cacheRead = total prompt tokens.
|
|
483
|
+
* Upstream sometimes omits `promptTokenCount` or reports a cache count above the
|
|
484
|
+
* prompt (Antigravity), so the prompt falls back to `total − candidates − thoughts`
|
|
485
|
+
* and cached tokens are clamped to it: input is never negative.
|
|
486
|
+
* Ref: https://ai.google.dev/api/generate-content#v1beta.GenerateContentResponse.UsageMetadata
|
|
487
|
+
*/
|
|
488
|
+
export function mapGoogleUsage(metadata: UsageMetadata): Usage {
|
|
489
|
+
const candidates = metadata.candidatesTokenCount || 0;
|
|
490
|
+
const thinking = metadata.thoughtsTokenCount || 0;
|
|
491
|
+
const total = metadata.totalTokenCount || 0;
|
|
492
|
+
const prompt = metadata.promptTokenCount || Math.max(0, total - candidates - thinking);
|
|
493
|
+
const cacheRead = Math.min(metadata.cachedContentTokenCount || 0, prompt);
|
|
494
|
+
return {
|
|
495
|
+
input: prompt - cacheRead,
|
|
496
|
+
output: candidates + thinking,
|
|
497
|
+
cacheRead,
|
|
498
|
+
cacheWrite: 0,
|
|
499
|
+
totalTokens: total,
|
|
500
|
+
...(thinking > 0 ? { reasoningTokens: thinking } : {}),
|
|
501
|
+
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
|
|
502
|
+
};
|
|
503
|
+
}
|
|
504
|
+
|
|
476
505
|
/**
|
|
477
506
|
* Module-local counter for generating unique tool call IDs across Google providers.
|
|
478
507
|
* Shared so that a single monotonically-increasing sequence is used regardless of which
|
|
@@ -739,28 +768,7 @@ export async function consumeGoogleStream<T extends GoogleApiType>(args: {
|
|
|
739
768
|
}
|
|
740
769
|
|
|
741
770
|
if (chunk.usageMetadata) {
|
|
742
|
-
|
|
743
|
-
// Subtract to get non-cached input, matching the OpenAI convention where
|
|
744
|
-
// input = uncached prompt tokens and cacheRead = cached tokens so that
|
|
745
|
-
// input + cacheRead = total prompt tokens (no double-counting).
|
|
746
|
-
// Ref: https://ai.google.dev/api/generate-content#v1beta.GenerateContentResponse.UsageMetadata
|
|
747
|
-
const cachedTokens = chunk.usageMetadata.cachedContentTokenCount || 0;
|
|
748
|
-
const thinkingTokens = chunk.usageMetadata.thoughtsTokenCount || 0;
|
|
749
|
-
output.usage = {
|
|
750
|
-
input: (chunk.usageMetadata.promptTokenCount || 0) - cachedTokens,
|
|
751
|
-
output: (chunk.usageMetadata.candidatesTokenCount || 0) + thinkingTokens,
|
|
752
|
-
cacheRead: cachedTokens,
|
|
753
|
-
cacheWrite: 0,
|
|
754
|
-
totalTokens: chunk.usageMetadata.totalTokenCount || 0,
|
|
755
|
-
...(thinkingTokens > 0 ? { reasoningTokens: thinkingTokens } : {}),
|
|
756
|
-
cost: {
|
|
757
|
-
input: 0,
|
|
758
|
-
output: 0,
|
|
759
|
-
cacheRead: 0,
|
|
760
|
-
cacheWrite: 0,
|
|
761
|
-
total: 0,
|
|
762
|
-
},
|
|
763
|
-
};
|
|
771
|
+
output.usage = mapGoogleUsage(chunk.usageMetadata);
|
|
764
772
|
calculateCost(model, output.usage, output.timestamp);
|
|
765
773
|
}
|
|
766
774
|
}
|