Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion packages/client/src/promise/generated/types.ts

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

31 changes: 21 additions & 10 deletions packages/core/src/plugin/warming.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,13 @@ import { define } from "@opencode/plugin/effect/plugin"
import type { SessionHooks } from "@opencode/plugin/effect/session"
import type { Provider } from "@opencode/schema/provider"
import type { Session } from "@opencode/schema/session"
import { Clock, Duration, Effect, Scope, Stream } from "effect"
import { ConfigWarming } from "@opencode/schema/config/warming"
import { Clock, Duration, Effect, Option, Schema, Scope, Stream } from "effect"
import { Config } from "../config.js"
import { resolveModel } from "./optimize.js"

const decodeWarming = Schema.decodeUnknownOption(ConfigWarming.Warming)

const defaults = {
prompt: "This is a keep-alive request. Do not perform any work or use tools. Reply with exactly: OK",
interval: Duration.minutes(4),
Expand All @@ -18,16 +21,24 @@ export const Plugin = define({
id: "opencode.warming",
effect: Effect.fn(function* (ctx) {
const config = yield* Config.Service
const modelTTL = (event: SessionHooks["context"]) =>
resolveModel(ctx, event).pipe(
Effect.map((model) => model.settings?.cache?.ttl),
Effect.orElseSucceed(() => undefined),
)
// A model's or provider's `cache.warming` overrides the top-level `warming`.
const loadSettings = Effect.fn("WarmingPlugin.loadSettings")(function* (event: SessionHooks["context"]) {
const warming = Config.latest(yield* config.entries(), "warming")
if (!warming) return
const settings = warming === true ? defaults : { ...defaults, ...warming }
const interval = settings.interval === "auto" ? autoInterval(yield* modelTTL(event)) : settings.interval
// Warming must never fail the request it hooks, so an unreadable catalog falls back to the top-level setting.
const cache = (yield* resolveModel(ctx, event).pipe(Effect.orElseSucceed(() => undefined)))?.settings?.cache
const local = cache?.warming === undefined ? undefined : Option.getOrUndefined(decodeWarming(cache.warming))
if (cache?.warming !== undefined && local === undefined)
yield* Effect.logWarning("invalid cache.warming setting; using the top-level warming setting", {
model: event.model,
})
const global = Config.latest(yield* config.entries(), "warming")
if (!(local ?? global)) return
// The keep-alive prompt is only set at the top level; a model's setting changes timing.
const settings = {
...defaults,
...(typeof global === "object" ? global : {}),
...(typeof local === "object" ? local : {}),
}
const interval = settings.interval === "auto" ? autoInterval(cache?.ttl) : settings.interval
// "auto" doesn't warm a model without a cache.ttl.
if (interval === undefined) return
const { duration } = settings
Expand Down
30 changes: 30 additions & 0 deletions packages/protocol/openapi.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

14 changes: 14 additions & 0 deletions packages/schema/src/provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,9 +53,23 @@ export const CacheTTL = Schema.TemplateLiteral([Schema.Number, Schema.Literals([
.annotate({ identifier: "Provider.CacheTTL" })
export type CacheTTL = typeof CacheTTL.Type

/** Keep-alive requests for this provider's or model's sessions; overrides the top-level `warming`. */
export const CacheWarming = Schema.Union([
Schema.Boolean,
Schema.Struct({
// Settings hold JSON, so durations stay strings here; the warming plugin parses them.
interval: Schema.Union([Schema.Literal("auto"), Schema.String])
.pipe(optional)
.annotate({ description: 'Duration such as "4 minutes", or "auto" for 90% of the model\'s cache.ttl' }),
duration: Schema.String.pipe(optional).annotate({ description: 'Duration such as "30 minutes"' }),
}),
]).annotate({ identifier: "Provider.CacheWarming" })
export type CacheWarming = typeof CacheWarming.Type

/** Prompt-cache settings. `ttl` is sent to the provider as given; each provider accepts its own set. */
export const Cache = Schema.Struct({
ttl: CacheTTL.pipe(optional),
warming: CacheWarming.pipe(optional),
}).annotate({ identifier: "Provider.Cache" })
export type Cache = typeof Cache.Type

Expand Down
Loading