From efc8f992dfd23764f2b77464c3329fd2d02f888e Mon Sep 17 00:00:00 2001 From: Janghoon Lee <44862514+savagemanage@users.noreply.github.com> Date: Thu, 15 Oct 2026 02:34:46 +0000 Subject: [PATCH] Provide the LLM client to the chat-completions route /v1/chat/completions calls the model directly, with no session in between, so it needs @redrob/LLMClient in the route tree itself. Both route trees (packages/server routes and the daemon's httpapi server) listed httpClient but not llmClient, and the layer compiler only exposes group members to handlers. Every call to the OpenAI-compatible route on v0.6.4 therefore died with a 500 "Service not found: @redrob/LLMClient" before a provider was chosen, for every app on the shared daemon. Adds the node to both groups and a route-level regression test that drives the production tree and asserts the request reaches the model layer. --- .../server/routes/instance/httpapi/server.ts | 6 +- .../test/server/chat-completion-route.test.ts | 72 +++++++++++++++++++ packages/server/src/routes.ts | 6 +- 3 files changed, 82 insertions(+), 2 deletions(-) create mode 100644 packages/redrob/test/server/chat-completion-route.test.ts diff --git a/packages/redrob/src/server/routes/instance/httpapi/server.ts b/packages/redrob/src/server/routes/instance/httpapi/server.ts index f2fe99aaa5..81476d3a4d 100644 --- a/packages/redrob/src/server/routes/instance/httpapi/server.ts +++ b/packages/redrob/src/server/routes/instance/httpapi/server.ts @@ -53,7 +53,7 @@ import { MoveSession } from "@redrob-code/core/control-plane/move-session" import { Database } from "@redrob-code/core/database/database" import { AppNodeBuilderV1 } from "@/effect/app-node-builder-v1" import { LayerNode } from "@redrob-code/core/effect/layer-node" -import { httpClient } from "@redrob-code/core/effect/app-node-platform" +import { httpClient, llmClient } from "@redrob-code/core/effect/app-node-platform" import { EventV2 } from "@redrob-code/core/event" import { ModelsDev } from "@redrob-code/core/models-dev" import { Npm } from "@redrob-code/core/npm" @@ -249,6 +249,10 @@ const app = LayerNode.group([ SessionShare.node, InstanceStore.node, httpClient, + // `/v1/chat/completions` (handlers from @redrob-code/server) calls the model with no session in + // between, so it needs the LLM client in the route tree itself; only group members are exposed + // to handlers. Missing in v0.6.4: every call died with "Service not found: @redrob/LLMClient". + llmClient, EventV2.node, ProjectV2.node, ProjectCopy.node, diff --git a/packages/redrob/test/server/chat-completion-route.test.ts b/packages/redrob/test/server/chat-completion-route.test.ts new file mode 100644 index 0000000000..8957023db6 --- /dev/null +++ b/packages/redrob/test/server/chat-completion-route.test.ts @@ -0,0 +1,72 @@ +import { NodeHttpServer, NodeServices } from "@effect/platform-node" +import { Flag } from "@redrob-code/core/flag/flag" +import { describe, expect } from "bun:test" +import { Config, Effect, Layer } from "effect" +import { HttpClient, HttpClientRequest, HttpRouter, HttpServer } from "effect/unstable/http" +import * as Socket from "effect/unstable/socket/Socket" +import { HttpApiApp } from "../../src/server/routes/instance/httpapi/server" +import { resetDatabase } from "../fixture/db" +import { testEffect } from "../lib/effect" + +// The OpenAI-compatible route is what every Redrob app (Grow, Canvas, Query, Recall, Office) calls +// on the shared daemon. v0.6.4 shipped with the route's service graph missing the LLM client, so +// every call -- with or without tools, any model -- died with a 500 "Service not found: +// @redrob/LLMClient" before a provider was even chosen. This test drives the production route tree +// and only asserts the one thing that bug broke: the request reaches the model layer. Whatever +// the provider answers without a credential is fine; a missing service is not. + +const testStateLayer = Layer.effectDiscard( + Effect.gen(function* () { + const original = { REDROB_SERVER_PASSWORD: Flag.REDROB_SERVER_PASSWORD } + Flag.REDROB_SERVER_PASSWORD = "secret" + yield* Effect.promise(() => resetDatabase()) + yield* Effect.addFinalizer(() => + Effect.promise(async () => { + Flag.REDROB_SERVER_PASSWORD = original.REDROB_SERVER_PASSWORD + await resetDatabase() + }), + ) + }), +) + +const servedRoutes: Layer.Layer = HttpRouter.serve( + HttpApiApp.routes, + { disableListenLog: true, disableLogger: true }, +) + +const it = testEffect( + Layer.mergeAll( + testStateLayer, + servedRoutes.pipe( + Layer.provide(Socket.layerWebSocketConstructorGlobal), + Layer.provideMerge(NodeHttpServer.layerTest), + Layer.provideMerge(NodeServices.layer), + ), + ), +) + +const basic = "Basic " + Buffer.from("redrob:secret").toString("base64") + +describe("POST /v1/chat/completions", () => { + it.live("reaches the model layer instead of failing on a missing LLM client service", () => + Effect.gen(function* () { + const response = yield* HttpClientRequest.post("/v1/chat/completions").pipe( + HttpClientRequest.setHeaders({ authorization: basic, "x-redrob-app": "grow" }), + HttpClientRequest.bodyJson({ + model: "redrob/auto", + messages: [{ role: "user", content: "say OK" }], + tools: [ + { + type: "function", + function: { name: "note", description: "record a note", parameters: { type: "object", properties: {} } }, + }, + ], + }), + Effect.flatMap(HttpClient.execute), + ) + const text = yield* response.text + expect(text).not.toContain("Service not found") + expect(response.status).not.toBe(500) + }), + ) +}) diff --git a/packages/server/src/routes.ts b/packages/server/src/routes.ts index 111d7e1cdf..b069680cc2 100644 --- a/packages/server/src/routes.ts +++ b/packages/server/src/routes.ts @@ -1,6 +1,6 @@ import { Database } from "@redrob-code/core/database/database" import { LayerNode } from "@redrob-code/core/effect/layer-node" -import { httpClient } from "@redrob-code/core/effect/app-node-platform" +import { httpClient, llmClient } from "@redrob-code/core/effect/app-node-platform" import { AppNodeBuilder } from "@redrob-code/core/effect/app-node-builder" import { EventV2 } from "@redrob-code/core/event" import { Credential } from "@redrob-code/core/credential" @@ -27,6 +27,10 @@ const applicationServices = LayerNode.group([ Database.node, EventV2.node, httpClient, + // The OpenAI-compatible chat-completions route calls the model directly (no session), so the + // LLM client has to be in the route tree itself. Without it every /v1/chat/completions died + // with "Service not found: @redrob/LLMClient" (v0.6.4). + llmClient, ToolOutputStore.cleanupNode, SessionV2.node, PermissionSaved.node,