diff --git a/packages/ai/src/route/client.ts b/packages/ai/src/route/client.ts
index 067292329b..6316643c07 100644
--- a/packages/ai/src/route/client.ts
+++ b/packages/ai/src/route/client.ts
@@ -155,8 +155,11 @@ export interface Interface {
readonly prepare:
(request: LLMRequest) => Effect.Effect, LLMError>
readonly stream: StreamMethod
readonly generate: GenerateMethod
+ readonly withOptionsTransform: (transform: OptionsTransform) => Interface
}
+export type OptionsTransform = (request: LLMRequest) => Effect.Effect
+
export interface StreamMethod {
(request: LLMRequest): Stream.Stream
}
@@ -373,43 +376,59 @@ export function make(
// `compile` is the important boundary: it turns a common `LLMRequest` into a
// validated provider body plus transport-private prepared data, but does not
// execute transport.
-const compile = Effect.fn("LLM.compile")(function* (request: LLMRequest) {
- const resolved = applyCachePolicy(resolveRequestOptions(request))
- const route = resolved.model.route
+const compileWith = (transform?: OptionsTransform) =>
+ Effect.fn("LLM.compile")(function* (request: LLMRequest) {
+ const options = resolveRequestOptions(request)
+ const resolved = applyCachePolicy(transform ? yield* transform(options) : options)
+ const route = resolved.model.route
- const body = yield* route.body
- .from(resolved)
- .pipe(Effect.flatMap(ProviderShared.validateWith(Schema.decodeUnknownEffect(route.body.schema))))
- const prepared = yield* route.prepareTransport(body, resolved)
+ const body = yield* route.body
+ .from(resolved)
+ .pipe(Effect.flatMap(ProviderShared.validateWith(Schema.decodeUnknownEffect(route.body.schema))))
+ const prepared = yield* route.prepareTransport(body, resolved)
- return {
- request: resolved,
- route,
- body,
- prepared,
- }
-})
-
-const prepareWith = Effect.fn("LLMClient.prepare")(function* (request: LLMRequest) {
- const compiled = yield* compile(request)
-
- return new PreparedRequest({
- id: compiled.request.id ?? "request",
- route: compiled.route.id,
- protocol: compiled.route.protocol,
- model: compiled.request.model,
- body: compiled.body,
- metadata: { transport: compiled.route.transport.id },
+ return {
+ request: resolved,
+ route,
+ body,
+ prepared,
+ }
})
-})
-const streamRequestWith = (runtime: TransportRuntime) => (request: LLMRequest) =>
- Stream.unwrap(
- Effect.gen(function* () {
- const compiled = yield* compile(request)
- return compiled.route.streamPrepared(compiled.prepared, compiled.request, runtime)
- }),
- )
+const prepareWith = (compile: ReturnType) =>
+ Effect.fn("LLMClient.prepare")(function* (request: LLMRequest) {
+ const compiled = yield* compile(request)
+
+ return new PreparedRequest({
+ id: compiled.request.id ?? "request",
+ route: compiled.route.id,
+ protocol: compiled.route.protocol,
+ model: compiled.request.model,
+ body: compiled.body,
+ metadata: { transport: compiled.route.transport.id },
+ })
+ })
+
+const streamRequestWith =
+ (runtime: TransportRuntime, compile: ReturnType) => (request: LLMRequest) =>
+ Stream.unwrap(
+ Effect.gen(function* () {
+ const compiled = yield* compile(request)
+ return compiled.route.streamPrepared(compiled.prepared, compiled.request, runtime)
+ }),
+ )
+
+const makeClient = (runtime: TransportRuntime, transform?: OptionsTransform): Interface => {
+ const compile = compileWith(transform)
+ const stream = streamRequestWith(runtime, compile)
+ return {
+ prepare: prepareWith(compile) as Interface["prepare"],
+ stream,
+ generate: generateWith(stream),
+ withOptionsTransform: (next) =>
+ makeClient(runtime, transform ? (request) => transform(request).pipe(Effect.flatMap(next)) : next),
+ }
+}
const generateWith = (stream: Interface["stream"]) =>
Effect.fn("LLM.generate")(function* (request: LLMRequest) {
@@ -423,7 +442,7 @@ const generateWith = (stream: Interface["stream"]) =>
})
export const prepare = (request: LLMRequest) =>
- prepareWith(request) as Effect.Effect, LLMError>
+ prepareWith(compileWith())(request) as Effect.Effect, LLMError>
export function stream(request: LLMRequest): Stream.Stream {
return Stream.unwrap(
@@ -449,11 +468,12 @@ export const streamRequest = (request: LLMRequest) =>
export const layer: Layer.Layer = Layer.effect(
Service,
Effect.gen(function* () {
- const stream = streamRequestWith({
- http: yield* RequestExecutor.Service,
- webSocket: Option.getOrUndefined(yield* Effect.serviceOption(WebSocketExecutor.Service)),
- })
- return Service.of({ prepare: prepareWith as Interface["prepare"], stream, generate: generateWith(stream) })
+ return Service.of(
+ makeClient({
+ http: yield* RequestExecutor.Service,
+ webSocket: Option.getOrUndefined(yield* Effect.serviceOption(WebSocketExecutor.Service)),
+ }),
+ )
}),
)
diff --git a/packages/ai/src/route/index.ts b/packages/ai/src/route/index.ts
index 70db881ea4..632c1dae06 100644
--- a/packages/ai/src/route/index.ts
+++ b/packages/ai/src/route/index.ts
@@ -7,6 +7,7 @@ export type {
RouteDefaultsInput,
AnyRoute,
Interface as LLMClientShape,
+ OptionsTransform,
Service as LLMClientService,
} from "./client"
export * from "./executor"
diff --git a/packages/ai/test/provider/openai-chat.test.ts b/packages/ai/test/provider/openai-chat.test.ts
index e2efa1717d..1890c5ce90 100644
--- a/packages/ai/test/provider/openai-chat.test.ts
+++ b/packages/ai/test/provider/openai-chat.test.ts
@@ -694,4 +694,47 @@ describe("OpenAI Chat route", () => {
expect(events.map((event) => event.type)).toEqual(["step-start"])
}),
)
+
+ it.effect("transforms resolved options before provider lowering", () =>
+ Effect.gen(function* () {
+ const llm = yield* LLMClient.Service
+ const resolvedModel = OpenAIChat.route
+ .with({
+ endpoint: { baseURL: "https://api.openai.test/v1/" },
+ auth: Auth.bearer("test"),
+ generation: { topP: 0.8 },
+ })
+ .model({ id: "gpt-4o-mini", defaults: { generation: { topK: 4 } } })
+ let seen: typeof request.generation
+ const transformed = LLM.request({
+ model: resolvedModel,
+ prompt: "Say hello.",
+ generation: { maxTokens: 20, temperature: 0.1 },
+ })
+
+ yield* llm
+ .withOptionsTransform((request) =>
+ Effect.sync(() => {
+ seen = request.generation
+ const generation = { ...request.generation, temperature: 0.7 }
+ delete generation.topP
+ return LLM.updateRequest(request, { generation })
+ }),
+ )
+ .stream(transformed)
+ .pipe(Stream.runDrain)
+
+ expect(seen).toMatchObject({ maxTokens: 20, temperature: 0.1, topP: 0.8, topK: 4 })
+ }).pipe(
+ Effect.provide(
+ dynamicResponse((input) => {
+ expect(JSON.parse(input.text)).toMatchObject({ max_tokens: 20, temperature: 0.7 })
+ expect(JSON.parse(input.text)).not.toHaveProperty("top_p")
+ return Effect.succeed(
+ input.respond(sseEvents(deltaChunk({}, "stop")), { headers: { "content-type": "text/event-stream" } }),
+ )
+ }),
+ ),
+ ),
+ )
})
diff --git a/packages/core/src/plugin/hooks.ts b/packages/core/src/plugin/hooks.ts
index 4ac437d392..8d3d73fbd9 100644
--- a/packages/core/src/plugin/hooks.ts
+++ b/packages/core/src/plugin/hooks.ts
@@ -16,6 +16,10 @@ export interface Domains {
type Callback = (event: Event) => Effect.Effect
export interface Interface {
+ readonly has: (
+ domain: Domain,
+ name: Name,
+ ) => boolean
readonly register: (
domain: Domain,
name: Name,
@@ -60,7 +64,7 @@ const layer = Layer.effect(
return event
})
- return Service.of({ register, trigger })
+ return Service.of({ has: (domain, name) => callbacks.has(key(domain, name)), register, trigger })
}),
)
diff --git a/packages/core/src/session/generate-node.ts b/packages/core/src/session/generate-node.ts
index 7d7e88e90f..46489a12fd 100644
--- a/packages/core/src/session/generate-node.ts
+++ b/packages/core/src/session/generate-node.ts
@@ -10,6 +10,7 @@ import { SessionContext } from "./context"
import { SessionGenerate } from "./generate"
import { SessionHistory } from "./history"
import { SessionModelHeaders } from "./model-headers"
+import { SessionOptionsHook } from "./options-hook"
import { SessionRunnerModel } from "./runner/model"
import PROMPT_DEFAULT from "./runner/prompt/base.txt"
import { toLLMMessages } from "./runner/to-llm-message"
@@ -46,7 +47,11 @@ const layer = Layer.effect(
],
tools: {},
})
- return (yield* llm.generate(
+ return (yield* SessionOptionsHook.client(llm, hooks, {
+ sessionID: selection.session.id,
+ agent: selection.agent.id,
+ model: model.ref,
+ }).generate(
LLM.request({
model: model.model,
http: { headers: SessionModelHeaders.make(selection.session) },
diff --git a/packages/core/src/session/options-hook.ts b/packages/core/src/session/options-hook.ts
new file mode 100644
index 0000000000..7807dfb78c
--- /dev/null
+++ b/packages/core/src/session/options-hook.ts
@@ -0,0 +1,26 @@
+export * as SessionOptionsHook from "./options-hook"
+
+import { LLM } from "@opencode-ai/ai"
+import type { LLMClientShape } from "@opencode-ai/ai/route"
+import type { SessionHooks } from "@opencode-ai/plugin/v2/effect/session"
+import { Effect } from "effect"
+import { PluginHooks } from "../plugin/hooks"
+
+type Identity = Pick
+
+export const client = (llm: LLMClientShape, hooks: PluginHooks.Interface, identity: Identity) =>
+ hooks.has("session", "options")
+ ? llm.withOptionsTransform((request) =>
+ Effect.gen(function* () {
+ const event = yield* hooks.trigger("session", "options", {
+ ...identity,
+ generation: { ...request.generation },
+ providerOptions: structuredClone(request.providerOptions ?? {}),
+ })
+ return LLM.updateRequest(request, {
+ generation: event.generation,
+ providerOptions: event.providerOptions,
+ })
+ }),
+ )
+ : llm
diff --git a/packages/core/src/session/runner/llm.ts b/packages/core/src/session/runner/llm.ts
index 3495cfdc4f..778471c7c6 100644
--- a/packages/core/src/session/runner/llm.ts
+++ b/packages/core/src/session/runner/llm.ts
@@ -6,6 +6,7 @@ import { Cause, Effect, Exit, Fiber, FiberSet, Layer, Option, Semaphore, Stream
import { Database } from "../../database/database"
import { EventV2 } from "../../event"
import { PermissionV2 } from "../../permission"
+import { PluginHooks } from "../../plugin/hooks"
import { QuestionTool } from "../../tool/question"
import { ToolOutputStore } from "../../tool-output-store"
import { InstructionState } from "../instruction-state"
@@ -13,6 +14,7 @@ import { SessionCompaction } from "../compaction"
import { SessionContext } from "../context"
import { SessionEvent } from "../event"
import { SessionPending } from "../pending"
+import { SessionOptionsHook } from "../options-hook"
import { SessionModelRequest } from "../model-request"
import { SessionMessage } from "../message"
import { SessionSchema } from "../schema"
@@ -40,6 +42,7 @@ const layer = Layer.effect(
const db = (yield* Database.Service).db
const compaction = yield* SessionCompaction.Service
const title = yield* SessionTitle.Service
+ const hooks = yield* PluginHooks.Service
// Title generation is a side effect of the first step; it must not delay step continuation.
// Tracked per process so repeated wakes before the second user message arrives don't
// re-fire a redundant LLM call; `SessionTitle` itself is idempotent based on durable history.
@@ -132,7 +135,12 @@ const layer = Layer.effect(
const serialized = (effect: Effect.Effect) => publication.withPermit(effect)
const publish = (event: LLMEvent, error?: SessionError.Error) => serialized(publisher.publish(event, error))
let overflowFailure: ProviderErrorEvent | undefined
- const providerStream = llm.stream(prepared.request).pipe(
+ const sessionLLM = SessionOptionsHook.client(llm, hooks, {
+ sessionID: session.id,
+ agent: agent.id,
+ model: resolved.ref,
+ })
+ const providerStream = sessionLLM.stream(prepared.request).pipe(
Stream.runForEach((event) =>
Effect.gen(function* () {
if (overflowFailure || publisher.hasProviderError()) return
@@ -482,5 +490,6 @@ export const node = makeLocationNode({
SessionTitle.node,
Snapshot.node,
Database.node,
+ PluginHooks.node,
],
})
diff --git a/packages/core/test/plugin/promise.test.ts b/packages/core/test/plugin/promise.test.ts
index 7922d965bb..1da7e4aa2b 100644
--- a/packages/core/test/plugin/promise.test.ts
+++ b/packages/core/test/plugin/promise.test.ts
@@ -218,6 +218,38 @@ describe("fromPromise", () => {
}),
)
+ it.effect("forwards session option hooks", () =>
+ Effect.gen(function* () {
+ const plugin = yield* PluginV2.Service
+ const hooks = yield* PluginHooks.Service
+ const host = yield* PluginHost.make(plugin)
+ yield* PluginPromise.fromPromise(
+ Plugin.define({
+ id: "promise-session-options",
+ setup: async (ctx) => {
+ await ctx.session.hook("options", (event) => {
+ event.generation.temperature = 0.3
+ event.providerOptions.openai ??= {}
+ event.providerOptions.openai.reasoningEffort = "high"
+ })
+ },
+ }),
+ ).effect(host)
+ const event: SessionHooks["options"] = {
+ sessionID: SessionV2.ID.make("ses_promise_session_options"),
+ agent: AgentV2.ID.make("build"),
+ model: Model.Ref.make({ providerID: Provider.ID.make("test"), id: Model.ID.make("model") }),
+ generation: { temperature: 0.7 },
+ providerOptions: {},
+ }
+
+ yield* hooks.trigger("session", "options", event)
+
+ expect(event.generation.temperature).toBe(0.3)
+ expect(event.providerOptions).toEqual({ openai: { reasoningEffort: "high" } })
+ }),
+ )
+
it.effect("disposes a hook registration on request", () =>
Effect.gen(function* () {
const agents = yield* AgentV2.Service
diff --git a/packages/core/test/session-compact.test.ts b/packages/core/test/session-compact.test.ts
index da90068ea4..06896c3b49 100644
--- a/packages/core/test/session-compact.test.ts
+++ b/packages/core/test/session-compact.test.ts
@@ -1,6 +1,7 @@
import { describe, expect } from "bun:test"
import { LLMClient, LLMEvent, Model, type LLMRequest } from "@opencode-ai/ai"
import { OpenAIChat } from "@opencode-ai/ai/protocols"
+import type { LLMClientShape } from "@opencode-ai/ai/route"
import { Config } from "@opencode-ai/core/config"
import { Database } from "@opencode-ai/core/database/database"
import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
@@ -40,14 +41,16 @@ const projects = Layer.succeed(
}),
)
let requests: LLMRequest[] = []
-const client = Layer.mock(LLMClient.Service)({
+const clientValue: LLMClientShape = {
prepare: () => Effect.die("unused"),
stream: (request: LLMRequest) => {
requests.push(request)
return Stream.make(LLMEvent.textDelta({ id: "summary", text: "manual session summary" }))
},
generate: () => Effect.die("unused"),
-})
+ withOptionsTransform: () => clientValue,
+}
+const client = Layer.mock(LLMClient.Service)(clientValue)
const config = Layer.mock(Config.Service)({ entries: () => Effect.succeed([]) })
const models = SessionRunnerModel.layerWith(() => Effect.succeed(SessionRunnerModel.resolved(model)))
const locations = Layer.effect(
diff --git a/packages/core/test/session-compaction.test.ts b/packages/core/test/session-compaction.test.ts
index ad3e0a2bfb..da6056aad2 100644
--- a/packages/core/test/session-compaction.test.ts
+++ b/packages/core/test/session-compaction.test.ts
@@ -1,6 +1,7 @@
import { expect, test } from "bun:test"
import { LLMClient, LLMEvent, Model, type LLMRequest } from "@opencode-ai/ai"
import { OpenAIChat } from "@opencode-ai/ai/protocols"
+import type { LLMClientShape } from "@opencode-ai/ai/route"
import { Config } from "@opencode-ai/core/config"
import { Database } from "@opencode-ai/core/database/database"
import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
@@ -42,7 +43,7 @@ const cost = [
},
},
]
-const client = Layer.mock(LLMClient.Service)({
+const clientValue: LLMClientShape = {
prepare: () => Effect.die("unused"),
stream: (request: LLMRequest) => {
requests.push(request)
@@ -66,7 +67,9 @@ const client = Layer.mock(LLMClient.Service)({
)
},
generate: () => Effect.die("unused"),
-})
+ withOptionsTransform: () => clientValue,
+}
+const client = Layer.mock(LLMClient.Service)(clientValue)
const config = Layer.mock(Config.Service)({ entries: () => Effect.succeed([]) })
const models = Layer.mock(SessionRunnerModel.Service)({
resolve: () => Effect.succeed(SessionRunnerModel.resolved(model, undefined, cost)),
diff --git a/packages/core/test/session-generate.test.ts b/packages/core/test/session-generate.test.ts
index a66ac6a69e..df83da8f22 100644
--- a/packages/core/test/session-generate.test.ts
+++ b/packages/core/test/session-generate.test.ts
@@ -1,6 +1,7 @@
import { expect } from "bun:test"
import { LLMClient, LLMEvent, LLMResponse, Model, SystemPart, type LLMRequest } from "@opencode-ai/ai"
import { OpenAIChat } from "@opencode-ai/ai/protocols"
+import type { LLMClientShape } from "@opencode-ai/ai/route"
import { AgentV2 } from "@opencode-ai/core/agent"
import { Database } from "@opencode-ai/core/database/database"
import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
@@ -47,7 +48,7 @@ let instruction: string | Instructions.Unavailable = "Initial context"
const sessionID = SessionSchema.ID.make("ses_generate_test")
const model = Model.make({ id: "generate-model", provider: "test", route: OpenAIChat.route })
-const client = Layer.mock(LLMClient.Service)({
+const clientValue: LLMClientShape = {
prepare: () => Effect.die(new Error("unused")),
stream: () => Stream.die(new Error("unused")),
generate: (request) =>
@@ -64,7 +65,9 @@ const client = Layer.mock(LLMClient.Service)({
if (!response) throw new Error("Incomplete generate response")
return response
}),
-})
+ withOptionsTransform: () => clientValue,
+}
+const client = Layer.mock(LLMClient.Service)(clientValue)
const models = SessionRunnerModel.layerWith(() => Effect.succeed(SessionRunnerModel.resolved(model)))
const builtins = Layer.mock(InstructionBuiltIns.Service, {
load: () =>
diff --git a/packages/core/test/session-options-hook.test.ts b/packages/core/test/session-options-hook.test.ts
new file mode 100644
index 0000000000..157e5e68eb
--- /dev/null
+++ b/packages/core/test/session-options-hook.test.ts
@@ -0,0 +1,57 @@
+import { expect } from "bun:test"
+import { LLM, LLMClient, Model as LLMModel } from "@opencode-ai/ai"
+import { OpenAIChat } from "@opencode-ai/ai/protocols"
+import type { LLMClientShape, OptionsTransform } from "@opencode-ai/ai/route"
+import { Agent } from "@opencode-ai/schema/agent"
+import { Model } from "@opencode-ai/schema/model"
+import { Provider } from "@opencode-ai/schema/provider"
+import { Session } from "@opencode-ai/schema/session"
+import { Effect, Layer, Stream } from "effect"
+import { PluginHooks } from "../src/plugin/hooks"
+import { SessionOptionsHook } from "../src/session/options-hook"
+import { testEffect } from "./lib/effect"
+
+const layer = PluginHooks.node.implementation as Layer.Layer
+const it = testEffect(layer)
+
+it.effect("applies session option hooks to resolved model options", () =>
+ Effect.gen(function* () {
+ const hooks = yield* PluginHooks.Service
+ const sessionID = Session.ID.make("ses_options")
+ const agent = Agent.ID.make("build")
+ const model = Model.Ref.make({ providerID: Provider.ID.make("test"), id: Model.ID.make("model") })
+ yield* hooks.register("session", "options", (event) =>
+ Effect.sync(() => {
+ expect(event).toMatchObject({ sessionID, agent, model })
+ event.generation.temperature = 0.2
+ delete event.generation.topP
+ event.providerOptions.openai ??= {}
+ event.providerOptions.openai.reasoningEffort = "high"
+ }),
+ )
+ let transform: OptionsTransform | undefined
+ const llm: LLMClientShape = {
+ prepare: () => Effect.die("unused"),
+ stream: () => Stream.die("unused"),
+ generate: () => Effect.die("unused"),
+ withOptionsTransform: (next) => {
+ transform = next
+ return llm
+ },
+ }
+
+ SessionOptionsHook.client(llm, hooks, { sessionID, agent, model })
+ if (!transform) return yield* Effect.die("options transform was not installed")
+ const result = yield* transform(
+ LLM.request({
+ model: LLMModel.make({ id: "model", provider: "test", route: OpenAIChat.route }),
+ generation: { temperature: 0.7, topP: 0.9 },
+ providerOptions: { openai: { promptCacheKey: "cache" } },
+ }),
+ )
+
+ expect(result.generation).toMatchObject({ temperature: 0.2 })
+ expect(result.generation?.topP).toBeUndefined()
+ expect(result.providerOptions).toEqual({ openai: { promptCacheKey: "cache", reasoningEffort: "high" } })
+ }),
+)
diff --git a/packages/core/test/session-runner.test.ts b/packages/core/test/session-runner.test.ts
index 39cf62e847..8abfc2944b 100644
--- a/packages/core/test/session-runner.test.ts
+++ b/packages/core/test/session-runner.test.ts
@@ -90,9 +90,7 @@ let toolExecutionsStarted: Deferred.Deferred | undefined
let toolExecutionsReady = 5
let activeToolExecutions = 0
let maxActiveToolExecutions = 0
-const client = Layer.succeed(
- LLMClient.Service,
- LLMClient.Service.of({
+const clientValue: LLMClientShape = LLMClient.Service.of({
prepare: () => Effect.die("unused"),
stream: ((request: LLMRequest) => {
requests.push(request)
@@ -114,8 +112,9 @@ const client = Layer.succeed(
)
}) as unknown as LLMClientShape["stream"],
generate: () => Effect.die("unused"),
- }),
-)
+ withOptionsTransform: () => clientValue,
+ })
+const client = Layer.succeed(LLMClient.Service, clientValue)
const reply = {
stop: () => [
LLMEvent.stepStart({ index: 0 }),
diff --git a/packages/core/test/session-title.test.ts b/packages/core/test/session-title.test.ts
index cf3d7f4ea0..9e101e26d9 100644
--- a/packages/core/test/session-title.test.ts
+++ b/packages/core/test/session-title.test.ts
@@ -1,6 +1,7 @@
import { expect } from "bun:test"
import { LLMClient, LLMEvent, Model, type LLMRequest } from "@opencode-ai/ai"
import { OpenAIChat } from "@opencode-ai/ai/protocols"
+import type { LLMClientShape } from "@opencode-ai/ai/route"
import { AgentV2 } from "@opencode-ai/core/agent"
import { Database } from "@opencode-ai/core/database/database"
import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
@@ -40,7 +41,7 @@ const cost = [
},
},
]
-const client = Layer.mock(LLMClient.Service)({
+const clientValue: LLMClientShape = {
prepare: () => Effect.die("unused"),
stream: (request: LLMRequest) => {
requests.push(request)
@@ -64,7 +65,9 @@ const client = Layer.mock(LLMClient.Service)({
)
},
generate: () => Effect.die("unused"),
-})
+ withOptionsTransform: () => clientValue,
+}
+const client = Layer.mock(LLMClient.Service)(clientValue)
const models = Layer.mock(SessionRunnerModel.Service)({
resolve: () => Effect.succeed(SessionRunnerModel.resolved(model, undefined, cost)),
})
diff --git a/packages/plugin/src/v2/effect/README.md b/packages/plugin/src/v2/effect/README.md
index 8ce7ddb414..20fb28c52f 100644
--- a/packages/plugin/src/v2/effect/README.md
+++ b/packages/plugin/src/v2/effect/README.md
@@ -92,6 +92,20 @@ yield *
)
```
+Resolved generation and provider options are mutable before provider lowering:
+
+```ts
+yield *
+ ctx.session.hook("options", (event) =>
+ Effect.sync(() => {
+ event.generation.temperature = 0.2
+ event.generation.topP = 0.9
+ event.providerOptions.openai ??= {}
+ event.providerOptions.openai.reasoningEffort = "high"
+ }),
+ )
+```
+
## Reloading A Domain
When data captured by a transform changes, reload the affected domain:
diff --git a/packages/plugin/src/v2/effect/session.ts b/packages/plugin/src/v2/effect/session.ts
index 73b87fb3d1..d3b885999e 100644
--- a/packages/plugin/src/v2/effect/session.ts
+++ b/packages/plugin/src/v2/effect/session.ts
@@ -1,5 +1,5 @@
import type { SessionApi } from "@opencode-ai/client/effect/api"
-import type { Message, SystemPart } from "@opencode-ai/ai"
+import type { GenerationOptionsFields, Message, SystemPart } from "@opencode-ai/ai"
import type { Agent } from "@opencode-ai/schema/agent"
import type { Model } from "@opencode-ai/schema/model"
import type { Session } from "@opencode-ai/schema/session"
@@ -15,8 +15,23 @@ export interface SessionContext {
tools: Record
}
+export type SessionGenerationOptions = {
+ -readonly [Key in keyof GenerationOptionsFields]: GenerationOptionsFields[Key]
+}
+
+export type SessionProviderOptions = Record>
+
+export interface SessionOptions {
+ readonly sessionID: Session.ID
+ readonly agent: Agent.ID
+ readonly model: Model.Ref
+ generation: SessionGenerationOptions
+ providerOptions: SessionProviderOptions
+}
+
export interface SessionHooks {
readonly context: SessionContext
+ readonly options: SessionOptions
}
export type SessionDomain = Pick<
diff --git a/packages/plugin/src/v2/promise/README.md b/packages/plugin/src/v2/promise/README.md
index e23a231e4f..4d4d5bf8e1 100644
--- a/packages/plugin/src/v2/promise/README.md
+++ b/packages/plugin/src/v2/promise/README.md
@@ -94,6 +94,17 @@ await ctx.session.hook("context", (event) => {
})
```
+Resolved generation and provider options are mutable before provider lowering:
+
+```ts
+await ctx.session.hook("options", (event) => {
+ event.generation.temperature = 0.2
+ event.generation.topP = 0.9
+ event.providerOptions.openai ??= {}
+ event.providerOptions.openai.reasoningEffort = "high"
+})
+```
+
Promise tools use plain object declarations with async executors:
```ts
diff --git a/packages/plugin/src/v2/promise/session.ts b/packages/plugin/src/v2/promise/session.ts
index 99346de3ea..ec084d3b57 100644
--- a/packages/plugin/src/v2/promise/session.ts
+++ b/packages/plugin/src/v2/promise/session.ts
@@ -1,5 +1,5 @@
import type { SessionApi } from "@opencode-ai/client/promise/api"
-import type { Message, SystemPart } from "@opencode-ai/ai"
+import type { GenerationOptionsFields, Message, SystemPart } from "@opencode-ai/ai"
import type { Agent } from "@opencode-ai/schema/agent"
import type { Model } from "@opencode-ai/schema/model"
import type { Session } from "@opencode-ai/schema/session"
@@ -15,8 +15,23 @@ export interface SessionContext {
tools: Record
}
+export type SessionGenerationOptions = {
+ -readonly [Key in keyof GenerationOptionsFields]: GenerationOptionsFields[Key]
+}
+
+export type SessionProviderOptions = Record>
+
+export interface SessionOptions {
+ readonly sessionID: Session.ID
+ readonly agent: Agent.ID
+ readonly model: Model.Ref
+ generation: SessionGenerationOptions
+ providerOptions: SessionProviderOptions
+}
+
export interface SessionHooks {
readonly context: SessionContext
+ readonly options: SessionOptions
}
export type SessionDomain = Pick<