Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion packages/redrob/src/server/routes/instance/httpapi/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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,
Expand Down
72 changes: 72 additions & 0 deletions packages/redrob/test/server/chat-completion-route.test.ts
Original file line number Diff line number Diff line change
@@ -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<never, Config.ConfigError, HttpServer.HttpServer> = 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)
}),
)
})
6 changes: 5 additions & 1 deletion packages/server/src/routes.ts
Original file line number Diff line number Diff line change
@@ -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"
Expand All @@ -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,
Expand Down
Loading