Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions apps/server/src/provider/Layers/CursorAdapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -867,6 +867,11 @@ export function makeCursorAdapter(
}),
);
return;
case "TokenUsageUpdated":
// Parsed from ACP usage_update; Grok wires this to the context
// meter. Cursor can emit the same makeAcpTokenUsageUpdatedEvent
// path once verified against a real Cursor ACP install.
return;
}
}),
),
Expand Down
82 changes: 80 additions & 2 deletions apps/server/src/provider/Layers/GrokAdapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,15 +49,21 @@ import {
makeAcpPlanUpdatedEvent,
makeAcpRequestOpenedEvent,
makeAcpRequestResolvedEvent,
makeAcpTokenUsageUpdatedEvent,
makeAcpToolCallEvent,
} from "../acp/AcpCoreRuntimeEvents.ts";
import { parsePermissionRequest } from "../acp/AcpRuntimeModel.ts";
import { makeAcpNativeLoggerFactory } from "../acp/AcpNativeLogging.ts";
import {
applyGrokAcpModelSelection,
contextWindowForModelId,
contextWindowsFromSessionModels,
currentGrokModelIdFromSessionSetup,
enrichGrokTokenUsage,
makeGrokAcpRuntime,
resolveGrokAcpBaseModelId,
resolveInitialGrokContextWindow,
tokenUsageFromGrokPromptMeta,
} from "../acp/GrokAcpSupport.ts";
import {
extractXAiAskUserQuestions,
Expand Down Expand Up @@ -117,6 +123,10 @@ interface GrokSessionContext {
* continues it, and only the last remaining prompt settles the turn. */
promptsInFlight: number;
currentModelId: string | undefined;
/** modelId → totalContextTokens from Grok session model `_meta`. */
contextWindowsByModelId: ReadonlyMap<string, number>;
/** Last resolved window size so fill % still works if model id lookup misses. */
lastKnownMaxTokens: number | undefined;
stopped: boolean;
}

Expand Down Expand Up @@ -745,6 +755,15 @@ export function makeGrokAdapter(grokSettings: GrokSettings, options?: GrokAdapte
mapError: (cause) =>
mapAcpToAdapterError(PROVIDER, input.threadId, "session/set_model", cause),
});
const setupModelId = currentGrokModelIdFromSessionSetup(started.sessionSetupResult);
const contextWindowsByModelId = contextWindowsFromSessionModels(
started.sessionSetupResult.models,
);
const lastKnownMaxTokens = resolveInitialGrokContextWindow({
windows: contextWindowsByModelId,
boundModelId,
setupModelId,
});

const now = yield* nowIso;
const session: ProviderSession = {
Expand Down Expand Up @@ -777,7 +796,9 @@ export function makeGrokAdapter(grokSettings: GrokSettings, options?: GrokAdapte
activeTurnId: undefined,
interruptedTurnIds: new Set(),
promptsInFlight: 0,
currentModelId: boundModelId,
currentModelId: boundModelId ?? setupModelId,
contextWindowsByModelId,
lastKnownMaxTokens,
stopped: false,
};

Expand All @@ -791,7 +812,8 @@ export function makeGrokAdapter(grokSettings: GrokSettings, options?: GrokAdapte
if (
event._tag === "PlanUpdated" ||
event._tag === "ToolCallUpdated" ||
event._tag === "ContentDelta"
event._tag === "ContentDelta" ||
event._tag === "TokenUsageUpdated"
) {
yield* logNative(ctx.threadId, "session/update", event.rawPayload);
}
Expand All @@ -800,6 +822,33 @@ export function makeGrokAdapter(grokSettings: GrokSettings, options?: GrokAdapte
return;
}

// Session-level usage can arrive after activeTurnId is cleared; still
// surface it so the composer context meter can update between turns.
if (event._tag === "TokenUsageUpdated") {
const usageTurnId = resolveNotificationTurnId(ctx);
if (usageTurnId !== undefined && ctx.interruptedTurnIds.has(usageTurnId)) {
return;
}
const maxTokens =
contextWindowForModelId(ctx.contextWindowsByModelId, ctx.currentModelId) ??
contextWindowForModelId(ctx.contextWindowsByModelId, ctx.session.model) ??
ctx.lastKnownMaxTokens;
if (maxTokens !== undefined) {
ctx.lastKnownMaxTokens = maxTokens;
}
yield* offerRuntimeEvent(
makeAcpTokenUsageUpdatedEvent({
stamp: yield* makeEventStamp(),
provider: PROVIDER,
threadId: ctx.threadId,
turnId: usageTurnId,
usage: enrichGrokTokenUsage(event.usage, maxTokens),
rawPayload: event.rawPayload,
}),
);
return;
}

const notificationTurnId = resolveNotificationTurnId(ctx);
if (
notificationTurnId === undefined ||
Expand Down Expand Up @@ -998,6 +1047,13 @@ export function makeGrokAdapter(grokSettings: GrokSettings, options?: GrokAdapte
}

ctx.currentModelId = currentModelId;
const turnMaxTokens = contextWindowForModelId(
ctx.contextWindowsByModelId,
currentModelId,
);
if (turnMaxTokens !== undefined) {
ctx.lastKnownMaxTokens = turnMaxTokens;
}
const displayModel = currentModelId
? resolveGrokAcpBaseModelId(currentModelId)
: undefined;
Expand Down Expand Up @@ -1150,6 +1206,28 @@ export function makeGrokAdapter(grokSettings: GrokSettings, options?: GrokAdapte
updatedAt: yield* nowIso,
...(prepared.displayModel ? { model: prepared.displayModel } : {}),
};
// Grok streams fill via session/update _meta; prompt response
// carries the final totals as a reliable end-of-turn snapshot.
const promptMaxTokens =
contextWindowForModelId(ctx.contextWindowsByModelId, ctx.currentModelId) ??
contextWindowForModelId(ctx.contextWindowsByModelId, ctx.session.model) ??
ctx.lastKnownMaxTokens;
if (promptMaxTokens !== undefined) {
ctx.lastKnownMaxTokens = promptMaxTokens;
}
const promptUsage = tokenUsageFromGrokPromptMeta(result._meta, promptMaxTokens);
if (promptUsage) {
yield* offerRuntimeEvent(
makeAcpTokenUsageUpdatedEvent({
stamp: yield* makeEventStamp(),
provider: PROVIDER,
threadId: ctx.threadId,
turnId: prepared.turnId,
usage: promptUsage,
rawPayload: result,
}),
);
}
const remainingPrompts = Math.max(0, ctx.promptsInFlight - 1);
ctx.promptsInFlight = remainingPrompts;

Expand Down
31 changes: 31 additions & 0 deletions apps/server/src/provider/acp/AcpCoreRuntimeEvents.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
makeAcpPlanUpdatedEvent,
makeAcpRequestOpenedEvent,
makeAcpRequestResolvedEvent,
makeAcpTokenUsageUpdatedEvent,
makeAcpToolCallEvent,
} from "./AcpCoreRuntimeEvents.ts";

Expand Down Expand Up @@ -151,5 +152,35 @@ describe("AcpCoreRuntimeEvents", () => {
status: "inProgress",
},
});

expect(
makeAcpTokenUsageUpdatedEvent({
stamp,
provider: ProviderDriverKind.make("grok"),
threadId: "thread-1" as never,
turnId,
usage: {
usedTokens: 12_345,
maxTokens: 200_000,
},
rawPayload: {
sessionId: "session-1",
update: { sessionUpdate: "usage_update", used: 12_345, size: 200_000 },
},
}),
).toMatchObject({
type: "thread.token-usage.updated",
provider: "grok",
payload: {
usage: {
usedTokens: 12_345,
maxTokens: 200_000,
},
},
raw: {
source: "acp.jsonrpc",
method: "session/update",
},
});
});
});
26 changes: 26 additions & 0 deletions apps/server/src/provider/acp/AcpCoreRuntimeEvents.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
type ProviderRuntimeEvent,
type RuntimeRequestId,
type ThreadId,
type ThreadTokenUsageSnapshot,
type ToolLifecycleItemType,
type TurnId,
} from "@t3tools/contracts";
Expand Down Expand Up @@ -240,3 +241,28 @@ export function makeAcpContentDeltaEvent(input: {
},
};
}

export function makeAcpTokenUsageUpdatedEvent(input: {
readonly stamp: AcpEventStamp;
readonly provider: ProviderDriverKind;
readonly threadId: ThreadId;
readonly turnId: TurnId | undefined;
readonly usage: ThreadTokenUsageSnapshot;
readonly rawPayload: unknown;
}): ProviderRuntimeEvent {
return {
type: "thread.token-usage.updated",
...input.stamp,
provider: input.provider,
threadId: input.threadId,
turnId: input.turnId,
payload: {
usage: input.usage,
},
raw: {
source: "acp.jsonrpc",
method: "session/update",
payload: input.rawPayload,
},
};
}
114 changes: 114 additions & 0 deletions apps/server/src/provider/acp/AcpRuntimeModel.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -336,6 +336,120 @@ describe("AcpRuntimeModel", () => {
]);
});

it("projects ACP usage_update into TokenUsageUpdated snapshots", () => {
const raw = {
sessionId: "session-1",
update: {
sessionUpdate: "usage_update" as const,
used: 12_345,
size: 200_000,
},
} satisfies EffectAcpSchema.SessionNotification;

expect(parseSessionUpdateEvent(raw).events).toEqual([
{
_tag: "TokenUsageUpdated",
usage: {
usedTokens: 12_345,
maxTokens: 200_000,
},
rawPayload: raw,
},
]);

expect(
parseSessionUpdateEvent({
sessionId: "session-1",
update: {
sessionUpdate: "usage_update",
used: 0,
size: 200_000,
},
} satisfies EffectAcpSchema.SessionNotification).events,
).toEqual([]);

expect(
parseSessionUpdateEvent({
sessionId: "session-1",
update: {
sessionUpdate: "usage_update",
used: 99,
size: 0,
},
} satisfies EffectAcpSchema.SessionNotification).events,
).toEqual([
{
_tag: "TokenUsageUpdated",
usage: {
usedTokens: 99,
},
rawPayload: {
sessionId: "session-1",
update: {
sessionUpdate: "usage_update",
used: 99,
size: 0,
},
},
},
]);
});

it("projects Grok-style session/update _meta.totalTokens into TokenUsageUpdated", () => {
const raw = {
sessionId: "session-1",
update: {
sessionUpdate: "agent_message_chunk",
content: {
type: "text",
text: "hello",
},
},
_meta: {
totalTokens: 19_267,
},
} satisfies EffectAcpSchema.SessionNotification;

expect(parseSessionUpdateEvent(raw).events).toEqual([
{
_tag: "ContentDelta",
text: "hello",
rawPayload: raw,
},
{
_tag: "TokenUsageUpdated",
usage: {
usedTokens: 19_267,
},
rawPayload: raw,
},
]);

// Explicit usage_update wins over notification _meta when both exist.
const withBoth = {
sessionId: "session-1",
update: {
sessionUpdate: "usage_update" as const,
used: 1_000,
size: 50_000,
},
_meta: {
totalTokens: 9_999,
},
} satisfies EffectAcpSchema.SessionNotification;

expect(parseSessionUpdateEvent(withBoth).events).toEqual([
{
_tag: "TokenUsageUpdated",
usage: {
usedTokens: 1_000,
maxTokens: 50_000,
},
rawPayload: withBoth,
},
]);
});

it("keeps permission request parsing compatible with loose extension payloads", () => {
const request = parsePermissionRequest({
sessionId: "session-1",
Expand Down
Loading
Loading