Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/strip-agent-internal-options.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@kilocode/cli": patch
---

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.
31 changes: 31 additions & 0 deletions packages/opencode/src/kilocode/agent/options.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
// 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")
// - `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
// 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", "reference", "resolved"] as const

const internal: ReadonlySet<string> = 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<string, any>): Record<string, any> {
const result: Record<string, any> = {}
for (const key in options) {
if (internal.has(key)) continue
result[key] = options[key]
}
return result
}
7 changes: 6 additions & 1 deletion packages/opencode/src/session/llm/request.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down Expand Up @@ -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)
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")
Expand Down
54 changes: 54 additions & 0 deletions packages/opencode/test/kilocode/agent-options-strip.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
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("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", "reference", "resolved"])
})
})
Loading