Skip to content

Commit 11e0c79

Browse files
feat(config): allow unknown keys with warnings for forward compatibility
Change user-facing config schemas (Keybinds, Provider, Info) from .strict() to .passthrough() so unknown config keys are ignored rather than causing validation errors. Add warning logs for unknown keys so users still get feedback about potential typos while configs from newer versions still work. This enables configs with newer options (e.g., new keybinds) to work on older OpenCode versions that don't recognize those keys.
1 parent 73bc3e7 commit 11e0c79

File tree

7 files changed

+301
-19
lines changed

7 files changed

+301
-19
lines changed

packages/opencode/src/cli/cmd/tui/context/keybind.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ export const { use: useKeybind, provider: KeybindProvider } = createSimpleContex
1515
const keybinds = createMemo(() => {
1616
return pipe(
1717
sync.data.config.keybinds ?? {},
18-
mapValues((value) => Keybind.parse(value)),
18+
mapValues((value) => (typeof value === "string" ? Keybind.parse(value) : [])),
1919
)
2020
})
2121
const [store, setStore] = createStore({

packages/opencode/src/config/config.ts

Lines changed: 40 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -581,7 +581,7 @@ export namespace Config {
581581
terminal_title_toggle: z.string().optional().default("none").describe("Toggle terminal title"),
582582
tips_toggle: z.string().optional().default("<leader>h").describe("Toggle tips on home screen"),
583583
})
584-
.strict()
584+
.passthrough()
585585
.meta({
586586
ref: "KeybindsConfig",
587587
})
@@ -646,7 +646,7 @@ export namespace Config {
646646
.catchall(z.any())
647647
.optional(),
648648
})
649-
.strict()
649+
.passthrough()
650650
.meta({
651651
ref: "ProviderConfig",
652652
})
@@ -849,7 +849,7 @@ export namespace Config {
849849
})
850850
.optional(),
851851
})
852-
.strict()
852+
.passthrough()
853853
.meta({
854854
ref: "Config",
855855
})
@@ -882,6 +882,36 @@ export namespace Config {
882882
return result
883883
})
884884

885+
const INFO_KNOWN_KEYS = new Set(Object.keys(Info.shape))
886+
const KEYBINDS_KNOWN_KEYS = new Set(Object.keys(Keybinds.shape))
887+
const PROVIDER_KNOWN_KEYS = new Set(Object.keys(Provider.shape))
888+
889+
function warnUnknownKeys(raw: Record<string, unknown>, filepath: string) {
890+
for (const key of Object.keys(raw)) {
891+
if (!INFO_KNOWN_KEYS.has(key)) {
892+
log.warn("unknown config key", { key, path: filepath })
893+
}
894+
}
895+
if (raw.keybinds && typeof raw.keybinds === "object") {
896+
for (const key of Object.keys(raw.keybinds)) {
897+
if (!KEYBINDS_KNOWN_KEYS.has(key)) {
898+
log.warn("unknown keybind", { key, path: filepath })
899+
}
900+
}
901+
}
902+
if (raw.provider && typeof raw.provider === "object") {
903+
for (const [name, provider] of Object.entries(raw.provider)) {
904+
if (provider && typeof provider === "object") {
905+
for (const key of Object.keys(provider)) {
906+
if (!PROVIDER_KNOWN_KEYS.has(key)) {
907+
log.warn("unknown provider key", { provider: name, key, path: filepath })
908+
}
909+
}
910+
}
911+
}
912+
}
913+
}
914+
885915
async function loadFile(filepath: string): Promise<Info> {
886916
log.info("loading", { path: filepath })
887917
let text = await Bun.file(filepath)
@@ -962,20 +992,21 @@ export namespace Config {
962992

963993
const parsed = Info.safeParse(data)
964994
if (parsed.success) {
995+
warnUnknownKeys(data, configFilepath)
965996
if (!parsed.data.$schema) {
966997
parsed.data.$schema = "https://opencode.ai/config.json"
967998
await Bun.write(configFilepath, JSON.stringify(parsed.data, null, 2))
968999
}
969-
const data = parsed.data
970-
if (data.plugin) {
971-
for (let i = 0; i < data.plugin.length; i++) {
972-
const plugin = data.plugin[i]
1000+
const result = parsed.data
1001+
if (result.plugin) {
1002+
for (let i = 0; i < result.plugin.length; i++) {
1003+
const plugin = result.plugin[i]
9731004
try {
974-
data.plugin[i] = import.meta.resolve!(plugin, configFilepath)
1005+
result.plugin[i] = import.meta.resolve!(plugin, configFilepath)
9751006
} catch (err) {}
9761007
}
9771008
}
978-
return data
1009+
return result
9791010
}
9801011

9811012
throw new InvalidError({

packages/opencode/test/config/config.test.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -148,23 +148,24 @@ test("handles file inclusion substitution", async () => {
148148
})
149149
})
150150

151-
test("validates config schema and throws on invalid fields", async () => {
151+
test("ignores unknown config fields for forward compatibility", async () => {
152152
await using tmp = await tmpdir({
153153
init: async (dir) => {
154154
await Bun.write(
155155
path.join(dir, "opencode.json"),
156156
JSON.stringify({
157157
$schema: "https://opencode.ai/config.json",
158-
invalid_field: "should cause error",
158+
unknown_future_field: "should be ignored",
159+
theme: "known_theme",
159160
}),
160161
)
161162
},
162163
})
163164
await Instance.provide({
164165
directory: tmp.path,
165166
fn: async () => {
166-
// Strict schema should throw an error for invalid fields
167-
await expect(Config.get()).rejects.toThrow()
167+
const config = await Config.get()
168+
expect(config.theme).toBe("known_theme")
168169
},
169170
})
170171
})

packages/plugin/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,4 +24,4 @@
2424
"typescript": "catalog:",
2525
"@typescript/native-preview": "catalog:"
2626
}
27-
}
27+
}

packages/sdk/js/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,4 +29,4 @@
2929
"publishConfig": {
3030
"directory": "dist"
3131
}
32-
}
32+
}

0 commit comments

Comments
 (0)