From 35d31d8ec15e3f479f25968a73e79ced003a2d06 Mon Sep 17 00:00:00 2001 From: Shoubhit Dash Date: Fri, 24 Jul 2026 19:49:07 +0530 Subject: [PATCH] refactor(ai): remove dead LLM exports (#38700) --- packages/ai/AGENTS.md | 2 +- packages/ai/DESIGN.md | 5 +- packages/ai/example/tutorial.ts | 4 +- packages/ai/src/llm.ts | 22 +------- packages/ai/test/adapter.test.ts | 6 +-- packages/ai/test/llm.test.ts | 15 ++++-- .../test/provider/anthropic-messages.test.ts | 34 ++++++------ .../ai/test/provider/bedrock-converse.test.ts | 44 ++++++++------- packages/ai/test/provider/gemini.test.ts | 20 +++---- packages/ai/test/provider/openai-chat.test.ts | 53 ++++++++++-------- .../provider/openai-compatible-chat.test.ts | 8 +-- .../ai/test/provider/openai-responses.test.ts | 54 ++++++++++--------- packages/ai/test/recorded-scenarios.ts | 12 ++--- packages/ai/test/tool-runtime.test.ts | 4 +- 14 files changed, 150 insertions(+), 133 deletions(-) diff --git a/packages/ai/AGENTS.md b/packages/ai/AGENTS.md index f02b5616b22..6c35cafc344 100644 --- a/packages/ai/AGENTS.md +++ b/packages/ai/AGENTS.md @@ -241,7 +241,7 @@ const get_weather = tool({ const tools = { get_weather, get_time, ... } const events = yield* LLM.stream( - LLM.updateRequest(request, { tools: Tool.toDefinitions(tools) }), + LLMRequest.update(request, { tools: Tool.toDefinitions(tools) }), ).pipe(Stream.runCollect) const call = Array.from(events).find(LLMEvent.is.toolCall) diff --git a/packages/ai/DESIGN.md b/packages/ai/DESIGN.md index 2e73360300d..5629c13408d 100644 --- a/packages/ai/DESIGN.md +++ b/packages/ai/DESIGN.md @@ -315,7 +315,8 @@ const longer = { } ``` -There is no `LLM.updateRequest(...)` helper and no request Schema class. +There is no `LLM.updateRequest(...)` helper. The current Schema-backed implementation +uses `LLMRequest.update(...)` when canonical request data must be derived. ### Conversation history @@ -436,7 +437,7 @@ const call = Array.from(events).find(LLMEvent.is.toolCall) if (call && !call.providerExecuted) { const dispatched = yield * ToolRuntime.dispatch(tools, call) - const followUp = LLM.updateRequest(request, { + const followUp = LLMRequest.update(request, { messages: [...request.messages, Message.assistant([call]), Message.tool({ ...call, result: dispatched.result })], }) // Caller must invoke the provider again and repeat the loop. diff --git a/packages/ai/example/tutorial.ts b/packages/ai/example/tutorial.ts index 3fc0603b734..7ee4abb1463 100644 --- a/packages/ai/example/tutorial.ts +++ b/packages/ai/example/tutorial.ts @@ -1,5 +1,5 @@ import { Config, Effect, Formatter, Layer, Schema, Stream } from "effect" -import { LLM, LLMClient, Message, ProviderID, Tool, ToolRuntime } from "@opencode-ai/ai" +import { LLM, LLMClient, LLMRequest, Message, ProviderID, Tool, ToolRuntime } from "@opencode-ai/ai" import { Route, Auth, Endpoint, Framing, Protocol, RequestExecutor, WebSocketExecutor } from "@opencode-ai/ai/route" import { OpenAI } from "@opencode-ai/ai/providers" @@ -116,7 +116,7 @@ const streamWithTools = Effect.gen(function* () { // A durable agent would persist these messages before starting another // raw model turn. This tutorial keeps the boundary visible instead. - const followUp = LLM.updateRequest(request, { + const followUp = LLMRequest.update(request, { messages: [ ...request.messages, Message.assistant([event]), diff --git a/packages/ai/src/llm.ts b/packages/ai/src/llm.ts index ecdf30ae47d..8b6f904a5b4 100644 --- a/packages/ai/src/llm.ts +++ b/packages/ai/src/llm.ts @@ -9,24 +9,13 @@ import { LLMRequest, LLMResponse, Message, - type ModelInput as SchemaModelInput, SystemPart, ToolChoice, ToolDefinition, type ContentPart, - ToolResultPart, } from "./schema" import { make as makeTool, toDefinitions, type ToolSchema } from "./tool" -export type ModelInput = SchemaModelInput - -export type MessageInput = Message.Input - -export type ToolChoiceInput = ToolChoice.Input -export type ToolChoiceMode = ToolChoice.Mode - -export type ToolResultInput = Parameters[0] - /** Input accepted by `LLM.request`, normalized into the canonical `LLMRequest` class. */ export type RequestInput = Omit< ConstructorParameters[0], @@ -34,9 +23,9 @@ export type RequestInput = Omit< > & { readonly system?: string | SystemPart | ReadonlyArray readonly prompt?: string | ContentPart | ReadonlyArray - readonly messages?: ReadonlyArray + readonly messages?: ReadonlyArray readonly tools?: ReadonlyArray - readonly toolChoice?: ToolChoiceInput + readonly toolChoice?: ToolChoice.Input readonly generation?: GenerationOptions.Input readonly providerOptions?: ConstructorParameters[0]["providerOptions"] readonly http?: HttpOptions.Input @@ -46,10 +35,6 @@ export const generate = LLMClient.generate export const stream = LLMClient.stream -export const requestInput = (input: LLMRequest): RequestInput => ({ - ...LLMRequest.input(input), -}) - export const request = (input: RequestInput) => { const { system: requestSystem, @@ -74,9 +59,6 @@ export const request = (input: RequestInput) => { }) } -export const updateRequest = (input: LLMRequest, patch: Partial) => - request({ ...requestInput(input), ...patch }) - const GENERATE_OBJECT_TOOL_NAME = "generate_object" const GENERATE_OBJECT_TOOL_DESCRIPTION = "Return the structured result by calling this tool." diff --git a/packages/ai/test/adapter.test.ts b/packages/ai/test/adapter.test.ts index 346013ced6d..b2b180a2b22 100644 --- a/packages/ai/test/adapter.test.ts +++ b/packages/ai/test/adapter.test.ts @@ -1,6 +1,6 @@ import { describe, expect } from "bun:test" import { Effect, Schema, Stream } from "effect" -import { LLM, LLMResponse } from "../src" +import { LLM, LLMRequest, LLMResponse } from "../src" import { Route, Endpoint, LLMClient, Protocol, type FramingDef } from "../src/route" import { Model } from "../src/schema" import { testEffect } from "./lib/effect" @@ -141,7 +141,7 @@ describe("llm route", () => { Effect.gen(function* () { const llm = yield* LLMClient.Service const prepared = yield* llm.prepare( - LLM.updateRequest(request, { model: updateModel(request.model, { route: configuredGemini }) }), + LLMRequest.update(request, { model: updateModel(request.model, { route: configuredGemini }) }), ) expect(prepared.route).toBe("gemini-fake") @@ -174,7 +174,7 @@ describe("llm route", () => { }) const prepared = yield* (yield* LLMClient.Service).prepare( - LLM.updateRequest(request, { model: updateModel(request.model, { route: duplicate }) }), + LLMRequest.update(request, { model: updateModel(request.model, { route: duplicate }) }), ) expect(prepared.body).toEqual({ body: "late-default" }) diff --git a/packages/ai/test/llm.test.ts b/packages/ai/test/llm.test.ts index ca882935856..e5588cf95d6 100644 --- a/packages/ai/test/llm.test.ts +++ b/packages/ai/test/llm.test.ts @@ -2,7 +2,16 @@ import { describe, expect, test } from "bun:test" import { CacheHint, LLM, LLMResponse } from "../src" import * as OpenAIChat from "../src/protocols/openai-chat" import * as OpenAIResponses from "../src/protocols/openai-responses" -import { LLMRequest, Message, Model, ToolCallPart, ToolChoice, ToolDefinition, ToolResultPart } from "../src/schema" +import { + GenerationOptions, + LLMRequest, + Message, + Model, + ToolCallPart, + ToolChoice, + ToolDefinition, + ToolResultPart, +} from "../src/schema" const chatRoute = OpenAIChat.route const responsesRoute = OpenAIResponses.route @@ -31,8 +40,8 @@ describe("llm constructors", () => { model: Model.make({ id: "fake-model", provider: "fake", route: chatRoute }), prompt: "Say hello.", }) - const updated = LLM.updateRequest(base, { - generation: { maxTokens: 20 }, + const updated = LLMRequest.update(base, { + generation: GenerationOptions.make({ maxTokens: 20 }), messages: [...base.messages, Message.assistant("Hi.")], }) diff --git a/packages/ai/test/provider/anthropic-messages.test.ts b/packages/ai/test/provider/anthropic-messages.test.ts index 564cf499001..acdab6a7b0c 100644 --- a/packages/ai/test/provider/anthropic-messages.test.ts +++ b/packages/ai/test/provider/anthropic-messages.test.ts @@ -1,7 +1,7 @@ import { describe, expect } from "bun:test" import { Effect } from "effect" import { HttpClientRequest } from "effect/unstable/http" -import { CacheHint, LLM, LLMError, Message, ToolCallPart, Usage } from "../../src" +import { CacheHint, LLM, LLMError, LLMRequest, Message, ToolCallPart, ToolDefinition, Usage } from "../../src" import { Auth, LLMClient } from "../../src/route" import * as AnthropicMessages from "../../src/protocols/anthropic-messages" import { continuationRequest, nativeAnthropicMessagesContinuation } from "../continuation-scenarios" @@ -60,7 +60,7 @@ describe("Anthropic Messages route", () => { it.effect("lowers adaptive thinking settings with effort", () => Effect.gen(function* () { const prepared = yield* LLMClient.prepare( - LLM.updateRequest(request, { + LLMRequest.update(request, { providerOptions: { anthropic: { thinking: { type: "adaptive", display: "summarized" }, effort: "low" }, }, @@ -77,17 +77,17 @@ describe("Anthropic Messages route", () => { it.effect("normalizes enabled and disabled thinking settings", () => Effect.gen(function* () { const enabled = yield* LLMClient.prepare( - LLM.updateRequest(request, { + LLMRequest.update(request, { providerOptions: { anthropic: { thinking: { type: "enabled", budgetTokens: 1_024 } } }, }), ) const legacy = yield* LLMClient.prepare( - LLM.updateRequest(request, { + LLMRequest.update(request, { providerOptions: { anthropic: { thinking: { type: "enabled", budget_tokens: 2_048 } } }, }), ) const disabled = yield* LLMClient.prepare( - LLM.updateRequest(request, { + LLMRequest.update(request, { providerOptions: { anthropic: { thinking: { type: "disabled" } } }, }), ) @@ -101,7 +101,7 @@ describe("Anthropic Messages route", () => { it.effect("rejects enabled thinking without a budget", () => Effect.gen(function* () { const error = yield* LLMClient.prepare( - LLM.updateRequest(request, { + LLMRequest.update(request, { providerOptions: { anthropic: { thinking: { type: "enabled" } } }, }), ).pipe(Effect.flip) @@ -548,8 +548,8 @@ describe("Anthropic Messages route", () => { // contents are provider-owned and must be replayed without inspection. const redactedData = "cmVkYWN0ZWQtdGhpbmtpbmc=" const response = yield* LLMClient.generate( - LLM.updateRequest(request, { - tools: [{ name: "lookup", description: "Lookup data", inputSchema: { type: "object" } }], + LLMRequest.update(request, { + tools: [ToolDefinition.make({ name: "lookup", description: "Lookup data", inputSchema: { type: "object" } })], }), ).pipe( Effect.provide( @@ -587,7 +587,7 @@ describe("Anthropic Messages route", () => { response.message, Message.tool({ id: "call_1", name: "lookup", result: "sunny", resultType: "text" }), ], - tools: [{ name: "lookup", description: "Lookup data", inputSchema: { type: "object" } }], + tools: [ToolDefinition.make({ name: "lookup", description: "Lookup data", inputSchema: { type: "object" } })], cache: "none", }), ) @@ -666,8 +666,8 @@ describe("Anthropic Messages route", () => { { type: "message_delta", delta: { stop_reason: "tool_use" }, usage: { output_tokens: 1 } }, ) const response = yield* LLMClient.generate( - LLM.updateRequest(request, { - tools: [{ name: "lookup", description: "Lookup data", inputSchema: { type: "object" } }], + LLMRequest.update(request, { + tools: [ToolDefinition.make({ name: "lookup", description: "Lookup data", inputSchema: { type: "object" } })], }), ).pipe(Effect.provide(fixedResponse(body))) const usage = new Usage({ @@ -851,8 +851,10 @@ describe("Anthropic Messages route", () => { { type: "message_delta", delta: { stop_reason: "end_turn" }, usage: { output_tokens: 8 } }, ) const response = yield* LLMClient.generate( - LLM.updateRequest(request, { - tools: [{ name: "web_search", description: "Web search", inputSchema: { type: "object" } }], + LLMRequest.update(request, { + tools: [ + ToolDefinition.make({ name: "web_search", description: "Web search", inputSchema: { type: "object" } }), + ], }), ).pipe(Effect.provide(fixedResponse(body))) @@ -912,8 +914,10 @@ describe("Anthropic Messages route", () => { { type: "message_delta", delta: { stop_reason: "end_turn" }, usage: { output_tokens: 1 } }, ) const response = yield* LLMClient.generate( - LLM.updateRequest(request, { - tools: [{ name: "web_search", description: "Web search", inputSchema: { type: "object" } }], + LLMRequest.update(request, { + tools: [ + ToolDefinition.make({ name: "web_search", description: "Web search", inputSchema: { type: "object" } }), + ], }), ).pipe(Effect.provide(fixedResponse(body))) diff --git a/packages/ai/test/provider/bedrock-converse.test.ts b/packages/ai/test/provider/bedrock-converse.test.ts index 2aa75057f63..05c0c669fc1 100644 --- a/packages/ai/test/provider/bedrock-converse.test.ts +++ b/packages/ai/test/provider/bedrock-converse.test.ts @@ -2,7 +2,16 @@ import { EventStreamCodec } from "@smithy/eventstream-codec" import { fromUtf8, toUtf8 } from "@smithy/util-utf8" import { describe, expect } from "bun:test" import { Effect } from "effect" -import { CacheHint, LLM, Message, ToolCallPart, ToolChoice } from "../../src" +import { + CacheHint, + GenerationOptions, + LLM, + LLMRequest, + Message, + ToolCallPart, + ToolChoice, + ToolDefinition, +} from "../../src" import { LLMClient } from "../../src/route" import { AmazonBedrock } from "../../src/providers" import * as BedrockConverse from "../../src/protocols/bedrock-converse" @@ -86,7 +95,9 @@ describe("Bedrock Converse route", () => { it.effect("passes topK through additionalModelRequestFields as top_k", () => Effect.gen(function* () { const prepared = yield* LLMClient.prepare( - LLM.updateRequest(baseRequest, { generation: { maxTokens: 64, temperature: 0, topK: 40 } }), + LLMRequest.update(baseRequest, { + generation: GenerationOptions.make({ maxTokens: 64, temperature: 0, topK: 40 }), + }), ) // Converse's inferenceConfig has no topK; Anthropic/Nova read it from @@ -123,13 +134,13 @@ describe("Bedrock Converse route", () => { it.effect("prepares tool config with toolSpec and toolChoice", () => Effect.gen(function* () { const prepared = yield* LLMClient.prepare( - LLM.updateRequest(baseRequest, { + LLMRequest.update(baseRequest, { tools: [ - { + ToolDefinition.make({ name: "lookup", description: "Lookup data", inputSchema: { type: "object", properties: { query: { type: "string" } }, required: ["query"] }, - }, + }), ], toolChoice: ToolChoice.make({ type: "required" }), }), @@ -157,13 +168,13 @@ describe("Bedrock Converse route", () => { it.effect("keeps tools and omits the unsupported choice when tool choice is none", () => Effect.gen(function* () { const prepared = yield* LLMClient.prepare( - LLM.updateRequest(baseRequest, { + LLMRequest.update(baseRequest, { tools: [ - { + ToolDefinition.make({ name: "lookup", description: "Lookup data", inputSchema: { type: "object", properties: { query: { type: "string" } } }, - }, + }), ], toolChoice: ToolChoice.make({ type: "none" }), }), @@ -369,8 +380,8 @@ describe("Bedrock Converse route", () => { ["messageStop", { stopReason: "tool_use" }], ) const response = yield* LLMClient.generate( - LLM.updateRequest(baseRequest, { - tools: [{ name: "lookup", description: "Lookup", inputSchema: { type: "object" } }], + LLMRequest.update(baseRequest, { + tools: [ToolDefinition.make({ name: "lookup", description: "Lookup", inputSchema: { type: "object" } })], }), ).pipe(Effect.provide(fixedBytes(body))) @@ -473,8 +484,8 @@ describe("Bedrock Converse route", () => { // wire. The provider owns the payload and requires byte-exact replay. const redactedData = "cmVkYWN0ZWQtdGhpbmtpbmc=" const response = yield* LLMClient.generate( - LLM.updateRequest(baseRequest, { - tools: [{ name: "lookup", description: "Lookup data", inputSchema: { type: "object" } }], + LLMRequest.update(baseRequest, { + tools: [ToolDefinition.make({ name: "lookup", description: "Lookup data", inputSchema: { type: "object" } })], }), ).pipe( Effect.provide( @@ -493,10 +504,7 @@ describe("Bedrock Converse route", () => { start: { toolUse: { toolUseId: "tool_1", name: "lookup" } }, }, ], - [ - "contentBlockDelta", - { contentBlockIndex: 1, delta: { toolUse: { input: '{"query":"weather"}' } } }, - ], + ["contentBlockDelta", { contentBlockIndex: 1, delta: { toolUse: { input: '{"query":"weather"}' } } }], ["contentBlockStop", { contentBlockIndex: 1 }], ["messageStop", { stopReason: "tool_use" }], ), @@ -567,7 +575,7 @@ describe("Bedrock Converse route", () => { const unsignedModel = AmazonBedrock.configure({ baseURL: "https://bedrock-runtime.test", }).model("anthropic.claude-3-5-sonnet-20240620-v1:0") - const error = yield* LLMClient.generate(LLM.updateRequest(baseRequest, { model: unsignedModel })).pipe( + const error = yield* LLMClient.generate(LLMRequest.update(baseRequest, { model: unsignedModel })).pipe( Effect.provide(fixedBytes(eventStreamBody(["messageStop", { stopReason: "end_turn" }]))), Effect.flip, ) @@ -586,7 +594,7 @@ describe("Bedrock Converse route", () => { secretAccessKey: "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY", }, }).model("anthropic.claude-3-5-sonnet-20240620-v1:0") - const prepared = yield* LLMClient.prepare(LLM.updateRequest(baseRequest, { model: signed })) + const prepared = yield* LLMClient.prepare(LLMRequest.update(baseRequest, { model: signed })) expect(prepared.route).toBe("bedrock-converse") expect(prepared.model).toBe(signed) diff --git a/packages/ai/test/provider/gemini.test.ts b/packages/ai/test/provider/gemini.test.ts index 7549f36996c..2b22401e0a5 100644 --- a/packages/ai/test/provider/gemini.test.ts +++ b/packages/ai/test/provider/gemini.test.ts @@ -1,6 +1,6 @@ import { describe, expect } from "bun:test" import { Effect } from "effect" -import { LLM, LLMError, Message, ToolCallPart, Usage } from "../../src" +import { LLM, LLMError, LLMRequest, Message, ToolCallPart, ToolDefinition, Usage } from "../../src" import { Auth, LLMClient } from "../../src/route" import * as Gemini from "../../src/protocols/gemini" import { ProviderShared } from "../../src/protocols/shared" @@ -39,12 +39,12 @@ describe("Gemini route", () => { it.effect("normalizes Gemini thinking options", () => Effect.gen(function* () { const prepared = yield* LLMClient.prepare( - LLM.updateRequest(request, { + LLMRequest.update(request, { providerOptions: { gemini: { thinkingConfig: { thinkingBudget: 0, includeThoughts: false } } }, }), ) const filtered = yield* LLMClient.prepare( - LLM.updateRequest(request, { + LLMRequest.update(request, { providerOptions: { gemini: { thinkingConfig: { thinkingBudget: "invalid", includeThoughts: false } } }, }), ) @@ -261,7 +261,7 @@ describe("Gemini route", () => { id: "req_tool_choice_none", model, prompt: "Say hello.", - tools: [{ name: "lookup", description: "Lookup data", inputSchema: { type: "object" } }], + tools: [ToolDefinition.make({ name: "lookup", description: "Lookup data", inputSchema: { type: "object" } })], toolChoice: { type: "none" }, }), ) @@ -431,8 +431,8 @@ describe("Gemini route", () => { ], }) const response = yield* LLMClient.generate( - LLM.updateRequest(request, { - tools: [{ name: "lookup", description: "Lookup data", inputSchema: { type: "object" } }], + LLMRequest.update(request, { + tools: [ToolDefinition.make({ name: "lookup", description: "Lookup data", inputSchema: { type: "object" } })], }), ).pipe(Effect.provide(fixedResponse(body))) const reasoning = response.events.find((event) => event.type === "reasoning-start") @@ -522,8 +522,8 @@ describe("Gemini route", () => { usageMetadata: { promptTokenCount: 5, candidatesTokenCount: 1 }, }) const response = yield* LLMClient.generate( - LLM.updateRequest(request, { - tools: [{ name: "lookup", description: "Lookup data", inputSchema: { type: "object" } }], + LLMRequest.update(request, { + tools: [ToolDefinition.make({ name: "lookup", description: "Lookup data", inputSchema: { type: "object" } })], }), ).pipe(Effect.provide(fixedResponse(body))) const usage = new Usage({ @@ -589,8 +589,8 @@ describe("Gemini route", () => { ], }) const response = yield* LLMClient.generate( - LLM.updateRequest(request, { - tools: [{ name: "lookup", description: "Lookup data", inputSchema: { type: "object" } }], + LLMRequest.update(request, { + tools: [ToolDefinition.make({ name: "lookup", description: "Lookup data", inputSchema: { type: "object" } })], }), ).pipe(Effect.provide(fixedResponse(body))) diff --git a/packages/ai/test/provider/openai-chat.test.ts b/packages/ai/test/provider/openai-chat.test.ts index fb926c03080..926ea30dba0 100644 --- a/packages/ai/test/provider/openai-chat.test.ts +++ b/packages/ai/test/provider/openai-chat.test.ts @@ -1,7 +1,18 @@ import { describe, expect } from "bun:test" import { Effect, Schema, Stream } from "effect" import { HttpClientRequest } from "effect/unstable/http" -import { LLM, LLMError, LLMEvent, Message, Model, ToolCallPart, Usage } from "../../src" +import { + HttpOptions, + LLM, + LLMError, + LLMEvent, + LLMRequest, + Message, + Model, + ToolCallPart, + ToolDefinition, + Usage, +} from "../../src" import * as Azure from "../../src/providers/azure" import * as OpenAI from "../../src/providers/openai" import * as OpenAIChat from "../../src/protocols/openai-chat" @@ -162,7 +173,7 @@ describe("OpenAI Chat route", () => { it.effect("adds native query params to the Chat Completions URL", () => LLMClient.generate( - LLM.updateRequest(request, { + LLMRequest.update(request, { model: Model.update(model, { route: model.route.with({ endpoint: { query: { "api-version": "v1" } } }) }), }), ).pipe( @@ -182,7 +193,7 @@ describe("OpenAI Chat route", () => { it.effect("uses Azure api-key header for static OpenAI Chat keys", () => LLMClient.generate( - LLM.updateRequest(request, { + LLMRequest.update(request, { model: Azure.configure({ baseURL: "https://opencode-test.openai.azure.com/openai/v1/", apiKey: "azure-key", @@ -208,15 +219,15 @@ describe("OpenAI Chat route", () => { it.effect("applies serializable HTTP overlays after payload lowering", () => LLMClient.generate( - LLM.updateRequest(request, { + LLMRequest.update(request, { model: model.route .with({ auth: Auth.bearer("fresh-key"), headers: { authorization: "Bearer stale" } }) .model({ id: model.id }), - http: { + http: HttpOptions.make({ body: { metadata: { source: "test" } }, headers: { authorization: "Bearer request", "x-custom": "yes" }, query: { debug: "1" }, - }, + }), }), ).pipe( Effect.provide( @@ -618,7 +629,7 @@ describe("OpenAI Chat route", () => { it.effect("parses and replays a configured custom reasoning field", () => Effect.gen(function* () { const custom = Model.update(model, { compatibility: { reasoningField: "vendor_reasoning" } }) - const response = yield* LLMClient.generate(LLM.updateRequest(request, { model: custom })).pipe( + const response = yield* LLMClient.generate(LLMRequest.update(request, { model: custom })).pipe( Effect.provide( fixedResponse( sseEvents( @@ -638,9 +649,7 @@ describe("OpenAI Chat route", () => { const replay = yield* LLMClient.prepare( LLM.request({ model: custom, messages: [response.message] }), ) - expect(replay.body.messages).toEqual([ - { role: "assistant", content: "Hello", vendor_reasoning: "thinking" }, - ]) + expect(replay.body.messages).toEqual([{ role: "assistant", content: "Hello", vendor_reasoning: "thinking" }]) }), ) @@ -651,8 +660,8 @@ describe("OpenAI Chat route", () => { { type: "reasoning.encrypted", data: "opaque", format: "anthropic-claude-v1", index: 1 }, ] const response = yield* LLMClient.generate( - LLM.updateRequest(request, { - tools: [{ name: "lookup", description: "Lookup data", inputSchema: { type: "object" } }], + LLMRequest.update(request, { + tools: [ToolDefinition.make({ name: "lookup", description: "Lookup data", inputSchema: { type: "object" } })], }), ).pipe( Effect.provide( @@ -1024,8 +1033,8 @@ describe("OpenAI Chat route", () => { deltaChunk({}, "tool_calls"), ) const response = yield* LLMClient.generate( - LLM.updateRequest(request, { - tools: [{ name: "lookup", description: "Lookup data", inputSchema: { type: "object" } }], + LLMRequest.update(request, { + tools: [ToolDefinition.make({ name: "lookup", description: "Lookup data", inputSchema: { type: "object" } })], }), ).pipe(Effect.provide(fixedResponse(body))) @@ -1067,8 +1076,8 @@ describe("OpenAI Chat route", () => { deltaChunk({}, "tool_calls"), ) const response = yield* LLMClient.generate( - LLM.updateRequest(request, { - tools: [{ name: "lookup", description: "Lookup data", inputSchema: { type: "object" } }], + LLMRequest.update(request, { + tools: [ToolDefinition.make({ name: "lookup", description: "Lookup data", inputSchema: { type: "object" } })], }), ).pipe(Effect.provide(fixedResponse(body))) @@ -1089,8 +1098,8 @@ describe("OpenAI Chat route", () => { deltaChunk({}, "tool_calls"), ) const response = yield* LLMClient.generate( - LLM.updateRequest(request, { - tools: [{ name: "lookup", description: "Lookup data", inputSchema: { type: "object" } }], + LLMRequest.update(request, { + tools: [ToolDefinition.make({ name: "lookup", description: "Lookup data", inputSchema: { type: "object" } })], }), ).pipe(Effect.provide(fixedResponse(body))) @@ -1107,8 +1116,8 @@ describe("OpenAI Chat route", () => { deltaChunk({}, "tool_calls"), ) const error = yield* LLMClient.generate( - LLM.updateRequest(request, { - tools: [{ name: "lookup", description: "Lookup data", inputSchema: { type: "object" } }], + LLMRequest.update(request, { + tools: [ToolDefinition.make({ name: "lookup", description: "Lookup data", inputSchema: { type: "object" } })], }), ).pipe(Effect.provide(fixedResponse(body)), Effect.flip) @@ -1125,8 +1134,8 @@ describe("OpenAI Chat route", () => { }), deltaChunk({ tool_calls: [{ index: 0, function: { arguments: ':"weather"}' } }] }), ) - const input = LLM.updateRequest(request, { - tools: [{ name: "lookup", description: "Lookup data", inputSchema: { type: "object" } }], + const input = LLMRequest.update(request, { + tools: [ToolDefinition.make({ name: "lookup", description: "Lookup data", inputSchema: { type: "object" } })], }) const events: LLMEvent[] = [] const streamError = yield* LLMClient.stream(input).pipe( diff --git a/packages/ai/test/provider/openai-compatible-chat.test.ts b/packages/ai/test/provider/openai-compatible-chat.test.ts index f32b2bc2d9a..6565820af89 100644 --- a/packages/ai/test/provider/openai-compatible-chat.test.ts +++ b/packages/ai/test/provider/openai-compatible-chat.test.ts @@ -1,7 +1,7 @@ import { describe, expect } from "bun:test" import { Effect, Schema } from "effect" import { HttpClientRequest } from "effect/unstable/http" -import { LLM, Message, ToolCallPart } from "../../src" +import { LLM, LLMRequest, Message, ToolCallPart, ToolChoice, ToolDefinition } from "../../src" import { Auth, LLMClient } from "../../src/route" import * as OpenAICompatible from "../../src/providers/openai-compatible" import * as OpenAICompatibleChat from "../../src/protocols/openai-compatible-chat" @@ -53,9 +53,9 @@ describe("OpenAI-compatible Chat route", () => { it.effect("prepares generic Chat target", () => Effect.gen(function* () { const prepared = yield* LLMClient.prepare( - LLM.updateRequest(request, { - tools: [{ name: "lookup", description: "Lookup data", inputSchema: { type: "object" } }], - toolChoice: { type: "required" }, + LLMRequest.update(request, { + tools: [ToolDefinition.make({ name: "lookup", description: "Lookup data", inputSchema: { type: "object" } })], + toolChoice: ToolChoice.make({ type: "required" }), }), ) diff --git a/packages/ai/test/provider/openai-responses.test.ts b/packages/ai/test/provider/openai-responses.test.ts index 72a2383b041..cf66e5cbd58 100644 --- a/packages/ai/test/provider/openai-responses.test.ts +++ b/packages/ai/test/provider/openai-responses.test.ts @@ -1,7 +1,18 @@ import { describe, expect } from "bun:test" import { ConfigProvider, Effect, Layer, Stream } from "effect" import { Headers, HttpClientRequest } from "effect/unstable/http" -import { LLM, LLMError, LLMEvent, Message, Model, ToolCallPart, ToolResultPart, Usage } from "../../src" +import { + LLM, + LLMError, + LLMEvent, + LLMRequest, + Message, + Model, + ToolCallPart, + ToolDefinition, + ToolResultPart, + Usage, +} from "../../src" import { Auth, LLMClient, RequestExecutor, WebSocketExecutor } from "../../src/route" import * as Azure from "../../src/providers/azure" import * as OpenAI from "../../src/providers/openai" @@ -96,7 +107,7 @@ describe("OpenAI Responses route", () => { it.effect("lowers semantic service tier options", () => Effect.gen(function* () { - const input = LLM.updateRequest(request, { providerOptions: { openai: { serviceTier: "priority" } } }) + const input = LLMRequest.update(request, { providerOptions: { openai: { serviceTier: "priority" } } }) expect(input.providerOptions).toEqual({ openai: { serviceTier: "priority" } }) const prepared = yield* LLMClient.prepare(input) @@ -108,7 +119,7 @@ describe("OpenAI Responses route", () => { it.effect("passes through custom OpenAI reasoning effort strings", () => Effect.gen(function* () { const prepared = yield* LLMClient.prepare( - LLM.updateRequest(request, { providerOptions: { openai: { reasoningEffort: "experimental" } } }), + LLMRequest.update(request, { providerOptions: { openai: { reasoningEffort: "experimental" } } }), ) expect(prepared.body.reasoning).toEqual({ effort: "experimental" }) @@ -118,7 +129,7 @@ describe("OpenAI Responses route", () => { it.effect("omits unsupported semantic service tiers", () => Effect.gen(function* () { const prepared = yield* LLMClient.prepare( - LLM.updateRequest(request, { providerOptions: { openai: { serviceTier: "unsupported" } } }), + LLMRequest.update(request, { providerOptions: { openai: { serviceTier: "unsupported" } } }), ) expect(prepared.body).not.toHaveProperty("service_tier") @@ -128,9 +139,9 @@ describe("OpenAI Responses route", () => { it.effect("flattens top-level object unions in function schemas", () => Effect.gen(function* () { const prepared = yield* LLMClient.prepare( - LLM.updateRequest(request, { + LLMRequest.update(request, { tools: [ - { + ToolDefinition.make({ name: "read", description: "Read a path or resource.", inputSchema: { @@ -152,7 +163,7 @@ describe("OpenAI Responses route", () => { }, ], }, - }, + }), ], }), ) @@ -207,7 +218,7 @@ describe("OpenAI Responses route", () => { it.effect("prepares OpenAI Responses WebSocket target", () => Effect.gen(function* () { const prepared = yield* LLMClient.prepare( - LLM.updateRequest(request, { + LLMRequest.update(request, { model: OpenAIResponses.webSocketRoute .with({ endpoint: { baseURL: "https://api.openai.test/v1/" }, auth: Auth.bearer("test") }) .model({ id: "gpt-4.1-mini" }), @@ -291,7 +302,7 @@ describe("OpenAI Responses route", () => { it.effect("adds native query params to the Responses URL", () => Effect.gen(function* () { yield* LLMClient.generate( - LLM.updateRequest(request, { + LLMRequest.update(request, { model: Model.update(model, { route: model.route.with({ endpoint: { query: { "api-version": "v1" } } }) }), }), ).pipe( @@ -313,7 +324,7 @@ describe("OpenAI Responses route", () => { it.effect("uses Azure api-key header for static OpenAI Responses keys", () => Effect.gen(function* () { yield* LLMClient.generate( - LLM.updateRequest(request, { + LLMRequest.update(request, { model: Azure.configure({ baseURL: "https://opencode-test.openai.azure.com/openai/v1/", apiKey: "azure-key", @@ -340,7 +351,7 @@ describe("OpenAI Responses route", () => { it.effect("loads OpenAI default auth from Effect Config", () => LLMClient.generate( - LLM.updateRequest(request, { + LLMRequest.update(request, { model: OpenAI.configure({ baseURL: "https://api.openai.test/v1/" }).responses("gpt-4.1-mini"), }), ).pipe( @@ -361,7 +372,7 @@ describe("OpenAI Responses route", () => { it.effect("lets explicit auth override OpenAI default API key auth", () => LLMClient.generate( - LLM.updateRequest(request, { + LLMRequest.update(request, { model: OpenAI.configure({ baseURL: "https://api.openai.test/v1/", auth: Auth.bearer("oauth-token"), @@ -889,12 +900,7 @@ describe("OpenAI Responses route", () => { const unknown = yield* generate({}) const custom = yield* generate({ reason: "provider_limit" }) - expect([ - length.finishReason, - contentFilter.finishReason, - unknown.finishReason, - custom.finishReason, - ]).toEqual([ + expect([length.finishReason, contentFilter.finishReason, unknown.finishReason, custom.finishReason]).toEqual([ { normalized: "length", raw: "max_output_tokens" }, { normalized: "content-filter", raw: "content_filter" }, { normalized: "unknown", raw: undefined }, @@ -999,7 +1005,7 @@ describe("OpenAI Responses route", () => { it.effect("streams each reasoning summary part as a separate block", () => Effect.gen(function* () { const response = yield* LLMClient.generate( - LLM.updateRequest(request, { providerOptions: { openai: { store: false } } }), + LLMRequest.update(request, { providerOptions: { openai: { store: false } } }), ).pipe( Effect.provide( fixedResponse( @@ -1054,7 +1060,7 @@ describe("OpenAI Responses route", () => { it.effect("closes reasoning summary parts when storage is not disabled", () => Effect.gen(function* () { const response = yield* LLMClient.generate( - LLM.updateRequest(request, { providerOptions: { openai: { store: true } } }), + LLMRequest.update(request, { providerOptions: { openai: { store: true } } }), ).pipe( Effect.provide( fixedResponse( @@ -1381,8 +1387,8 @@ describe("OpenAI Responses route", () => { { type: "response.completed", response: { usage: { input_tokens: 5, output_tokens: 1 } } }, ) const response = yield* LLMClient.generate( - LLM.updateRequest(request, { - tools: [{ name: "lookup", description: "Lookup data", inputSchema: { type: "object" } }], + LLMRequest.update(request, { + tools: [ToolDefinition.make({ name: "lookup", description: "Lookup data", inputSchema: { type: "object" } })], }), ).pipe(Effect.provide(fixedResponse(body))) const usage = new Usage({ @@ -1467,8 +1473,8 @@ describe("OpenAI Responses route", () => { { type: "response.completed", response: { usage: { input_tokens: 5, output_tokens: 1 } } }, ) const response = yield* LLMClient.generate( - LLM.updateRequest(request, { - tools: [{ name: "lookup", description: "Lookup data", inputSchema: { type: "object" } }], + LLMRequest.update(request, { + tools: [ToolDefinition.make({ name: "lookup", description: "Lookup data", inputSchema: { type: "object" } })], }), ).pipe(Effect.provide(fixedResponse(body))) diff --git a/packages/ai/test/recorded-scenarios.ts b/packages/ai/test/recorded-scenarios.ts index cd762dc4fd9..d0f043cca74 100644 --- a/packages/ai/test/recorded-scenarios.ts +++ b/packages/ai/test/recorded-scenarios.ts @@ -3,6 +3,7 @@ import { Effect, Schema } from "effect" import { LLM, LLMEvent, + LLMRequest, LLMResponse, Message, ToolRuntime, @@ -11,7 +12,6 @@ import { toDefinitions, type ContentPart, type FinishReason, - type LLMRequest, type Model, } from "../src" import { LLMClient } from "../src/route" @@ -91,7 +91,7 @@ const restroomImage = () => export const runWeatherToolLoop = (request: LLMRequest) => Effect.gen(function* () { const tools = { [weatherToolName]: weatherRuntimeTool } - let next = LLM.updateRequest(request, { tools: toDefinitions(tools) }) + let next = LLMRequest.update(request, { tools: toDefinitions(tools) }) const events: LLMEvent[] = [] for (let step = 0; step < 10; step++) { @@ -108,7 +108,7 @@ export const runWeatherToolLoop = (request: LLMRequest) => ToolRuntime.dispatch(tools, call).pipe(Effect.map((result) => [call, result] as const)), ) events.push(...dispatched.flatMap(([, result]) => result.events)) - next = LLM.updateRequest(next, { + next = LLMRequest.update(next, { messages: [ ...next.messages, Message.assistant(assistantContent(response.events)), @@ -123,10 +123,8 @@ export const runWeatherToolLoop = (request: LLMRequest) => const assistantContent = (events: ReadonlyArray) => events.reduce(LLMResponse.reduce, LLMResponse.empty()).message.content -export const expectFinish = ( - events: ReadonlyArray, - reason: FinishReason, -) => expect(events.at(-1)).toMatchObject({ type: "finish", reason: { normalized: reason } }) +export const expectFinish = (events: ReadonlyArray, reason: FinishReason) => + expect(events.at(-1)).toMatchObject({ type: "finish", reason: { normalized: reason } }) export const expectWeatherToolCall = (response: LLMResponse) => expect(response.toolCalls).toMatchObject([ diff --git a/packages/ai/test/tool-runtime.test.ts b/packages/ai/test/tool-runtime.test.ts index e6e97887193..aa4c85f6efe 100644 --- a/packages/ai/test/tool-runtime.test.ts +++ b/packages/ai/test/tool-runtime.test.ts @@ -553,7 +553,7 @@ describe("LLMClient tools", () => { ) yield* TestToolRuntime.runTools({ - request: LLM.updateRequest(baseRequest, { + request: LLMRequest.update(baseRequest, { model: AnthropicMessages.route .with({ auth: Auth.header("x-api-key", "test") }) .model({ id: "claude-sonnet-4-5" }), @@ -808,7 +808,7 @@ describe("LLMClient tools", () => { ) const events = Array.from( yield* TestToolRuntime.runTools({ - request: LLM.updateRequest(baseRequest, { + request: LLMRequest.update(baseRequest, { model: AnthropicMessages.route .with({ auth: Auth.header("x-api-key", "test") }) .model({ id: "claude-sonnet-4-5" }),