mirror of
https://github.com/anomalyco/opencode.git
synced 2026-11-14 20:21:39 +03:00
refactor(core): derive provider option keys from the settings schema (#59175)
This commit is contained in:
committed by
GitHub
Unverified
parent
041202b160
commit
490456db53
+14
-27
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
}),
|
||||
)
|
||||
|
||||
|
||||
@@ -31,6 +31,8 @@ describe("Provider", () => {
|
||||
reasoningEffort: "high",
|
||||
chunkTimeout: 1000,
|
||||
compaction: { type: "native" },
|
||||
cache: { ttl: "1h" },
|
||||
encoding: "gzip",
|
||||
transport: "websocket",
|
||||
}),
|
||||
).toEqual({
|
||||
|
||||
@@ -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),
|
||||
}) {}
|
||||
|
||||
@@ -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" })
|
||||
|
||||
@@ -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),
|
||||
|
||||
Reference in New Issue
Block a user