Skip to content

Commit 023e1c7

Browse files
authored
refactor(llm): colocate per-type factories on their namespaces (#26799)
1 parent 52f7ba7 commit 023e1c7

13 files changed

Lines changed: 91 additions & 111 deletions

packages/llm/AGENTS.md

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,10 @@
88
- In `Effect.gen`, yield yieldable errors directly (`return yield* new MyError(...)`) instead of `Effect.fail(new MyError(...))`.
99
- Use `Effect.void` instead of `Effect.succeed(undefined)` when the successful value is intentionally void.
1010

11+
## Conventions
12+
13+
Per-type constructors live on the type's namespace, not as top-level re-exports. Use `Message.user(...)`, `Message.assistant(...)`, `Message.tool(...)`, `ToolDefinition.make(...)`, `ToolCallPart.make(...)`, `ToolResultPart.make(...)`, `ToolChoice.make(...)`, `ToolChoice.named(...)`, `SystemPart.make(...)`, and `GenerationOptions.make(...)` directly. The top-level `LLM` namespace is reserved for the request-shaped call API: `LLM.request`, `LLM.generate`, `LLM.stream`, `LLM.model`, `LLM.updateRequest`, `LLM.generateObject`. Two ways to construct the same thing is one too many.
14+
1115
## Tests
1216

1317
- Use `testEffect(...)` from `test/lib/effect.ts` for tests requiring Effect layers.
@@ -166,12 +170,12 @@ If you find yourself copying a 3-to-5-line snippet between two protocols, lift i
166170
Tool loops are represented in common messages and events:
167171

168172
```ts
169-
const call = LLM.toolCall({ id: "call_1", name: "lookup", input: { query: "weather" } })
170-
const result = LLM.toolMessage({ id: "call_1", name: "lookup", result: { forecast: "sunny" } })
173+
const call = ToolCallPart.make({ id: "call_1", name: "lookup", input: { query: "weather" } })
174+
const result = Message.tool({ id: "call_1", name: "lookup", result: { forecast: "sunny" } })
171175

172176
const followUp = LLM.request({
173177
model,
174-
messages: [LLM.user("Weather?"), LLM.assistant([call]), result],
178+
messages: [Message.user("Weather?"), Message.assistant([call]), result],
175179
})
176180
```
177181

packages/llm/src/llm.ts

Lines changed: 4 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -44,32 +44,8 @@ export type RequestInput = Omit<
4444

4545
export const limits = modelLimits
4646

47-
export const text = Message.text
48-
49-
export const system = SystemPart.make
50-
51-
export const message = Message.make
52-
53-
export const user = Message.user
54-
55-
export const assistant = Message.assistant
56-
5747
export const model = modelRef
5848

59-
export const toolDefinition = ToolDefinition.make
60-
61-
export const toolCall = ToolCallPart.make
62-
63-
export const toolResult = ToolResultPart.make
64-
65-
export const toolMessage = Message.tool
66-
67-
export const toolChoiceName = ToolChoice.named
68-
69-
export const toolChoice = ToolChoice.make
70-
71-
export const generation = GenerationOptions.make
72-
7349
export const generate = LLMClient.generate
7450

7551
export const stream = LLMClient.stream
@@ -95,10 +71,10 @@ export const request = (input: RequestInput) => {
9571
return new LLMRequest({
9672
...rest,
9773
system: SystemPart.content(requestSystem),
98-
messages: [...(messages?.map(message) ?? []), ...(prompt === undefined ? [] : [user(prompt)])],
99-
tools: tools?.map(toolDefinition) ?? [],
100-
toolChoice: requestToolChoice ? toolChoice(requestToolChoice) : undefined,
101-
generation: requestGeneration === undefined ? undefined : generation(requestGeneration),
74+
messages: [...(messages?.map(Message.make) ?? []), ...(prompt === undefined ? [] : [Message.user(prompt)])],
75+
tools: tools?.map(ToolDefinition.make) ?? [],
76+
toolChoice: requestToolChoice ? ToolChoice.make(requestToolChoice) : undefined,
77+
generation: requestGeneration === undefined ? undefined : GenerationOptions.make(requestGeneration),
10278
providerOptions: requestProviderOptions,
10379
http: requestHttp === undefined ? undefined : HttpOptions.make(requestHttp),
10480
})

packages/llm/test/cache-policy.test.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { describe, expect, test } from "bun:test"
22
import { Effect } from "effect"
3-
import { CacheHint, LLM } from "../src"
3+
import { CacheHint, LLM, Message } from "../src"
44
import { LLMClient } from "../src/route"
55
import * as AnthropicMessages from "../src/protocols/anthropic-messages"
66
import * as BedrockConverse from "../src/protocols/bedrock-converse"
@@ -59,7 +59,7 @@ describe("applyCachePolicy", () => {
5959
model: anthropicModel,
6060
system: "Sys A",
6161
tools: [{ name: "t1", description: "t1", inputSchema: { type: "object", properties: {} } }],
62-
messages: [LLM.user("first user"), LLM.assistant("assistant reply"), LLM.user("latest user message")],
62+
messages: [Message.user("first user"), Message.assistant("assistant reply"), Message.user("latest user message")],
6363
cache: "auto",
6464
}),
6565
)
@@ -122,7 +122,7 @@ describe("applyCachePolicy", () => {
122122
model: bedrockModel,
123123
system: "Sys",
124124
tools: [{ name: "t1", description: "t1", inputSchema: { type: "object", properties: {} } }],
125-
messages: [LLM.user("first user"), LLM.assistant("reply"), LLM.user("latest user")],
125+
messages: [Message.user("first user"), Message.assistant("reply"), Message.user("latest user")],
126126
cache: "auto",
127127
}),
128128
)
@@ -221,7 +221,7 @@ describe("applyCachePolicy", () => {
221221
const prepared = yield* LLMClient.prepare(
222222
LLM.request({
223223
model: anthropicModel,
224-
messages: [LLM.user("u1"), LLM.assistant("a1"), LLM.user("u2"), LLM.assistant("a2")],
224+
messages: [Message.user("u1"), Message.assistant("a1"), Message.user("u2"), Message.assistant("a2")],
225225
cache: { messages: { tail: 2 } },
226226
}),
227227
)
@@ -239,7 +239,7 @@ describe("applyCachePolicy", () => {
239239
const prepared = yield* LLMClient.prepare(
240240
LLM.request({
241241
model: anthropicModel,
242-
messages: [LLM.user("u1"), LLM.assistant("a1"), LLM.user("u2")],
242+
messages: [Message.user("u1"), Message.assistant("a1"), Message.user("u2")],
243243
cache: { messages: "latest-assistant" },
244244
}),
245245
)

packages/llm/test/llm.test.ts

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { describe, expect, test } from "bun:test"
22
import { LLM, LLMResponse } from "../src"
3-
import { LLMRequest, Message, ModelRef, ToolChoice, ToolDefinition } from "../src/schema"
3+
import { LLMRequest, Message, ModelRef, ToolCallPart, ToolChoice, ToolDefinition, ToolResultPart } from "../src/schema"
44

55
describe("llm constructors", () => {
66
test("builds canonical schema classes from ergonomic input", () => {
@@ -28,7 +28,7 @@ describe("llm constructors", () => {
2828
})
2929
const updated = LLM.updateRequest(base, {
3030
generation: { maxTokens: 20 },
31-
messages: [...base.messages, LLM.assistant("Hi.")],
31+
messages: [...base.messages, Message.assistant("Hi.")],
3232
})
3333

3434
expect(updated).toBeInstanceOf(LLMRequest)
@@ -70,7 +70,7 @@ describe("llm constructors", () => {
7070
model: LLM.model({ id: "fake-model", provider: "fake", route: "openai-chat", baseURL: "https://fake.local" }),
7171
prompt: "Say hello.",
7272
})
73-
const updated = LLMRequest.update(base, { messages: [...base.messages, LLM.assistant("Hi.")] })
73+
const updated = LLMRequest.update(base, { messages: [...base.messages, Message.assistant("Hi.")] })
7474

7575
expect(updated).toBeInstanceOf(LLMRequest)
7676
expect(updated.id).toBe("req_1")
@@ -91,18 +91,18 @@ describe("llm constructors", () => {
9191
})
9292

9393
test("builds tool choices from names and tools", () => {
94-
const tool = LLM.toolDefinition({ name: "lookup", description: "Lookup data", inputSchema: { type: "object" } })
94+
const tool = ToolDefinition.make({ name: "lookup", description: "Lookup data", inputSchema: { type: "object" } })
9595

9696
expect(tool).toBeInstanceOf(ToolDefinition)
97-
expect(LLM.toolChoice("lookup")).toEqual(new ToolChoice({ type: "tool", name: "lookup" }))
98-
expect(LLM.toolChoiceName("required")).toEqual(new ToolChoice({ type: "tool", name: "required" }))
99-
expect(LLM.toolChoice(tool)).toEqual(new ToolChoice({ type: "tool", name: "lookup" }))
97+
expect(ToolChoice.make("lookup")).toEqual(new ToolChoice({ type: "tool", name: "lookup" }))
98+
expect(ToolChoice.named("required")).toEqual(new ToolChoice({ type: "tool", name: "required" }))
99+
expect(ToolChoice.make(tool)).toEqual(new ToolChoice({ type: "tool", name: "lookup" }))
100100
})
101101

102102
test("builds tool choice modes from reserved strings", () => {
103-
expect(LLM.toolChoice("auto")).toEqual(new ToolChoice({ type: "auto" }))
104-
expect(LLM.toolChoice("none")).toEqual(new ToolChoice({ type: "none" }))
105-
expect(LLM.toolChoice("required")).toEqual(new ToolChoice({ type: "required" }))
103+
expect(ToolChoice.make("auto")).toEqual(new ToolChoice({ type: "auto" }))
104+
expect(ToolChoice.make("none")).toEqual(new ToolChoice({ type: "none" }))
105+
expect(ToolChoice.make("required")).toEqual(new ToolChoice({ type: "required" }))
106106
expect(
107107
LLM.request({
108108
model: LLM.model({ id: "fake-model", provider: "fake", route: "openai-chat", baseURL: "https://fake.local" }),
@@ -113,11 +113,11 @@ describe("llm constructors", () => {
113113
})
114114

115115
test("builds assistant tool calls and tool result messages", () => {
116-
const call = LLM.toolCall({ id: "call_1", name: "lookup", input: { query: "weather" } })
117-
const result = LLM.toolResult({ id: "call_1", name: "lookup", result: { temperature: 72 } })
116+
const call = ToolCallPart.make({ id: "call_1", name: "lookup", input: { query: "weather" } })
117+
const result = ToolResultPart.make({ id: "call_1", name: "lookup", result: { temperature: 72 } })
118118

119-
expect(LLM.assistant([call]).content).toEqual([call])
120-
expect(LLM.toolMessage(result).content).toEqual([
119+
expect(Message.assistant([call]).content).toEqual([call])
120+
expect(Message.tool(result).content).toEqual([
121121
{ type: "tool-result", id: "call_1", name: "lookup", result: { type: "json", value: { temperature: 72 } } },
122122
])
123123
})

packages/llm/test/provider/anthropic-messages.recorded.test.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { Redactor } from "@opencode-ai/http-recorder"
22
import { describe, expect } from "bun:test"
33
import { Effect } from "effect"
4-
import { LLM, LLMError } from "../../src"
4+
import { LLM, LLMError, Message, ToolCallPart } from "../../src"
55
import { LLMClient } from "../../src/route"
66
import * as AnthropicMessages from "../../src/protocols/anthropic-messages"
77
import { weatherToolName } from "../recorded-scenarios"
@@ -16,12 +16,12 @@ const malformedToolOrderRequest = LLM.request({
1616
id: "recorded_anthropic_malformed_tool_order",
1717
model,
1818
messages: [
19-
LLM.assistant([
20-
LLM.toolCall({ id: "call_1", name: weatherToolName, input: { city: "Paris" } }),
19+
Message.assistant([
20+
ToolCallPart.make({ id: "call_1", name: weatherToolName, input: { city: "Paris" } }),
2121
{ type: "text", text: "I will check the weather." },
2222
]),
23-
LLM.toolMessage({ id: "call_1", name: weatherToolName, result: { temperature: "72F" } }),
24-
LLM.user("Use that result to answer briefly."),
23+
Message.tool({ id: "call_1", name: weatherToolName, result: { temperature: "72F" } }),
24+
Message.user("Use that result to answer briefly."),
2525
],
2626
tools: [{ name: weatherToolName, description: "Get weather", inputSchema: { type: "object", properties: {} } }],
2727
})

packages/llm/test/provider/anthropic-messages.test.ts

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { describe, expect } from "bun:test"
22
import { Effect } from "effect"
3-
import { CacheHint, LLM, LLMError, Usage } from "../../src"
3+
import { CacheHint, LLM, LLMError, Message, ToolCallPart, Usage } from "../../src"
44
import { LLMClient } from "../../src/route"
55
import * as AnthropicMessages from "../../src/protocols/anthropic-messages"
66
import { it } from "../lib/effect"
@@ -47,9 +47,9 @@ describe("Anthropic Messages route", () => {
4747
id: "req_tool_result",
4848
model,
4949
messages: [
50-
LLM.user("What is the weather?"),
51-
LLM.assistant([LLM.toolCall({ id: "call_1", name: "lookup", input: { query: "weather" } })]),
52-
LLM.toolMessage({ id: "call_1", name: "lookup", result: { forecast: "sunny" } }),
50+
Message.user("What is the weather?"),
51+
Message.assistant([ToolCallPart.make({ id: "call_1", name: "lookup", input: { query: "weather" } })]),
52+
Message.tool({ id: "call_1", name: "lookup", result: { forecast: "sunny" } }),
5353
],
5454
cache: "none",
5555
}),
@@ -77,7 +77,7 @@ describe("Anthropic Messages route", () => {
7777
LLM.request({
7878
model,
7979
messages: [
80-
LLM.assistant([
80+
Message.assistant([
8181
{ type: "reasoning", text: "thinking", providerMetadata: { anthropic: { signature: "sig_1" } } },
8282
]),
8383
],
@@ -304,8 +304,8 @@ describe("Anthropic Messages route", () => {
304304
id: "req_round_trip",
305305
model,
306306
messages: [
307-
LLM.user("Search for something."),
308-
LLM.assistant([
307+
Message.user("Search for something."),
308+
Message.assistant([
309309
{
310310
type: "tool-call",
311311
id: "srvtoolu_abc",
@@ -322,7 +322,7 @@ describe("Anthropic Messages route", () => {
322322
},
323323
{ type: "text", text: "Found it." },
324324
]),
325-
LLM.user("Thanks."),
325+
Message.user("Thanks."),
326326
],
327327
}),
328328
)
@@ -355,7 +355,7 @@ describe("Anthropic Messages route", () => {
355355
id: "req_unknown_server_tool",
356356
model,
357357
messages: [
358-
LLM.assistant([
358+
Message.assistant([
359359
{
360360
type: "tool-result",
361361
id: "srvtoolu_abc",
@@ -378,7 +378,7 @@ describe("Anthropic Messages route", () => {
378378
LLM.request({
379379
id: "req_media",
380380
model,
381-
messages: [LLM.user({ type: "media", mediaType: "image/png", data: "AAECAw==" })],
381+
messages: [Message.user({ type: "media", mediaType: "image/png", data: "AAECAw==" })],
382382
}),
383383
).pipe(Effect.flip)
384384

@@ -416,9 +416,9 @@ describe("Anthropic Messages route", () => {
416416
},
417417
],
418418
messages: [
419-
LLM.user("What's the weather?"),
420-
LLM.assistant([LLM.toolCall({ id: "call_1", name: "lookup", input: {} })]),
421-
LLM.toolMessage({
419+
Message.user("What's the weather?"),
420+
Message.assistant([ToolCallPart.make({ id: "call_1", name: "lookup", input: {} })]),
421+
Message.tool({
422422
id: "call_1",
423423
name: "lookup",
424424
result: { temp: 72 },
@@ -501,7 +501,7 @@ describe("Anthropic Messages route", () => {
501501
},
502502
],
503503
system: [{ type: "text", text: "system-tail", cache: hint }],
504-
messages: [LLM.user([{ type: "text", text: "message-tail", cache: hint }])],
504+
messages: [Message.user([{ type: "text", text: "message-tail", cache: hint }])],
505505
}),
506506
)
507507

0 commit comments

Comments
 (0)