-
Notifications
You must be signed in to change notification settings - Fork 68
Expose messageId and usage endpoints #642
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -4,6 +4,7 @@ import { | |
| SessionMetadataSchema, | ||
| InternalMessageSchema, | ||
| StandardErrorEnvelopeSchema, | ||
| UsageSummarySchema, | ||
| } from '../schemas/responses.js'; | ||
| import type { GetAgentFn } from '../index.js'; | ||
|
|
||
|
|
@@ -196,6 +197,76 @@ export function createSessionsRouter(getAgent: GetAgentFn) { | |
| }, | ||
| }); | ||
|
|
||
| const usageRoute = createRoute({ | ||
| method: 'get', | ||
| path: '/sessions/{sessionId}/usage', | ||
| summary: 'Get Session Usage', | ||
| description: 'Returns aggregated token usage for a session', | ||
| tags: ['sessions'], | ||
| request: { params: z.object({ sessionId: z.string().describe('Session identifier') }) }, | ||
| responses: { | ||
| 200: { | ||
| description: 'Aggregated usage for session', | ||
| content: { | ||
| 'application/json': { | ||
| schema: z | ||
| .object({ | ||
| sessionId: z.string().describe('Session identifier'), | ||
| usage: UsageSummarySchema, | ||
| }) | ||
| .strict(), | ||
| }, | ||
| }, | ||
| }, | ||
| 404: { | ||
| description: 'Session not found', | ||
| content: { | ||
| 'application/json': { | ||
| schema: z.object({ error: z.string().describe('Error message') }).strict(), | ||
| }, | ||
| }, | ||
|
Comment on lines
+221
to
+227
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Use the shared error envelope for these 404s. Lines 225/263 document Based on learnings, error responses must follow the standard middleware format from Also applies to: 259-264, 541-542, 569-572 🤖 Prompt for AI Agents |
||
| }, | ||
| }, | ||
| }); | ||
|
|
||
| const messageUsageRoute = createRoute({ | ||
| method: 'get', | ||
| path: '/sessions/{sessionId}/messages/{messageId}/usage', | ||
| summary: 'Get Message Usage', | ||
| description: 'Returns token usage for a specific assistant message', | ||
| tags: ['sessions'], | ||
| request: { | ||
| params: z.object({ | ||
| sessionId: z.string().describe('Session identifier'), | ||
| messageId: z.string().describe('Assistant message identifier'), | ||
| }), | ||
| }, | ||
| responses: { | ||
| 200: { | ||
| description: 'Usage for assistant message', | ||
| content: { | ||
| 'application/json': { | ||
| schema: z | ||
| .object({ | ||
| sessionId: z.string().describe('Session identifier'), | ||
| messageId: z.string().describe('Assistant message identifier'), | ||
| usage: UsageSummarySchema, | ||
| }) | ||
| .strict(), | ||
| }, | ||
| }, | ||
| }, | ||
| 404: { | ||
| description: 'Session or message not found', | ||
| content: { | ||
| 'application/json': { | ||
| schema: z.object({ error: z.string().describe('Error message') }).strict(), | ||
| }, | ||
| }, | ||
| }, | ||
| }, | ||
| }); | ||
|
|
||
| const deleteRoute = createRoute({ | ||
| method: 'delete', | ||
| path: '/sessions/{sessionId}', | ||
|
|
@@ -462,6 +533,57 @@ export function createSessionsRouter(getAgent: GetAgentFn) { | |
| isBusy, | ||
| }); | ||
| }) | ||
| .openapi(usageRoute, async (ctx) => { | ||
| const agent = await getAgent(ctx); | ||
| const { sessionId } = ctx.req.param(); | ||
| const metadata = await agent.getSessionMetadata(sessionId); | ||
| if (!metadata) { | ||
| return ctx.json({ error: `Session not found: ${sessionId}` }, 404); | ||
| } | ||
|
Comment on lines
+539
to
+542
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Catch thrown metadata lookups before returning 404. Line 539 assumes 🤖 Prompt for AI Agents |
||
|
|
||
| const usage = metadata.tokenUsage ?? { | ||
| inputTokens: 0, | ||
| outputTokens: 0, | ||
| totalTokens: 0, | ||
| reasoningTokens: 0, | ||
| cacheReadTokens: 0, | ||
| cacheWriteTokens: 0, | ||
| }; | ||
|
|
||
| return ctx.json({ | ||
| sessionId, | ||
| usage: { | ||
| promptTokens: usage.inputTokens ?? 0, | ||
| completionTokens: usage.outputTokens ?? 0, | ||
| totalTokens: usage.totalTokens ?? | ||
| (usage.inputTokens ?? 0) + (usage.outputTokens ?? 0), | ||
| }, | ||
| }); | ||
| }) | ||
| .openapi(messageUsageRoute, async (ctx) => { | ||
| const agent = await getAgent(ctx); | ||
| const { sessionId, messageId } = ctx.req.param(); | ||
| const history = await agent.getSessionHistory(sessionId); | ||
| const message = history.find((item) => item.id === messageId); | ||
| if (!message || message.role !== 'assistant' || !message.tokenUsage) { | ||
| return ctx.json( | ||
| { error: `Message usage not found for ${messageId}` }, | ||
| 404 | ||
| ); | ||
| } | ||
|
|
||
| const usage = message.tokenUsage; | ||
| return ctx.json({ | ||
| sessionId, | ||
| messageId, | ||
| usage: { | ||
| promptTokens: usage.inputTokens ?? 0, | ||
| completionTokens: usage.outputTokens ?? 0, | ||
| totalTokens: usage.totalTokens ?? | ||
| (usage.inputTokens ?? 0) + (usage.outputTokens ?? 0), | ||
| }, | ||
| }); | ||
| }) | ||
| .openapi(deleteRoute, async (ctx) => { | ||
| const agent = await getAgent(ctx); | ||
| const { sessionId } = ctx.req.param(); | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Mirror
messageIdinto the session-scopedllm:responsetype.StreamProcessornow emitsmessageIdonSessionEventBus, butSessionEventMap['llm:response']still omits it. That leaves the session contract out of sync with the agent contract and hides the new field from session-level consumers.Suggested fix
// SessionEventMap 'llm:response': { content: string; reasoning?: string; provider?: LLMProvider; model?: string; /** Reasoning tuning variant used for this call, when the provider exposes it. */ reasoningVariant?: ReasoningVariant; /** Reasoning budget tokens used for this call, when the provider exposes it. */ reasoningBudgetTokens?: number; tokenUsage?: { inputTokens?: number; outputTokens?: number; reasoningTokens?: number; totalTokens?: number; cacheReadTokens?: number; cacheWriteTokens?: number; }; + /** Stable assistant message id for idempotency/billing */ + messageId?: string; /** Estimated input tokens before LLM call (for analytics/calibration) */ estimatedInputTokens?: number; /** Finish reason: 'tool-calls' means more steps coming, others indicate completion */ finishReason?: LLMFinishReason; };📝 Committable suggestion
🤖 Prompt for AI Agents