mirror of
https://gitee.com/zhangsken/opencode.git
synced 2026-10-16 10:30:01 +08:00
refactor(core): move provider policies into settings (#49666)
This commit is contained in:
@@ -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 }
|
||||
}>
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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",
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
})
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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)
|
||||
},
|
||||
}),
|
||||
)
|
||||
|
||||
@@ -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")
|
||||
}),
|
||||
)
|
||||
|
||||
|
||||
@@ -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")
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
@@ -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",
|
||||
})
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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],
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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")
|
||||
})
|
||||
|
||||
|
||||
@@ -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
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user