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
6 changes: 6 additions & 0 deletions .changeset/deny-gateway-data-collection.md
Original file line number Diff line number Diff line change
@@ -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.
4 changes: 2 additions & 2 deletions packages/kilo-gateway/src/provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, string>, requestHeaders?: HeadersInit): Headers {
const headers = new Headers(defaultHeaders)
Expand Down Expand Up @@ -55,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 = sanitizeResponsesBody(input, init?.body)
const body = transformRequestBody(input, init?.body, options.dataCollection)

// Add authorization if API key exists
if (apiKey) {
Expand Down
23 changes: 16 additions & 7 deletions packages/kilo-gateway/src/responses.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = (() => {
Expand All @@ -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 } } : {}),
})
}
5 changes: 5 additions & 0 deletions packages/kilo-gateway/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,11 @@ export interface KiloProviderOptions {
*/
name?: string

/**
* Data collection preference for upstream provider routing
*/
dataCollection?: "allow" | "deny"

/**
* Custom fetch function
*/
Expand Down
35 changes: 29 additions & 6 deletions packages/kilo-gateway/test/responses.test.ts
Original file line number Diff line number Diff line change
@@ -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", () => {
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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", () => {
Expand All @@ -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" },
})
})
})
11 changes: 10 additions & 1 deletion packages/opencode/src/kilocode/provider/provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,11 @@ function useLanguageModel(sdk: any) {
return sdk.responses === undefined && sdk.chat === undefined
}

export function patchKiloProviderPrivacy(provider: { options?: Record<string, any> } | undefined, config: any) {
if (!provider || config.hide_prompt_training_models !== true) return
provider.options = { ...provider.options, dataCollection: "deny" }
}

export function kiloCustomLoaders(dep: CustomDep): Record<string, CustomLoader> {
return {
"github-copilot-enterprise": () =>
Expand All @@ -129,17 +134,21 @@ export function kiloCustomLoaders(dep: CustomDep): Record<string, CustomLoader>

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

const options: Record<string, string> = {}
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"
}
Expand Down
2 changes: 2 additions & 0 deletions packages/opencode/src/provider/provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import {
patchModelsDevModel as patchKiloModel,
patchConfigModel as patchKiloConfigModel,
patchCustomLoaderResult,
patchKiloProviderPrivacy,
kiloSmallModelPriority,
buildTimeoutSignal,
REQUEST_TIMEOUT_MS,
Expand Down Expand Up @@ -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)) {
Expand Down
17 changes: 16 additions & 1 deletion packages/opencode/test/kilocode/kilo-loader-auth.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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" } })
Expand Down
Loading