refactor(core): derive provider option keys from the settings schema (#59175)

This commit is contained in:
Aiden Cline
2026-10-26 01:32:34 -05:00
committed by GitHub
Unverified
parent 041202b160
commit 490456db53
7 changed files with 45 additions and 76 deletions
+14 -27
View File
@@ -33,7 +33,7 @@ import {
import { Auth, Endpoint, RequestExecutor, type AnyRoute, type HttpMiddleware } from "@opencode/ai/route"
import { ProviderShared } from "@opencode/ai/protocols/shared"
import { isRecord } from "@opencode/ai/utils/record"
import { Cause, Context, Effect, Layer, Option, Schema, Scope, Stream } from "effect"
import { Cause, Context, Effect, Layer, Option, Schema, Scope, Stream, Struct } from "effect"
import { makeParser } from "effect/encoding/Sse"
import { HttpClientRequest, HttpClientResponse } from "effect/http"
import { AsyncLocalStorage } from "node:async_hooks"
@@ -122,20 +122,16 @@ function wrapSSE(res: Response, ms: number, ctl: AbortController) {
function prepareOptions(model: RuntimeInfo, pkg: string) {
const projected = mapBodyToProviderOptions(model, pkg)
const settings = model.settings ?? {}
const options: Record<string, unknown> = {
name: model.canonical ?? model.providerID,
...(model.settings ?? {}),
...Provider.nativeSettings(settings),
headers: model.headers,
body: projected.body,
}
const customFetch = options.fetch
const timeouts = Provider.timeouts(options)
delete options.headerTimeout
delete options.chunkTimeout
delete options.compaction
delete options.cache
delete options.transport
const customFetch = settings.fetch
const timeouts = Provider.timeouts(settings)
options.fetch = async (input: Parameters<typeof fetch>[0], init?: RequestInit) => {
const opts = { ...(init ?? {}) }
// A plain AbortSignal.timeout would keep running into the body stream, so the header limit uses
@@ -454,25 +450,16 @@ function providerOptionKey(packageName: string | undefined, providerID: Provider
return providerID
}
function requestSettings(settings: Readonly<Record<string, unknown>> | undefined) {
function requestSettings(settings: Provider.Settings | undefined) {
if (settings === undefined) return undefined
const result = Object.fromEntries(
Object.entries(settings).filter(
([key]) =>
![
"apiKey",
"authToken",
"baseURL",
"cache",
"chunkTimeout",
"compaction",
"fetch",
"headerTimeout",
"timeout",
"transport",
].includes(key),
),
)
// These configure the SDK provider when it is created, not each request.
const result = Struct.omit(Provider.nativeSettings(settings), [
"accessToken",
"apiKey",
"authToken",
"baseURL",
"fetch",
])
return Object.keys(result).length === 0 ? undefined : result
}
+7 -15
View File
@@ -183,25 +183,17 @@ export const loadPackage = Effect.fn("Provider.loadPackage")(function* (input: s
return yield* importPackage(specifier, entrypoint)
})
/** opencode settings consumed in Core; native packages never receive them. */
const CORE_KEYS = [
"cache",
"chunkTimeout",
"compaction",
"encoding",
"fetch",
"headerTimeout",
"timeout",
"transport",
] as const
const PROVIDER_ONLY_KEYS = ["chunkTimeout", "headerTimeout", "timeout", "transport"] as const
const settingsKeys = Struct.keys(Provider.Settings.schema.fields)
const providerOnlyKeys = settingsKeys.filter((key) => !(key in Model.Settings.schema.fields))
export function nativeSettings(settings: Settings): Settings {
return Struct.omit(settings, CORE_KEYS)
/** The provider options: every settings key OpenCode doesn't read itself. */
export function nativeSettings(settings: Settings) {
return Struct.omit(settings, settingsKeys)
}
/** Settings a model inherits from its provider. */
export function modelSettings(settings: Settings | undefined) {
return settings && Struct.omit(settings, PROVIDER_ONLY_KEYS)
return settings && Struct.omit(settings, providerOnlyKeys)
}
const decodeTimeout = Schema.decodeUnknownOption(HttpTimeout)
+5 -2
View File
@@ -170,6 +170,9 @@ it.effect("projects request settings, headers, and body overlays", () =>
apiKey: "secret",
thinkingConfig: { thinkingBudget: 1024 },
compaction: { type: "native" },
cache: { ttl: "1h" },
encoding: "gzip",
timeout: 600_000,
transport: "websocket",
})
const resolved = yield* aisdk.model({
@@ -193,8 +196,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")
for (const key of ["compaction", "cache", "encoding", "timeout", "transport"])
expect(options).not.toHaveProperty(key)
}),
)
+2
View File
@@ -31,6 +31,8 @@ describe("Provider", () => {
reasoningEffort: "high",
chunkTimeout: 1000,
compaction: { type: "native" },
cache: { ttl: "1h" },
encoding: "gzip",
transport: "websocket",
}),
).toEqual({
+9 -23
View File
@@ -2,33 +2,19 @@ export * as ConfigProvider from "./provider.js"
import { Schema } from "effect"
import { Money } from "../money.js"
import { Capabilities, Compatibility, Family, ID, VariantID } from "../model.js"
import { Capabilities, Compatibility, Family, ID, Model, VariantID } from "../model.js"
import { ProviderCompatibility } from "../model-compatibility.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),
headerTimeout: Schema.Union([Schema.Finite, Schema.Literal(false)]).pipe(optional),
chunkTimeout: Schema.Union([Schema.Finite, Schema.Literal(false)]).pipe(optional),
compaction: Provider.Compaction.pipe(optional),
cache: Provider.Cache.pipe(optional),
transport: Provider.Transport.pipe(optional),
encoding: Provider.Encoding.pipe(optional),
}),
[Schema.Record(Schema.String, Schema.UndefinedOr(Schema.Json))],
).annotate({ identifier: "Config.Provider.Settings" })
export const Settings = Schema.StructWithRest(Schema.Struct(Provider.Settings.schema.fields), [
Schema.Record(Schema.String, Schema.UndefinedOr(Schema.Json)),
]).annotate({ identifier: "Config.Provider.Settings" })
export type Settings = typeof Settings.Type
export const ModelSettings = Schema.StructWithRest(
Schema.Struct({
compaction: Provider.Compaction.pipe(optional),
cache: Provider.Cache.pipe(optional),
encoding: Provider.Encoding.pipe(optional),
}),
[Schema.Record(Schema.String, Schema.UndefinedOr(Schema.Json))],
).annotate({ identifier: "Config.Model.Settings" })
export const ModelSettings = Schema.StructWithRest(Schema.Struct(Model.Settings.schema.fields), [
Schema.Record(Schema.String, Schema.UndefinedOr(Schema.Json)),
]).annotate({ identifier: "Config.Model.Settings" })
export type ModelSettings = typeof ModelSettings.Type
const JsonRecord = Schema.Record(Schema.String, Schema.Json)
@@ -86,7 +72,7 @@ export const Reasoning = Schema.Union([
]).annotate({ identifier: "Config.Model.Reasoning" })
export type Reasoning = typeof Reasoning.Type
class Model extends Schema.Class<Model>("Config.Model")({
class ModelConfig extends Schema.Class<ModelConfig>("Config.Model")({
modelID: ID.pipe(optional),
family: Family.pipe(optional),
name: Schema.String.pipe(optional),
@@ -123,5 +109,5 @@ export class Info extends Schema.Class<Info>("Config.Provider")({
// Defaults for every model of this provider; model compatibility overrides individual fields.
compatibility: ProviderCompatibility.pipe(optional),
...Overlays,
models: Schema.Record(Schema.String, Model).pipe(optional),
models: Schema.Record(Schema.String, ModelConfig).pipe(optional),
}) {}
+2 -6
View File
@@ -1,6 +1,6 @@
export * as Model from "./model.js"
import { Schema } from "effect"
import { Schema, Struct } from "effect"
import { optional, statics } from "./schema.js"
import { Provider } from "./provider.js"
import { Compatibility, MaxTokensField, ReasoningField } from "./model-compatibility.js"
@@ -49,11 +49,7 @@ export const Family = Schema.String.pipe(Schema.brand("Model.Family"))
export type Family = typeof Family.Type
export const Settings = Schema.StructWithRest(
Schema.Struct({
compaction: Provider.Compaction.pipe(optional),
cache: Provider.Cache.pipe(optional),
encoding: Provider.Encoding.pipe(optional),
}),
Schema.Struct(Struct.pick(Provider.Settings.schema.fields, ["compaction", "cache", "encoding"])),
// Provider packages may define arbitrary model-level options beyond OpenCode's shared compaction policy.
[Schema.Record(Schema.String, Schema.Any)],
).annotate({ identifier: "Model.Settings" })
+6 -3
View File
@@ -62,11 +62,14 @@ export type Cache = typeof Cache.Type
export const Encoding = Schema.Literals(["zstd", "gzip", "none"]).annotate({ identifier: "Provider.Encoding" })
export type Encoding = typeof Encoding.Type
const Timeout = Schema.Union([Schema.Finite, Schema.Literal(false)]).pipe(optional)
// The named fields are the settings OpenCode reads itself; every other key is a provider option for the provider package.
export const Settings = Schema.StructWithRest(
Schema.Struct({
timeout: Schema.Union([Schema.Finite, Schema.Literal(false)]).pipe(optional),
headerTimeout: Schema.Union([Schema.Finite, Schema.Literal(false)]).pipe(optional),
chunkTimeout: Schema.Union([Schema.Finite, Schema.Literal(false)]).pipe(optional),
timeout: Timeout,
headerTimeout: Timeout,
chunkTimeout: Timeout,
compaction: Compaction.pipe(optional),
cache: Cache.pipe(optional),
transport: Transport.pipe(optional),