From b25257341ec5b9a15486e463207b83de11b1c214 Mon Sep 17 00:00:00 2001 From: dexto-cloud Date: Fri, 6 Mar 2026 07:54:12 +0000 Subject: [PATCH] Expose messageId and usage endpoints --- packages/core/src/agent/DextoAgent.ts | 1 + packages/core/src/agent/types.ts | 1 + packages/core/src/events/index.ts | 2 + .../core/src/llm/executor/stream-processor.ts | 1 + .../server/src/events/a2a-sse-subscriber.ts | 2 + packages/server/src/hono/routes/messages.ts | 6 + packages/server/src/hono/routes/sessions.ts | 122 ++++++++++++++++++ packages/server/src/hono/schemas/responses.ts | 13 ++ 8 files changed, 148 insertions(+) diff --git a/packages/core/src/agent/DextoAgent.ts b/packages/core/src/agent/DextoAgent.ts index 4fbb421cf..b2926229c 100644 --- a/packages/core/src/agent/DextoAgent.ts +++ b/packages/core/src/agent/DextoAgent.ts @@ -844,6 +844,7 @@ export class DextoAgent { usage: usage as import('./types.js').TokenUsage, toolCalls, sessionId, + messageId: responseEvent.messageId, }; } diff --git a/packages/core/src/agent/types.ts b/packages/core/src/agent/types.ts index bb86272ed..52d52cf7a 100644 --- a/packages/core/src/agent/types.ts +++ b/packages/core/src/agent/types.ts @@ -71,6 +71,7 @@ export interface GenerateResponse { usage: LLMTokenUsage; toolCalls: AgentToolCall[]; sessionId: string; + messageId?: string; } /** diff --git a/packages/core/src/events/index.ts b/packages/core/src/events/index.ts index e8a6188f3..7d41b0f89 100644 --- a/packages/core/src/events/index.ts +++ b/packages/core/src/events/index.ts @@ -370,6 +370,8 @@ export interface AgentEventMap { 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 */ diff --git a/packages/core/src/llm/executor/stream-processor.ts b/packages/core/src/llm/executor/stream-processor.ts index 387a7f649..a6629c990 100644 --- a/packages/core/src/llm/executor/stream-processor.ts +++ b/packages/core/src/llm/executor/stream-processor.ts @@ -699,6 +699,7 @@ export class StreamProcessor { model: this.config.model, ...this.getReasoningResponseFields(), tokenUsage: config.tokenUsage, + ...(this.assistantMessageId && { messageId: this.assistantMessageId }), ...(this.config.estimatedInputTokens !== undefined && { estimatedInputTokens: this.config.estimatedInputTokens, }), diff --git a/packages/server/src/events/a2a-sse-subscriber.ts b/packages/server/src/events/a2a-sse-subscriber.ts index 025f98bb2..93f1e02ea 100644 --- a/packages/server/src/events/a2a-sse-subscriber.ts +++ b/packages/server/src/events/a2a-sse-subscriber.ts @@ -139,10 +139,12 @@ export class A2ASseEventSubscriber { role: 'agent', content: [{ type: 'text', text: payload.content }], timestamp: new Date().toISOString(), + ...(payload.messageId && { messageId: payload.messageId }), }, tokenUsage: payload.tokenUsage, provider: payload.provider, model: payload.model, + ...(payload.messageId && { messageId: payload.messageId }), }); }, { signal } diff --git a/packages/server/src/hono/routes/messages.ts b/packages/server/src/hono/routes/messages.ts index 1a8d00559..292be7dd7 100644 --- a/packages/server/src/hono/routes/messages.ts +++ b/packages/server/src/hono/routes/messages.ts @@ -122,6 +122,11 @@ export function createMessagesRouter( sessionId: z.string().describe('Session ID used for this message'), tokenUsage: TokenUsageSchema.optional().describe('Token usage statistics'), + messageId: z + .string() + .uuid() + .optional() + .describe('Assistant message ID for idempotency'), reasoning: z .string() .optional() @@ -268,6 +273,7 @@ export function createMessagesRouter( response: result.content, sessionId: result.sessionId, tokenUsage: result.usage, + messageId: result.messageId, reasoning: result.reasoning, model: llmConfig.model, provider: llmConfig.provider, diff --git a/packages/server/src/hono/routes/sessions.ts b/packages/server/src/hono/routes/sessions.ts index 88dd9b1d2..3d8d21104 100644 --- a/packages/server/src/hono/routes/sessions.ts +++ b/packages/server/src/hono/routes/sessions.ts @@ -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(), + }, + }, + }, + }, + }); + + 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); + } + + 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(); diff --git a/packages/server/src/hono/schemas/responses.ts b/packages/server/src/hono/schemas/responses.ts index 3ce25d6f0..9d329d715 100644 --- a/packages/server/src/hono/schemas/responses.ts +++ b/packages/server/src/hono/schemas/responses.ts @@ -244,6 +244,19 @@ export const SessionTokenUsageSchema = z .strict() .describe('Session-level token usage (all fields required for cumulative totals)'); +export const UsageSummarySchema = z + .object({ + promptTokens: z.number().int().nonnegative().describe('Aggregated prompt/input tokens'), + completionTokens: z + .number() + .int() + .nonnegative() + .describe('Aggregated completion/output tokens'), + totalTokens: z.number().int().nonnegative().describe('Aggregated total tokens'), + }) + .strict() + .describe('Usage summary for external billing'); + export const ModelStatisticsSchema = z .object({ provider: z.string().describe('LLM provider identifier'),