File size: 9,803 Bytes
eb1202c | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 | import { Config, Effect, Formatter, Layer, Schema, Stream } from "effect"
import { LLM, LLMClient, Message, ProviderID, Tool, ToolRuntime } from "@opencode-ai/llm"
import { Route, Auth, Endpoint, Framing, Protocol, RequestExecutor, WebSocketExecutor } from "@opencode-ai/llm/route"
import { OpenAI } from "@opencode-ai/llm/providers"
/**
* A runnable walkthrough of the LLM package use-site API.
*
* Run from `packages/llm` with an OpenAI key in the environment:
*
* OPENAI_API_KEY=... bun example/tutorial.ts
*
* The file is intentionally written as a normal TypeScript program. You can
* hover imports and local values to see how the public API is typed.
*/
const apiKey = Config.redacted("OPENAI_API_KEY")
// 1. Pick a model. The provider helper records provider identity, protocol
// choice, capabilities, deployment options, authentication, and defaults.
const model = OpenAI.configure({
apiKey,
generation: { maxTokens: 160 },
providerOptions: {
openai: { store: false },
},
}).model("gpt-4o-mini")
// 2. Build a provider-neutral request. This is useful when reusing one request
// across generate and stream examples.
//
// Options can live on both the configured route/provider facade and the request:
//
// - `generation`: common controls such as max tokens, temperature, topP/topK,
// penalties, seed, and stop sequences.
// - `providerOptions`: namespaced provider-native behavior. For example,
// OpenAI cache keys and store behavior, Anthropic thinking, Gemini thinking
// config, or OpenRouter routing/reasoning.
// - `http`: last-resort serializable overlays for final request body, headers,
// and query params. Prefer typed `providerOptions` when a field is stable.
//
// Route/provider options are defaults. Request options override them for this call.
const request = LLM.request({
model,
system: "You are concise and practical.",
prompt: "Tell me a joke",
generation: { maxTokens: 80, temperature: 0.7 },
providerOptions: {
openai: { promptCacheKey: "tutorial-joke" },
},
})
// `http` is intentionally not needed for normal calls. This shows the shape for
// newly released provider fields before they deserve a typed provider option.
const rawOverlayExample = LLM.request({
model,
prompt: "Show the final HTTP overlay shape.",
http: {
body: { metadata: { example: "tutorial" } },
headers: { "x-opencode-tutorial": "1" },
query: { debug: "1" },
},
})
// 3. `generate` sends the request and collects the event stream into one
// response object. `response.text` is the collected text output.
const generateOnce = Effect.gen(function* () {
const response = yield* LLM.generate(request)
console.log("\n== generate ==")
console.log("generated text:", response.text)
console.log("usage", Formatter.formatJson(response.usage, { space: 2 }))
})
// 4. `stream` exposes provider output as common `LLMEvent`s for UIs that want
// incremental text, reasoning, tool input, usage, or finish events.
const streamText = LLM.stream(request).pipe(
Stream.tap((event) =>
Effect.sync(() => {
if (event.type === "text-delta") process.stdout.write(`\ntext: ${event.text}`)
if (event.type === "finish") process.stdout.write(`\nfinish: ${event.reason}\n`)
}),
),
Stream.runDrain,
)
// 5. Tools are typed with Effect Schema. Provider turns remain explicit:
// advertise definitions on the request, stream one turn, dispatch local calls,
// then persist/build follow-up history in the enclosing product flow.
const tools = {
get_weather: Tool.make({
description: "Get current weather for a city.",
parameters: Schema.Struct({ city: Schema.String }),
success: Schema.Struct({ forecast: Schema.String }),
execute: (input) => Effect.succeed({ forecast: `${input.city}: sunny, 72F` }),
}),
}
const streamWithTools = Effect.gen(function* () {
const request = LLM.request({
model,
prompt: "Use get_weather for San Francisco, then answer in one sentence.",
generation: { maxTokens: 80, temperature: 0 },
tools: Tool.toDefinitions(tools),
})
const events = Array.from(yield* LLM.stream(request).pipe(Stream.runCollect))
for (const event of events) {
if (event.type === "tool-call") console.log("tool call", event.name, event.input)
if (event.type === "text-delta") process.stdout.write(event.text)
if (event.type !== "tool-call" || event.providerExecuted) continue
const dispatched = yield* ToolRuntime.dispatch(tools, event)
console.log("tool result", event.name, dispatched.result)
// 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, {
messages: [
...request.messages,
Message.assistant([event]),
Message.tool({ ...event, result: dispatched.result }),
],
})
console.log("follow-up history messages:", followUp.messages.length)
}
})
// 6. `generateObject` is the structured-output helper. It forces a synthetic
// tool call internally, so the same call site works across providers instead of
// depending on provider-specific JSON mode flags.
const WeatherReport = Schema.Struct({
city: Schema.String,
forecast: Schema.String,
highFahrenheit: Schema.Number,
})
const generateStructuredObject = Effect.gen(function* () {
const response = yield* LLM.generateObject({
model,
system: "Return only structured weather data.",
prompt: "Give me today's weather for San Francisco.",
schema: WeatherReport,
generation: { maxTokens: 120, temperature: 0 },
})
console.log("\n== generateObject ==")
console.log(Formatter.formatJson(response.object, { space: 2 }))
})
// If the shape is only known at runtime, pass raw JSON Schema instead. The
// `.object` type is `unknown`; callers that need static types should validate it.
const generateDynamicObject = LLM.generateObject({
model,
prompt: "Extract the city and forecast from: San Francisco is sunny.",
jsonSchema: {
type: "object",
properties: {
city: { type: "string" },
forecast: { type: "string" },
},
required: ["city", "forecast"],
},
})
// -----------------------------------------------------------------------------
// Part 2: provider composition with a fake provider
// -----------------------------------------------------------------------------
// A protocol is the provider-native API shape: common request -> body, response
// frames -> common events. This fake one turns text prompts into a JSON body
// and treats every SSE frame as output text.
const FakeBody = Schema.Struct({
model: Schema.String,
input: Schema.String,
})
type FakeBody = Schema.Schema.Type<typeof FakeBody>
const FakeProtocol = Protocol.make<FakeBody, string, string, void>({
// Protocol ids are open strings, so external packages can define their own
// protocols without changing this package.
id: "fake-echo",
body: {
schema: FakeBody,
from: (request) =>
Effect.succeed({
model: request.model.id,
input: request.messages
.flatMap((message) => message.content)
.filter((part) => part.type === "text")
.map((part) => part.text)
.join("\n"),
}),
},
stream: {
event: Schema.String,
initial: () => undefined,
step: (_, frame) => Effect.succeed([undefined, [{ type: "text-delta", id: "text-0", text: frame }]] as const),
onHalt: () => [{ type: "finish", reason: "stop" }],
},
})
// An route is the runnable binding for that protocol. It adds the deployment
// axes that the protocol deliberately does not know: URL, auth, and framing.
const FakeAdapter = Route.make({
id: "fake-echo",
provider: "fake-echo",
protocol: FakeProtocol,
endpoint: Endpoint.path("/v1/echo", { baseURL: "https://fake.local" }),
auth: Auth.passthrough,
framing: Framing.sse,
})
// A provider module exports a configured facade. Configuration happens before
// model selection; model selectors accept ids only.
const FakeEcho = {
id: ProviderID.make("fake-echo"),
configure: () => ({
id: ProviderID.make("fake-echo"),
model: (id: string) => FakeAdapter.model({ id }),
}),
}
// `LLMClient.prepare` is the lower-level inspection hook: it compiles through
// body conversion, validation, endpoint, auth, and HTTP construction without
// sending anything over the network.
const inspectFakeProvider = Effect.gen(function* () {
const prepared = yield* LLMClient.prepare(
LLM.request({
model: FakeEcho.configure().model("tiny-echo"),
prompt: "Show me the provider pipeline.",
}),
)
console.log("\n== fake provider prepare ==")
console.log("route:", prepared.route)
console.log("body:", Formatter.formatJson(prepared.body, { space: 2 }))
})
// Provide the LLM runtime and the HTTP request executor once. Keep one path
// enabled at a time so the tutorial can demonstrate generate, prepare, stream,
// or tool-loop behavior without spending tokens on every example.
const requestExecutorLayer = RequestExecutor.fetchLayer
const llmDeps = Layer.mergeAll(requestExecutorLayer, WebSocketExecutor.layer)
const llmClientLayer = LLMClient.layer.pipe(Layer.provide(llmDeps))
const program = Effect.gen(function* () {
// yield* generateOnce
// yield* inspectFakeProvider
// yield* LLMClient.prepare(rawOverlayExample).pipe(Effect.andThen((prepared) => Effect.sync(() => console.log(prepared.body))))
// yield* streamText
// yield* generateStructuredObject
// yield* generateDynamicObject.pipe(Effect.andThen((response) => Effect.sync(() => console.log(response.object))))
yield* streamWithTools
}).pipe(Effect.provide(Layer.mergeAll(llmDeps, llmClientLayer)))
Effect.runPromise(program)
|