From c94a097758b76ff5890a8a85ddb647f1e0879375 Mon Sep 17 00:00:00 2001 From: chrarnoldus <12196001+chrarnoldus@users.noreply.github.com> Date: Mon, 29 Jun 2026 07:50:24 +0000 Subject: [PATCH 1/3] fix(cli): strip internal agent metadata from provider request options Co-authored-by: kiloconnect[bot] <240665456+kiloconnect[bot]@users.noreply.github.com> --- .changeset/strip-agent-internal-options.md | 5 +++ .../opencode/src/kilocode/agent/options.ts | 29 ++++++++++++ packages/opencode/src/session/llm/request.ts | 7 ++- .../test/kilocode/agent-options-strip.test.ts | 45 +++++++++++++++++++ 4 files changed, 85 insertions(+), 1 deletion(-) create mode 100644 .changeset/strip-agent-internal-options.md create mode 100644 packages/opencode/src/kilocode/agent/options.ts create mode 100644 packages/opencode/test/kilocode/agent-options-strip.test.ts diff --git a/.changeset/strip-agent-internal-options.md b/.changeset/strip-agent-internal-options.md new file mode 100644 index 00000000000..bfa7e2c0354 --- /dev/null +++ b/.changeset/strip-agent-internal-options.md @@ -0,0 +1,5 @@ +--- +"@kilocode/cli": patch +--- + +Fix 400 errors on non-default agents (Ask, Plan, org modes, marketplace agents) where internal agent metadata (`displayName`, `id`, `source`) leaked into the model request body and was rejected by strict providers. diff --git a/packages/opencode/src/kilocode/agent/options.ts b/packages/opencode/src/kilocode/agent/options.ts new file mode 100644 index 00000000000..710537d0e01 --- /dev/null +++ b/packages/opencode/src/kilocode/agent/options.ts @@ -0,0 +1,29 @@ +// kilocode_change - new file + +// Kilo stores internal/UI-only metadata on an agent's `options` record: +// - `id`: mode identifier used to recognize built-in modes (see session/prompt.ts) +// - `displayName`: human-readable name for org/marketplace modes +// - `source`: origin marker ("organization" | "global" | "project") +// +// These are NOT provider request parameters. The agent `options` record is +// otherwise forwarded verbatim into providerOptions, so any of these keys that +// survive into the request body get rejected by strict providers +// (e.g. NVIDIA NIM: 400 "Unsupported parameter(s): displayName, id"). +// +// We strip only this known denylist rather than allowlisting provider options, +// so genuine provider options an agent sets continue to pass through untouched. +export const INTERNAL_OPTION_KEYS = ["id", "displayName", "source"] as const + +const internal: ReadonlySet = new Set(INTERNAL_OPTION_KEYS) + +// Returns a shallow copy of `options` with Kilo-internal metadata keys removed. +// Used at the provider-request boundary so agent metadata never leaks into the +// request body. The original `options` object is left untouched. +export function stripInternalOptions(options: Record): Record { + const result: Record = {} + for (const key in options) { + if (internal.has(key)) continue + result[key] = options[key] + } + return result +} diff --git a/packages/opencode/src/session/llm/request.ts b/packages/opencode/src/session/llm/request.ts index 5939b6cdfd5..c61f56b6706 100644 --- a/packages/opencode/src/session/llm/request.ts +++ b/packages/opencode/src/session/llm/request.ts @@ -24,6 +24,7 @@ import { } from "@kilocode/kilo-gateway" import { Identity } from "@kilocode/kilo-telemetry" import { KiloSession } from "@/kilocode/session" +import { stripInternalOptions } from "@/kilocode/agent/options" // kilocode_change end type PrepareInput = { @@ -100,7 +101,11 @@ export const prepare = Effect.fn("LLMRequestPrep.prepare")(function* (input: Pre sessionID: input.sessionID, providerOptions: input.provider.options, }) - const options = mergeOptions(mergeOptions(mergeOptions(base, input.model.options), input.agent.options), variant) + // kilocode_change start - drop Kilo-internal agent metadata (id/displayName/source) + // so it never leaks into providerOptions and gets rejected by strict providers + const agentOptions = stripInternalOptions(input.agent.options) + // kilocode_change end + const options = mergeOptions(mergeOptions(mergeOptions(base, input.model.options), agentOptions), variant) if (isOpenaiOauth) { // kilocode_change start - prepend soul to instructions options.instructions = SystemPrompt.soul() + "\n" + system.join("\n") diff --git a/packages/opencode/test/kilocode/agent-options-strip.test.ts b/packages/opencode/test/kilocode/agent-options-strip.test.ts new file mode 100644 index 00000000000..8bc6e49389d --- /dev/null +++ b/packages/opencode/test/kilocode/agent-options-strip.test.ts @@ -0,0 +1,45 @@ +import { describe, expect, test } from "bun:test" +import { stripInternalOptions, INTERNAL_OPTION_KEYS } from "../../src/kilocode/agent/options" + +describe("stripInternalOptions", () => { + test("removes Kilo-internal metadata keys", () => { + const result = stripInternalOptions({ + id: "architect", + displayName: "Architect", + source: "organization", + }) + expect(result).toEqual({}) + }) + + test("preserves genuine provider options", () => { + const result = stripInternalOptions({ + id: "code-reviewer", + displayName: "Code Reviewer", + source: "organization", + reasoningEffort: "high", + reasoning: { enabled: true }, + verbosity: "low", + }) + expect(result).toEqual({ + reasoningEffort: "high", + reasoning: { enabled: true }, + verbosity: "low", + }) + }) + + test("does not mutate the input", () => { + const input = { id: "ask", displayName: "Ask", temperature: 0.5 } + const result = stripInternalOptions(input) + expect(input).toEqual({ id: "ask", displayName: "Ask", temperature: 0.5 }) + expect(result).toEqual({ temperature: 0.5 }) + }) + + test("is a no-op when there is no internal metadata", () => { + const result = stripInternalOptions({ reasoningEffort: "medium" }) + expect(result).toEqual({ reasoningEffort: "medium" }) + }) + + test("denylist covers exactly the documented internal keys", () => { + expect([...INTERNAL_OPTION_KEYS]).toEqual(["id", "displayName", "source"]) + }) +}) From be7e978a637e8a8778dbaf8b2341558de235317f Mon Sep 17 00:00:00 2001 From: chrarnoldus <12196001+chrarnoldus@users.noreply.github.com> Date: Mon, 29 Jun 2026 18:59:25 +0000 Subject: [PATCH 2/3] fix(cli): annotate modified options merge line; clarify changeset Co-authored-by: kiloconnect[bot] <240665456+kiloconnect[bot]@users.noreply.github.com> --- .changeset/strip-agent-internal-options.md | 2 +- packages/opencode/src/session/llm/request.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.changeset/strip-agent-internal-options.md b/.changeset/strip-agent-internal-options.md index bfa7e2c0354..352100935ba 100644 --- a/.changeset/strip-agent-internal-options.md +++ b/.changeset/strip-agent-internal-options.md @@ -2,4 +2,4 @@ "@kilocode/cli": patch --- -Fix 400 errors on non-default agents (Ask, Plan, org modes, marketplace agents) where internal agent metadata (`displayName`, `id`, `source`) leaked into the model request body and was rejected by strict providers. +Fix non-default agents (Ask, Plan, and custom or organization agents) failing with a "Bad Request: Unsupported parameter(s)" error on some models and providers. diff --git a/packages/opencode/src/session/llm/request.ts b/packages/opencode/src/session/llm/request.ts index c61f56b6706..57b33dedc59 100644 --- a/packages/opencode/src/session/llm/request.ts +++ b/packages/opencode/src/session/llm/request.ts @@ -104,8 +104,8 @@ export const prepare = Effect.fn("LLMRequestPrep.prepare")(function* (input: Pre // kilocode_change start - drop Kilo-internal agent metadata (id/displayName/source) // so it never leaks into providerOptions and gets rejected by strict providers const agentOptions = stripInternalOptions(input.agent.options) - // kilocode_change end const options = mergeOptions(mergeOptions(mergeOptions(base, input.model.options), agentOptions), variant) + // kilocode_change end if (isOpenaiOauth) { // kilocode_change start - prepend soul to instructions options.instructions = SystemPrompt.soul() + "\n" + system.join("\n") From 275af49905ca4f46b6f4168d68dc4a68e506942c Mon Sep 17 00:00:00 2001 From: chrarnoldus <12196001+chrarnoldus@users.noreply.github.com> Date: Mon, 29 Jun 2026 19:06:29 +0000 Subject: [PATCH 3/3] fix(cli): also strip reference/resolved Scout agent metadata from provider options Co-authored-by: kiloconnect[bot] <240665456+kiloconnect[bot]@users.noreply.github.com> --- packages/opencode/src/kilocode/agent/options.ts | 4 +++- .../test/kilocode/agent-options-strip.test.ts | 11 ++++++++++- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/packages/opencode/src/kilocode/agent/options.ts b/packages/opencode/src/kilocode/agent/options.ts index 710537d0e01..e79e786f6ce 100644 --- a/packages/opencode/src/kilocode/agent/options.ts +++ b/packages/opencode/src/kilocode/agent/options.ts @@ -4,6 +4,8 @@ // - `id`: mode identifier used to recognize built-in modes (see session/prompt.ts) // - `displayName`: human-readable name for org/marketplace modes // - `source`: origin marker ("organization" | "global" | "project") +// - `reference`: configured reference descriptor for Scout/reference agents (see agent/agent.ts) +// - `resolved`: resolved reference data for Scout/reference agents // // These are NOT provider request parameters. The agent `options` record is // otherwise forwarded verbatim into providerOptions, so any of these keys that @@ -12,7 +14,7 @@ // // We strip only this known denylist rather than allowlisting provider options, // so genuine provider options an agent sets continue to pass through untouched. -export const INTERNAL_OPTION_KEYS = ["id", "displayName", "source"] as const +export const INTERNAL_OPTION_KEYS = ["id", "displayName", "source", "reference", "resolved"] as const const internal: ReadonlySet = new Set(INTERNAL_OPTION_KEYS) diff --git a/packages/opencode/test/kilocode/agent-options-strip.test.ts b/packages/opencode/test/kilocode/agent-options-strip.test.ts index 8bc6e49389d..f686a82084f 100644 --- a/packages/opencode/test/kilocode/agent-options-strip.test.ts +++ b/packages/opencode/test/kilocode/agent-options-strip.test.ts @@ -39,7 +39,16 @@ describe("stripInternalOptions", () => { expect(result).toEqual({ reasoningEffort: "medium" }) }) + test("strips Scout/reference agent metadata while keeping provider options", () => { + const result = stripInternalOptions({ + reference: { name: "docs" }, + resolved: { name: "docs", path: "/tmp/docs" }, + reasoningEffort: "high", + }) + expect(result).toEqual({ reasoningEffort: "high" }) + }) + test("denylist covers exactly the documented internal keys", () => { - expect([...INTERNAL_OPTION_KEYS]).toEqual(["id", "displayName", "source"]) + expect([...INTERNAL_OPTION_KEYS]).toEqual(["id", "displayName", "source", "reference", "resolved"]) }) })