mirror of
https://gitee.com/zhangsken/opencode.git
synced 2026-10-16 10:30:01 +08:00
fix(core): map v1 setCacheKey to compatibility
V1 provider.options.setCacheKey was passed through as an inert provider setting, so the prompt cache key was never sent for providers that opt in (and could not be disabled for providers that default it on). Lower it to compatibility.supportsPromptCacheKey on the provider, add provider-level compatibility to the config schema, and apply it to every model of the provider with model-level compatibility taking precedence.
This commit is contained in:
@@ -2117,6 +2117,7 @@ export type ConfigEntry =
|
||||
name?: string
|
||||
env?: Array<string>
|
||||
package?: string
|
||||
compatibility?: ModelCompatibility
|
||||
settings?: ConfigProviderSettings
|
||||
headers?: { [x: string]: string }
|
||||
body?: { [x: string]: JsonValue }
|
||||
|
||||
@@ -99,6 +99,13 @@ export const Plugin = define({
|
||||
if (configuredDefault !== undefined) models.default.set(configuredDefault.providerID, configuredDefault.model)
|
||||
for (const [item, definition] of sources.models) {
|
||||
const providerID = definition.providerID
|
||||
if (item.compatibility !== undefined)
|
||||
for (const model of models.list(providerID)) {
|
||||
if (Object.hasOwn(item.models ?? {}, model.id)) continue
|
||||
models.update(providerID, model.id, (draft) => {
|
||||
draft.compatibility = { ...draft.compatibility, ...item.compatibility }
|
||||
})
|
||||
}
|
||||
for (const [id, config] of Object.entries(item.models ?? {})) {
|
||||
const source = definition.models.get(id)
|
||||
const inherit = source?.inherit || !models.get(providerID, id)
|
||||
@@ -111,6 +118,8 @@ export const Plugin = define({
|
||||
if (config.family !== undefined) model.family = config.family
|
||||
if (config.name !== undefined) model.name = config.name
|
||||
if (config.modelID !== undefined) model.modelID = config.modelID
|
||||
if (item.compatibility !== undefined)
|
||||
model.compatibility = { ...model.compatibility, ...item.compatibility }
|
||||
if (config.compatibility !== undefined)
|
||||
model.compatibility = { ...model.compatibility, ...config.compatibility }
|
||||
if (config.package !== undefined) model.package = config.package
|
||||
|
||||
@@ -250,6 +250,7 @@ function migrateStandardProvider(info: ConfigProviderV1.Info) {
|
||||
name: info.name,
|
||||
env: info.env,
|
||||
package: info.npm ? Provider.aisdk(info.npm) : undefined,
|
||||
compatibility: options.compatibility,
|
||||
settings: info.api ? { ...options.settings, baseURL: info.api } : info.options ? options.settings : undefined,
|
||||
headers: info.options && options.headers,
|
||||
body: info.options && options.body,
|
||||
|
||||
@@ -1,17 +1,22 @@
|
||||
export * as ConfigProviderOptionsV1 from "./provider-options.js"
|
||||
|
||||
import type { Model } from "@opencode/schema/model"
|
||||
|
||||
type Options = Readonly<Record<string, unknown>>
|
||||
|
||||
export interface ProviderResult {
|
||||
readonly settings: Record<string, unknown>
|
||||
readonly headers?: Record<string, string>
|
||||
readonly body?: Record<string, unknown>
|
||||
readonly compatibility?: Model.Compatibility
|
||||
}
|
||||
|
||||
export function provider(options: Options): ProviderResult {
|
||||
const headers = options.headers
|
||||
const body = options.body
|
||||
const settings = Object.fromEntries(Object.entries(options).filter(([key]) => key !== "headers" && key !== "body"))
|
||||
const settings = Object.fromEntries(
|
||||
Object.entries(options).filter(([key]) => key !== "headers" && key !== "body" && key !== "setCacheKey"),
|
||||
)
|
||||
const headerOverlay =
|
||||
typeof headers === "object" && headers !== null && !Array.isArray(headers)
|
||||
? Object.fromEntries(
|
||||
@@ -23,6 +28,10 @@ export function provider(options: Options): ProviderResult {
|
||||
settings,
|
||||
headers: headerOverlay,
|
||||
body: bodyOverlay,
|
||||
// V1 `setCacheKey` was a provider-wide switch for sending the prompt cache key; false also disabled the
|
||||
// built-in defaults for OpenAI and xAI.
|
||||
compatibility:
|
||||
typeof options.setCacheKey === "boolean" ? { supportsPromptCacheKey: options.setCacheKey } : undefined,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -835,6 +835,22 @@ describe("Config", () => {
|
||||
expect(migrated.providers?.custom?.models?.boolean?.compatibility).toBeUndefined()
|
||||
})
|
||||
|
||||
test("migrates v1 setCacheKey to provider compatibility", () => {
|
||||
const migrated = ConfigMigrateV1.migrate({
|
||||
provider: {
|
||||
enabled: { options: { setCacheKey: true, baseURL: "https://enabled.example/v1" } },
|
||||
disabled: { options: { setCacheKey: false } },
|
||||
unset: { options: { baseURL: "https://unset.example/v1" } },
|
||||
},
|
||||
})
|
||||
|
||||
expect(migrated.providers?.enabled?.compatibility).toEqual({ supportsPromptCacheKey: true })
|
||||
expect(migrated.providers?.enabled?.settings).toEqual({ baseURL: "https://enabled.example/v1" })
|
||||
expect(migrated.providers?.disabled?.compatibility).toEqual({ supportsPromptCacheKey: false })
|
||||
expect(migrated.providers?.disabled?.settings).toEqual({})
|
||||
expect(migrated.providers?.unset?.compatibility).toBeUndefined()
|
||||
})
|
||||
|
||||
for (const subtask of [true, false]) {
|
||||
test(`migrates v1 command configuration with subtask: ${subtask}`, () => {
|
||||
expect(
|
||||
|
||||
@@ -24,6 +24,19 @@ describe("ConfigProviderOptionsV1", () => {
|
||||
})
|
||||
})
|
||||
|
||||
test("lowers setCacheKey to prompt cache key compatibility", () => {
|
||||
expect(ConfigProviderOptionsV1.provider({ apiKey: "secret", setCacheKey: true })).toEqual({
|
||||
settings: { apiKey: "secret" },
|
||||
headers: undefined,
|
||||
body: undefined,
|
||||
compatibility: { supportsPromptCacheKey: true },
|
||||
})
|
||||
expect(ConfigProviderOptionsV1.provider({ setCacheKey: false }).compatibility).toEqual({
|
||||
supportsPromptCacheKey: false,
|
||||
})
|
||||
expect(ConfigProviderOptionsV1.provider({ apiKey: "secret" }).compatibility).toBeUndefined()
|
||||
})
|
||||
|
||||
test("keeps model and variant options unchanged", () => {
|
||||
expect(
|
||||
ConfigProviderOptionsV1.model({
|
||||
|
||||
@@ -420,6 +420,84 @@ describe("ConfigProviderPlugin.Plugin", () => {
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("applies provider compatibility to every model unless the model overrides it", () =>
|
||||
Effect.gen(function* () {
|
||||
const providers = yield* Provider.Service
|
||||
const models = yield* Model.Service
|
||||
const providerID = Provider.ID.make("custom")
|
||||
yield* providers.transform((editor) => {
|
||||
editor.models.update(providerID, Model.ID.make("catalog"), (model) => {
|
||||
model.compatibility = { requireFinishReason: true }
|
||||
})
|
||||
editor.models.update(Provider.ID.make("other"), Model.ID.make("catalog"), () => {})
|
||||
})
|
||||
|
||||
yield* addPlugin([
|
||||
new Document({
|
||||
type: "document",
|
||||
info: decode({
|
||||
providers: {
|
||||
custom: {
|
||||
compatibility: { supportsPromptCacheKey: true },
|
||||
models: {
|
||||
inherited: {},
|
||||
overridden: { compatibility: { supportsPromptCacheKey: false } },
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
}),
|
||||
])
|
||||
|
||||
expect((yield* models.get(providerID, Model.ID.make("catalog")))?.compatibility).toEqual({
|
||||
requireFinishReason: true,
|
||||
supportsPromptCacheKey: true,
|
||||
})
|
||||
expect((yield* models.get(providerID, Model.ID.make("inherited")))?.compatibility).toEqual({
|
||||
supportsPromptCacheKey: true,
|
||||
})
|
||||
expect((yield* models.get(providerID, Model.ID.make("overridden")))?.compatibility).toEqual({
|
||||
supportsPromptCacheKey: false,
|
||||
})
|
||||
expect((yield* models.get(Provider.ID.make("other"), Model.ID.make("catalog")))?.compatibility).toBeUndefined()
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("maps migrated v1 setCacheKey onto provider models", () =>
|
||||
Effect.gen(function* () {
|
||||
const providers = yield* Provider.Service
|
||||
const models = yield* Model.Service
|
||||
yield* providers.transform((editor) => {
|
||||
editor.models.update(Provider.ID.make("enabled"), Model.ID.make("catalog"), () => {})
|
||||
editor.models.update(Provider.ID.make("disabled"), Model.ID.make("catalog"), (model) => {
|
||||
model.compatibility = { supportsPromptCacheKey: true }
|
||||
})
|
||||
editor.models.update(Provider.ID.make("unset"), Model.ID.make("catalog"), () => {})
|
||||
})
|
||||
const result = ConfigNormalize.normalize({
|
||||
provider: {
|
||||
enabled: { options: { setCacheKey: true }, models: { chat: {} } },
|
||||
disabled: { options: { setCacheKey: false } },
|
||||
unset: { options: { baseURL: "https://unset.example/v1" } },
|
||||
},
|
||||
})
|
||||
if (result.type !== "normalized") throw new Error("Expected normalized config")
|
||||
expect(result.diagnostics).toEqual([])
|
||||
|
||||
yield* addPlugin([new Document({ type: "document", info: decode(result.encoded) })])
|
||||
|
||||
const compatibility = (providerID: string, modelID: string) =>
|
||||
models
|
||||
.get(Provider.ID.make(providerID), Model.ID.make(modelID))
|
||||
.pipe(Effect.map((model) => model?.compatibility?.supportsPromptCacheKey))
|
||||
expect(yield* compatibility("enabled", "catalog")).toBe(true)
|
||||
expect(yield* compatibility("enabled", "chat")).toBe(true)
|
||||
expect(yield* compatibility("disabled", "catalog")).toBe(false)
|
||||
expect(yield* compatibility("unset", "catalog")).toBeUndefined()
|
||||
expect((yield* providers.get(Provider.ID.make("enabled")))?.settings).not.toHaveProperty("setCacheKey")
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("generates variants after rewriting a configured model package", () =>
|
||||
Effect.gen(function* () {
|
||||
const models = yield* Model.Service
|
||||
|
||||
@@ -87,6 +87,8 @@ export class Info extends Schema.Class<Info>("Config.Provider")({
|
||||
name: Schema.String.pipe(optional),
|
||||
env: Schema.String.pipe(Schema.Array, optional),
|
||||
package: Schema.String.pipe(optional),
|
||||
/** Compatibility defaults for every model of this provider. Model-level compatibility takes precedence. */
|
||||
compatibility: Compatibility.pipe(optional),
|
||||
...Overlays,
|
||||
models: Schema.Record(Schema.String, Model).pipe(optional),
|
||||
}) {}
|
||||
|
||||
Reference in New Issue
Block a user