Skip to content
This repository was archived by the owner on Feb 25, 2026. It is now read-only.

Commit e02debf

Browse files
Merge pull request #407 from Kilo-Org/feat/usage-feature-attribution
feat(usage): add feature attribution header to API requests
2 parents c96b670 + 1be28bb commit e02debf

7 files changed

Lines changed: 113 additions & 3 deletions

File tree

packages/kilo-gateway/src/api/constants.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,3 +59,11 @@ export const ENV_EDITOR_NAME = "KILOCODE_EDITOR_NAME"
5959

6060
/** Tester header value for suppressing warnings */
6161
export const TESTER_SUPPRESS_VALUE = "SUPPRESS"
62+
63+
// kilocode_change start
64+
/** Header name for feature tracking */
65+
export const HEADER_FEATURE = "X-KILOCODE-FEATURE"
66+
67+
/** Environment variable name for feature override */
68+
export const ENV_FEATURE = "KILOCODE_FEATURE"
69+
// kilocode_change end

packages/kilo-gateway/src/headers.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,13 @@ import {
44
HEADER_PROJECTID,
55
HEADER_TESTER,
66
HEADER_EDITORNAME,
7+
HEADER_FEATURE, // kilocode_change
78
USER_AGENT,
89
CONTENT_TYPE,
910
DEFAULT_EDITOR_NAME,
1011
ENV_EDITOR_NAME,
1112
TESTER_SUPPRESS_VALUE,
13+
ENV_FEATURE, // kilocode_change
1214
} from "./api/constants.js"
1315

1416
/**
@@ -20,6 +22,18 @@ export const X_KILOCODE_TASKID = HEADER_TASKID
2022
export const X_KILOCODE_PROJECTID = HEADER_PROJECTID
2123
export const X_KILOCODE_TESTER = HEADER_TESTER
2224
export const X_KILOCODE_EDITORNAME = HEADER_EDITORNAME
25+
export const X_KILOCODE_FEATURE = HEADER_FEATURE // kilocode_change
26+
27+
/**
28+
* Get feature header value from KILOCODE_FEATURE env var.
29+
* Returns undefined when not set — the gateway stores NULL (unattributed).
30+
* Callers must explicitly set the env var to get attribution.
31+
*/
32+
// kilocode_change start
33+
export function getFeatureHeader(): string | undefined {
34+
return process.env[ENV_FEATURE] || undefined
35+
}
36+
// kilocode_change end
2337

2438
/**
2539
* Default headers for KiloCode requests
@@ -47,9 +61,13 @@ export function buildKiloHeaders(
4761
kilocodeTesterWarningsDisabledUntil?: number
4862
},
4963
): Record<string, string> {
64+
// kilocode_change start
65+
const feature = getFeatureHeader()
5066
const headers: Record<string, string> = {
5167
[X_KILOCODE_EDITORNAME]: getEditorNameHeader(),
68+
...(feature ? { [X_KILOCODE_FEATURE]: feature } : {}),
5269
}
70+
// kilocode_change end
5371

5472
if (metadata?.taskId) {
5573
headers[X_KILOCODE_TASKID] = metadata.taskId

packages/kilo-gateway/src/index.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ export { KiloAuthPlugin, default } from "./plugin.js"
99
export { createKilo } from "./provider.js"
1010
export { createKiloDebug } from "./provider-debug.js"
1111
export { kiloCustomLoader } from "./loader.js"
12-
export { buildKiloHeaders, getEditorNameHeader, DEFAULT_HEADERS } from "./headers.js"
12+
export { buildKiloHeaders, getEditorNameHeader, getFeatureHeader, DEFAULT_HEADERS } from "./headers.js" // kilocode_change - added getFeatureHeader
1313

1414
// ============================================================================
1515
// Auth
@@ -89,7 +89,9 @@ export {
8989
HEADER_PROJECTID,
9090
HEADER_TESTER,
9191
HEADER_EDITORNAME,
92+
HEADER_FEATURE, // kilocode_change
9293
DEFAULT_EDITOR_NAME,
9394
ENV_EDITOR_NAME,
9495
TESTER_SUPPRESS_VALUE,
96+
ENV_FEATURE, // kilocode_change
9597
} from "./api/constants.js"

packages/kilo-gateway/src/server/routes.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,8 @@
88

99
import { fetchProfile, fetchBalance } from "../api/profile.js"
1010
import { fetchKilocodeNotifications, KilocodeNotificationSchema } from "../api/notifications.js"
11-
import { KILO_API_BASE } from "../api/constants.js"
11+
import { KILO_API_BASE, HEADER_FEATURE } from "../api/constants.js" // kilocode_change - added HEADER_FEATURE
12+
import { buildKiloHeaders } from "../headers.js" // kilocode_change
1213

1314
// Type definitions for OpenCode dependencies (injected at runtime)
1415
type Hono = any
@@ -216,6 +217,10 @@ export function createKiloRoutes(deps: KiloRoutesDeps) {
216217
headers: {
217218
"Content-Type": "application/json",
218219
Authorization: `Bearer ${token}`,
220+
// kilocode_change start - include kilo headers with autocomplete feature override
221+
...buildKiloHeaders(),
222+
[HEADER_FEATURE]: "autocomplete",
223+
// kilocode_change end
219224
},
220225
body: JSON.stringify({
221226
model: fimModel,

packages/kilo-vscode/src/services/cli-backend/server-manager.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ export class ServerManager {
6666
...process.env,
6767
KILO_SERVER_PASSWORD: password,
6868
KILO_CLIENT: "vscode",
69+
KILOCODE_FEATURE: "vscode-extension", // kilocode_change - feature tracking
6970
KILO_TELEMETRY_LEVEL: vscode.env.isTelemetryEnabled ? "all" : "off",
7071
KILO_APP_NAME: "kilo-code",
7172
KILO_EDITOR_NAME: vscode.env.appName,

packages/opencode/src/index.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,16 @@ import { PrCommand } from "./cli/cmd/pr"
2828
import { SessionCommand } from "./cli/cmd/session"
2929
// kilocode_change start - Import telemetry and legacy migration
3030
import { Telemetry } from "@kilocode/kilo-telemetry"
31-
import { migrateLegacyKiloAuth } from "@kilocode/kilo-gateway"
31+
import { migrateLegacyKiloAuth, ENV_FEATURE } from "@kilocode/kilo-gateway"
32+
33+
// kilocode_change - set feature for tracking. 'serve' is spawned by other services
34+
// (extension, cloud) which set their own KILOCODE_FEATURE env var. Direct CLI use
35+
// (any command other than 'serve') is tagged as 'cli'. If 'serve' is spawned without
36+
// the env var, it gets 'unknown' so the misconfiguration is visible in data.
37+
if (!process.env[ENV_FEATURE]) {
38+
const isServe = process.argv.includes("serve")
39+
process.env[ENV_FEATURE] = isServe ? "unknown" : "cli"
40+
}
3241
import { Global } from "./global"
3342
import { Config } from "./config/config"
3443
import { Auth } from "./auth"
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
// kilocode_change - new file
2+
import { describe, it, expect, afterEach } from "bun:test"
3+
import { buildKiloHeaders, getFeatureHeader, getEditorNameHeader } from "@kilocode/kilo-gateway"
4+
import { HEADER_FEATURE, ENV_FEATURE } from "@kilocode/kilo-gateway"
5+
6+
describe("getFeatureHeader", () => {
7+
const original = process.env[ENV_FEATURE]
8+
9+
afterEach(() => {
10+
if (original === undefined) {
11+
delete process.env[ENV_FEATURE]
12+
} else {
13+
process.env[ENV_FEATURE] = original
14+
}
15+
})
16+
17+
it("returns undefined when env var is not set", () => {
18+
delete process.env[ENV_FEATURE]
19+
expect(getFeatureHeader()).toBeUndefined()
20+
})
21+
22+
it("returns the env var value when set", () => {
23+
process.env[ENV_FEATURE] = "cloud-agent"
24+
expect(getFeatureHeader()).toBe("cloud-agent")
25+
})
26+
27+
it("returns undefined for empty string", () => {
28+
process.env[ENV_FEATURE] = ""
29+
expect(getFeatureHeader()).toBeUndefined()
30+
})
31+
})
32+
33+
describe("buildKiloHeaders", () => {
34+
const original = process.env[ENV_FEATURE]
35+
36+
afterEach(() => {
37+
if (original === undefined) {
38+
delete process.env[ENV_FEATURE]
39+
} else {
40+
process.env[ENV_FEATURE] = original
41+
}
42+
})
43+
44+
it("includes feature header when env var is set", () => {
45+
process.env[ENV_FEATURE] = "vscode-extension"
46+
const headers = buildKiloHeaders()
47+
expect(headers[HEADER_FEATURE]).toBe("vscode-extension")
48+
})
49+
50+
it("omits feature header when env var is not set", () => {
51+
delete process.env[ENV_FEATURE]
52+
const headers = buildKiloHeaders()
53+
expect(headers[HEADER_FEATURE]).toBeUndefined()
54+
})
55+
56+
it("always includes editor name header", () => {
57+
delete process.env[ENV_FEATURE]
58+
const headers = buildKiloHeaders()
59+
expect(headers["X-KILOCODE-EDITORNAME"]).toBe(getEditorNameHeader())
60+
})
61+
62+
it("passes through any feature value from env", () => {
63+
process.env[ENV_FEATURE] = "custom-feature"
64+
const headers = buildKiloHeaders()
65+
expect(headers[HEADER_FEATURE]).toBe("custom-feature")
66+
})
67+
})

0 commit comments

Comments
 (0)