diff options
| author | Adam Malczewski <[email protected]> | 2026-06-11 12:45:21 +0900 |
|---|---|---|
| committer | Adam Malczewski <[email protected]> | 2026-06-11 12:45:21 +0900 |
| commit | 27fd0be36b2f6395249de5aacc86e41fe4e0207f (patch) | |
| tree | 67ae766c1985344878d6a2e71da18834fa73e47d /packages/transport-http/src/app.ts | |
| parent | c2b4c05d91fa88b8d02c055a0e15c22abd8e21f3 (diff) | |
| download | dispatch-27fd0be36b2f6395249de5aacc86e41fe4e0207f.tar.gz dispatch-27fd0be36b2f6395249de5aacc86e41fe4e0207f.zip | |
feat(cache-warming): manual POST /chat/warm trigger endpoint
A frontend 'warm now' button (and fast tests) can trigger a warm on demand
instead of waiting for the automatic timer.
- transport-contract: WarmRequest / WarmResponse wire types
- transport-http: POST /chat/warm → cacheWarmHandle.warm(); 200 with cachePct,
409 when the conversation is generating, 400 on missing conversationId
Live-verified vs claude haiku: seed turn cacheWrite=6799 → POST /chat/warm
returns cacheReadTokens=6799 cachePct=100 (100% hit). 760 vitest + 109 bun green.
Diffstat (limited to 'packages/transport-http/src/app.ts')
| -rw-r--r-- | packages/transport-http/src/app.ts | 56 |
1 files changed, 56 insertions, 0 deletions
diff --git a/packages/transport-http/src/app.ts b/packages/transport-http/src/app.ts index 3c9ae85..a8cef51 100644 --- a/packages/transport-http/src/app.ts +++ b/packages/transport-http/src/app.ts @@ -4,14 +4,17 @@ import type { ConversationMetricsResponse, ModelsResponse, ThroughputResponse, + WarmResponse, } from "@dispatch/transport-contract"; import { Hono } from "hono"; import { cors } from "hono/cors"; import { + computeCachePct, isParseError, isSinceSeqError, parseChatBody, parseSinceSeq, + parseWarmBody, serializeEventLine, } from "./logic.js"; import { @@ -20,12 +23,14 @@ import { type SessionOrchestrator, ThroughputQueryError, type ThroughputStore, + type WarmService, } from "./seam.js"; export interface CreateServerOptions { readonly conversationStore: ConversationStore; readonly orchestrator: SessionOrchestrator; readonly credentialStore: CredentialStore; + readonly warmService?: WarmService; /** Optional — defaults to a no-op store (recording disabled, empty reports). */ readonly throughputStore?: ThroughputStore; readonly logger?: Logger; @@ -232,6 +237,57 @@ export function createApp(opts: CreateServerOptions): Hono { }); }); + app.post("/chat/warm", async (c) => { + if (opts.warmService === undefined) { + return c.json({ error: "Warm service not available" }, 503); + } + + let body: unknown; + try { + body = await c.req.json(); + } catch { + log.warn("chat/warm: invalid JSON body"); + return c.json({ error: "Invalid JSON body" }, 400); + } + + const parsed = parseWarmBody(body); + if ("error" in parsed) { + log.warn("chat/warm: validation failed", { reason: parsed.error }); + return c.json({ error: parsed.error }, 400); + } + + const { conversationId, model, cwd } = parsed; + log.info("chat/warm: request accepted", { + conversationId, + hasModel: model !== undefined, + hasCwd: cwd !== undefined, + }); + + const warmOpts: { readonly cwd?: string; readonly modelName?: string } | undefined = + model !== undefined || cwd !== undefined + ? { + ...(cwd !== undefined ? { cwd } : {}), + ...(model !== undefined ? { modelName: model } : {}), + } + : undefined; + + const result = await opts.warmService.warm(conversationId, warmOpts); + + if ("error" in result) { + log.warn("chat/warm: service returned error", { conversationId, error: result.error }); + return c.json({ error: result.error }, 409); + } + + const response: WarmResponse = { + inputTokens: result.inputTokens, + outputTokens: result.outputTokens, + cacheReadTokens: result.cacheReadTokens, + cacheWriteTokens: result.cacheWriteTokens, + cachePct: computeCachePct(result.inputTokens, result.cacheReadTokens), + }; + return c.json(response, 200); + }); + app.get("/metrics/throughput", async (c) => { const period = c.req.query("period"); const date = c.req.query("date"); |
