refactor(core): move provider policies into settings (#49666)

This commit is contained in:
Aiden Cline
2026-09-18 10:18:33 -05:00
committed by GitHub
parent 9e1fa80fb7
commit 219ecada68
31 changed files with 442 additions and 323 deletions
+65 -61
View File
@@ -8,7 +8,9 @@ export type LocationPublicRef = { directory: string }
export type ModelRef = { id: string; providerID: string; variant?: string }
export type ProviderSettings = { [x: string]: any }
export type ProviderCompaction = { type: "summary" } | { type: "native" }
export type ProviderTransport = "http" | "websocket"
export type AgentColor = string
@@ -218,19 +220,8 @@ export type ModelReasoningField = "reasoning" | "reasoning_content" | "reasoning
export type ModelMaxTokensField = "max_completion_tokens" | "max_tokens"
export type ProviderCompaction = { mode: "local" } | { mode: "provider"; threshold?: number }
export type ProviderTransport = "http" | "websocket"
export type ModelCapabilities = { tools: boolean; input: Array<string>; output: Array<string> }
export type ModelVariant = {
id: string
settings?: { [x: string]: any }
headers?: { [x: string]: string }
body?: { [x: string]: any }
}
export type MoneyUSDPerMillionTokens = number
export type GenerateTextResponse = { data: { text: string } }
@@ -465,11 +456,19 @@ export type V2EventServerConnected = {
data: {}
}
export type ProviderRequest = {
settings: ProviderSettings
headers: { [x: string]: string }
body: { [x: string]: any }
}
export type ProviderSettings = {
timeout?: number | false
chunkTimeout?: number
compaction?: ProviderCompaction
transport?: ProviderTransport
} & { [x: string]: any }
export type ConfigProviderSettings = {
timeout?: number | false
chunkTimeout?: number
compaction?: ProviderCompaction
transport?: ProviderTransport
} & { [x: string]: JsonValue | null }
export type PermissionRule = { action: string; resource: string; effect: PermissionEffect }
@@ -1443,20 +1442,6 @@ export type ModelCompatibility = {
supportsPromptCacheKey?: boolean
}
export type ProviderInfo = {
id: string
canonical?: string
integrationID?: string
name: string
activation: "auto" | "enabled" | "disabled"
package: string
compaction?: ProviderCompaction
transport?: ProviderTransport
settings?: { [x: string]: any }
headers?: { [x: string]: string }
body?: { [x: string]: any }
}
export type ModelCost = {
tier?: { type: "context"; size: number }
input: MoneyUSDPerMillionTokens
@@ -1671,6 +1656,31 @@ export type SessionInboxMove = {
payload: SessionInboxMovePayload
}
export type ProviderRequest = {
settings: ProviderSettings
headers: { [x: string]: string }
body: { [x: string]: any }
}
export type ModelVariant = {
id: string
settings?: ProviderSettings
headers?: { [x: string]: string }
body?: { [x: string]: any }
}
export type ProviderInfo = {
id: string
canonical?: string
integrationID?: string
name: string
activation: "auto" | "enabled" | "disabled"
package: string
settings?: ProviderSettings
headers?: { [x: string]: string }
body?: { [x: string]: any }
}
export type PermissionRuleset = Array<PermissionRule>
export type SessionRevertStaged = {
@@ -1854,29 +1864,6 @@ export type FormField =
export type FormState = { status: "pending" } | { status: "answered"; answer: FormAnswer } | { status: "cancelled" }
export type ModelInfo = {
id: string
modelID: string
providerID: string
canonical?: string
family?: string
name: string
compatibility?: ModelCompatibility
package?: string
compaction?: ProviderCompaction
transport?: ProviderTransport
settings?: { [x: string]: any }
headers?: { [x: string]: string }
body?: { [x: string]: any }
capabilities: ModelCapabilities
variants: Array<ModelVariant>
time: { released: number }
cost: Array<ModelCost>
status: "alpha" | "beta" | "deprecated" | "active"
enabled: boolean
limit: { context: number; input?: number; output: number }
}
export type FormField1 =
| FormStringField1
| FormNumberField1
@@ -1902,6 +1889,27 @@ export type ReferenceInfo = {
source: ReferenceSource
}
export type ModelInfo = {
id: string
modelID: string
providerID: string
canonical?: string
family?: string
name: string
compatibility?: ModelCompatibility
package?: string
settings?: ProviderSettings
headers?: { [x: string]: string }
body?: { [x: string]: any }
capabilities: ModelCapabilities
variants: Array<ModelVariant>
time: { released: number }
cost: Array<ModelCost>
status: "alpha" | "beta" | "deprecated" | "active"
enabled: boolean
limit: { context: number; input?: number; output: number }
}
export type AgentInfo = {
id: string
name: string
@@ -2085,31 +2093,27 @@ export type ConfigEntry =
warming?: boolean | { prompt?: string; interval?: string; duration?: string }
providers?: {
[x: string]: {
compaction?: ProviderCompaction
transport?: ProviderTransport
canonical?: string
name?: string
env?: Array<string>
package?: string
settings?: { [x: string]: JsonValue }
settings?: ConfigProviderSettings
headers?: { [x: string]: string }
body?: { [x: string]: JsonValue }
models?: {
[x: string]: {
compaction?: ProviderCompaction
transport?: ProviderTransport
modelID?: string
family?: string
name?: string
compatibility?: ModelCompatibility
package?: string
settings?: { [x: string]: JsonValue }
settings?: ConfigProviderSettings
headers?: { [x: string]: string }
body?: { [x: string]: JsonValue }
capabilities?: ModelCapabilities
variants?: Array<{
id: string
settings?: { [x: string]: JsonValue }
settings?: ConfigProviderSettings
headers?: { [x: string]: string }
body?: { [x: string]: JsonValue }
}>
+6 -1
View File
@@ -128,6 +128,8 @@ function prepareOptions(model: Info, pkg: string) {
const customFetch = options.fetch
const chunkTimeout = options.chunkTimeout
delete options.chunkTimeout
delete options.compaction
delete options.transport
options.fetch = async (input: Parameters<typeof fetch>[0], init?: RequestInit) => {
const opts = { ...(init ?? {}) }
const signals = [
@@ -388,7 +390,10 @@ function requestSettings(settings: Readonly<Record<string, unknown>> | undefined
if (settings === undefined) return undefined
const result = Object.fromEntries(
Object.entries(settings).filter(
([key]) => !["apiKey", "authToken", "baseURL", "chunkTimeout", "fetch", "timeout"].includes(key),
([key]) =>
!["apiKey", "authToken", "baseURL", "chunkTimeout", "compaction", "fetch", "timeout", "transport"].includes(
key,
),
),
)
return Object.keys(result).length === 0 ? undefined : result
@@ -68,8 +68,6 @@ export const Plugin = define({
if (item.canonical !== undefined) provider.canonical = item.canonical
if (item.name !== undefined) provider.name = item.name
if (item.package !== undefined) provider.package = item.package
if (item.compaction !== undefined) provider.compaction = { ...item.compaction }
if (item.transport !== undefined) provider.transport = item.transport
if (item.settings !== undefined) provider.settings = Provider.mergeOverlay(provider.settings, item.settings)
if (item.headers !== undefined) provider.headers = Provider.mergeHeaders(provider.headers, item.headers)
if (item.body !== undefined) provider.body = Provider.mergeOverlay(provider.body, item.body)
@@ -116,8 +114,6 @@ export const Plugin = define({
if (config.compatibility !== undefined)
model.compatibility = { ...model.compatibility, ...config.compatibility }
if (config.package !== undefined) model.package = config.package
if (config.compaction !== undefined) model.compaction = { ...config.compaction }
if (config.transport !== undefined) model.transport = config.transport
if (config.settings !== undefined) model.settings = Provider.mergeOverlay(model.settings, config.settings)
if (config.headers !== undefined) model.headers = Provider.mergeHeaders(model.headers, config.headers)
if (config.body !== undefined) model.body = Provider.mergeOverlay(model.body, config.body)
+9 -5
View File
@@ -118,9 +118,9 @@ export interface Resolved {
/** Catalog token limits used by Core for context management. */
readonly limit: Info["limit"]
/** Model policy overrides the provider policy; omitted means local compaction. */
readonly compaction?: Info["compaction"]
readonly compaction?: Provider.Compaction
/** Model transport overrides the provider transport; omitted means HTTP. */
readonly transport?: Info["transport"]
readonly transport?: Provider.Transport
}
export interface Interface {
@@ -178,7 +178,11 @@ export const fromCatalogModel = (
Effect.flatMap((resolved) => validateProviderVariables(model, resolved)),
Effect.flatMap((resolved) => {
// Reject provider compaction policies up front so the misconfiguration surfaces before any step runs.
if (model.compaction?.mode !== "provider" || resolved.route.compact?.trigger || resolved.route.compact?.endpoint)
if (
model.settings?.compaction?.type !== "native" ||
resolved.route.compact?.trigger ||
resolved.route.compact?.endpoint
)
return Effect.succeed(resolved)
return Effect.fail(
new UnsupportedCompactionError({ providerID: model.providerID, modelID: model.id, route: resolved.route.id }),
@@ -377,8 +381,8 @@ export const layer = Layer.effect(
capabilities: selected.capabilities,
cost: selected.cost,
limit: selected.limit,
compaction: selected.compaction,
transport: selected.transport,
compaction: runtimeInfo.settings?.compaction,
transport: runtimeInfo.settings?.transport,
}
})
return Service.of({
-2
View File
@@ -188,8 +188,6 @@ const layer = Layer.effect(
...model,
...(provider?.canonical === undefined ? {} : { canonical: provider.canonical }),
package: model.package ?? provider?.package,
compaction: model.compaction ?? provider?.compaction,
transport: model.transport ?? provider?.transport,
settings: Provider.mergeOverlay(provider?.settings, model.settings),
headers: Provider.mergeHeaders(provider?.headers, model.headers),
body: Provider.mergeOverlay(provider?.body, model.body),
+3 -1
View File
@@ -168,7 +168,9 @@ export const AzurePlugin = define({
resolveResourceName(draft.settings, resourceName) ?? resourceName,
)
if (responsesWebSocketCapable(item.provider, draft))
draft.transport = item.provider.transport ?? "websocket"
draft.settings = Provider.mergeOverlay(draft.settings, {
transport: item.provider.settings?.transport ?? "websocket",
})
})
}
}
+3 -1
View File
@@ -270,7 +270,9 @@ export const OpenAIPlugin = define({
// ChatGPT-plan tokens only authorize codex-eligible models, and the
// subscription covers usage, so hide the rest and zero the cost.
models.update(model.providerID, model.id, (draft) => {
draft.transport = models.provider.get(model.providerID)?.provider.transport ?? "websocket"
draft.settings = Provider.mergeOverlay(draft.settings, {
transport: models.provider.get(model.providerID)?.provider.settings?.transport ?? "websocket",
})
if (!chatgpt) return
if (Schema.is(Schema.Struct({ mode: Schema.Literal("pro") }))(draft.body?.reasoning)) {
draft.enabled = false
+3 -1
View File
@@ -98,7 +98,9 @@ export const XAIPlugin = define({
yield* ctx.model.transform((models) => {
for (const model of models.list(providerID)) {
models.update(providerID, model.id, (draft) => {
draft.transport = models.provider.get(providerID)?.provider.transport ?? "websocket"
draft.settings = Provider.mergeOverlay(draft.settings, {
transport: models.provider.get(providerID)?.provider.settings?.transport ?? "websocket",
})
})
}
})
+9 -3
View File
@@ -132,11 +132,11 @@ export const loadPackage = Effect.fn("Provider.loadPackage")(function* (input: s
return yield* importPackage(specifier, entrypoint)
})
/** opencode transport settings consumed in aisdk.ts; native packages never receive them. */
const TRANSPORT_KEYS = ["chunkTimeout", "fetch", "timeout"] as const
/** opencode settings consumed in Core; native packages never receive them. */
const CORE_KEYS = ["chunkTimeout", "compaction", "fetch", "timeout", "transport"] as const
export function nativeSettings(settings: Settings): Settings {
return Struct.omit(settings, TRANSPORT_KEYS)
return Struct.omit(settings, CORE_KEYS)
}
export function mergeOverlay(
@@ -177,6 +177,12 @@ export function mergeHeaders(
export const Request = Provider.Request
export type Request = Provider.Request
export const Compaction = Provider.Compaction
export type Compaction = Provider.Compaction
export const Transport = Provider.Transport
export type Transport = Provider.Transport
export const Settings = Provider.Settings
export type Settings = Provider.Settings
+3 -8
View File
@@ -736,7 +736,7 @@ export const layer = Layer.effect(
const compact = Effect.fn("SessionCompaction.compact")(function* (input: AutoInput): Effect.fn.Return<Outcome> {
const request = { ...input, reason: "auto" as const }
if (input.overflow) return yield* recoverLocally(request)
if (input.context.model.compaction?.mode !== "provider") return yield* execute(request)
if (input.context.model.compaction?.type !== "native") return yield* execute(request)
return yield* executeProvider(request)
})
const required = (input: RequiredInput) => {
@@ -759,12 +759,7 @@ export const layer = Layer.effect(
limit.input === undefined ? Number.POSITIVE_INFINITY : limit.input - config.buffer,
context - Math.max(output, config.buffer),
)
const policy = input.resolved.compaction
const threshold =
policy?.mode === "provider" && policy.threshold !== undefined
? Math.min(policy.threshold, promptCeiling)
: promptCeiling
return estimateTokens(input) >= threshold
return estimateTokens(input) >= promptCeiling
}
const compactManual = Effect.fn("SessionCompaction.compactManual")(function* (input: ManualInput) {
if (findTailStart(input.messages, state.get().tokens) === undefined)
@@ -792,7 +787,7 @@ export const layer = Layer.effect(
inputID: input.inputID,
started: input.started,
}
return context.model.compaction?.mode === "provider" ? executeProvider(request) : execute(request)
return context.model.compaction?.type === "native" ? executeProvider(request) : execute(request)
},
}),
)
+6
View File
@@ -155,14 +155,18 @@ it.effect("projects request settings, headers, and body overlays", () =>
Effect.gen(function* () {
const aisdk = yield* AISDK.Service
let body: unknown
let options: Record<string, unknown> | undefined
yield* aisdk.hook.sdk((event) => {
body = event.options.body
options = event.options
event.sdk = { languageModel: () => ({ provider: event.model.providerID }) }
})
const input = model("@ai-sdk/google", {
apiKey: "secret",
thinkingConfig: { thinkingBudget: 1024 },
compaction: { type: "native" },
transport: "websocket",
})
const resolved = yield* aisdk.model({
...input,
@@ -185,6 +189,8 @@ it.effect("projects request settings, headers, and body overlays", () =>
})
expect(prepared.body.headers).toEqual({ "x-test": "header" })
expect(body).toEqual({ safety_setting: "strict" })
expect(options).not.toHaveProperty("compaction")
expect(options).not.toHaveProperty("transport")
}),
)
+16 -25
View File
@@ -34,7 +34,7 @@ function required<T>(value: T | undefined): T {
const decode = Schema.decodeUnknownSync(Info)
describe("ConfigProviderPlugin.Plugin", () => {
it.effect("inherits provider compaction policy with model overrides and rejects unsupported routes", () =>
it.effect("inherits the provider compaction setting with model overrides and rejects unsupported routes", () =>
Effect.gen(function* () {
const models = yield* Model.Service
yield* addPlugin([
@@ -44,12 +44,10 @@ describe("ConfigProviderPlugin.Plugin", () => {
providers: {
custom: {
package: "@opencode/ai/providers/openai/responses",
compaction: { mode: "provider", threshold: 120_000 },
settings: { compaction: { type: "native" } },
models: {
native: {},
reset: { compaction: { mode: "provider" } },
threshold: { compaction: { mode: "provider", threshold: 90_000 } },
local: { compaction: { mode: "local" }, package: "@opencode/ai/providers/openai/chat" },
local: { settings: { compaction: { type: "summary" } }, package: "@opencode/ai/providers/openai/chat" },
unsupported: { package: "@opencode/ai/providers/openai/chat" },
},
},
@@ -62,16 +60,9 @@ describe("ConfigProviderPlugin.Plugin", () => {
const local = required(yield* models.get(Provider.ID.make("custom"), Model.ID.make("local")))
const unsupported = required(yield* models.get(Provider.ID.make("custom"), Model.ID.make("unsupported")))
const defaultModel = required(yield* models.get(Provider.ID.make("default"), Model.ID.make("chat")))
expect(native.compaction).toEqual({ mode: "provider", threshold: 120_000 })
expect((yield* models.get(Provider.ID.make("custom"), Model.ID.make("reset")))?.compaction).toEqual({
mode: "provider",
})
expect((yield* models.get(Provider.ID.make("custom"), Model.ID.make("threshold")))?.compaction).toEqual({
mode: "provider",
threshold: 90_000,
})
expect(local.compaction).toEqual({ mode: "local" })
expect(defaultModel.compaction).toBeUndefined()
expect(native.settings?.compaction).toEqual({ type: "native" })
expect(local.settings?.compaction).toEqual({ type: "summary" })
expect(defaultModel.settings?.compaction).toBeUndefined()
yield* ModelResolver.fromCatalogModel(native)
yield* ModelResolver.fromCatalogModel(local)
yield* ModelResolver.fromCatalogModel(defaultModel)
@@ -92,8 +83,8 @@ describe("ConfigProviderPlugin.Plugin", () => {
providers: {
custom: {
package: "@opencode/ai/providers/openai/responses",
transport: "http",
models: { inherited: {}, override: { transport: "websocket" } },
settings: { transport: "http" },
models: { inherited: {}, override: { settings: { transport: "websocket" } } },
},
default: { package: "@opencode/ai/providers/openai/responses", models: { untouched: {} } },
},
@@ -103,9 +94,9 @@ describe("ConfigProviderPlugin.Plugin", () => {
const inherited = required(yield* models.get(Provider.ID.make("custom"), Model.ID.make("inherited")))
const override = required(yield* models.get(Provider.ID.make("custom"), Model.ID.make("override")))
const untouched = required(yield* models.get(Provider.ID.make("default"), Model.ID.make("untouched")))
expect(inherited.transport).toBe("http")
expect(override.transport).toBe("websocket")
expect(untouched.transport).toBeUndefined()
expect(inherited.settings?.transport).toBe("http")
expect(override.settings?.transport).toBe("websocket")
expect(untouched.settings?.transport).toBeUndefined()
}),
)
@@ -131,7 +122,7 @@ describe("ConfigProviderPlugin.Plugin", () => {
editor.models.update(providerID, modelID, () => {})
})
yield* builtin.plugin.effect(host)
expect((yield* models.get(providerID, modelID))?.transport).toBe("websocket")
expect((yield* models.get(providerID, modelID))?.settings?.transport).toBe("websocket")
yield* addPlugin([
new Document({
@@ -139,16 +130,16 @@ describe("ConfigProviderPlugin.Plugin", () => {
info: decode({
providers: {
[builtin.id]: {
transport: "http",
models: { override: { modelID: builtin.model, transport: "websocket" } },
settings: { transport: "http" },
models: { override: { modelID: builtin.model, settings: { transport: "websocket" } } },
},
},
}),
}),
])
expect((yield* models.get(providerID, modelID))?.transport).toBe("http")
expect((yield* models.get(providerID, Model.ID.make("override")))?.transport).toBe("websocket")
expect((yield* models.get(providerID, modelID))?.settings?.transport).toBe("http")
expect((yield* models.get(providerID, Model.ID.make("override")))?.settings?.transport).toBe("websocket")
}),
)
}
@@ -473,10 +473,10 @@ describe("AzurePlugin", () => {
yield* addPlugin()
const responses = required(yield* service.get(Provider.ID.azure, models.responses))
expect(responses.transport).toBe("websocket")
expect(responses.settings?.transport).toBe("websocket")
for (const modelID of [models.chat, models.preview, models.deploymentURL, models.gateway, models.nonAzure]) {
const model = required(yield* service.get(Provider.ID.azure, modelID))
expect(model.transport).toBeUndefined()
expect(model.settings?.transport).toBeUndefined()
}
}),
),
@@ -205,7 +205,7 @@ describe("OpenAIPlugin", () => {
expect(model.package).toBe("@opencode/ai/providers/openai")
expect(model.enabled).toBe(true)
expect(model.limit).toEqual({ context: 1_050_000, input: 922_000, output: 128_000 })
expect(model.transport).toBe("websocket")
expect(model.settings?.transport).toBe("websocket")
expect(direct.headers).not.toHaveProperty("originator")
expect(direct.baseURL).toBe("https://api.openai.com/v1")
expect(provider.headers).not.toHaveProperty("x-codex-beta-features")
@@ -236,7 +236,7 @@ describe("OpenAIPlugin", () => {
id: "deployment-responses",
provider: Provider.ID.azure,
})
const prepare = (preference?: Model.Info["transport"]) =>
const prepare = (preference?: Provider.Transport) =>
Effect.gen(function* () {
const model = SessionRunnerModel.resolved(route.model({ id: "gpt-5.5" }), {
capabilities: { tools: true, input: ["text"], output: ["text"] },
@@ -84,7 +84,7 @@ describe("XAIPlugin", () => {
yield* addPlugin()
const model = yield* models.get(providerID, Model.ID.make("grok-4.6"))
expect(model?.transport).toBe("websocket")
expect(model?.settings?.transport).toBe("websocket")
}),
)
})
+10 -2
View File
@@ -28,8 +28,16 @@ describe("Provider", () => {
}
})
test("passes flat settings to native packages without opencode transport keys", () => {
expect(Provider.nativeSettings({ apiKey: "secret", reasoningEffort: "high", chunkTimeout: 1000 })).toEqual({
test("passes flat settings to native packages without Core settings", () => {
expect(
Provider.nativeSettings({
apiKey: "secret",
reasoningEffort: "high",
chunkTimeout: 1000,
compaction: { type: "native" },
transport: "websocket",
}),
).toEqual({
apiKey: "secret",
reasoningEffort: "high",
})
+3 -10
View File
@@ -207,20 +207,13 @@ it.effect("auto compaction estimates current content against the buffered prompt
const inputLimited = { context: 400_000, input: 272_000, output: 128_000 }
expect(compaction.required(input(251_999, inputLimited))).toBe(false)
expect(compaction.required(input(252_000, inputLimited))).toBe(true)
const native = (
tokens: number,
limit: { context: number; input?: number; output: number } = inputLimited,
threshold?: number,
) => {
const native = (tokens: number, limit: { context: number; input?: number; output: number } = inputLimited) => {
const selected = input(tokens, limit)
return { ...selected, resolved: { ...selected.resolved, compaction: { mode: "provider" as const, threshold } } }
return { ...selected, resolved: { ...selected.resolved, compaction: { type: "native" as const } } }
}
expect(compaction.required(native(251_999))).toBe(false)
expect(compaction.required(native(252_000))).toBe(true)
expect(compaction.required(native(99_999, inputLimited, 100_000))).toBe(false)
expect(compaction.required(native(100_000, inputLimited, 100_000))).toBe(true)
expect(compaction.required(native(252_000, inputLimited, 500_000))).toBe(true)
expect(compaction.required(native(1_000_000, { context: 0, input: undefined, output: 0 }, 100_000))).toBe(false)
expect(compaction.required(native(1_000_000, { context: 0, input: undefined, output: 0 }))).toBe(false)
const contextLimited = { context: 100_000, output: 10_000 }
expect(compaction.required(input(79_999, contextLimited))).toBe(false)
@@ -164,7 +164,7 @@ const setup = Effect.fnUntraced(function* (options: { endpoint?: boolean; plugin
capabilities: { tools: true, input: ["text", "image"], output: ["text"] },
cost: [],
limit: { context: 200_000, output: 32_000 },
compaction: { mode: "provider" },
compaction: { type: "native" },
},
)
const sessionID = SessionSchema.ID.create()
+4 -2
View File
@@ -2863,7 +2863,8 @@ describe("SessionRunnerLLM", () => {
scenario("automatically persists native windows, retains earlier users, and waits for fresh usage", function* (s) {
s.currentModel = LanguageModel.make({ id: "native", provider: "openai", route: OpenAIResponses.route })
s.compaction = { mode: "provider", threshold: 10_000 }
modelLimits.set("native", { context: 42_000, output: 32_000 })
s.compaction = { type: "native" }
const agents = yield* Agent.Service
yield* agents.transform((editor) =>
editor.update(Agent.defaultID, (agent) => {
@@ -2914,7 +2915,8 @@ describe("SessionRunnerLLM", () => {
scenario("recovers an overflowing native window locally from original durable history", function* (s) {
s.currentModel = LanguageModel.make({ id: "native", provider: "openai", route: OpenAIResponses.route })
s.compaction = { mode: "provider", threshold: 10_000 }
modelLimits.set("native", { context: 42_000, output: 32_000 })
s.compaction = { type: "native" }
yield* s.llm.push(TestLLM.textWithUsage("Earlier answer", "before-native", 10_000))
yield* s.runPrompt("Original durable request")
yield* s.llm.push(
+3
View File
@@ -106,6 +106,9 @@ test("Core reuses the canonical shared schemas", async () => {
[coreModel.Info, Model.Info],
[coreProvider.ID, Provider.ID],
[coreProvider.Request, Provider.Request],
[coreProvider.Compaction, Provider.Compaction],
[coreProvider.Transport, Provider.Transport],
[coreProvider.Settings, Provider.Settings],
[coreProvider.Info, Provider.Info],
[corePermission.Effect, Permission.Effect],
[corePermission.Rule, Permission.Rule],
+76 -38
View File
@@ -13161,7 +13161,7 @@
"properties": {
"action": {
"type": "string",
"enum": ["provider.use"]
"enum": ["provider.use", "permission"]
},
"resource": {
"type": "string"
@@ -13254,12 +13254,6 @@
"Config.ModelEncoded": {
"type": "object",
"properties": {
"compaction": {
"$ref": "#/components/schemas/Provider.Compaction"
},
"transport": {
"$ref": "#/components/schemas/Provider.Transport"
},
"modelID": {
"type": "string"
},
@@ -13276,7 +13270,7 @@
"type": "string"
},
"settings": {
"type": "object"
"$ref": "#/components/schemas/Config.Provider.Settings"
},
"headers": {
"type": "object",
@@ -13299,7 +13293,7 @@
"type": "string"
},
"settings": {
"type": "object"
"$ref": "#/components/schemas/Config.Provider.Settings"
},
"headers": {
"type": "object",
@@ -13379,15 +13373,47 @@
"required": ["package"],
"additionalProperties": false
},
"Config.ProviderEncoded": {
"Config.Provider.Settings": {
"type": "object",
"properties": {
"timeout": {
"anyOf": [
{
"type": "number"
},
{
"type": "boolean",
"enum": [false]
}
]
},
"chunkTimeout": {
"type": "number"
},
"compaction": {
"$ref": "#/components/schemas/Provider.Compaction"
},
"transport": {
"$ref": "#/components/schemas/Provider.Transport"
},
}
},
"allOf": [
{
"type": "object",
"additionalProperties": {
"anyOf": [
{},
{
"type": "null"
}
]
}
}
]
},
"Config.ProviderEncoded": {
"type": "object",
"properties": {
"canonical": {
"type": "string"
},
@@ -13404,7 +13430,7 @@
"type": "string"
},
"settings": {
"type": "object"
"$ref": "#/components/schemas/Config.Provider.Settings"
},
"headers": {
"type": "object",
@@ -15481,14 +15507,8 @@
"package": {
"type": "string"
},
"compaction": {
"$ref": "#/components/schemas/Provider.Compaction"
},
"transport": {
"$ref": "#/components/schemas/Provider.Transport"
},
"settings": {
"type": "object"
"$ref": "#/components/schemas/Provider.Settings"
},
"headers": {
"type": "object",
@@ -15601,7 +15621,7 @@
"type": "string"
},
"settings": {
"type": "object"
"$ref": "#/components/schemas/Provider.Settings"
},
"headers": {
"type": "object",
@@ -16418,27 +16438,23 @@
{
"type": "object",
"properties": {
"mode": {
"type": {
"type": "string",
"enum": ["local"]
"enum": ["summary"]
}
},
"required": ["mode"],
"required": ["type"],
"additionalProperties": false
},
{
"type": "object",
"properties": {
"mode": {
"type": {
"type": "string",
"enum": ["provider"]
},
"threshold": {
"type": "integer",
"exclusiveMinimum": 0
"enum": ["native"]
}
},
"required": ["mode"],
"required": ["type"],
"additionalProperties": false
}
]
@@ -16465,14 +16481,8 @@
"package": {
"type": "string"
},
"compaction": {
"$ref": "#/components/schemas/Provider.Compaction"
},
"transport": {
"$ref": "#/components/schemas/Provider.Transport"
},
"settings": {
"type": "object"
"$ref": "#/components/schemas/Provider.Settings"
},
"headers": {
"type": "object",
@@ -16507,7 +16517,35 @@
"additionalProperties": false
},
"Provider.Settings": {
"type": "object"
"type": "object",
"properties": {
"timeout": {
"anyOf": [
{
"type": "number"
},
{
"type": "boolean",
"enum": [false]
}
]
},
"chunkTimeout": {
"type": "number"
},
"compaction": {
"$ref": "#/components/schemas/Provider.Compaction"
},
"transport": {
"$ref": "#/components/schemas/Provider.Transport"
}
},
"allOf": [
{
"type": "object",
"additionalProperties": {}
}
]
},
"Provider.Transport": {
"type": "string",
+12 -10
View File
@@ -6,10 +6,21 @@ import { Capabilities, Compatibility, Family, ID, VariantID } from "../model.js"
import { Provider } from "../provider.js"
import { optional } from "../schema.js"
export const Settings = Schema.StructWithRest(
Schema.Struct({
timeout: Schema.Union([Schema.Finite, Schema.Literal(false)]).pipe(optional),
chunkTimeout: Schema.Finite.pipe(optional),
compaction: Provider.Compaction.pipe(optional),
transport: Provider.Transport.pipe(optional),
}),
[Schema.Record(Schema.String, Schema.UndefinedOr(Schema.Json))],
).annotate({ identifier: "Config.Provider.Settings" })
export type Settings = typeof Settings.Type
const JsonRecord = Schema.Record(Schema.String, Schema.Json)
export const Overlays = {
settings: JsonRecord.pipe(optional),
settings: Settings.pipe(optional),
headers: Schema.Record(Schema.String, Schema.String).pipe(optional),
body: JsonRecord.pipe(optional),
}
@@ -41,10 +52,6 @@ class Limit extends Schema.Class<Limit>("Config.Model.Limit")({
}) {}
class Model extends Schema.Class<Model>("Config.Model")({
compaction: Provider.Compaction.pipe(optional),
transport: Provider.Transport.pipe(optional).annotate({
description: "Session transport for this model. Defaults to the provider transport.",
}),
modelID: ID.pipe(optional),
family: Family.pipe(optional),
name: Schema.String.pipe(optional),
@@ -62,11 +69,6 @@ class Model extends Schema.Class<Model>("Config.Model")({
}) {}
export class Info extends Schema.Class<Info>("Config.Provider")({
compaction: Provider.Compaction.pipe(optional),
transport: Provider.Transport.pipe(optional).annotate({
description:
"Session transport for this provider's models. Defaults to the built-in policy; \"websocket\" on a route without a WebSocket channel warns and falls back to HTTP.",
}),
canonical: Provider.ID.pipe(optional),
name: Schema.String.pipe(optional),
env: Schema.String.pipe(Schema.Array, optional),
-3
View File
@@ -110,9 +110,6 @@ export const Info = Schema.Struct({
name: Schema.String,
compatibility: Compatibility.pipe(optional),
package: Provider.Package.pipe(optional),
compaction: Provider.Compaction.pipe(optional),
/** Session transport; omitted inherits the provider transport, then defaults to HTTP. */
transport: Provider.Transport.pipe(optional),
...Provider.Overlays,
capabilities: Capabilities,
variants: Schema.Array(Variant),
+19 -12
View File
@@ -2,7 +2,7 @@ export * as Provider from "./provider.js"
import { Effect, Schema } from "effect"
import { Integration } from "./integration.js"
import { optional, PositiveInt, statics } from "./schema.js"
import { optional, statics } from "./schema.js"
import { ephemeral, inventory } from "./event.js"
export const ID = Schema.String.pipe(
@@ -32,25 +32,35 @@ export type Package = typeof Package.Type
export const Activation = Schema.Literals(["auto", "enabled", "disabled"])
export type Activation = typeof Activation.Type
export type Compaction = typeof Compaction.Type
export const Compaction = Schema.Union([
Schema.Struct({ mode: Schema.Literal("local") }),
Schema.Struct({ mode: Schema.Literal("provider"), threshold: PositiveInt.pipe(optional) }),
]).annotate({ identifier: "Provider.Compaction" })
Schema.Struct({ type: Schema.Literal("summary") }),
Schema.Struct({ type: Schema.Literal("native") }),
])
.pipe(Schema.toTaggedUnion("type"))
.annotate({ identifier: "Provider.Compaction" })
export type Compaction = typeof Compaction.Type
/** "websocket" on a route without a WebSocket channel warns and falls back to HTTP. */
export const Transport = Schema.Literals(["http", "websocket"]).annotate({ identifier: "Provider.Transport" })
export type Transport = typeof Transport.Type
export const Settings = Schema.StructWithRest(
Schema.Struct({
timeout: Schema.Union([Schema.Finite, Schema.Literal(false)]).pipe(optional),
chunkTimeout: Schema.Finite.pipe(optional),
compaction: Compaction.pipe(optional),
transport: Transport.pipe(optional),
}),
[Schema.Record(Schema.String, Schema.Any)],
).annotate({ identifier: "Provider.Settings" })
export type Settings = typeof Settings.Type
export const Overlays = {
settings: Schema.Record(Schema.String, Schema.Any).pipe(optional),
settings: Settings.pipe(optional),
headers: Schema.Record(Schema.String, Schema.String).pipe(optional),
body: Schema.Record(Schema.String, Schema.Any).pipe(optional),
}
export const Settings = Schema.Record(Schema.String, Schema.Any).annotate({ identifier: "Provider.Settings" })
export type Settings = typeof Settings.Type
export interface Request extends Schema.Schema.Type<typeof Request> {}
export const Request = Schema.Struct({
settings: Settings.pipe(Schema.withConstructorDefault(Effect.succeed({}))),
@@ -66,9 +76,6 @@ export const Info = Schema.Struct({
name: Schema.String,
activation: Activation,
package: Package,
compaction: Compaction.pipe(optional),
/** Session transport for this provider's models; omitted means HTTP. */
transport: Transport.pipe(optional),
...Overlays,
})
.annotate({ identifier: "Provider.Info" })
@@ -2,6 +2,7 @@ import { describe, expect, test } from "bun:test"
import { DateTime, Schema } from "effect"
import { Agent } from "../src/agent.js"
import { ConfigAgent } from "../src/config/agent.js"
import { ConfigProvider } from "../src/config/provider.js"
import { FileSystem } from "../src/filesystem.js"
import { Form } from "../src/form.js"
import { Mcp } from "../src/mcp.js"
@@ -168,6 +169,7 @@ describe("contract hygiene", () => {
test("reusable public identifiers are stable and unique", () => {
const identifiers = [
Agent.Color,
ConfigProvider.Settings,
FileSystem.Submatch,
Form.Field,
Form.Fields,
@@ -226,7 +228,7 @@ describe("contract hygiene", () => {
}
})
test("current source limits Any to provider options and avoids mutable contract wrappers", async () => {
test("current source limits Any to reviewed boundaries and avoids mutable contract wrappers", async () => {
const files = [...new Bun.Glob("*.ts").scanSync(new URL("../src", import.meta.url).pathname)].filter(
(file) => !file.endsWith("-v1.ts"),
)
@@ -237,11 +239,12 @@ describe("contract hygiene", () => {
expect(
sources
.filter((item) => item.file !== "provider.ts")
.filter((item) => item.file !== "provider.ts" && item.file !== "integration.ts")
.map((item) => item.source)
.join("\n"),
).not.toContain("Schema.Any")
expect(sources.find((item) => item.file === "provider.ts")?.source.match(/Schema\.Any/g)).toHaveLength(4)
expect(sources.find((item) => item.file === "provider.ts")?.source.match(/Schema\.Any/g)).toHaveLength(3)
expect(sources.find((item) => item.file === "integration.ts")?.source.match(/Schema\.Any/g)).toHaveLength(2)
expect(source).not.toContain("Schema.mutable")
})
+14 -19
View File
@@ -56,23 +56,16 @@ describe("Model.Compatibility", () => {
})
describe("Model.Info", () => {
test("provider compaction policy is optional and uses the canonical closed schema", () => {
test("provider compaction policy is a typed setting", () => {
const model = Model.Info.default(Provider.ID.openai, Model.ID.make("gpt-5.4-mini"))
expect(Schema.encodeSync(Model.Info)({ ...model, compaction: undefined })).not.toHaveProperty("compaction")
expect(Schema.decodeUnknownSync(Model.Info)({ ...model, compaction: { mode: "provider" } }).compaction).toEqual({
mode: "provider",
expect(Schema.encodeSync(Model.Info)({ ...model, settings: { compaction: undefined } }).settings).toEqual({})
expect(
Schema.decodeUnknownSync(Model.Info)({ ...model, settings: { compaction: { type: "native" } } }).settings,
).toEqual({
compaction: { type: "native" },
})
expect(Schema.decodeUnknownSync(Provider.Compaction)({ mode: "local" })).toEqual({ mode: "local" })
expect(Schema.encodeSync(Provider.Compaction)({ mode: "provider", threshold: undefined })).toEqual({
mode: "provider",
})
expect(Schema.decodeUnknownSync(Provider.Compaction)({ mode: "provider", threshold: 120_000 })).toEqual({
mode: "provider",
threshold: 120_000,
})
for (const threshold of [0, -1, 1.5])
expect(() => Schema.decodeUnknownSync(Provider.Compaction)({ mode: "provider", threshold })).toThrow()
expect(() => Schema.decodeUnknownSync(Provider.Compaction)({ mode: "automatic" })).toThrow()
expect(Schema.decodeUnknownSync(Provider.Compaction)({ type: "summary" })).toEqual({ type: "summary" })
expect(() => Schema.decodeUnknownSync(Provider.Compaction)({ type: "automatic" })).toThrow()
})
test("uses practical token limits for unknown models", () => {
@@ -83,10 +76,12 @@ describe("Model.Info", () => {
})
describe("Model.Capabilities", () => {
test("decodes the optional transport preference", () => {
test("decodes the optional transport setting", () => {
const model = Model.Info.default(Provider.ID.openai, Model.ID.make("gpt-5.4-mini"))
expect(Schema.encodeSync(Model.Info)({ ...model, transport: undefined })).not.toHaveProperty("transport")
expect(Schema.decodeUnknownSync(Model.Info)({ ...model, transport: "websocket" }).transport).toBe("websocket")
expect(() => Schema.decodeUnknownSync(Model.Info)({ ...model, transport: "sse" })).toThrow()
expect(Schema.encodeSync(Model.Info)({ ...model, settings: { transport: undefined } }).settings).toEqual({})
expect(Schema.decodeUnknownSync(Model.Info)({ ...model, settings: { transport: "websocket" } }).settings).toEqual({
transport: "websocket",
})
expect(() => Schema.decodeUnknownSync(Model.Info)({ ...model, settings: { transport: "sse" } })).toThrow()
})
})
+76 -38
View File
@@ -13161,7 +13161,7 @@
"properties": {
"action": {
"type": "string",
"enum": ["provider.use"]
"enum": ["provider.use", "permission"]
},
"resource": {
"type": "string"
@@ -13254,12 +13254,6 @@
"Config.ModelEncoded": {
"type": "object",
"properties": {
"compaction": {
"$ref": "#/components/schemas/Provider.Compaction"
},
"transport": {
"$ref": "#/components/schemas/Provider.Transport"
},
"modelID": {
"type": "string"
},
@@ -13276,7 +13270,7 @@
"type": "string"
},
"settings": {
"type": "object"
"$ref": "#/components/schemas/Config.Provider.Settings"
},
"headers": {
"type": "object",
@@ -13299,7 +13293,7 @@
"type": "string"
},
"settings": {
"type": "object"
"$ref": "#/components/schemas/Config.Provider.Settings"
},
"headers": {
"type": "object",
@@ -13379,15 +13373,47 @@
"required": ["package"],
"additionalProperties": false
},
"Config.ProviderEncoded": {
"Config.Provider.Settings": {
"type": "object",
"properties": {
"timeout": {
"anyOf": [
{
"type": "number"
},
{
"type": "boolean",
"enum": [false]
}
]
},
"chunkTimeout": {
"type": "number"
},
"compaction": {
"$ref": "#/components/schemas/Provider.Compaction"
},
"transport": {
"$ref": "#/components/schemas/Provider.Transport"
},
}
},
"allOf": [
{
"type": "object",
"additionalProperties": {
"anyOf": [
{},
{
"type": "null"
}
]
}
}
]
},
"Config.ProviderEncoded": {
"type": "object",
"properties": {
"canonical": {
"type": "string"
},
@@ -13404,7 +13430,7 @@
"type": "string"
},
"settings": {
"type": "object"
"$ref": "#/components/schemas/Config.Provider.Settings"
},
"headers": {
"type": "object",
@@ -15481,14 +15507,8 @@
"package": {
"type": "string"
},
"compaction": {
"$ref": "#/components/schemas/Provider.Compaction"
},
"transport": {
"$ref": "#/components/schemas/Provider.Transport"
},
"settings": {
"type": "object"
"$ref": "#/components/schemas/Provider.Settings"
},
"headers": {
"type": "object",
@@ -15601,7 +15621,7 @@
"type": "string"
},
"settings": {
"type": "object"
"$ref": "#/components/schemas/Provider.Settings"
},
"headers": {
"type": "object",
@@ -16418,27 +16438,23 @@
{
"type": "object",
"properties": {
"mode": {
"type": {
"type": "string",
"enum": ["local"]
"enum": ["summary"]
}
},
"required": ["mode"],
"required": ["type"],
"additionalProperties": false
},
{
"type": "object",
"properties": {
"mode": {
"type": {
"type": "string",
"enum": ["provider"]
},
"threshold": {
"type": "integer",
"exclusiveMinimum": 0
"enum": ["native"]
}
},
"required": ["mode"],
"required": ["type"],
"additionalProperties": false
}
]
@@ -16465,14 +16481,8 @@
"package": {
"type": "string"
},
"compaction": {
"$ref": "#/components/schemas/Provider.Compaction"
},
"transport": {
"$ref": "#/components/schemas/Provider.Transport"
},
"settings": {
"type": "object"
"$ref": "#/components/schemas/Provider.Settings"
},
"headers": {
"type": "object",
@@ -16507,7 +16517,35 @@
"additionalProperties": false
},
"Provider.Settings": {
"type": "object"
"type": "object",
"properties": {
"timeout": {
"anyOf": [
{
"type": "number"
},
{
"type": "boolean",
"enum": [false]
}
]
},
"chunkTimeout": {
"type": "number"
},
"compaction": {
"$ref": "#/components/schemas/Provider.Compaction"
},
"transport": {
"$ref": "#/components/schemas/Provider.Transport"
}
},
"allOf": [
{
"type": "object",
"additionalProperties": {}
}
]
},
"Provider.Transport": {
"type": "string",
+76 -38
View File
@@ -13161,7 +13161,7 @@
"properties": {
"action": {
"type": "string",
"enum": ["provider.use"]
"enum": ["provider.use", "permission"]
},
"resource": {
"type": "string"
@@ -13254,12 +13254,6 @@
"Config.ModelEncoded": {
"type": "object",
"properties": {
"compaction": {
"$ref": "#/components/schemas/Provider.Compaction"
},
"transport": {
"$ref": "#/components/schemas/Provider.Transport"
},
"modelID": {
"type": "string"
},
@@ -13276,7 +13270,7 @@
"type": "string"
},
"settings": {
"type": "object"
"$ref": "#/components/schemas/Config.Provider.Settings"
},
"headers": {
"type": "object",
@@ -13299,7 +13293,7 @@
"type": "string"
},
"settings": {
"type": "object"
"$ref": "#/components/schemas/Config.Provider.Settings"
},
"headers": {
"type": "object",
@@ -13379,15 +13373,47 @@
"required": ["package"],
"additionalProperties": false
},
"Config.ProviderEncoded": {
"Config.Provider.Settings": {
"type": "object",
"properties": {
"timeout": {
"anyOf": [
{
"type": "number"
},
{
"type": "boolean",
"enum": [false]
}
]
},
"chunkTimeout": {
"type": "number"
},
"compaction": {
"$ref": "#/components/schemas/Provider.Compaction"
},
"transport": {
"$ref": "#/components/schemas/Provider.Transport"
},
}
},
"allOf": [
{
"type": "object",
"additionalProperties": {
"anyOf": [
{},
{
"type": "null"
}
]
}
}
]
},
"Config.ProviderEncoded": {
"type": "object",
"properties": {
"canonical": {
"type": "string"
},
@@ -13404,7 +13430,7 @@
"type": "string"
},
"settings": {
"type": "object"
"$ref": "#/components/schemas/Config.Provider.Settings"
},
"headers": {
"type": "object",
@@ -15481,14 +15507,8 @@
"package": {
"type": "string"
},
"compaction": {
"$ref": "#/components/schemas/Provider.Compaction"
},
"transport": {
"$ref": "#/components/schemas/Provider.Transport"
},
"settings": {
"type": "object"
"$ref": "#/components/schemas/Provider.Settings"
},
"headers": {
"type": "object",
@@ -15601,7 +15621,7 @@
"type": "string"
},
"settings": {
"type": "object"
"$ref": "#/components/schemas/Provider.Settings"
},
"headers": {
"type": "object",
@@ -16418,27 +16438,23 @@
{
"type": "object",
"properties": {
"mode": {
"type": {
"type": "string",
"enum": ["local"]
"enum": ["summary"]
}
},
"required": ["mode"],
"required": ["type"],
"additionalProperties": false
},
{
"type": "object",
"properties": {
"mode": {
"type": {
"type": "string",
"enum": ["provider"]
},
"threshold": {
"type": "integer",
"exclusiveMinimum": 0
"enum": ["native"]
}
},
"required": ["mode"],
"required": ["type"],
"additionalProperties": false
}
]
@@ -16465,14 +16481,8 @@
"package": {
"type": "string"
},
"compaction": {
"$ref": "#/components/schemas/Provider.Compaction"
},
"transport": {
"$ref": "#/components/schemas/Provider.Transport"
},
"settings": {
"type": "object"
"$ref": "#/components/schemas/Provider.Settings"
},
"headers": {
"type": "object",
@@ -16507,7 +16517,35 @@
"additionalProperties": false
},
"Provider.Settings": {
"type": "object"
"type": "object",
"properties": {
"timeout": {
"anyOf": [
{
"type": "number"
},
{
"type": "boolean",
"enum": [false]
}
]
},
"chunkTimeout": {
"type": "number"
},
"compaction": {
"$ref": "#/components/schemas/Provider.Compaction"
},
"transport": {
"$ref": "#/components/schemas/Provider.Transport"
}
},
"allOf": [
{
"type": "object",
"additionalProperties": {}
}
]
},
"Provider.Transport": {
"type": "string",
+4 -15
View File
@@ -148,23 +148,20 @@ selected provider's native compaction for both automatic and manual requests:
"$schema": "https://opencode.ai/config.json",
"providers": {
"openai": {
"compaction": { "mode": "provider", "threshold": 120000 },
"settings": { "compaction": { "type": "native" } },
"models": {
"gpt-5.4-mini": { "compaction": { "mode": "provider" } },
"gpt-4.1": { "compaction": { "mode": "local" } },
"gpt-4.1": { "settings": { "compaction": { "type": "summary" } } },
},
},
},
}
```
A model policy replaces the entire provider policy. In this example,
`gpt-5.4-mini` therefore uses its own usable input ceiling rather than inheriting
the provider's `120000` threshold.
A model setting overrides the provider setting.
| Topic | Provider behavior |
| --- | --- |
| Threshold | `threshold` is an optional positive integer in provider mode. When omitted, OpenCode uses the model's usable input ceiling; when larger than that ceiling, it is clamped. |
| Threshold | OpenCode uses the model's usable input ceiling configured by its context, input, and output limits plus the global compaction buffer. |
| Scheduling | Checkpoints run at normal safe step boundaries. `compaction.auto: false` disables all new automatic work. |
| Usage | After a native checkpoint is installed, automatic checks wait for fresh model usage because encrypted checkpoint bytes cannot provide a meaningful token count. |
| OpenAI | Responses routes use a streamed compaction trigger when supported; endpoint-only routes use the standalone compaction endpoint. Deployment and model support vary. |
@@ -172,14 +169,6 @@ the provider's `120000` threshold.
| Routes | Unsupported routes fail during model resolution. Configure custom endpoints with provider or model `settings.baseURL`. Native compaction rejects endpoint rewrites made by a `model.request` hook. |
| Replay | Native checkpoints are reused only with the same provider, model, protocol, and endpoint. An incompatible route falls back to an earlier compatible checkpoint or retained transcript. |
Choose a threshold comfortably above `keep.tokens` plus the system prompt and
tools. For example, avoid a 20,000-token threshold when the retained input alone
is 15,000 tokens and the prompt and tools require another 8,000:
```text
15,000 retained + 8,000 prompt and tools > 20,000 threshold
```
Otherwise each fresh usage report can immediately trigger another checkpoint.
Disabling automatic compaction does not remove a checkpoint already installed.
+5 -6
View File
@@ -328,18 +328,17 @@ automatic and manual requests with a provider or model policy:
{
"providers": {
"openai": {
"compaction": { "mode": "provider", "threshold": 120000 },
"settings": { "compaction": { "type": "native" } },
"models": {
"gpt-4.1": { "compaction": { "mode": "local" } },
"gpt-4.1": { "settings": { "compaction": { "type": "summary" } } },
},
},
},
}
```
A model policy replaces the whole provider policy. The optional positive integer
`threshold` defaults to the selected model's usable input budget and cannot exceed
its safe ceiling. Provider checkpoints keep recent user messages within the same
A model setting overrides the provider setting. Automatic compaction uses the
selected model's usable input budget. Provider checkpoints keep recent user messages within the same
`compaction.tokens` budget that local summaries use for their retained tail.
Top-level `compaction.auto: false` disables new automatic compaction without
discarding installed checkpoints. See the [compaction guide](/compaction) for
@@ -507,7 +506,7 @@ headers, and model variants.
}
```
`transport: "websocket"` on a provider or model selects its session
`settings.transport: "websocket"` on a provider or model selects its session
WebSocket; a model value overrides the provider value.
See the [providers guide](/providers) for credentials, custom endpoints, provider packages, the WebSocket transport, and model configuration.
+5 -9
View File
@@ -61,12 +61,10 @@ The `providers` object is keyed by the provider ID used in model references, suc
| `env` | Ordered environment variable names that can provide the credential. |
| `package` | Runtime provider package. |
| `canonical` | Built-in provider ID whose catalog defaults this provider inherits. |
| `settings` | JSON options passed to the runtime package. |
| `settings` | Typed OpenCode controls and JSON options passed to the runtime package. |
| `headers` | String-valued HTTP headers added to requests. |
| `body` | JSON fields merged into request bodies. |
| `models` | Models to add or override, keyed by the OpenCode model ID. |
| `transport` | Provider-level session transport, `"http"` or `"websocket"`. |
| `compaction` | Local or provider [compaction](/compaction) policy. |
## Endpoint
@@ -222,7 +220,7 @@ ID sent to the provider.
| `name` | Display name. |
| `family` | Model family used for grouping related models. |
| `package` | Runtime override for this model. |
| `settings` | Package-specific JSON settings. |
| `settings` | Model-level OpenCode controls and package-specific JSON settings. |
| `headers` | Additional string-valued request headers. |
| `body` | Additional JSON request body fields. |
| `capabilities` | Tool support plus accepted input and output media types. |
@@ -231,8 +229,6 @@ ID sent to the provider.
| `cost` | Input, output, and optional cache pricing per million tokens. |
| `limit` | Context, input, and output token limits. |
| `disabled` | Removes the model from selection when `true`. |
| `transport` | Model-level session transport; overrides the provider value. |
| `compaction` | Model-level [compaction](/compaction) policy; overrides the provider policy. |
See [Models](/models) for selection, defaults, capabilities, limits, costs, and variants.
@@ -561,9 +557,9 @@ sessions.
"$schema": "https://opencode.ai/config.json",
"providers": {
"openai": {
"transport": "http",
"settings": { "transport": "http" },
"models": {
"gpt-5.5": { "transport": "websocket" },
"gpt-5.5": { "settings": { "transport": "websocket" } },
},
},
},
@@ -573,7 +569,7 @@ sessions.
WebSocket behavior follows these rules:
- Built-in providers opt supported models in according to their own policy.
- `transport: "websocket"` enables a provider or model; `"http"` disables it.
- `settings.transport: "websocket"` enables a provider or model; `"http"` disables it.
- A model value overrides its provider value.
- `"websocket"` on a route without a WebSocket channel logs a warning and falls back to HTTP.
- OpenAI provider compaction uses the same connection.