Skip to content

Commit 181b5f6

Browse files
kitlangtonopencode
authored andcommitted
refactor(prompt): use Provider service in effect layers (#20167)
1 parent 6314f09 commit 181b5f6

8 files changed

Lines changed: 163 additions & 322 deletions

File tree

packages/opencode/src/agent/agent.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@ export namespace Agent {
7575
const config = yield* Config.Service
7676
const auth = yield* Auth.Service
7777
const skill = yield* Skill.Service
78+
const provider = yield* Provider.Service
7879

7980
const state = yield* InstanceState.make<State>(
8081
Effect.fn("Agent.state")(function* (ctx) {
@@ -330,9 +331,9 @@ export namespace Agent {
330331
model?: { providerID: ProviderID; modelID: ModelID }
331332
}) {
332333
const cfg = yield* config.get()
333-
const model = input.model ?? (yield* Effect.promise(() => Provider.defaultModel()))
334-
const resolved = yield* Effect.promise(() => Provider.getModel(model.providerID, model.modelID))
335-
const language = yield* Effect.promise(() => Provider.getLanguage(resolved))
334+
const model = input.model ?? (yield* provider.defaultModel())
335+
const resolved = yield* provider.getModel(model.providerID, model.modelID)
336+
const language = yield* provider.getLanguage(resolved)
336337

337338
const system = [PROMPT_GENERATE]
338339
yield* Effect.promise(() =>
@@ -393,6 +394,7 @@ export namespace Agent {
393394
)
394395

395396
export const defaultLayer = layer.pipe(
397+
Layer.provide(Provider.defaultLayer),
396398
Layer.provide(Auth.defaultLayer),
397399
Layer.provide(Config.defaultLayer),
398400
Layer.provide(Skill.defaultLayer),

packages/opencode/src/provider/provider.ts

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1541,10 +1541,9 @@ export namespace Provider {
15411541
}),
15421542
)
15431543

1544-
const { runPromise } = makeRuntime(
1545-
Service,
1546-
layer.pipe(Layer.provide(Config.defaultLayer), Layer.provide(Auth.defaultLayer)),
1547-
)
1544+
export const defaultLayer = layer.pipe(Layer.provide(Config.defaultLayer), Layer.provide(Auth.defaultLayer))
1545+
1546+
const { runPromise } = makeRuntime(Service, defaultLayer)
15481547

15491548
export async function list() {
15501549
return runPromise((svc) => svc.list())

packages/opencode/src/session/compaction.ts

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,13 @@ export namespace SessionCompaction {
6363
export const layer: Layer.Layer<
6464
Service,
6565
never,
66-
Bus.Service | Config.Service | Session.Service | Agent.Service | Plugin.Service | SessionProcessor.Service
66+
| Bus.Service
67+
| Config.Service
68+
| Session.Service
69+
| Agent.Service
70+
| Plugin.Service
71+
| SessionProcessor.Service
72+
| Provider.Service
6773
> = Layer.effect(
6874
Service,
6975
Effect.gen(function* () {
@@ -73,6 +79,7 @@ export namespace SessionCompaction {
7379
const agents = yield* Agent.Service
7480
const plugin = yield* Plugin.Service
7581
const processors = yield* SessionProcessor.Service
82+
const provider = yield* Provider.Service
7683

7784
const isOverflow = Effect.fn("SessionCompaction.isOverflow")(function* (input: {
7885
tokens: MessageV2.Assistant["tokens"]
@@ -170,11 +177,9 @@ export namespace SessionCompaction {
170177
}
171178

172179
const agent = yield* agents.get("compaction")
173-
const model = yield* Effect.promise(() =>
174-
agent.model
175-
? Provider.getModel(agent.model.providerID, agent.model.modelID)
176-
: Provider.getModel(userMessage.model.providerID, userMessage.model.modelID),
177-
)
180+
const model = agent.model
181+
? yield* provider.getModel(agent.model.providerID, agent.model.modelID)
182+
: yield* provider.getModel(userMessage.model.providerID, userMessage.model.modelID)
178183
// Allow plugins to inject context or replace compaction prompt.
179184
const compacting = yield* plugin.trigger(
180185
"experimental.session.compacting",
@@ -377,6 +382,7 @@ When constructing the summary, try to stick to this template:
377382
export const defaultLayer = Layer.unwrap(
378383
Effect.sync(() =>
379384
layer.pipe(
385+
Layer.provide(Provider.defaultLayer),
380386
Layer.provide(Session.defaultLayer),
381387
Layer.provide(SessionProcessor.defaultLayer),
382388
Layer.provide(Agent.defaultLayer),

packages/opencode/src/session/prompt.ts

Lines changed: 44 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@ export namespace SessionPrompt {
8484
const status = yield* SessionStatus.Service
8585
const sessions = yield* Session.Service
8686
const agents = yield* Agent.Service
87+
const provider = yield* Provider.Service
8788
const processor = yield* SessionProcessor.Service
8889
const compaction = yield* SessionCompaction.Service
8990
const plugin = yield* Plugin.Service
@@ -206,14 +207,14 @@ export namespace SessionPrompt {
206207

207208
const ag = yield* agents.get("title")
208209
if (!ag) return
210+
const mdl = ag.model
211+
? yield* provider.getModel(ag.model.providerID, ag.model.modelID)
212+
: ((yield* provider.getSmallModel(input.providerID)) ??
213+
(yield* provider.getModel(input.providerID, input.modelID)))
214+
const msgs = onlySubtasks
215+
? [{ role: "user" as const, content: subtasks.map((p) => p.prompt).join("\n") }]
216+
: yield* Effect.promise(() => MessageV2.toModelMessages(context, mdl))
209217
const text = yield* Effect.promise(async (signal) => {
210-
const mdl = ag.model
211-
? await Provider.getModel(ag.model.providerID, ag.model.modelID)
212-
: ((await Provider.getSmallModel(input.providerID)) ??
213-
(await Provider.getModel(input.providerID, input.modelID)))
214-
const msgs = onlySubtasks
215-
? [{ role: "user" as const, content: subtasks.map((p) => p.prompt).join("\n") }]
216-
: await MessageV2.toModelMessages(context, mdl)
217218
const result = await LLM.stream({
218219
agent: ag,
219220
user: firstInfo,
@@ -932,21 +933,35 @@ NOTE: At any point in time through this workflow you should feel free to ask the
932933
return { info: msg, parts: [part] }
933934
})
934935

935-
const getModel = (providerID: ProviderID, modelID: ModelID, sessionID: SessionID) =>
936-
Effect.promise(() =>
937-
Provider.getModel(providerID, modelID).catch((e) => {
938-
if (Provider.ModelNotFoundError.isInstance(e)) {
939-
const hint = e.data.suggestions?.length ? ` Did you mean: ${e.data.suggestions.join(", ")}?` : ""
940-
Bus.publish(Session.Event.Error, {
941-
sessionID,
942-
error: new NamedError.Unknown({
943-
message: `Model not found: ${e.data.providerID}/${e.data.modelID}.${hint}`,
944-
}).toObject(),
945-
})
946-
}
947-
throw e
948-
}),
949-
)
936+
const getModel = Effect.fn("SessionPrompt.getModel")(function* (
937+
providerID: ProviderID,
938+
modelID: ModelID,
939+
sessionID: SessionID,
940+
) {
941+
const exit = yield* provider.getModel(providerID, modelID).pipe(Effect.exit)
942+
if (Exit.isSuccess(exit)) return exit.value
943+
const err = Cause.squash(exit.cause)
944+
if (Provider.ModelNotFoundError.isInstance(err)) {
945+
const hint = err.data.suggestions?.length ? ` Did you mean: ${err.data.suggestions.join(", ")}?` : ""
946+
yield* bus.publish(Session.Event.Error, {
947+
sessionID,
948+
error: new NamedError.Unknown({
949+
message: `Model not found: ${err.data.providerID}/${err.data.modelID}.${hint}`,
950+
}).toObject(),
951+
})
952+
}
953+
return yield* Effect.failCause(exit.cause)
954+
})
955+
956+
const lastModel = Effect.fnUntraced(function* (sessionID: SessionID) {
957+
const model = yield* Effect.promise(async () => {
958+
for await (const item of MessageV2.stream(sessionID)) {
959+
if (item.info.role === "user" && item.info.model) return item.info.model
960+
}
961+
})
962+
if (model) return model
963+
return yield* provider.defaultModel()
964+
})
950965

951966
const createUserMessage = Effect.fn("SessionPrompt.createUserMessage")(function* (input: PromptInput) {
952967
const agentName = input.agent || (yield* agents.defaultAgent())
@@ -960,9 +975,12 @@ NOTE: At any point in time through this workflow you should feel free to ask the
960975
}
961976

962977
const model = input.model ?? ag.model ?? (yield* lastModel(input.sessionID))
978+
const same = ag.model && model.providerID === ag.model.providerID && model.modelID === ag.model.modelID
963979
const full =
964-
!input.variant && ag.variant
965-
? yield* Effect.promise(() => Provider.getModel(model.providerID, model.modelID).catch(() => undefined))
980+
!input.variant && ag.variant && same
981+
? yield* provider
982+
.getModel(model.providerID, model.modelID)
983+
.pipe(Effect.catch(() => Effect.succeed(undefined)))
966984
: undefined
967985
const variant = input.variant ?? (ag.variant && full?.variants?.[ag.variant] ? ag.variant : undefined)
968986

@@ -1109,7 +1127,7 @@ NOTE: At any point in time through this workflow you should feel free to ask the
11091127
]
11101128
const read = yield* Effect.promise(() => ReadTool.init()).pipe(
11111129
Effect.flatMap((t) =>
1112-
Effect.promise(() => Provider.getModel(info.model.providerID, info.model.modelID)).pipe(
1130+
provider.getModel(info.model.providerID, info.model.modelID).pipe(
11131131
Effect.flatMap((mdl) =>
11141132
Effect.promise(() =>
11151133
t.execute(args, {
@@ -1711,6 +1729,7 @@ NOTE: At any point in time through this workflow you should feel free to ask the
17111729
Layer.provide(FileTime.defaultLayer),
17121730
Layer.provide(ToolRegistry.defaultLayer),
17131731
Layer.provide(Truncate.layer),
1732+
Layer.provide(Provider.defaultLayer),
17141733
Layer.provide(AppFileSystem.defaultLayer),
17151734
Layer.provide(Plugin.defaultLayer),
17161735
Layer.provide(Session.defaultLayer),
@@ -1856,15 +1875,6 @@ NOTE: At any point in time through this workflow you should feel free to ask the
18561875
return runPromise((svc) => svc.command(CommandInput.parse(input)))
18571876
}
18581877

1859-
const lastModel = Effect.fnUntraced(function* (sessionID: SessionID) {
1860-
return yield* Effect.promise(async () => {
1861-
for await (const item of MessageV2.stream(sessionID)) {
1862-
if (item.info.role === "user" && item.info.model) return item.info.model
1863-
}
1864-
return Provider.defaultModel()
1865-
})
1866-
})
1867-
18681878
/** @internal Exported for testing */
18691879
export function createStructuredOutputTool(input: {
18701880
schema: Record<string, any>
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
import { Effect, Layer } from "effect"
2+
import { Provider } from "../../src/provider/provider"
3+
import { ModelID, ProviderID } from "../../src/provider/schema"
4+
5+
export namespace ProviderTest {
6+
export function model(override: Partial<Provider.Model> = {}): Provider.Model {
7+
const id = override.id ?? ModelID.make("gpt-5.2")
8+
const providerID = override.providerID ?? ProviderID.make("openai")
9+
return {
10+
id,
11+
providerID,
12+
name: "Test Model",
13+
capabilities: {
14+
toolcall: true,
15+
attachment: false,
16+
reasoning: false,
17+
temperature: true,
18+
interleaved: false,
19+
input: { text: true, image: false, audio: false, video: false, pdf: false },
20+
output: { text: true, image: false, audio: false, video: false, pdf: false },
21+
},
22+
api: { id, url: "https://example.com", npm: "@ai-sdk/openai" },
23+
cost: { input: 0, output: 0, cache: { read: 0, write: 0 } },
24+
limit: { context: 200_000, output: 10_000 },
25+
status: "active",
26+
options: {},
27+
headers: {},
28+
release_date: "2025-01-01",
29+
...override,
30+
}
31+
}
32+
33+
export function info(override: Partial<Provider.Info> = {}, mdl = model()): Provider.Info {
34+
const id = override.id ?? mdl.providerID
35+
return {
36+
id,
37+
name: "Test Provider",
38+
source: "config",
39+
env: [],
40+
options: {},
41+
models: { [mdl.id]: mdl },
42+
...override,
43+
}
44+
}
45+
46+
export function fake(override: Partial<Provider.Interface> & { model?: Provider.Model; info?: Provider.Info } = {}) {
47+
const mdl = override.model ?? model()
48+
const row = override.info ?? info({}, mdl)
49+
return {
50+
model: mdl,
51+
info: row,
52+
layer: Layer.succeed(
53+
Provider.Service,
54+
Provider.Service.of({
55+
list: Effect.fn("TestProvider.list")(() => Effect.succeed({ [row.id]: row })),
56+
getProvider: Effect.fn("TestProvider.getProvider")((providerID) => {
57+
if (providerID === row.id) return Effect.succeed(row)
58+
return Effect.die(new Error(`Unknown test provider: ${providerID}`))
59+
}),
60+
getModel: Effect.fn("TestProvider.getModel")((providerID, modelID) => {
61+
if (providerID === row.id && modelID === mdl.id) return Effect.succeed(mdl)
62+
return Effect.die(new Error(`Unknown test model: ${providerID}/${modelID}`))
63+
}),
64+
getLanguage: Effect.fn("TestProvider.getLanguage")(() =>
65+
Effect.die(new Error("ProviderTest.getLanguage not configured")),
66+
),
67+
closest: Effect.fn("TestProvider.closest")((providerID) =>
68+
Effect.succeed(providerID === row.id ? { providerID: row.id, modelID: mdl.id } : undefined),
69+
),
70+
getSmallModel: Effect.fn("TestProvider.getSmallModel")((providerID) =>
71+
Effect.succeed(providerID === row.id ? mdl : undefined),
72+
),
73+
defaultModel: Effect.fn("TestProvider.defaultModel")(() =>
74+
Effect.succeed({ providerID: row.id, modelID: mdl.id }),
75+
),
76+
...override,
77+
}),
78+
),
79+
}
80+
}
81+
}

0 commit comments

Comments
 (0)