From 2c9e72c14a87387199fd42546746bbea30aa1570 Mon Sep 17 00:00:00 2001 From: chrarnoldus <12196001+chrarnoldus@users.noreply.github.com> Date: Thu, 18 Jun 2026 10:34:30 +0000 Subject: [PATCH 1/2] fix: deny Kilo Gateway data collection Co-authored-by: kiloconnect[bot] <240665456+kiloconnect[bot]@users.noreply.github.com> --- .changeset/deny-gateway-data-collection.md | 6 +++++ packages/kilo-gateway/src/provider.ts | 17 ++++++++++++- packages/kilo-gateway/src/types.ts | 5 ++++ packages/kilo-gateway/test/provider.test.ts | 25 ++++++++++++++++++- .../src/kilocode/provider/provider.ts | 11 +++++++- packages/opencode/src/provider/provider.ts | 2 ++ .../test/kilocode/kilo-loader-auth.test.ts | 17 ++++++++++++- 7 files changed, 79 insertions(+), 4 deletions(-) create mode 100644 .changeset/deny-gateway-data-collection.md diff --git a/.changeset/deny-gateway-data-collection.md b/.changeset/deny-gateway-data-collection.md new file mode 100644 index 000000000000..48286252fa00 --- /dev/null +++ b/.changeset/deny-gateway-data-collection.md @@ -0,0 +1,6 @@ +--- +"@kilocode/cli": patch +"@kilocode/kilo-gateway": patch +--- + +Deny provider data collection for Kilo Gateway requests when prompt-training models are hidden. diff --git a/packages/kilo-gateway/src/provider.ts b/packages/kilo-gateway/src/provider.ts index b0a54bded8e0..ddf079c28a7c 100644 --- a/packages/kilo-gateway/src/provider.ts +++ b/packages/kilo-gateway/src/provider.ts @@ -19,6 +19,21 @@ export function buildRequestHeaders(defaultHeaders: Record, requ return headers } +export function addDataCollection(body: BodyInit | null | undefined, value?: "allow" | "deny") { + if (!value || typeof body !== "string") return body + const data = (() => { + try { + return JSON.parse(body) as unknown + } catch { + return undefined + } + })() + if (typeof data !== "object" || data === null || Array.isArray(data)) return body + const current = "provider" in data ? data.provider : undefined + const provider = typeof current === "object" && current !== null && !Array.isArray(current) ? current : {} + return JSON.stringify({ ...data, provider: { ...provider, data_collection: value } }) +} + /** * Create a KiloCode provider instance * @@ -55,7 +70,7 @@ export function createKilo(options: KiloProviderOptions = {}): KiloProvider { const originalFetch = options.fetch ?? fetch const wrappedFetch = async (input: string | URL | Request, init?: RequestInit) => { const headers = buildRequestHeaders(customHeaders, init?.headers) - const body = sanitizeResponsesBody(input, init?.body) + const body = addDataCollection(sanitizeResponsesBody(input, init?.body), options.dataCollection) // Add authorization if API key exists if (apiKey) { diff --git a/packages/kilo-gateway/src/types.ts b/packages/kilo-gateway/src/types.ts index e9e8080f02c8..ab214d3274c5 100644 --- a/packages/kilo-gateway/src/types.ts +++ b/packages/kilo-gateway/src/types.ts @@ -95,6 +95,11 @@ export interface KiloProviderOptions { */ name?: string + /** + * Data collection preference for upstream provider routing + */ + dataCollection?: "allow" | "deny" + /** * Custom fetch function */ diff --git a/packages/kilo-gateway/test/provider.test.ts b/packages/kilo-gateway/test/provider.test.ts index 34f468a80b08..80c8ef34af08 100644 --- a/packages/kilo-gateway/test/provider.test.ts +++ b/packages/kilo-gateway/test/provider.test.ts @@ -1,5 +1,5 @@ import { describe, expect, test } from "bun:test" -import { buildRequestHeaders } from "../src/provider" +import { addDataCollection, buildRequestHeaders } from "../src/provider" describe("Kilo provider request headers", () => { test("request headers override provider defaults", () => { @@ -21,3 +21,26 @@ describe("Kilo provider request headers", () => { expect(headers.get("x-request-only")).toBe("kept-too") }) }) + +describe("Kilo provider request body", () => { + test("denies data collection while preserving provider routing", () => { + const body = JSON.stringify({ + model: "anthropic/claude-sonnet-4", + provider: { order: ["anthropic"] }, + }) + const result = addDataCollection(body, "deny") + + expect(JSON.parse(result as string)).toEqual({ + model: "anthropic/claude-sonnet-4", + provider: { + order: ["anthropic"], + data_collection: "deny", + }, + }) + }) + + test("leaves the request body unchanged without a preference", () => { + const body = JSON.stringify({ model: "anthropic/claude-sonnet-4" }) + expect(addDataCollection(body)).toBe(body) + }) +}) diff --git a/packages/opencode/src/kilocode/provider/provider.ts b/packages/opencode/src/kilocode/provider/provider.ts index cb35d737ac6f..6bd6d0e9bfd4 100644 --- a/packages/opencode/src/kilocode/provider/provider.ts +++ b/packages/opencode/src/kilocode/provider/provider.ts @@ -115,6 +115,11 @@ function useLanguageModel(sdk: any) { return sdk.responses === undefined && sdk.chat === undefined } +export function patchKiloProviderPrivacy(provider: { options?: Record } | undefined, config: any) { + if (!provider || config.hide_prompt_training_models !== true) return + provider.options = { ...provider.options, dataCollection: "deny" } +} + export function kiloCustomLoaders(dep: CustomDep): Record { return { "github-copilot-enterprise": () => @@ -129,10 +134,11 @@ export function kiloCustomLoaders(dep: CustomDep): Record kilo: Effect.fnUntraced(function* (input: any) { const env = yield* dep.env() + const config = yield* dep.config() const hasKey = yield* Effect.gen(function* () { if (input.env.some((item: string) => env[item])) return true if (yield* dep.auth(input.id)) return true - if ((yield* dep.config()).provider?.["kilo"]?.options?.apiKey) return true + if (config.provider?.["kilo"]?.options?.apiKey) return true return false }) @@ -140,6 +146,9 @@ export function kiloCustomLoaders(dep: CustomDep): Record if (env.KILO_ORG_ID) { options.kilocodeOrganizationId = env.KILO_ORG_ID } + if (config.hide_prompt_training_models === true) { + options.dataCollection = "deny" + } if (!hasKey) { options.apiKey = "anonymous" } diff --git a/packages/opencode/src/provider/provider.ts b/packages/opencode/src/provider/provider.ts index 6d3962cfa57a..38ec0465d596 100644 --- a/packages/opencode/src/provider/provider.ts +++ b/packages/opencode/src/provider/provider.ts @@ -35,6 +35,7 @@ import { patchModelsDevModel as patchKiloModel, patchConfigModel as patchKiloConfigModel, patchCustomLoaderResult, + patchKiloProviderPrivacy, kiloSmallModelPriority, buildTimeoutSignal, REQUEST_TIMEOUT_MS, @@ -1489,6 +1490,7 @@ export const layer = Layer.effect( if (provider.options) partial.options = provider.options mergeProvider(providerID, partial) } + patchKiloProviderPrivacy(providers[ProviderID.make("kilo")], cfg) // kilocode_change const gitlab = ProviderID.make("gitlab") if (discoveryLoaders[gitlab] && providers[gitlab] && isProviderAllowed(gitlab)) { diff --git a/packages/opencode/test/kilocode/kilo-loader-auth.test.ts b/packages/opencode/test/kilocode/kilo-loader-auth.test.ts index 26567407aad1..8060dc44628e 100644 --- a/packages/opencode/test/kilocode/kilo-loader-auth.test.ts +++ b/packages/opencode/test/kilocode/kilo-loader-auth.test.ts @@ -7,7 +7,7 @@ import { ModelsDev } from "../../src/provider/models" import * as CoreModels from "@opencode-ai/core/models" import { Effect, Layer } from "effect" import { FetchHttpClient } from "effect/unstable/http" -import { kiloCustomLoaders } from "../../src/kilocode/provider/provider" +import { kiloCustomLoaders, patchKiloProviderPrivacy } from "../../src/kilocode/provider/provider" import { Auth } from "../../src/auth" import { ModelCache } from "../../src/provider/model-cache" import { Provider } from "../../src/provider/provider" @@ -165,6 +165,21 @@ it.effect("enables a paid catalog when config apiKey is present", () => }), ) +it.effect("denies provider data collection when prompt-training models are hidden", () => + Effect.gen(function* () { + const result = yield* load({ config: { hide_prompt_training_models: true } }) + expect(result.options).toEqual({ apiKey: "anonymous", dataCollection: "deny" }) + }), +) + +it.effect("keeps data collection denied after configured options are applied", () => + Effect.sync(() => { + const provider = { options: { dataCollection: "allow", baseURL: "https://api.kilo.ai" } } + patchKiloProviderPrivacy(provider, { hide_prompt_training_models: true }) + expect(provider.options).toEqual({ dataCollection: "deny", baseURL: "https://api.kilo.ai" }) + }), +) + it.effect("enables a paid catalog when auth exists", () => Effect.gen(function* () { const result = yield* load({ auth: { type: "api", key: "test-key" } }) From 3bb2b20268479d7f964afcce5568670e27e9eeb3 Mon Sep 17 00:00:00 2001 From: chrarnoldus <12196001+chrarnoldus@users.noreply.github.com> Date: Thu, 18 Jun 2026 10:45:30 +0000 Subject: [PATCH 2/2] refactor(gateway): transform request bodies once Co-authored-by: kiloconnect[bot] <240665456+kiloconnect[bot]@users.noreply.github.com> --- packages/kilo-gateway/src/provider.ts | 19 ++--------- packages/kilo-gateway/src/responses.ts | 23 +++++++++---- packages/kilo-gateway/test/provider.test.ts | 25 +------------- packages/kilo-gateway/test/responses.test.ts | 35 ++++++++++++++++---- 4 files changed, 48 insertions(+), 54 deletions(-) diff --git a/packages/kilo-gateway/src/provider.ts b/packages/kilo-gateway/src/provider.ts index ddf079c28a7c..1f52b09c2d69 100644 --- a/packages/kilo-gateway/src/provider.ts +++ b/packages/kilo-gateway/src/provider.ts @@ -9,7 +9,7 @@ import { getApiKey } from "./auth/token.js" import { buildKiloHeaders, getDefaultHeaders } from "./headers.js" import { ANONYMOUS_API_KEY } from "./api/constants.js" import { resolveKiloOpenRouterBaseUrl } from "./api/url.js" -import { sanitizeResponsesBody } from "./responses.js" +import { transformRequestBody } from "./responses.js" export function buildRequestHeaders(defaultHeaders: Record, requestHeaders?: HeadersInit): Headers { const headers = new Headers(defaultHeaders) @@ -19,21 +19,6 @@ export function buildRequestHeaders(defaultHeaders: Record, requ return headers } -export function addDataCollection(body: BodyInit | null | undefined, value?: "allow" | "deny") { - if (!value || typeof body !== "string") return body - const data = (() => { - try { - return JSON.parse(body) as unknown - } catch { - return undefined - } - })() - if (typeof data !== "object" || data === null || Array.isArray(data)) return body - const current = "provider" in data ? data.provider : undefined - const provider = typeof current === "object" && current !== null && !Array.isArray(current) ? current : {} - return JSON.stringify({ ...data, provider: { ...provider, data_collection: value } }) -} - /** * Create a KiloCode provider instance * @@ -70,7 +55,7 @@ export function createKilo(options: KiloProviderOptions = {}): KiloProvider { const originalFetch = options.fetch ?? fetch const wrappedFetch = async (input: string | URL | Request, init?: RequestInit) => { const headers = buildRequestHeaders(customHeaders, init?.headers) - const body = addDataCollection(sanitizeResponsesBody(input, init?.body), options.dataCollection) + const body = transformRequestBody(input, init?.body, options.dataCollection) // Add authorization if API key exists if (apiKey) { diff --git a/packages/kilo-gateway/src/responses.ts b/packages/kilo-gateway/src/responses.ts index 3697c5cf775e..5ba68266edc4 100644 --- a/packages/kilo-gateway/src/responses.ts +++ b/packages/kilo-gateway/src/responses.ts @@ -28,8 +28,13 @@ function strip(input: unknown[]) { return { kept, changed } } -export function sanitizeResponsesBody(input: string | URL | Request, body: BodyInit | null | undefined) { - if (!endpoint(input)) return body +export function transformRequestBody( + input: string | URL | Request, + body: BodyInit | null | undefined, + value?: "allow" | "deny", +) { + const responses = endpoint(input) + if (!responses && !value) return body if (typeof body !== "string") return body const data = (() => { @@ -40,10 +45,14 @@ export function sanitizeResponsesBody(input: string | URL | Request, body: BodyI } })() if (!record(data)) return body - if (data.store === true) return body - if (!Array.isArray(data.input)) return body - const result = strip(data.input) - if (!result.changed) return body - return JSON.stringify({ ...data, input: result.kept }) + const result = responses && data.store !== true && Array.isArray(data.input) ? strip(data.input) : undefined + if (!result?.changed && !value) return body + + const provider = record(data.provider) ? data.provider : {} + return JSON.stringify({ + ...data, + ...(result?.changed ? { input: result.kept } : {}), + ...(value ? { provider: { ...provider, data_collection: value } } : {}), + }) } diff --git a/packages/kilo-gateway/test/provider.test.ts b/packages/kilo-gateway/test/provider.test.ts index 80c8ef34af08..34f468a80b08 100644 --- a/packages/kilo-gateway/test/provider.test.ts +++ b/packages/kilo-gateway/test/provider.test.ts @@ -1,5 +1,5 @@ import { describe, expect, test } from "bun:test" -import { addDataCollection, buildRequestHeaders } from "../src/provider" +import { buildRequestHeaders } from "../src/provider" describe("Kilo provider request headers", () => { test("request headers override provider defaults", () => { @@ -21,26 +21,3 @@ describe("Kilo provider request headers", () => { expect(headers.get("x-request-only")).toBe("kept-too") }) }) - -describe("Kilo provider request body", () => { - test("denies data collection while preserving provider routing", () => { - const body = JSON.stringify({ - model: "anthropic/claude-sonnet-4", - provider: { order: ["anthropic"] }, - }) - const result = addDataCollection(body, "deny") - - expect(JSON.parse(result as string)).toEqual({ - model: "anthropic/claude-sonnet-4", - provider: { - order: ["anthropic"], - data_collection: "deny", - }, - }) - }) - - test("leaves the request body unchanged without a preference", () => { - const body = JSON.stringify({ model: "anthropic/claude-sonnet-4" }) - expect(addDataCollection(body)).toBe(body) - }) -}) diff --git a/packages/kilo-gateway/test/responses.test.ts b/packages/kilo-gateway/test/responses.test.ts index 49520300f506..9d73d65c7816 100644 --- a/packages/kilo-gateway/test/responses.test.ts +++ b/packages/kilo-gateway/test/responses.test.ts @@ -1,5 +1,5 @@ import { describe, expect, test } from "bun:test" -import { sanitizeResponsesBody } from "../src/responses" +import { transformRequestBody } from "../src/responses" describe("Responses request sanitization", () => { test("strips item ids when storage is disabled", () => { @@ -30,7 +30,7 @@ describe("Responses request sanitization", () => { ], }) - const result = sanitizeResponsesBody("https://api.kilo.ai/api/openrouter/responses", body) + const result = transformRequestBody("https://api.kilo.ai/api/openrouter/responses", body) const data = JSON.parse(result as string) expect(data.input).toHaveLength(3) @@ -60,19 +60,19 @@ describe("Responses request sanitization", () => { ], }) - expect(sanitizeResponsesBody("https://api.kilo.ai/api/openrouter/responses", body)).toBe(body) + expect(transformRequestBody("https://api.kilo.ai/api/openrouter/responses", body)).toBe(body) }) test("leaves non-responses requests unchanged", () => { const body = "not json" - expect(sanitizeResponsesBody("https://api.kilo.ai/api/openrouter/chat/completions", body)).toBe(body) + expect(transformRequestBody("https://api.kilo.ai/api/openrouter/chat/completions", body)).toBe(body) }) test("leaves invalid responses JSON unchanged", () => { const body = "not json" - expect(sanitizeResponsesBody("https://api.kilo.ai/api/openrouter/responses", body)).toBe(body) + expect(transformRequestBody("https://api.kilo.ai/api/openrouter/responses", body)).toBe(body) }) test("matches relative responses paths without a placeholder host", () => { @@ -86,9 +86,32 @@ describe("Responses request sanitization", () => { }, ], }) - const result = sanitizeResponsesBody("/api/openrouter/responses?stream=true", body) + const result = transformRequestBody("/api/openrouter/responses?stream=true", body) const data = JSON.parse(result as string) expect(data.input[0].id).toBeUndefined() }) + + test("sanitizes responses and denies data collection in one transform", () => { + const body = JSON.stringify({ + input: [{ type: "message", role: "assistant", id: "msg_tmp_123" }], + provider: { order: ["anthropic"] }, + }) + const result = transformRequestBody("https://api.kilo.ai/api/openrouter/responses", body, "deny") + + expect(JSON.parse(result as string)).toEqual({ + input: [{ type: "message", role: "assistant" }], + provider: { order: ["anthropic"], data_collection: "deny" }, + }) + }) + + test("denies data collection for non-responses requests", () => { + const body = JSON.stringify({ model: "anthropic/claude-sonnet-4" }) + const result = transformRequestBody("https://api.kilo.ai/api/openrouter/chat/completions", body, "deny") + + expect(JSON.parse(result as string)).toEqual({ + model: "anthropic/claude-sonnet-4", + provider: { data_collection: "deny" }, + }) + }) })