|
|
@@ -1,39 +1,12 @@
|
|
|
-import { NodeHttpServer } from "@effect/platform-node"
|
|
|
+import { NodeHttpServer, NodeHttpServerRequest } from "@effect/platform-node"
|
|
|
import * as Http from "node:http"
|
|
|
import { Deferred, Effect, Layer, ServiceMap, Stream } from "effect"
|
|
|
import * as HttpServer from "effect/unstable/http/HttpServer"
|
|
|
import { HttpRouter, HttpServerRequest, HttpServerResponse } from "effect/unstable/http"
|
|
|
|
|
|
-type Usage = { input: number; output: number }
|
|
|
+export type Usage = { input: number; output: number }
|
|
|
|
|
|
-type Step =
|
|
|
- | {
|
|
|
- type: "text"
|
|
|
- text: string
|
|
|
- usage?: Usage
|
|
|
- }
|
|
|
- | {
|
|
|
- type: "tool"
|
|
|
- tool: string
|
|
|
- input: unknown
|
|
|
- }
|
|
|
- | {
|
|
|
- type: "fail"
|
|
|
- message: string
|
|
|
- }
|
|
|
- | {
|
|
|
- type: "httpError"
|
|
|
- status: number
|
|
|
- body: unknown
|
|
|
- }
|
|
|
- | {
|
|
|
- type: "hang"
|
|
|
- }
|
|
|
- | {
|
|
|
- type: "hold"
|
|
|
- text: string
|
|
|
- wait: PromiseLike<unknown>
|
|
|
- }
|
|
|
+type Line = Record<string, unknown>
|
|
|
|
|
|
type Hit = {
|
|
|
url: URL
|
|
|
@@ -45,163 +18,293 @@ type Wait = {
|
|
|
ready: Deferred.Deferred<void>
|
|
|
}
|
|
|
|
|
|
-function sse(lines: unknown[]) {
|
|
|
- return HttpServerResponse.stream(
|
|
|
- Stream.fromIterable([
|
|
|
- [...lines.map((line) => `data: ${JSON.stringify(line)}`), "data: [DONE]"].join("\n\n") + "\n\n",
|
|
|
- ]).pipe(Stream.encodeText),
|
|
|
- { contentType: "text/event-stream" },
|
|
|
- )
|
|
|
+type Sse = {
|
|
|
+ type: "sse"
|
|
|
+ head: unknown[]
|
|
|
+ tail: unknown[]
|
|
|
+ wait?: PromiseLike<unknown>
|
|
|
+ hang?: boolean
|
|
|
+ error?: unknown
|
|
|
+ reset?: boolean
|
|
|
+}
|
|
|
+
|
|
|
+type HttpError = {
|
|
|
+ type: "http-error"
|
|
|
+ status: number
|
|
|
+ body: unknown
|
|
|
+}
|
|
|
+
|
|
|
+export type Item = Sse | HttpError
|
|
|
+
|
|
|
+const done = Symbol("done")
|
|
|
+
|
|
|
+function line(input: unknown) {
|
|
|
+ if (input === done) return "data: [DONE]\n\n"
|
|
|
+ return `data: ${JSON.stringify(input)}\n\n`
|
|
|
+}
|
|
|
+
|
|
|
+function tokens(input?: Usage) {
|
|
|
+ if (!input) return
|
|
|
+ return {
|
|
|
+ prompt_tokens: input.input,
|
|
|
+ completion_tokens: input.output,
|
|
|
+ total_tokens: input.input + input.output,
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
-function text(step: Extract<Step, { type: "text" }>) {
|
|
|
- const finish: Record<string, unknown> = {
|
|
|
+function chunk(input: { delta?: Record<string, unknown>; finish?: string; usage?: Usage }) {
|
|
|
+ return {
|
|
|
id: "chatcmpl-test",
|
|
|
object: "chat.completion.chunk",
|
|
|
- choices: [{ delta: {}, finish_reason: "stop" }],
|
|
|
- }
|
|
|
- if (step.usage) {
|
|
|
- finish.usage = {
|
|
|
- prompt_tokens: step.usage.input,
|
|
|
- completion_tokens: step.usage.output,
|
|
|
- total_tokens: step.usage.input + step.usage.output,
|
|
|
- }
|
|
|
- }
|
|
|
- return sse([
|
|
|
- {
|
|
|
- id: "chatcmpl-test",
|
|
|
- object: "chat.completion.chunk",
|
|
|
- choices: [{ delta: { role: "assistant" } }],
|
|
|
- },
|
|
|
- {
|
|
|
- id: "chatcmpl-test",
|
|
|
- object: "chat.completion.chunk",
|
|
|
- choices: [{ delta: { content: step.text } }],
|
|
|
- },
|
|
|
- finish,
|
|
|
- ])
|
|
|
+ choices: [
|
|
|
+ {
|
|
|
+ delta: input.delta ?? {},
|
|
|
+ ...(input.finish ? { finish_reason: input.finish } : {}),
|
|
|
+ },
|
|
|
+ ],
|
|
|
+ ...(input.usage ? { usage: tokens(input.usage) } : {}),
|
|
|
+ } satisfies Line
|
|
|
}
|
|
|
|
|
|
-function httpError(step: Extract<Step, { type: "httpError" }>) {
|
|
|
- return HttpServerResponse.text(JSON.stringify(step.body), {
|
|
|
- status: step.status,
|
|
|
- contentType: "application/json",
|
|
|
- })
|
|
|
+function role() {
|
|
|
+ return chunk({ delta: { role: "assistant" } })
|
|
|
}
|
|
|
|
|
|
-function tool(step: Extract<Step, { type: "tool" }>, seq: number) {
|
|
|
- const id = `call_${seq}`
|
|
|
- const args = JSON.stringify(step.input)
|
|
|
- return sse([
|
|
|
- {
|
|
|
- id: "chatcmpl-test",
|
|
|
- object: "chat.completion.chunk",
|
|
|
- choices: [{ delta: { role: "assistant" } }],
|
|
|
- },
|
|
|
- {
|
|
|
- id: "chatcmpl-test",
|
|
|
- object: "chat.completion.chunk",
|
|
|
- choices: [
|
|
|
+function textLine(value: string) {
|
|
|
+ return chunk({ delta: { content: value } })
|
|
|
+}
|
|
|
+
|
|
|
+function reasonLine(value: string) {
|
|
|
+ return chunk({ delta: { reasoning_content: value } })
|
|
|
+}
|
|
|
+
|
|
|
+function finishLine(reason: string, usage?: Usage) {
|
|
|
+ return chunk({ finish: reason, usage })
|
|
|
+}
|
|
|
+
|
|
|
+function toolStartLine(id: string, name: string) {
|
|
|
+ return chunk({
|
|
|
+ delta: {
|
|
|
+ tool_calls: [
|
|
|
{
|
|
|
- delta: {
|
|
|
- tool_calls: [
|
|
|
- {
|
|
|
- index: 0,
|
|
|
- id,
|
|
|
- type: "function",
|
|
|
- function: {
|
|
|
- name: step.tool,
|
|
|
- arguments: "",
|
|
|
- },
|
|
|
- },
|
|
|
- ],
|
|
|
+ index: 0,
|
|
|
+ id,
|
|
|
+ type: "function",
|
|
|
+ function: {
|
|
|
+ name,
|
|
|
+ arguments: "",
|
|
|
},
|
|
|
},
|
|
|
],
|
|
|
},
|
|
|
- {
|
|
|
- id: "chatcmpl-test",
|
|
|
- object: "chat.completion.chunk",
|
|
|
- choices: [
|
|
|
+ })
|
|
|
+}
|
|
|
+
|
|
|
+function toolArgsLine(value: string) {
|
|
|
+ return chunk({
|
|
|
+ delta: {
|
|
|
+ tool_calls: [
|
|
|
{
|
|
|
- delta: {
|
|
|
- tool_calls: [
|
|
|
- {
|
|
|
- index: 0,
|
|
|
- function: {
|
|
|
- arguments: args,
|
|
|
- },
|
|
|
- },
|
|
|
- ],
|
|
|
+ index: 0,
|
|
|
+ function: {
|
|
|
+ arguments: value,
|
|
|
},
|
|
|
},
|
|
|
],
|
|
|
},
|
|
|
- {
|
|
|
- id: "chatcmpl-test",
|
|
|
- object: "chat.completion.chunk",
|
|
|
- choices: [{ delta: {}, finish_reason: "tool_calls" }],
|
|
|
- },
|
|
|
- ])
|
|
|
+ })
|
|
|
}
|
|
|
|
|
|
-function fail(step: Extract<Step, { type: "fail" }>) {
|
|
|
- return HttpServerResponse.stream(
|
|
|
- Stream.fromIterable([
|
|
|
- 'data: {"id":"chatcmpl-test","object":"chat.completion.chunk","choices":[{"delta":{"role":"assistant"}}]}\n\n',
|
|
|
- ]).pipe(Stream.encodeText, Stream.concat(Stream.fail(new Error(step.message)))),
|
|
|
- { contentType: "text/event-stream" },
|
|
|
- )
|
|
|
+function bytes(input: Iterable<unknown>) {
|
|
|
+ return Stream.fromIterable([...input].map(line)).pipe(Stream.encodeText)
|
|
|
}
|
|
|
|
|
|
-function hang() {
|
|
|
- return HttpServerResponse.stream(
|
|
|
- Stream.fromIterable([
|
|
|
- 'data: {"id":"chatcmpl-test","object":"chat.completion.chunk","choices":[{"delta":{"role":"assistant"}}]}\n\n',
|
|
|
- ]).pipe(Stream.encodeText, Stream.concat(Stream.never)),
|
|
|
- { contentType: "text/event-stream" },
|
|
|
- )
|
|
|
+function send(item: Sse) {
|
|
|
+ const head = bytes(item.head)
|
|
|
+ const tail = bytes([...item.tail, ...(item.hang || item.error ? [] : [done])])
|
|
|
+ const empty = Stream.fromIterable<Uint8Array>([])
|
|
|
+ const wait = item.wait
|
|
|
+ const body: Stream.Stream<Uint8Array, unknown> = wait
|
|
|
+ ? Stream.concat(head, Stream.fromEffect(Effect.promise(() => wait)).pipe(Stream.flatMap(() => tail)))
|
|
|
+ : Stream.concat(head, tail)
|
|
|
+ let end: Stream.Stream<Uint8Array, unknown> = empty
|
|
|
+ if (item.error) end = Stream.concat(empty, Stream.fail(item.error))
|
|
|
+ else if (item.hang) end = Stream.concat(empty, Stream.never)
|
|
|
+
|
|
|
+ return HttpServerResponse.stream(Stream.concat(body, end), { contentType: "text/event-stream" })
|
|
|
}
|
|
|
|
|
|
-function hold(step: Extract<Step, { type: "hold" }>) {
|
|
|
- return HttpServerResponse.stream(
|
|
|
- Stream.fromIterable([
|
|
|
- 'data: {"id":"chatcmpl-test","object":"chat.completion.chunk","choices":[{"delta":{"role":"assistant"}}]}\n\n',
|
|
|
- ]).pipe(
|
|
|
- Stream.encodeText,
|
|
|
- Stream.concat(
|
|
|
- Stream.fromEffect(Effect.promise(() => step.wait)).pipe(
|
|
|
- Stream.flatMap(() =>
|
|
|
- Stream.fromIterable([
|
|
|
- `data: ${JSON.stringify({
|
|
|
- id: "chatcmpl-test",
|
|
|
- object: "chat.completion.chunk",
|
|
|
- choices: [{ delta: { content: step.text } }],
|
|
|
- })}\n\n`,
|
|
|
- `data: ${JSON.stringify({
|
|
|
- id: "chatcmpl-test",
|
|
|
- object: "chat.completion.chunk",
|
|
|
- choices: [{ delta: {}, finish_reason: "stop" }],
|
|
|
- })}\n\n`,
|
|
|
- "data: [DONE]\n\n",
|
|
|
- ]).pipe(Stream.encodeText),
|
|
|
- ),
|
|
|
- ),
|
|
|
- ),
|
|
|
- ),
|
|
|
- { contentType: "text/event-stream" },
|
|
|
- )
|
|
|
+const reset = Effect.fn("TestLLMServer.reset")(function* (item: Sse) {
|
|
|
+ const req = yield* HttpServerRequest.HttpServerRequest
|
|
|
+ const res = NodeHttpServerRequest.toServerResponse(req)
|
|
|
+ yield* Effect.sync(() => {
|
|
|
+ res.writeHead(200, { "content-type": "text/event-stream" })
|
|
|
+ for (const part of item.head) res.write(line(part))
|
|
|
+ for (const part of item.tail) res.write(line(part))
|
|
|
+ res.destroy(new Error("connection reset"))
|
|
|
+ })
|
|
|
+ yield* Effect.never
|
|
|
+})
|
|
|
+
|
|
|
+function fail(item: HttpError) {
|
|
|
+ return HttpServerResponse.text(JSON.stringify(item.body), {
|
|
|
+ status: item.status,
|
|
|
+ contentType: "application/json",
|
|
|
+ })
|
|
|
+}
|
|
|
+
|
|
|
+export class Reply {
|
|
|
+ #head: unknown[] = [role()]
|
|
|
+ #tail: unknown[] = []
|
|
|
+ #usage: Usage | undefined
|
|
|
+ #finish: string | undefined
|
|
|
+ #wait: PromiseLike<unknown> | undefined
|
|
|
+ #hang = false
|
|
|
+ #error: unknown
|
|
|
+ #reset = false
|
|
|
+ #seq = 0
|
|
|
+
|
|
|
+ #id() {
|
|
|
+ this.#seq += 1
|
|
|
+ return `call_${this.#seq}`
|
|
|
+ }
|
|
|
+
|
|
|
+ text(value: string) {
|
|
|
+ this.#tail = [...this.#tail, textLine(value)]
|
|
|
+ return this
|
|
|
+ }
|
|
|
+
|
|
|
+ reason(value: string) {
|
|
|
+ this.#tail = [...this.#tail, reasonLine(value)]
|
|
|
+ return this
|
|
|
+ }
|
|
|
+
|
|
|
+ usage(value: Usage) {
|
|
|
+ this.#usage = value
|
|
|
+ return this
|
|
|
+ }
|
|
|
+
|
|
|
+ wait(value: PromiseLike<unknown>) {
|
|
|
+ this.#wait = value
|
|
|
+ return this
|
|
|
+ }
|
|
|
+
|
|
|
+ stop() {
|
|
|
+ this.#finish = "stop"
|
|
|
+ this.#hang = false
|
|
|
+ this.#error = undefined
|
|
|
+ this.#reset = false
|
|
|
+ return this
|
|
|
+ }
|
|
|
+
|
|
|
+ toolCalls() {
|
|
|
+ this.#finish = "tool_calls"
|
|
|
+ this.#hang = false
|
|
|
+ this.#error = undefined
|
|
|
+ this.#reset = false
|
|
|
+ return this
|
|
|
+ }
|
|
|
+
|
|
|
+ tool(name: string, input: unknown) {
|
|
|
+ const id = this.#id()
|
|
|
+ const args = JSON.stringify(input)
|
|
|
+ this.#tail = [...this.#tail, toolStartLine(id, name), toolArgsLine(args)]
|
|
|
+ return this.toolCalls()
|
|
|
+ }
|
|
|
+
|
|
|
+ pendingTool(name: string, input: unknown) {
|
|
|
+ const id = this.#id()
|
|
|
+ const args = JSON.stringify(input)
|
|
|
+ const size = Math.max(1, Math.floor(args.length / 2))
|
|
|
+ this.#tail = [...this.#tail, toolStartLine(id, name), toolArgsLine(args.slice(0, size))]
|
|
|
+ return this
|
|
|
+ }
|
|
|
+
|
|
|
+ hang() {
|
|
|
+ this.#finish = undefined
|
|
|
+ this.#hang = true
|
|
|
+ this.#error = undefined
|
|
|
+ this.#reset = false
|
|
|
+ return this
|
|
|
+ }
|
|
|
+
|
|
|
+ streamError(error: unknown = "boom") {
|
|
|
+ this.#finish = undefined
|
|
|
+ this.#hang = false
|
|
|
+ this.#error = error
|
|
|
+ this.#reset = false
|
|
|
+ return this
|
|
|
+ }
|
|
|
+
|
|
|
+ reset() {
|
|
|
+ this.#finish = undefined
|
|
|
+ this.#hang = false
|
|
|
+ this.#error = undefined
|
|
|
+ this.#reset = true
|
|
|
+ return this
|
|
|
+ }
|
|
|
+
|
|
|
+ item(): Item {
|
|
|
+ return {
|
|
|
+ type: "sse",
|
|
|
+ head: this.#head,
|
|
|
+ tail: this.#finish ? [...this.#tail, finishLine(this.#finish, this.#usage)] : this.#tail,
|
|
|
+ wait: this.#wait,
|
|
|
+ hang: this.#hang,
|
|
|
+ error: this.#error,
|
|
|
+ reset: this.#reset,
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+export function reply() {
|
|
|
+ return new Reply()
|
|
|
+}
|
|
|
+
|
|
|
+export function httpError(status: number, body: unknown): Item {
|
|
|
+ return {
|
|
|
+ type: "http-error",
|
|
|
+ status,
|
|
|
+ body,
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+export function raw(input: {
|
|
|
+ chunks?: unknown[]
|
|
|
+ head?: unknown[]
|
|
|
+ tail?: unknown[]
|
|
|
+ wait?: PromiseLike<unknown>
|
|
|
+ hang?: boolean
|
|
|
+ error?: unknown
|
|
|
+ reset?: boolean
|
|
|
+}): Item {
|
|
|
+ return {
|
|
|
+ type: "sse",
|
|
|
+ head: input.head ?? input.chunks ?? [],
|
|
|
+ tail: input.tail ?? [],
|
|
|
+ wait: input.wait,
|
|
|
+ hang: input.hang,
|
|
|
+ error: input.error,
|
|
|
+ reset: input.reset,
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+function item(input: Item | Reply) {
|
|
|
+ return input instanceof Reply ? input.item() : input
|
|
|
}
|
|
|
|
|
|
namespace TestLLMServer {
|
|
|
export interface Service {
|
|
|
readonly url: string
|
|
|
+ readonly push: (...input: (Item | Reply)[]) => Effect.Effect<void>
|
|
|
readonly text: (value: string, opts?: { usage?: Usage }) => Effect.Effect<void>
|
|
|
- readonly tool: (tool: string, input: unknown) => Effect.Effect<void>
|
|
|
- readonly fail: (message?: string) => Effect.Effect<void>
|
|
|
+ readonly tool: (name: string, input: unknown) => Effect.Effect<void>
|
|
|
+ readonly toolHang: (name: string, input: unknown) => Effect.Effect<void>
|
|
|
+ readonly reason: (value: string, opts?: { text?: string; usage?: Usage }) => Effect.Effect<void>
|
|
|
+ readonly fail: (message?: unknown) => Effect.Effect<void>
|
|
|
readonly error: (status: number, body: unknown) => Effect.Effect<void>
|
|
|
readonly hang: Effect.Effect<void>
|
|
|
- readonly hold: (text: string, wait: PromiseLike<unknown>) => Effect.Effect<void>
|
|
|
+ readonly hold: (value: string, wait: PromiseLike<unknown>) => Effect.Effect<void>
|
|
|
readonly hits: Effect.Effect<Hit[]>
|
|
|
readonly calls: Effect.Effect<number>
|
|
|
readonly wait: (count: number) => Effect.Effect<void>
|
|
|
@@ -218,12 +321,11 @@ export class TestLLMServer extends ServiceMap.Service<TestLLMServer, TestLLMServ
|
|
|
const router = yield* HttpRouter.HttpRouter
|
|
|
|
|
|
let hits: Hit[] = []
|
|
|
- let list: Step[] = []
|
|
|
- let seq = 0
|
|
|
+ let list: Item[] = []
|
|
|
let waits: Wait[] = []
|
|
|
|
|
|
- const push = (step: Step) => {
|
|
|
- list = [...list, step]
|
|
|
+ const queue = (...input: (Item | Reply)[]) => {
|
|
|
+ list = [...list, ...input.map(item)]
|
|
|
}
|
|
|
|
|
|
const notify = Effect.fnUntraced(function* () {
|
|
|
@@ -234,11 +336,10 @@ export class TestLLMServer extends ServiceMap.Service<TestLLMServer, TestLLMServ
|
|
|
})
|
|
|
|
|
|
const pull = () => {
|
|
|
- const step = list[0]
|
|
|
- if (!step) return { step: undefined, seq }
|
|
|
- seq += 1
|
|
|
+ const first = list[0]
|
|
|
+ if (!first) return
|
|
|
list = list.slice(1)
|
|
|
- return { step, seq }
|
|
|
+ return first
|
|
|
}
|
|
|
|
|
|
yield* router.add(
|
|
|
@@ -247,22 +348,22 @@ export class TestLLMServer extends ServiceMap.Service<TestLLMServer, TestLLMServ
|
|
|
Effect.gen(function* () {
|
|
|
const req = yield* HttpServerRequest.HttpServerRequest
|
|
|
const next = pull()
|
|
|
- if (!next.step) return HttpServerResponse.text("unexpected request", { status: 500 })
|
|
|
- const json = yield* req.json.pipe(Effect.orElseSucceed(() => ({})))
|
|
|
+ if (!next) return HttpServerResponse.text("unexpected request", { status: 500 })
|
|
|
+ const body = yield* req.json.pipe(Effect.orElseSucceed(() => ({})))
|
|
|
hits = [
|
|
|
...hits,
|
|
|
{
|
|
|
url: new URL(req.originalUrl, "http://localhost"),
|
|
|
- body: json && typeof json === "object" ? (json as Record<string, unknown>) : {},
|
|
|
+ body: body && typeof body === "object" ? (body as Record<string, unknown>) : {},
|
|
|
},
|
|
|
]
|
|
|
yield* notify()
|
|
|
- if (next.step.type === "text") return text(next.step)
|
|
|
- if (next.step.type === "tool") return tool(next.step, next.seq)
|
|
|
- if (next.step.type === "fail") return fail(next.step)
|
|
|
- if (next.step.type === "httpError") return httpError(next.step)
|
|
|
- if (next.step.type === "hang") return hang()
|
|
|
- return hold(next.step)
|
|
|
+ if (next.type === "sse" && next.reset) {
|
|
|
+ yield* reset(next)
|
|
|
+ return HttpServerResponse.empty()
|
|
|
+ }
|
|
|
+ if (next.type === "sse") return send(next)
|
|
|
+ return fail(next)
|
|
|
}),
|
|
|
)
|
|
|
|
|
|
@@ -273,23 +374,37 @@ export class TestLLMServer extends ServiceMap.Service<TestLLMServer, TestLLMServ
|
|
|
server.address._tag === "TcpAddress"
|
|
|
? `http://127.0.0.1:${server.address.port}/v1`
|
|
|
: `unix://${server.address.path}/v1`,
|
|
|
+ push: Effect.fn("TestLLMServer.push")(function* (...input: (Item | Reply)[]) {
|
|
|
+ queue(...input)
|
|
|
+ }),
|
|
|
text: Effect.fn("TestLLMServer.text")(function* (value: string, opts?: { usage?: Usage }) {
|
|
|
- push({ type: "text", text: value, usage: opts?.usage })
|
|
|
+ const out = reply().text(value)
|
|
|
+ if (opts?.usage) out.usage(opts.usage)
|
|
|
+ queue(out.stop().item())
|
|
|
}),
|
|
|
- tool: Effect.fn("TestLLMServer.tool")(function* (tool: string, input: unknown) {
|
|
|
- push({ type: "tool", tool, input })
|
|
|
+ tool: Effect.fn("TestLLMServer.tool")(function* (name: string, input: unknown) {
|
|
|
+ queue(reply().tool(name, input).item())
|
|
|
}),
|
|
|
- fail: Effect.fn("TestLLMServer.fail")(function* (message = "boom") {
|
|
|
- push({ type: "fail", message })
|
|
|
+ toolHang: Effect.fn("TestLLMServer.toolHang")(function* (name: string, input: unknown) {
|
|
|
+ queue(reply().pendingTool(name, input).hang().item())
|
|
|
+ }),
|
|
|
+ reason: Effect.fn("TestLLMServer.reason")(function* (value: string, opts?: { text?: string; usage?: Usage }) {
|
|
|
+ const out = reply().reason(value)
|
|
|
+ if (opts?.text) out.text(opts.text)
|
|
|
+ if (opts?.usage) out.usage(opts.usage)
|
|
|
+ queue(out.stop().item())
|
|
|
+ }),
|
|
|
+ fail: Effect.fn("TestLLMServer.fail")(function* (message: unknown = "boom") {
|
|
|
+ queue(reply().streamError(message).item())
|
|
|
}),
|
|
|
- hang: Effect.gen(function* () {
|
|
|
- push({ type: "hang" })
|
|
|
- }).pipe(Effect.withSpan("TestLLMServer.hang")),
|
|
|
error: Effect.fn("TestLLMServer.error")(function* (status: number, body: unknown) {
|
|
|
- push({ type: "httpError", status, body })
|
|
|
+ queue(httpError(status, body))
|
|
|
}),
|
|
|
- hold: Effect.fn("TestLLMServer.hold")(function* (text: string, wait: PromiseLike<unknown>) {
|
|
|
- push({ type: "hold", text, wait })
|
|
|
+ hang: Effect.gen(function* () {
|
|
|
+ queue(reply().hang().item())
|
|
|
+ }).pipe(Effect.withSpan("TestLLMServer.hang")),
|
|
|
+ hold: Effect.fn("TestLLMServer.hold")(function* (value: string, wait: PromiseLike<unknown>) {
|
|
|
+ queue(reply().wait(wait).text(value).stop().item())
|
|
|
}),
|
|
|
hits: Effect.sync(() => [...hits]),
|
|
|
calls: Effect.sync(() => hits.length),
|
|
|
@@ -303,8 +418,5 @@ export class TestLLMServer extends ServiceMap.Service<TestLLMServer, TestLLMServ
|
|
|
pending: Effect.sync(() => list.length),
|
|
|
})
|
|
|
}),
|
|
|
- ).pipe(
|
|
|
- Layer.provide(HttpRouter.layer), //
|
|
|
- Layer.provide(NodeHttpServer.layer(() => Http.createServer(), { port: 0 })),
|
|
|
- )
|
|
|
+ ).pipe(Layer.provide(HttpRouter.layer), Layer.provide(NodeHttpServer.layer(() => Http.createServer(), { port: 0 })))
|
|
|
}
|