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
7 changes: 7 additions & 0 deletions .changeset/honest-model-badges.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"kilo-code": patch
"@kilocode/cli": patch
"@kilocode/kilo-gateway": patch
---

Show model free and prompt-training indicators only when their explicit catalog metadata is enabled.
1 change: 1 addition & 0 deletions packages/core/src/models.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ export const Model = Schema.Struct({
recommendedIndex: Schema.optional(Schema.Finite),
prompt: Schema.optional(Schema.String),
isFree: Schema.optional(Schema.Boolean),
mayTrainOnYourPrompts: Schema.optional(Schema.Boolean),
ai_sdk_provider: Schema.optional(Schema.String),
// kilocode_change end
experimental: Schema.optional(
Expand Down
2 changes: 2 additions & 0 deletions packages/kilo-gateway/src/api/models.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ const openRouterModelSchema = z.object({
supported_parameters: z.array(z.string()).optional(),
preferredIndex: z.number().optional(),
isFree: z.boolean().optional(),
mayTrainOnYourPrompts: z.boolean().optional(),
terminalBench: z
.object({
overallScore: z.number(),
Expand Down Expand Up @@ -190,6 +191,7 @@ function transformToModelDevFormat(model: OpenRouterModel): any {
ai_sdk_provider: model.opencode?.ai_sdk_provider,
tool_call: supportsTools,
isFree: model.isFree,
mayTrainOnYourPrompts: model.mayTrainOnYourPrompts,
...(model.terminalBench && { terminalBench: model.terminalBench }),
...(inputPrice !== undefined &&
outputPrice !== undefined && {
Expand Down
7 changes: 6 additions & 1 deletion packages/kilo-gateway/test/api/models.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ const VALID_RESPONSE = JSON.stringify({
output_modalities: ["text"],
},
supported_parameters: ["tools", "temperature"],
isFree: false,
mayTrainOnYourPrompts: true,
},
],
})
Expand Down Expand Up @@ -143,7 +145,10 @@ test("returns models without error on success", async () => {
;(globalThis as any).fetch = orig

expect(result.error).toBeUndefined()
expect(Object.keys(result.models).length).toBeGreaterThan(0)
expect(result.models["test/model-a"]).toMatchObject({
isFree: false,
mayTrainOnYourPrompts: true,
})
})

test("preserves Terminal Bench metadata as a dedicated model field", async () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,15 @@ describe("model preview data collection line", () => {
expect(styles).toContain(".model-preview-data-line")
})

it("renders prompt training independently from the free badge", () => {
expect(selector).toMatch(
/<Show when=\{isFree\(model\)\}>[\s\S]*?<\/Show>\s*<Show when=\{isDataCollectedModel\(model\)\}>/,
)
expect(preview).toMatch(
/<Show when=\{model\(\)\.isFree\}>[\s\S]*?<\/Show>\s*<Show when=\{isDataCollectedModel\(model\(\)\)\}>/,
)
})

it("uses the book open check icon for all webview model data disclosures", () => {
expect(selector).toContain('Icon name="book-open-check"')
expect(selector).not.toContain('Icon name="warning"')
Expand Down
17 changes: 13 additions & 4 deletions packages/kilo-vscode/tests/unit/model-selector-utils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
PROVIDER_ORDER,
freeDataLabel,
isDataCollectedModel,
isFree,
} from "../../webview-ui/src/components/shared/model-selector-utils"

const labels = { select: "Select model", noProviders: "No providers", notSet: "Not set" }
Expand Down Expand Up @@ -102,11 +103,19 @@ describe("freeDataLabel", () => {
})
})

describe("isFree", () => {
it("uses only explicit free metadata", () => {
expect(isFree({ isFree: true })).toBe(true)
expect(isFree({ isFree: false })).toBe(false)
expect(isFree({})).toBe(false)
})
})

describe("isDataCollectedModel", () => {
it("only marks free Kilo Gateway models with the training disclosure", () => {
expect(isDataCollectedModel({ providerID: KILO_GATEWAY_ID, isFree: true })).toBe(true)
expect(isDataCollectedModel({ providerID: "openrouter", isFree: true })).toBe(false)
expect(isDataCollectedModel({ providerID: KILO_GATEWAY_ID, isFree: false })).toBe(false)
it("uses only explicit prompt training metadata", () => {
expect(isDataCollectedModel({ mayTrainOnYourPrompts: true })).toBe(true)
expect(isDataCollectedModel({ mayTrainOnYourPrompts: false })).toBe(false)
expect(isDataCollectedModel({})).toBe(false)
})
})

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -91,9 +91,11 @@ export const ModelPreview: Component<Props> = (props) => {
})()}
</Show>
</div>
<Show when={model().isFree}>
<Show when={model().isFree || isDataCollectedModel(model())}>
<span class="model-preview-free-data">
<span class="model-preview-badge model-preview-badge--free">{freeLabel()}</span>
<Show when={model().isFree}>
<span class="model-preview-badge model-preview-badge--free">{freeLabel()}</span>
</Show>
<Show when={isDataCollectedModel(model())}>
<Tooltip value={dataLabel()} placement="top">
<span class="model-preview-free-data-icon" aria-label={dataLabel()}>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -828,11 +828,13 @@ export const ModelSelectorBase: Component<ModelSelectorBaseProps> = (props) => {
)
})()}
</span>
<Show when={isFree(model)}>
<Show when={isFree(model) || isDataCollectedModel(model)}>
<span class="model-selector-free-data">
<span class="model-selector-data-badge">
<Tag data-variant="member">{freeLabel()}</Tag>
</span>
<Show when={isFree(model)}>
<span class="model-selector-data-badge">
<Tag data-variant="member">{freeLabel()}</Tag>
</span>
</Show>
<Show when={isDataCollectedModel(model)}>
<Tooltip value={dataLabel()} placement="top">
<span class="model-selector-free-data-icon" aria-label={dataLabel()}>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,8 @@ export function isFree(model: Pick<EnrichedModel, "isFree">): boolean {
return model.isFree === true
}

export function isDataCollectedModel(model: Pick<EnrichedModel, "providerID" | "isFree">): boolean {
return model.isFree === true && model.providerID === KILO_GATEWAY_ID
export function isDataCollectedModel(model: Pick<EnrichedModel, "mayTrainOnYourPrompts">): boolean {
return model.mayTrainOnYourPrompts === true
}

export function freeDataLabel(_free: string, data: string): string {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ export interface ProviderModel {
options?: { description?: string }
recommendedIndex?: number
isFree?: boolean
mayTrainOnYourPrompts?: boolean
terminalBench?: {
overallScore: number
avgAttemptCostUsd: number
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
export const FreeModelDisclosure = {
label: "May train",
panel: "Free - data may be used for training",
collectsData(model: { isFree?: boolean; api?: { npm?: string } }): boolean {
return model.isFree === true && model.api?.npm === "@kilocode/kilo-gateway"
panel: "Data may be used for training",
collectsData(model: { mayTrainOnYourPrompts?: boolean }): boolean {
return model.mayTrainOnYourPrompts === true
},
} as const
6 changes: 4 additions & 2 deletions packages/opencode/src/kilocode/provider/provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ export const KILO_MODEL_SCHEMA_EXTENSIONS = {
recommendedIndex: optionalOmitUndefined(Schema.Finite),
prompt: Schema.optional(Schema.Literals(PROMPTS)),
isFree: Schema.optional(Schema.Boolean),
mayTrainOnYourPrompts: Schema.optional(Schema.Boolean),
terminalBench: optionalOmitUndefined(
Schema.Struct({
overallScore: Schema.Finite,
Expand All @@ -49,12 +50,12 @@ export const KILO_MODEL_SCHEMA_EXTENSIONS = {
// ---------------------------------------------------------------------------

export function patchModelsDevModel(providerID: string, source: any) {
const free = providerID === "kilo" && source.cost?.input === 0 && source.cost?.output === 0
return {
variants: providerID === "kilo" ? (source.variants ?? {}) : {},
recommendedIndex: source.recommendedIndex,
prompt: source.prompt,
isFree: source.isFree ?? (free ? true : undefined),
isFree: source.isFree,
mayTrainOnYourPrompts: source.mayTrainOnYourPrompts,
terminalBench: source.terminalBench,
ai_sdk_provider: source.ai_sdk_provider,
options: source.options ?? {},
Expand All @@ -70,6 +71,7 @@ export function patchConfigModel(cfg: any, existing: any) {
recommendedIndex: cfg.recommendedIndex ?? existing?.recommendedIndex,
prompt: cfg.prompt ?? existing?.prompt,
isFree: cfg.isFree ?? existing?.isFree,
mayTrainOnYourPrompts: cfg.mayTrainOnYourPrompts ?? existing?.mayTrainOnYourPrompts,
terminalBench: existing?.terminalBench,
ai_sdk_provider: cfg.ai_sdk_provider ?? existing?.ai_sdk_provider,
variants: cfg.variants
Expand Down
25 changes: 5 additions & 20 deletions packages/opencode/test/kilocode/free-model-disclosure.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,27 +4,12 @@ import { FreeModelDisclosure } from "../../src/kilocode/components/free-model-di
describe("FreeModelDisclosure", () => {
test("uses compact CLI labels", () => {
expect(FreeModelDisclosure.label).toBe("May train")
expect(FreeModelDisclosure.panel).toBe("Free - data may be used for training")
expect(FreeModelDisclosure.panel).toBe("Data may be used for training")
})

test("only Kilo Gateway free models get the training disclosure", () => {
expect(
FreeModelDisclosure.collectsData({
isFree: true,
api: { npm: "@kilocode/kilo-gateway" },
}),
).toBe(true)
expect(
FreeModelDisclosure.collectsData({
isFree: true,
api: { npm: "@openrouter/ai-sdk-provider" },
}),
).toBe(false)
expect(
FreeModelDisclosure.collectsData({
isFree: false,
api: { npm: "@kilocode/kilo-gateway" },
}),
).toBe(false)
test("uses only explicit prompt training metadata", () => {
expect(FreeModelDisclosure.collectsData({ mayTrainOnYourPrompts: true })).toBe(true)
expect(FreeModelDisclosure.collectsData({ mayTrainOnYourPrompts: false })).toBe(false)
expect(FreeModelDisclosure.collectsData({})).toBe(false)
})
})
8 changes: 6 additions & 2 deletions packages/opencode/test/kilocode/kilo-loader-auth.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,8 @@ function layer() {
id: "paid-model",
name: "Paid Model",
cost: { input: 1, output: 2 },
isFree: false,
mayTrainOnYourPrompts: true,
limit: { context: 128000, output: 4096 },
},
},
Expand Down Expand Up @@ -129,19 +131,21 @@ it.live("assembles paid Kilo models without auth", () =>
id: "paid-model",
providerID: "kilo",
cost: { input: 1, output: 2 },
isFree: false,
mayTrainOnYourPrompts: true,
})
}),
)

it.live("marks zero-cost Kilo models as free when the catalog omits isFree", () =>
it.live("does not infer free status from zero catalog prices", () =>
Effect.gen(function* () {
const providers = yield* ModelsDev.Service.use((models) => models.get()).pipe(
Effect.provide(layer()),
provideInstance(process.cwd()),
)
const kilo = Provider.fromModelsDevProvider(providers.kilo)

expect(kilo.models["free-model"].isFree).toBe(true)
expect(kilo.models["free-model"].isFree).toBeUndefined()
}),
)

Expand Down
Loading
Loading