Skip to content

Commit a81e3da

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 76880dc commit a81e3da

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
@@ -576,7 +576,7 @@ export namespace Config {
576576
terminal_title_toggle: z.string().optional().default("none").describe("Toggle terminal title"),
577577
tips_toggle: z.string().optional().default("<leader>h").describe("Toggle tips on home screen"),
578578
})
579-
.strict()
579+
.passthrough()
580580
.meta({
581581
ref: "KeybindsConfig",
582582
})
@@ -641,7 +641,7 @@ export namespace Config {
641641
.catchall(z.any())
642642
.optional(),
643643
})
644-
.strict()
644+
.passthrough()
645645
.meta({
646646
ref: "ProviderConfig",
647647
})
@@ -844,7 +844,7 @@ export namespace Config {
844844
})
845845
.optional(),
846846
})
847-
.strict()
847+
.passthrough()
848848
.meta({
849849
ref: "Config",
850850
})
@@ -877,6 +877,36 @@ export namespace Config {
877877
return result
878878
})
879879

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

958988
const parsed = Info.safeParse(data)
959989
if (parsed.success) {
990+
warnUnknownKeys(data, configFilepath)
960991
if (!parsed.data.$schema) {
961992
parsed.data.$schema = "https://opencode.ai/config.json"
962993
await Bun.write(configFilepath, JSON.stringify(parsed.data, null, 2))
963994
}
964-
const data = parsed.data
965-
if (data.plugin) {
966-
for (let i = 0; i < data.plugin.length; i++) {
967-
const plugin = data.plugin[i]
995+
const result = parsed.data
996+
if (result.plugin) {
997+
for (let i = 0; i < result.plugin.length; i++) {
998+
const plugin = result.plugin[i]
968999
try {
969-
data.plugin[i] = import.meta.resolve!(plugin, configFilepath)
1000+
result.plugin[i] = import.meta.resolve!(plugin, configFilepath)
9701001
} catch (err) {}
9711002
}
9721003
}
973-
return data
1004+
return result
9741005
}
9751006

9761007
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)