summaryrefslogtreecommitdiffhomepage
path: root/packages/transport-http/src/app.ts
diff options
context:
space:
mode:
authorAdam Malczewski <[email protected]>2026-06-11 12:45:21 +0900
committerAdam Malczewski <[email protected]>2026-06-11 12:45:21 +0900
commit27fd0be36b2f6395249de5aacc86e41fe4e0207f (patch)
tree67ae766c1985344878d6a2e71da18834fa73e47d /packages/transport-http/src/app.ts
parentc2b4c05d91fa88b8d02c055a0e15c22abd8e21f3 (diff)
downloaddispatch-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.ts56
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");