feat(cowork-service): add deferred IPC modules

This commit is contained in:
Saravana Achu Mac 2026-04-03 13:44:13 -07:00
parent 892c41be74
commit be2f29d1b8
11 changed files with 688 additions and 21 deletions

View File

@ -0,0 +1,117 @@
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import Fastify from 'fastify';
import { pluginRoutes } from './routes.js';
import { setIpcBridge } from '../../lib/ipc-bridge.js';
vi.mock('../../lib/product-config.js', () => ({
PRODUCT_ID: 'clawcowork',
productConfig: {
backendPort: 4009,
},
}));
vi.mock('../../lib/request-context.js', () => ({
getUserId: vi.fn(() => 'demo-user'),
}));
let app: ReturnType<typeof Fastify>;
const call = vi.fn();
beforeEach(async () => {
setIpcBridge({
isRunning: true,
call,
} as any);
app = Fastify({ logger: false });
app.decorateRequest('jwtPayload', null);
await app.register(pluginRoutes);
call.mockReset();
});
afterEach(async () => {
setIpcBridge(null);
await app.close();
});
describe('plugin routes', () => {
it('lists plugins via IPC', async () => {
call.mockResolvedValue({
result: { plugins: [{ name: 'alpha', version: '1.0.0' }] },
});
const res = await app.inject({ method: 'GET', url: '/api/plugins' });
expect(res.statusCode).toBe(200);
expect(JSON.parse(res.payload).plugins).toHaveLength(1);
expect(call).toHaveBeenCalledWith(
'list_plugins',
expect.objectContaining({
auth: expect.objectContaining({ userId: 'demo-user' }),
})
);
});
it('returns a single plugin via IPC', async () => {
call.mockResolvedValue({
result: { plugin: { name: 'alpha', version: '1.0.0' } },
});
const res = await app.inject({ method: 'GET', url: '/api/plugins/alpha' });
expect(res.statusCode).toBe(200);
expect(JSON.parse(res.payload).plugin.name).toBe('alpha');
});
it('maps missing plugin to 404', async () => {
call.mockResolvedValue({
error: { code: -32001, message: 'not found' },
});
const res = await app.inject({ method: 'GET', url: '/api/plugins/missing' });
expect(res.statusCode).toBe(404);
});
it('installs plugin via IPC', async () => {
call.mockResolvedValue({
result: { installed: true, pluginId: 'alpha' },
});
const res = await app.inject({
method: 'POST',
url: '/api/plugins/install',
payload: { pluginId: 'alpha' },
});
expect(res.statusCode).toBe(200);
expect(JSON.parse(res.payload).installed).toBe(true);
expect(call).toHaveBeenCalledWith(
'install_plugin',
expect.objectContaining({
pluginId: 'alpha',
})
);
});
it('surfaces install errors', async () => {
call.mockResolvedValue({
error: { code: -32603, message: 'install failed' },
});
const res = await app.inject({
method: 'POST',
url: '/api/plugins/install',
payload: { pluginId: 'alpha' },
});
expect(res.statusCode).toBe(400);
});
it('uninstalls plugin via IPC', async () => {
call.mockResolvedValue({
result: { deleted: true, pluginId: 'alpha' },
});
const res = await app.inject({ method: 'DELETE', url: '/api/plugins/alpha' });
expect(res.statusCode).toBe(200);
expect(JSON.parse(res.payload).deleted).toBe(true);
});
});

View File

@ -0,0 +1,74 @@
import type { FastifyInstance, FastifyRequest } from 'fastify';
import { BadRequestError, NotFoundError } from '@bytelyst/errors';
import { PRODUCT_ID } from '../../lib/product-config.js';
import { getUserId } from '../../lib/request-context.js';
import { getIpcBridge } from '../../lib/ipc-bridge.js';
import { InstallPluginSchema, PluginIdParamsSchema } from './types.js';
function buildAuth(req: FastifyRequest): Record<string, unknown> {
const userId = getUserId(req);
const role = (req.jwtPayload as Record<string, unknown> | undefined)?.role ?? 'user';
return { userId, role, productId: PRODUCT_ID, isPlatformAuth: !!req.jwtPayload };
}
export async function pluginRoutes(app: FastifyInstance) {
const bridge = getIpcBridge();
app.get('/api/plugins', async req => {
if (!bridge.isRunning) {
return { plugins: [] };
}
const resp = await bridge.call('list_plugins', { auth: buildAuth(req) });
if (resp.error) throw new BadRequestError(resp.error.message);
return resp.result;
});
app.get('/api/plugins/:id', async req => {
const parsed = PluginIdParamsSchema.safeParse(req.params);
if (!parsed.success)
throw new BadRequestError(parsed.error.issues.map(issue => issue.message).join('; '));
if (!bridge.isRunning) throw new NotFoundError('Plugin not found');
const resp = await bridge.call('get_plugin', {
auth: buildAuth(req),
pluginId: parsed.data.id,
});
if (resp.error) {
if (resp.error.code === -32001) throw new NotFoundError('Plugin not found');
throw new BadRequestError(resp.error.message);
}
return resp.result;
});
app.post('/api/plugins/install', async req => {
const parsed = InstallPluginSchema.safeParse(req.body);
if (!parsed.success)
throw new BadRequestError(parsed.error.issues.map(issue => issue.message).join('; '));
if (!bridge.isRunning) throw new BadRequestError('IPC bridge unavailable');
const resp = await bridge.call('install_plugin', {
auth: buildAuth(req),
pluginId: parsed.data.pluginId,
});
if (resp.error) throw new BadRequestError(resp.error.message);
return resp.result;
});
app.delete('/api/plugins/:id', async req => {
const parsed = PluginIdParamsSchema.safeParse(req.params);
if (!parsed.success)
throw new BadRequestError(parsed.error.issues.map(issue => issue.message).join('; '));
if (!bridge.isRunning) throw new NotFoundError('Plugin not found');
const resp = await bridge.call('uninstall_plugin', {
auth: buildAuth(req),
pluginId: parsed.data.id,
});
if (resp.error) {
if (resp.error.code === -32001) throw new NotFoundError('Plugin not found');
throw new BadRequestError(resp.error.message);
}
return resp.result;
});
}

View File

@ -0,0 +1,9 @@
import { z } from 'zod';
export const PluginIdParamsSchema = z.object({
id: z.string().min(1).max(256),
});
export const InstallPluginSchema = z.object({
pluginId: z.string().min(1).max(256),
});

View File

@ -0,0 +1,134 @@
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import Fastify from 'fastify';
import { scheduleRoutes } from './routes.js';
import { setIpcBridge } from '../../lib/ipc-bridge.js';
vi.mock('../../lib/product-config.js', () => ({
PRODUCT_ID: 'clawcowork',
productConfig: {
backendPort: 4009,
},
}));
vi.mock('../../lib/request-context.js', () => ({
getUserId: vi.fn(() => 'demo-user'),
}));
let app: ReturnType<typeof Fastify>;
const call = vi.fn();
beforeEach(async () => {
setIpcBridge({
isRunning: true,
call,
} as any);
app = Fastify({ logger: false });
app.decorateRequest('jwtPayload', null);
await app.register(scheduleRoutes);
call.mockReset();
});
afterEach(async () => {
setIpcBridge(null);
await app.close();
});
describe('schedule routes', () => {
it('lists schedules via IPC', async () => {
call.mockResolvedValue({
result: { schedules: [{ id: 'sched-1', name: 'Daily sync' }] },
});
const res = await app.inject({ method: 'GET', url: '/api/schedule' });
expect(res.statusCode).toBe(200);
expect(JSON.parse(res.payload).schedules).toHaveLength(1);
});
it('creates schedule via IPC', async () => {
call.mockResolvedValue({
result: { schedule: { id: 'sched-1', name: 'Daily sync' } },
});
const res = await app.inject({
method: 'POST',
url: '/api/schedule',
payload: {
name: 'Daily sync',
goal: 'Refresh the workspace',
folder: '/tmp/demo',
schedule: { type: 'interval', seconds: 3600 },
},
});
expect(res.statusCode).toBe(200);
expect(JSON.parse(res.payload).schedule.id).toBe('sched-1');
expect(call).toHaveBeenCalledWith(
'create_schedule',
expect.objectContaining({
name: 'Daily sync',
})
);
});
it('updates schedule via IPC', async () => {
call.mockResolvedValue({
result: { schedule: { id: 'sched-1', enabled: false } },
});
const res = await app.inject({
method: 'PATCH',
url: '/api/schedule/sched-1',
payload: { enabled: false },
});
expect(res.statusCode).toBe(200);
expect(JSON.parse(res.payload).schedule.enabled).toBe(false);
});
it('deletes schedule via IPC', async () => {
call.mockResolvedValue({
result: { deleted: true, scheduleId: 'sched-1' },
});
const res = await app.inject({ method: 'DELETE', url: '/api/schedule/sched-1' });
expect(res.statusCode).toBe(200);
expect(JSON.parse(res.payload).deleted).toBe(true);
});
it('pauses schedule via IPC', async () => {
call.mockResolvedValue({
result: { schedule: { id: 'sched-1', enabled: false } },
});
const res = await app.inject({
method: 'POST',
url: '/api/schedule/sched-1/pause',
payload: { paused: true },
});
expect(res.statusCode).toBe(200);
expect(JSON.parse(res.payload).schedule.enabled).toBe(false);
expect(call).toHaveBeenCalledWith(
'pause_schedule',
expect.objectContaining({
scheduleId: 'sched-1',
paused: true,
})
);
});
it('maps missing schedules to 404', async () => {
call.mockResolvedValue({
error: { code: -32001, message: 'not found' },
});
const res = await app.inject({
method: 'PATCH',
url: '/api/schedule/missing',
payload: { enabled: false },
});
expect(res.statusCode).toBe(404);
});
});

View File

@ -0,0 +1,102 @@
import type { FastifyInstance, FastifyRequest } from 'fastify';
import { BadRequestError, NotFoundError } from '@bytelyst/errors';
import { PRODUCT_ID } from '../../lib/product-config.js';
import { getUserId } from '../../lib/request-context.js';
import { getIpcBridge } from '../../lib/ipc-bridge.js';
import {
CreateScheduleSchema,
PauseScheduleSchema,
ScheduleIdParamsSchema,
UpdateScheduleSchema,
} from './types.js';
function buildAuth(req: FastifyRequest): Record<string, unknown> {
const userId = getUserId(req);
const role = (req.jwtPayload as Record<string, unknown> | undefined)?.role ?? 'user';
return { userId, role, productId: PRODUCT_ID, isPlatformAuth: !!req.jwtPayload };
}
function throwScheduleError(error: { code?: number; message: string }): never {
if (error.code === -32001) throw new NotFoundError('Schedule not found');
throw new BadRequestError(error.message);
}
export async function scheduleRoutes(app: FastifyInstance) {
const bridge = getIpcBridge();
app.get('/api/schedule', async req => {
if (!bridge.isRunning) {
return { schedules: [] };
}
const resp = await bridge.call('list_schedules', { auth: buildAuth(req) });
if (resp.error) throwScheduleError(resp.error);
return resp.result;
});
app.post('/api/schedule', async req => {
const parsed = CreateScheduleSchema.safeParse(req.body);
if (!parsed.success)
throw new BadRequestError(parsed.error.issues.map(issue => issue.message).join('; '));
if (!bridge.isRunning) throw new BadRequestError('IPC bridge unavailable');
const resp = await bridge.call('create_schedule', {
auth: buildAuth(req),
...parsed.data,
});
if (resp.error) throwScheduleError(resp.error);
return resp.result;
});
app.patch('/api/schedule/:id', async req => {
const params = ScheduleIdParamsSchema.safeParse(req.params);
if (!params.success)
throw new BadRequestError(params.error.issues.map(issue => issue.message).join('; '));
const body = UpdateScheduleSchema.safeParse(req.body);
if (!body.success)
throw new BadRequestError(body.error.issues.map(issue => issue.message).join('; '));
if (!bridge.isRunning) throw new NotFoundError('Schedule not found');
const resp = await bridge.call('update_schedule', {
auth: buildAuth(req),
scheduleId: params.data.id,
...body.data,
});
if (resp.error) throwScheduleError(resp.error);
return resp.result;
});
app.delete('/api/schedule/:id', async req => {
const parsed = ScheduleIdParamsSchema.safeParse(req.params);
if (!parsed.success)
throw new BadRequestError(parsed.error.issues.map(issue => issue.message).join('; '));
if (!bridge.isRunning) throw new NotFoundError('Schedule not found');
const resp = await bridge.call('delete_schedule', {
auth: buildAuth(req),
scheduleId: parsed.data.id,
});
if (resp.error) throwScheduleError(resp.error);
return resp.result;
});
app.post('/api/schedule/:id/pause', async req => {
const params = ScheduleIdParamsSchema.safeParse(req.params);
if (!params.success)
throw new BadRequestError(params.error.issues.map(issue => issue.message).join('; '));
const body = PauseScheduleSchema.safeParse(req.body ?? {});
if (!body.success)
throw new BadRequestError(body.error.issues.map(issue => issue.message).join('; '));
if (!bridge.isRunning) throw new NotFoundError('Schedule not found');
const resp = await bridge.call('pause_schedule', {
auth: buildAuth(req),
scheduleId: params.data.id,
paused: body.data.paused ?? true,
});
if (resp.error) throwScheduleError(resp.error);
return resp.result;
});
}

View File

@ -0,0 +1,57 @@
import { z } from 'zod';
const OnceScheduleSchema = z.object({
type: z.literal('once'),
at: z.number().int().nonnegative(),
});
const IntervalScheduleSchema = z.object({
type: z.literal('interval'),
seconds: z.number().int().positive(),
});
const CronScheduleSchema = z.object({
type: z.literal('cron'),
expression: z.string().min(1).max(256),
});
const OnChangeScheduleSchema = z.object({
type: z.literal('onchange'),
watch_patterns: z.array(z.string().min(1).max(512)).min(1),
});
export const ScheduleSchema = z.discriminatedUnion('type', [
OnceScheduleSchema,
IntervalScheduleSchema,
CronScheduleSchema,
OnChangeScheduleSchema,
]);
export const ScheduleIdParamsSchema = z.object({
id: z.string().min(1).max(256),
});
export const CreateScheduleSchema = z.object({
name: z.string().min(1).max(256),
goal: z.string().min(1).max(10_000),
folder: z.string().min(1).max(2048),
model: z.string().min(1).max(256).optional(),
schedule: ScheduleSchema,
});
export const UpdateScheduleSchema = z
.object({
name: z.string().min(1).max(256).optional(),
goal: z.string().min(1).max(10_000).optional(),
folder: z.string().min(1).max(2048).optional(),
model: z.string().min(1).max(256).optional(),
enabled: z.boolean().optional(),
schedule: ScheduleSchema.optional(),
})
.refine(value => Object.keys(value).length > 0, {
message: 'at least one field must be provided',
});
export const PauseScheduleSchema = z.object({
paused: z.boolean().optional(),
});

View File

@ -0,0 +1,82 @@
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import Fastify from 'fastify';
import { sessionRoutes } from './routes.js';
import { setIpcBridge } from '../../lib/ipc-bridge.js';
vi.mock('../../lib/product-config.js', () => ({
PRODUCT_ID: 'clawcowork',
productConfig: {
backendPort: 4009,
},
}));
vi.mock('../../lib/request-context.js', () => ({
getUserId: vi.fn(() => 'demo-user'),
}));
let app: ReturnType<typeof Fastify>;
const call = vi.fn();
beforeEach(async () => {
setIpcBridge({
isRunning: true,
call,
} as any);
app = Fastify({ logger: false });
app.decorateRequest('jwtPayload', null);
await app.register(sessionRoutes);
call.mockReset();
});
afterEach(async () => {
setIpcBridge(null);
await app.close();
});
describe('sessions routes', () => {
it('lists sessions via IPC', async () => {
call.mockResolvedValue({
result: { sessions: [{ id: 'sess-1', messageCount: 3 }] },
});
const res = await app.inject({ method: 'GET', url: '/api/sessions' });
expect(res.statusCode).toBe(200);
expect(JSON.parse(res.payload).sessions).toHaveLength(1);
expect(call).toHaveBeenCalledWith(
'list_sessions',
expect.objectContaining({
auth: expect.objectContaining({ userId: 'demo-user' }),
})
);
});
it('returns a single session via IPC', async () => {
call.mockResolvedValue({
result: { session: { id: 'sess-1', messages: [] } },
});
const res = await app.inject({ method: 'GET', url: '/api/sessions/sess-1' });
expect(res.statusCode).toBe(200);
expect(JSON.parse(res.payload).session.id).toBe('sess-1');
});
it('maps not found errors for session fetch', async () => {
call.mockResolvedValue({
error: { code: -32001, message: 'not found' },
});
const res = await app.inject({ method: 'GET', url: '/api/sessions/missing' });
expect(res.statusCode).toBe(404);
});
it('deletes a session via IPC', async () => {
call.mockResolvedValue({
result: { deleted: true, sessionId: 'sess-1' },
});
const res = await app.inject({ method: 'DELETE', url: '/api/sessions/sess-1' });
expect(res.statusCode).toBe(200);
expect(JSON.parse(res.payload).deleted).toBe(true);
});
});

View File

@ -0,0 +1,64 @@
import type { FastifyInstance, FastifyRequest } from 'fastify';
import { BadRequestError, NotFoundError } from '@bytelyst/errors';
import { PRODUCT_ID } from '../../lib/product-config.js';
import { getUserId } from '../../lib/request-context.js';
import { getIpcBridge } from '../../lib/ipc-bridge.js';
import { SessionIdParamsSchema } from './types.js';
function buildAuth(req: FastifyRequest): Record<string, unknown> {
const userId = getUserId(req);
const role = (req.jwtPayload as Record<string, unknown> | undefined)?.role ?? 'user';
return { userId, role, productId: PRODUCT_ID, isPlatformAuth: !!req.jwtPayload };
}
export async function sessionRoutes(app: FastifyInstance) {
const bridge = getIpcBridge();
app.get('/api/sessions', async req => {
if (!bridge.isRunning) {
return { sessions: [] };
}
const resp = await bridge.call('list_sessions', { auth: buildAuth(req) });
if (resp.error) throw new BadRequestError(resp.error.message);
return resp.result;
});
app.get('/api/sessions/:id', async req => {
const parsed = SessionIdParamsSchema.safeParse(req.params);
if (!parsed.success)
throw new BadRequestError(parsed.error.issues.map(issue => issue.message).join('; '));
if (!bridge.isRunning) throw new NotFoundError('Session not found');
const resp = await bridge.call('get_session', {
auth: buildAuth(req),
sessionId: parsed.data.id,
});
if (resp.error) {
if (resp.error.code === -32001) throw new NotFoundError('Session not found');
throw new BadRequestError(resp.error.message);
}
return resp.result;
});
app.delete('/api/sessions/:id', async req => {
const parsed = SessionIdParamsSchema.safeParse(req.params);
if (!parsed.success)
throw new BadRequestError(parsed.error.issues.map(issue => issue.message).join('; '));
if (!bridge.isRunning) throw new NotFoundError('Session not found');
const resp = await bridge.call('delete_session', {
auth: buildAuth(req),
sessionId: parsed.data.id,
});
if (resp.error) {
if (resp.error.code === -32001) throw new NotFoundError('Session not found');
throw new BadRequestError(resp.error.message);
}
return resp.result;
});
}

View File

@ -0,0 +1,7 @@
import { z } from 'zod';
export const SessionIdParamsSchema = z.object({
id: z.string().min(1).max(256),
});
export type SessionIdParams = z.infer<typeof SessionIdParamsSchema>;

View File

@ -53,7 +53,9 @@ vi.mock('./lib/request-context.js', () => ({
vi.mock('./lib/ipc-bridge.js', () => ({
getIpcBridge: vi.fn(() => ({
isRunning: false,
start: vi.fn(async () => { throw new Error('no binary in test'); }),
start: vi.fn(async () => {
throw new Error('no binary in test');
}),
shutdown: vi.fn(async () => undefined),
onIncomingRequest: vi.fn(),
})),
@ -77,6 +79,9 @@ vi.mock('./modules/usage/routes.js', () => ({ usageRoutes: vi.fn() }));
vi.mock('./modules/notifications/routes.js', () => ({ notificationRoutes: vi.fn() }));
vi.mock('./modules/extraction/routes.js', () => ({ extractionRoutes: vi.fn() }));
vi.mock('./modules/marketplace/routes.js', () => ({ marketplaceRoutes: vi.fn() }));
vi.mock('./modules/sessions/routes.js', () => ({ sessionRoutes: vi.fn() }));
vi.mock('./modules/plugins/routes.js', () => ({ pluginRoutes: vi.fn() }));
vi.mock('./modules/schedule/routes.js', () => ({ scheduleRoutes: vi.fn() }));
describe('cowork-service bootstrap', () => {
beforeEach(() => {
@ -96,9 +101,9 @@ describe('cowork-service bootstrap', () => {
expect(opts.version).toBe('0.1.0');
expect(opts.readiness).toBe(true);
// health + task + llm + audit + usage + notifications + extraction + marketplace = 8 register calls + 1 JWT
// health + task + llm + audit + usage + notifications + extraction + marketplace + sessions + plugins + schedule = 11 register calls + 1 JWT
expect(registerOptionalJwtContextMock).toHaveBeenCalledOnce();
expect(appMock.register).toHaveBeenCalledTimes(8);
expect(appMock.register).toHaveBeenCalledTimes(11);
expect(startServiceMock).toHaveBeenCalledWith(appMock, { port: 4009, host: '0.0.0.0' });
});
});

View File

@ -30,6 +30,9 @@ import { usageRoutes } from './modules/usage/routes.js';
import { notificationRoutes } from './modules/notifications/routes.js';
import { extractionRoutes } from './modules/extraction/routes.js';
import { marketplaceRoutes } from './modules/marketplace/routes.js';
import { sessionRoutes } from './modules/sessions/routes.js';
import { pluginRoutes } from './modules/plugins/routes.js';
import { scheduleRoutes } from './modules/schedule/routes.js';
import type { JwtPayload } from './lib/request-context.js';
const jwtSecret = new TextEncoder().encode(config.JWT_SECRET);
@ -64,6 +67,9 @@ await app.register(usageRoutes);
await app.register(notificationRoutes);
await app.register(extractionRoutes);
await app.register(marketplaceRoutes);
await app.register(sessionRoutes);
await app.register(pluginRoutes);
await app.register(scheduleRoutes);
// Bootstrap endpoint (same pattern as FlowMonk, ActionTrail, etc.)
app.get('/api/bootstrap', async () => ({
@ -84,12 +90,14 @@ try {
// Initialize LLM router (best-effort — works without API keys in dev)
// Set OLLAMA_MODELS=model1,model2 to add local Ollama as a provider.
const ollamaModels = config.OLLAMA_MODELS?.split(',').map(s => s.trim()).filter(Boolean);
const ollamaModels = config.OLLAMA_MODELS?.split(',')
.map(s => s.trim())
.filter(Boolean);
try {
const llm = initLlmRouter({
ollamaModels,
ollamaBaseUrl: config.OLLAMA_URL,
onTelemetry: (entry) => app.log.debug({ llmTelemetry: entry }, 'llm-router event'),
onTelemetry: entry => app.log.debug({ llmTelemetry: entry }, 'llm-router event'),
});
app.log.info({ providers: llm.getProviders() }, 'LLM router initialized');
} catch (err) {
@ -109,7 +117,10 @@ bridge.onIncomingRequest(async (method, params) => {
const startMs = Date.now();
const result = await getLlmRouter().chat({
messages: messages.map(m => ({ role: m.role as 'system' | 'user' | 'assistant', content: m.content })),
messages: messages.map(m => ({
role: m.role as 'system' | 'user' | 'assistant',
content: m.content,
})),
model: (params.model as string) || undefined,
temperature: (params.temperature as number) ?? undefined,
max_tokens: (params.max_tokens as number) ?? undefined,
@ -118,23 +129,28 @@ bridge.onIncomingRequest(async (method, params) => {
// Record spend for budget tracking (best-effort)
const costUsd = (result as unknown as Record<string, unknown>).costUsd as number | undefined;
if (costUsd && params.auth && params.taskId) {
bridge.recordSpend(
result.model,
0, // token counts not always available from router
0,
costUsd,
params.auth as Record<string, unknown>,
params.taskId as string,
).catch((err) => app.log.warn({ err }, 'Failed to record LLM spend'));
bridge
.recordSpend(
result.model,
0, // token counts not always available from router
0,
costUsd,
params.auth as Record<string, unknown>,
params.taskId as string
)
.catch(err => app.log.warn({ err }, 'Failed to record LLM spend'));
}
app.log.info({
method: 'intercept_llm',
provider: result.provider,
model: result.model,
latencyMs: Date.now() - startMs,
attempts: result.attempts,
}, 'LLM interception completed');
app.log.info(
{
method: 'intercept_llm',
provider: result.provider,
model: result.model,
latencyMs: Date.now() - startMs,
attempts: result.attempts,
},
'LLM interception completed'
);
return {
response: result.response,