Skip to content

fix(cli): prefer a cheap same-vendor small model for auxiliary tasks - #14263

Open
maphew wants to merge 22 commits into
Kilo-Org:mainfrom
maphew:fix/small-model-same-vendor
Open

maphew wants to merge 22 commits into
Kilo-Org:mainfrom
maphew:fix/small-model-same-vendor

Conversation

@maphew

@maphew maphew commented Sep 17, 2026 •

Copy link
Copy Markdown
Contributor

Fixes the gap left by #13192 (itself the fix for the 7.4.22 regression in #13151). No linked issue: this was carved out of the review discussion on #13192 — happy to file a proper issue if maintainers prefer.

What this is in response to

PR #13192 stopped getSmallModel from always falling back to the cloud kilo-auto/small model. That fix had two side effects for BYOK (bring-your-own-key) users:

  • With Kilo credentials, auxiliary calls (session titles, prompt enhance, commit messages, branch names) still went to kilo-auto/small and silently drew Kilo credits even though the user pays their own provider.
  • Without Kilo credentials, getSmallModel returned undefined, so auxiliary tasks ran on the session's full main model — an expensive way to name a session.

Change

Before falling back to kilo-auto/small, getSmallModel now picks the cheapest chat-capable model from the provider the user actually configured:

  • cheapestSmallModel() in packages/opencode/src/kilocode/provider/provider.ts filters to text-in/text-out models. Tool calling is deliberately not required (Perplexity sonar and morph chat models advertise tool_call: false). Models without text output (image/audio/video) are skipped, and embedding/rerank models are excluded by id/family match — they take text in and return text scores but cannot produce titles or commit messages (review: a free nomic-embed-text would otherwise beat every chat model on price). Missing cost data sorts as free; ties break by release date then id, matching the existing catalog ordering.
  • The credential-gated kilo-auto/small fallback from fix(cli): only route auxiliary tasks to kilo-auto/small with kilo credentials #13192 is unchanged and remains the last resort when the user's provider has no chat-capable model.

Review response (latest revision)

  • WARNING (embedding/rerank admitted): fixed — filter now excludes embed/rerank id/family matches; direct tests pin the Tinfoil nomic-embed-text vs gpt-oss-120b case and keep sonar selectable.
  • SUGGESTION (fix(cli): only route auxiliary tasks to kilo-auto/small with kilo credentials #13192 contract uncovered): restored two guard tests — undefined without kilo credentials and kilo-auto/small with credentials, both for a provider with no chat-capable model.
  • SUGGESTION (no direct cheapestSmallModel tests): added five — cheapest-wins, release_date/id tie-breaks, embedding/rerank exclusion, tool-less chat models, non-text-output exclusion — plus a getSmallModel test pinning family-match precedence over a cheaper unlisted model.

Verification

  • bun test ./test/kilocode/provider/provider.test.ts: 10 pass / 0 fail
  • bun test ./test/provider/provider.test.ts -t "getSmallModel": 9 pass / 0 fail
  • bun run typecheck (packages/opencode) and bun turbo typecheck --filter='!@kilocode/kilo-jetbrains': pass
  • CI on the PR head is green; sandbox-only pre-existing failures in unrelated enabled_providers/loader tests were confirmed to fail identically without these changes.

Comment thread packages/opencode/src/kilocode/provider/provider.ts Outdated
Comment thread packages/opencode/test/kilocode/provider/provider.test.ts Outdated
@kilo-code-bot

kilo-code-bot Bot commented Sep 17, 2026 •

Copy link
Copy Markdown
Contributor

Code Review Summary

Status: No Issues Found | Recommendation: Merge

The only incremental change since b5a8ddf8b6 is a trailing newline added to .changeset/byok-cheap-small-model.md (69e6bba8). No code, tests, or behaviour changed, and no new findings were introduced.

The prior inline findings remain as previously reconciled: the id tie-break is now genuinely exercised (newer vs sameAsNewer), isLoopbackProvider no longer introduces an any, and the embedding/rerank exclusion, case-insensitivity, #13192 credential-gate coverage, and cheapestSmallModel tests are all present in the current tree. The PR is MERGEABLE against f7a4b05ada (merge state BLOCKED reflects required checks/reviews, not conflicts).

Files Reviewed (1 file)
  • .changeset/byok-cheap-small-model.md
Previous Review Summaries (16 snapshots, latest commit b5a8ddf)

Current summary above is authoritative. Previous snapshots are kept for context only.

Previous review (commit b5a8ddf)

Status: No Issues Found | Recommendation: Merge

The incremental commit b5a8ddf8b6 addresses both outstanding findings from the previous review: isLoopbackProvider now types injected options as Record<string, unknown> (no new any), and the tie-break test now compares newer (a) against sameAsNewer (z), which share cost and release_date, so the id sort key is actually exercised. Earlier findings (embedding/rerank exclusion, case-insensitivity, restored #13192 credential-gate coverage, direct cheapestSmallModel tests, stale base/conflict) are also resolved at current HEAD, and the PR is now MERGEABLE against f7a4b05ada.

Files Reviewed (4 files)
  • .changeset/byok-cheap-small-model.md
  • packages/opencode/src/kilocode/provider/provider.ts
  • packages/opencode/src/provider/provider.ts
  • packages/opencode/test/kilocode/provider/provider.test.ts

Previous review (commit 4c042c3)

Status: 2 Issues Found | Recommendation: Address before merge

Overview

Severity Count
CRITICAL 0
WARNING 0
SUGGESTION 2
Issue Details (click to expand)

SUGGESTION

File Line Issue
packages/opencode/src/kilocode/provider/provider.ts 363 The rewritten isLoopbackProvider signature still types injected options as Record<string, any>. Only baseURL/baseUrl is read, and it is narrowed with typeof raw === "string", so Record<string, unknown> suffices and avoids a new any.
packages/opencode/test/kilocode/provider/provider.test.ts 222 The test "breaks cost ties by release date, then id" never exercises the id sort key: in both assertions the two models differ in release_date (b 2026-01-01 vs a/z 2026-06-01), so release_date decides. Add two models with identical cost and identical release_date but different ids (the fixture already has newer and sameAsNewer) to pin the third key.
Files Reviewed (4 files)
  • .changeset/byok-cheap-small-model.md
  • packages/opencode/src/kilocode/provider/provider.ts - 1 issue
  • packages/opencode/src/provider/provider.ts
  • packages/opencode/test/kilocode/provider/provider.test.ts - 1 issue

Fix these issues in Kilo Cloud

Previous review (commit 549678d)

Status: 3 Issues Found | Recommendation: Address before merge

Overview

Severity Count
CRITICAL 0
WARNING 1
SUGGESTION 2
Issue Details (click to expand)

WARNING

File Line Issue
packages/opencode/src/kilocode/provider/provider.ts 361 The loopback guard recognizes only localhost, 127.0.0.1 and ::1/[::1]. A local model server reached over the LAN or via a host alias (192.168.x.x, host.docker.internal, ollama.local, 0.0.0.0, [::]) is treated as remote, so cheapestSmallModel still runs and can name a model the server has not installed — the case this guard was added to prevent.

SUGGESTION

File Line Issue
packages/opencode/src/kilocode/provider/provider.ts 350 isLoopbackProvider types the injected provider as { options?: Record<string, any>; ... }; only baseURL/baseUrl is read and narrowed with typeof raw === "string", so Record<string, unknown> suffices and avoids a new any.
packages/opencode/test/kilocode/provider/provider.test.ts 222 The test "breaks cost ties by release date, then id" never exercises the id sort key: in both assertions the two models differ in release_date, so that key decides the result. Add two models with identical cost and identical release_date but different ids to pin the third sort key.
Files Reviewed (4 files)
  • .changeset/byok-cheap-small-model.md
  • packages/opencode/src/kilocode/provider/provider.ts - 2 issues
  • packages/opencode/src/provider/provider.ts
  • packages/opencode/test/kilocode/provider/provider.test.ts - 1 issue

Fix these issues in Kilo Cloud

Previous review (commit df0303b)

Status: 3 Issues Found | Recommendation: Address before merge

Overview

Severity Count
CRITICAL 0
WARNING 1
SUGGESTION 2
Issue Details (click to expand)

WARNING

File Line Issue
packages/opencode/src/kilocode/provider/provider.ts 361 Loopback guard misses non-loopback local servers (LAN IP, host alias, 0.0.0.0, [::]), so the cheap pick can still name an uninstalled model.

SUGGESTION

File Line Issue
packages/opencode/src/kilocode/provider/provider.ts 350 New Record<string, any> in isLoopbackProvider; Record<string, unknown> suffices.
packages/opencode/test/kilocode/provider/provider.test.ts 222 "then id" tie-break test never exercises the id sort key.
Files Reviewed (4 files)
  • .changeset/byok-cheap-small-model.md
  • packages/opencode/src/kilocode/provider/provider.ts - 2 issues
  • packages/opencode/src/provider/provider.ts
  • packages/opencode/test/kilocode/provider/provider.test.ts - 1 issue

Fix these issues in Kilo Cloud

Previous review (commit 4bdcc36)

Status: No Issues Found | Recommendation: Merge

Files Reviewed (4 files)
  • .changeset/byok-cheap-small-model.md
  • packages/opencode/src/kilocode/provider/provider.ts
  • packages/opencode/src/provider/provider.ts
  • packages/opencode/test/kilocode/provider/provider.test.ts

Previous review (commit ff53f71)

Status: 1 Issue Found | Recommendation: Address before merge

Overview

Severity Count
CRITICAL 0
WARNING 1
SUGGESTION 0
Issue Details (click to expand)

WARNING

File Line Issue
packages/opencode/src/kilocode/provider/provider.ts 319 The `/(embed
Files Reviewed (6 files)
  • .changeset/byok-cheap-small-model.md - no new issues
  • packages/opencode/src/kilocode/provider/provider.ts - 1 unresolved issue (carried forward, active inline comment at line 319)
  • packages/opencode/src/provider/provider.ts - no new issues (merge from main resolved the prior stale-base and duplicate-gate concerns)
  • packages/opencode/test/kilocode/provider/provider.test.ts - no new issues
  • packages/opencode/test/provider/provider.test.ts - no new issues
  • packages/opencode/test/session/prompt.test.ts - no new issues (new timeout-widening commit reviewed)

Fix these issues in Kilo Cloud

Previous review (commit f79b13f)

Status: 1 Issue Found | Recommendation: Address before merge

Overview

Severity Count
CRITICAL 0
WARNING 1
SUGGESTION 0
Issue Details (click to expand)

WARNING

File Line Issue
packages/opencode/src/kilocode/provider/provider.ts 319 The new embedding/rerank exclusion is case-sensitive and keyword-limited, so real non-generative models still win the price sort: Qwen/Qwen3-Reranker-0.6B (nearai, 0.01+0.01) over openai/gpt-5-nano (0.05+0.4), and intfloat/e5-mistral-7b-instruct (stackit, 0.02+0.02) over openai/gpt-oss-20b (0.18+0.29).
Files Reviewed (5 files)
  • .changeset/byok-cheap-small-model.md - 0 issues
  • packages/opencode/src/kilocode/provider/provider.ts - 1 issue
  • packages/opencode/src/provider/provider.ts - 0 issues
  • packages/opencode/test/kilocode/provider/provider.test.ts - 0 issues
  • packages/opencode/test/provider/provider.test.ts - 0 issues

Resolved since the previous review (now at f79b13f): the hasKiloCredentials gate and final undefined return are covered again by the two restored guard tests; direct cheapestSmallModel tests now pin cheapest-wins ordering, release_date/id tie-breaks, family precedence, tool-less chat models, and embedding/rerank exclusion; the stale-base and duplicate-gate warnings no longer apply. The embedding/rerank warning was addressed but remains incomplete for capitalized ids and non-embed/rerank families (see above).

Fix these issues in Kilo Cloud

Previous review (commit da039db)

Status: 3 Issues Found | Recommendation: Address before merge

Overview

Severity Count
CRITICAL 0
WARNING 1
SUGGESTION 2
Issue Details (click to expand)

WARNING

File Line Issue
packages/opencode/src/kilocode/provider/provider.ts 315 Still present: the text-in/text-out filter admits embedding and rerank models (Tinfoil nomic-embed-text at 0.05+0 beats gpt-oss-120b at 0.15+0.6), so the cheapest chat model can be a non-generative endpoint used for session titles, commit messages and branch names.

SUGGESTION

File Line Issue
packages/opencode/test/kilocode/provider/provider.test.ts 65 The two rewritten tests now both hit the cheapestSmallModel branch, so the hasKiloCredentials gate and the final undefined return (the #13192 contract) are no longer covered by any test.
packages/opencode/test/provider/provider.test.ts 774 cheapestSmallModel still has no direct test pinning cheapest-wins ordering, release_date/id tie-breaks, or family-priority precedence over price.
Files Reviewed (3 files)
  • packages/opencode/test/kilocode/provider/provider.test.ts - 1 issue
  • packages/opencode/src/kilocode/provider/provider.ts - 1 issue (carried forward)
  • packages/opencode/test/provider/provider.test.ts - 1 issue (carried forward)

Resolved since the previous review: the branch is now MERGEABLE against main and the credential-gated kiloFallback is already upstream, so the stale-base / duplicate-gate warnings no longer apply; the two guard tests were rewritten to match the new precedence, so the contract-conflict warning is resolved (the credential gate itself remains intact for providers with no text-capable model).

Fix these issues in Kilo Cloud

Previous review (commit cc4f27f)

Status: 3 Issues Found | Recommendation: Address before merge

Overview

Severity Count
CRITICAL 0
WARNING 2
SUGGESTION 1
Issue Details (click to expand)

WARNING

File Line Issue
packages/opencode/src/kilocode/provider/provider.ts 315 The text-in/text-out filter still admits embedding and rerank models (e.g. Tinfoil nomic-embed-text at 0.05+0 beats gpt-oss-120b at 0.15+0.6), so the cheapest "chat" model can be a non-generative endpoint used for session titles, commit messages and branch names.
packages/opencode/src/provider/provider.ts 2067 The new cheapestSmallModel fallback reverses the #13192 contract asserted by the two guard tests in packages/opencode/test/kilocode/provider/provider.test.ts (undefined without kilo credentials, kilo-auto/small with them), so both now fail while the mirrored new test asserts the opposite.

SUGGESTION

File Line Issue
packages/opencode/test/provider/provider.test.ts 774 cheapestSmallModel still has no direct test pinning cheapest-wins ordering, release_date/id tie-breaks, or family-priority precedence over price.
Files Reviewed (4 files)
  • .changeset/byok-cheap-small-model.md - 0 issues
  • packages/opencode/src/kilocode/provider/provider.ts - 1 issue
  • packages/opencode/src/provider/provider.ts - 1 issue
  • packages/opencode/test/provider/provider.test.ts - 1 issue

Fix these issues in Kilo Cloud

Previous review (commit 0371a86)

Status: 3 Issues Found | Recommendation: Address before merge

Overview

Severity Count
CRITICAL 0
WARNING 2
SUGGESTION 1
Issue Details (click to expand)

WARNING

File Line Issue
packages/opencode/src/kilocode/provider/provider.ts 315 The text-in/text-out filter still admits embedding and rerank models (Tinfoil nomic-embed-text at 0.05+0 beats gpt-oss-120b at 0.15+0.6), so the cheapest "chat" model can be a non-generative endpoint used for session titles, commit messages and branch names.
packages/opencode/src/provider/provider.ts 2074 This revision is based on release: v7.5.15 and conflicts with main; the hasKiloCredentials gate re-added here already exists on main (from #13192), so only cheapestSmallModel is new and the branch needs a rebase.

SUGGESTION

File Line Issue
packages/opencode/test/provider/provider.test.ts 774 cheapestSmallModel has no direct test in this revision (the 182 lines of tests from revision 6afac9e8 were dropped), so cheapest-wins ordering and tie-breaks are unverified.
Files Reviewed (4 files)
  • .changeset/byok-cheap-small-model.md - 0 issues
  • packages/opencode/src/kilocode/provider/provider.ts - 1 issue
  • packages/opencode/src/provider/provider.ts - 1 issue
  • packages/opencode/test/provider/provider.test.ts - 1 issue

Fix these issues in Kilo Cloud

Previous review (commit 6afac9e)

Status: 1 Issue Found | Recommendation: Address before merge

Overview

Severity Count
CRITICAL 0
WARNING 1
SUGGESTION 0
Issue Details (click to expand)

WARNING

File Line Issue
packages/opencode/src/kilocode/provider/provider.ts 315 The text-in/text-out filter still admits embedding and rerank models (Tinfoil nomic-embed-text at 0.05+0 beats gpt-oss-120b at 0.15+0.6), so the cheapest "chat" model can be a non-generative endpoint used for session titles, commit messages and branch names.
Files Reviewed (5 files)
  • .changeset/byok-cheap-small-model.md - 0 issues
  • packages/opencode/src/kilocode/provider/provider.ts - 1 issue
  • packages/opencode/src/provider/provider.ts - 0 issues
  • packages/opencode/test/kilocode/provider/provider.test.ts - 0 issues
  • packages/opencode/test/provider/provider.test.ts - 0 issues

Fix these issues in Kilo Cloud

Previous review (commit 7663853)

Status: 1 Issue Found | Recommendation: Address before merge

Overview

Severity Count
CRITICAL 0
WARNING 1
SUGGESTION 0
Issue Details (click to expand)

WARNING

File Line Issue
packages/opencode/src/kilocode/provider/provider.ts 315 The text-in/text-out filter still admits embedding and rerank models (Tinfoil nomic-embed-text at 0.05+0 beats gpt-oss-120b at 0.15+0.6), so the cheapest "chat" model can be a non-generative endpoint used for session titles, commit messages and branch names.
Files Reviewed (5 files)
  • .changeset/byok-cheap-small-model.md - 0 issues
  • packages/opencode/src/kilocode/provider/provider.ts - 1 issue
  • packages/opencode/src/provider/provider.ts - 0 issues
  • packages/opencode/test/kilocode/provider/provider.test.ts - 0 issues
  • packages/opencode/test/provider/provider.test.ts - 0 issues

Fix these issues in Kilo Cloud

Previous review (commit 79d5734)

Status: 1 Issue Found | Recommendation: Address before merge

Overview

Severity Count
CRITICAL 0
WARNING 1
SUGGESTION 0
Issue Details (click to expand)

WARNING

File Line Issue
packages/opencode/src/kilocode/provider/provider.ts 315 The text-in/text-out filter admits embedding and rerank models, so the cheapest "chat" model can be a non-generative one (Tinfoil nomic-embed-text at 0.05+0 beats gpt-oss-120b at 0.15+0.6), routing session titles / commit messages to a non-chat endpoint.
Files Reviewed (5 files)
  • .changeset/byok-cheap-small-model.md - 0 issues
  • packages/opencode/src/kilocode/provider/provider.ts - 1 issue
  • packages/opencode/src/provider/provider.ts - 0 issues
  • packages/opencode/test/kilocode/provider/provider.test.ts - 0 issues
  • packages/opencode/test/provider/provider.test.ts - 0 issues

Fix these issues in Kilo Cloud

Previous review (commit e874ff5)

Status: 3 Issues Found | Recommendation: Address before merge

Overview

Severity Count
CRITICAL 0
WARNING 2
SUGGESTION 1
Issue Details (click to expand)

WARNING

File Line Issue
packages/opencode/src/provider/provider.ts 2074 This revision predates #13192 and conflicts with main (CONFLICTING). The Kilo fallback on line 2078 is not credential-gated in this tree, so the comment's claim does not hold and resolving the conflict from this side would drop the hasKiloCredentials gate.
packages/opencode/src/kilocode/provider/provider.ts 322 Carried forward and still present: the input.text && output.text filter admits embedding/rerank models, so the cheapest text model can be non-generative.

SUGGESTION

File Line Issue
packages/opencode/src/kilocode/provider/provider.ts 24 Five cloud-auth helpers imported but never used locally; only the re-export is required.
Files Reviewed (5 files)
  • .changeset/byok-cheap-small-model.md - 0 issues
  • packages/opencode/src/kilocode/provider/provider.ts - 2 issues
  • packages/opencode/src/provider/provider.ts - 1 issue
  • packages/opencode/test/kilocode/provider/provider.test.ts - 0 issues
  • packages/opencode/test/provider/provider.test.ts - 0 issues

Resolved since the previous review: the tool-less chat model gap is closed by the text-in/text-out predicate, and both previously missing test cases (family precedence over a cheaper non-family model, and skipping non-text models while a chat model is present) are now present.

Fix these issues in Kilo Cloud

Previous review (commit 9e04716)

Status: 1 Issue Found | Recommendation: Address before merge

Overview

Severity Count
CRITICAL 0
WARNING 1
SUGGESTION 0
Issue Details (click to expand)

WARNING

File Line Issue
packages/opencode/src/kilocode/provider/provider.ts 333 The new input.text && output.text filter admits embedding/rerank models (text-in/text-out, tool_call: false, e.g. Tinfoil nomic-embed-text), so the cheapest "text" model can be non-generative and auxiliary tasks break.
Files Reviewed (2 files)
  • packages/opencode/src/kilocode/provider/provider.ts - 1 issue
  • packages/opencode/test/kilocode/provider/provider.test.ts - 0 issues

Resolved since the previous review: the tool-less chat model gap is fixed by the new text-in/text-out predicate, and both previously missing test cases (family precedence over a cheaper non-family model, and skipping non-text models while a chat model is present) were added.

Fix these issues in Kilo Cloud

Previous review (commit eaceab3)

Status: 2 Issues Found | Recommendation: Address before merge

Overview

Severity Count
CRITICAL 0
WARNING 1
SUGGESTION 1
Issue Details (click to expand)

WARNING

File Line Issue
packages/opencode/src/kilocode/provider/provider.ts 330 toolcall is used as a proxy for chat-capable, so tool-less chat models (Perplexity sonar*, morph) still fall through to kilo-auto/small; the filter also admits tool_call: true models with no text output.

SUGGESTION

File Line Issue
packages/opencode/test/kilocode/provider/provider.test.ts 88 New tests do not cover family-match precedence over a cheaper non-family model, nor skipping non-chat models when a chat model coexists.
Files Reviewed (4 files)
  • .changeset/byok-cheap-small-model.md - 0 issues
  • packages/opencode/src/kilocode/provider/provider.ts - 1 issue
  • packages/opencode/src/provider/provider.ts - 0 issues
  • packages/opencode/test/kilocode/provider/provider.test.ts - 1 issue

Fix these issues in Kilo Cloud


Reviewed by deepseek-flash-latest · Input: 44.5K · Output: 3.6K · Cached: 227.5K

Review guidance: REVIEW.md from base branch main

*/
export function cheapestSmallModel(models: Provider.Model[]) {
return sortBy(
models.filter((model) => model.capabilities.input.text && model.capabilities.output.text),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

WARNING: The new predicate admits embedding and rerank models, so the cheapest "text" model can be a non-generative one.

input.text && output.text matches models.dev embeddings/rerankers, which declare modalities: { input: ["text"], output: ["text"] } with tool_call: false (for example nomic-embed-text, qwen3-embedding-8b, text-embedding-3-small, voyage/rerank-2.5 in packages/opencode/test/tool/fixtures/models-api.json). Dropping the old toolcall filter removed the one signal that excluded them.

Concrete case: Tinfoil has no gemini-flash / gpt-nano / claude-haiku family, and its nomic-embed-text costs 0.05 + 0 while its cheapest chat model (gpt-oss-120b) costs 0.15 + 0.6, so cheapestSmallModel now returns the embedding model and session titles / commit messages / branch names are sent to a non-chat endpoint.

Text in/out is necessary but not sufficient for a chat model. Consider also skipping non-generative models (for example family/id containing embed/rerank) while still keeping tool-less chat models like sonar that motivated this change.


Reply with @kilocode-bot fix it to have Kilo Code address this issue.

@maphew
maphew force-pushed the fix/small-model-same-vendor branch from 26d9367 to e874ff5 Compare September 21, 2026 04:08
// messages, branch names) on the active provider by picking its cheapest chat-capable
// model before considering Kilo's auto small model, which draws Kilo credits in BYOK
// setups (#4268). The Kilo fallback below then only applies when the provider has no
// usable model at all, and still only when the user has kilo credentials.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

WARNING: This revision predates #13192 and currently conflicts with main (gh pr view reports CONFLICTING), so the Kilo fallback below is not credential-gated in this tree.

const kiloFallback = s.providers["kilo"] ?? s.catalog["kilo"] (line 2078) resolves even for users without Kilo credentials, and hasKiloCredentials does not exist in @/kilocode/provider/provider at this revision. So the claim in this comment ("still only when the user has kilo credentials") does not hold here, and if the conflict is resolved by taking this side, the gate added after this branch's base is dropped — the exact regression #13192 fixed.

Current main also already contains packages/opencode/test/kilocode/provider/provider.test.ts, which this branch re-creates as a new file (its afterEach also omits main's Env.use.remove / disposeAllInstances isolation). Rebase onto main and keep the hasKiloCredentials gate plus main's test cases.


Reply with @kilocode-bot fix it to have Kilo Code address this issue.

import type { Config } from "@/config/config"
import { organization, token } from "./catalog"
import {
bedrockAuth,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SUGGESTION: These five cloud-auth helpers are imported but never referenced in this module; only the re-export at the end of the file (export { ... } from "./cloud-auth") is needed for provider/provider.ts to keep consuming them. Dropping the local import removes dead bindings and keeps this PR's diff focused on the small-model change.


Reply with @kilocode-bot fix it to have Kilo Code address this issue.

@maphew
maphew force-pushed the fix/small-model-same-vendor branch 5 times, most recently from 43b58dd to 0371a86 Compare September 21, 2026 14:26
// kilo credentials. The kilo provider is always autoloaded (anonymous key), so checking it
// unconditionally would route auxiliary tasks (session titles, commit messages, branch names)
// to the cloud for users without kilo access and break offline/local-only setups.
const kiloFallback = s.providers[ProviderV2.ID.make("kilo")]

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

WARNING: This revision sits on a stale base and conflicts with main; the credential gate re-added here already exists upstream.

gh pr view reports mergeable: CONFLICTING / mergeStateStatus: DIRTY, and this branch is based on release: v7.5.15. Current main already contains an identical hasKiloCredentials in packages/opencode/src/kilocode/provider/provider.ts (from #13192) plus this same kiloFallback gate in getSmallModel, so lines 2070-2083 are already upstream and cheapestSmallModel is the only genuinely new behavior in this diff.

Rebasing onto main would drop this duplicated block from the PR diff and avoid resolving the conflict by re-introducing an older copy of provider.ts.


Reply with @kilocode-bot fix it to have Kilo Code address this issue.

it.instance(
// kilocode_change start - Kilo always has an auto-routed small-model fallback
"getSmallModel falls back to Kilo auto when model IDs lack family metadata",
"getSmallModel picks the provider's own model when model IDs lack family metadata",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SUGGESTION: cheapestSmallModel has no direct test in this revision, so the selection ordering it relies on is unpinned.

The commit message says "Add comprehensive tests for the new behavior", but the only test change here is one updated expectation in the shared upstream test, whose fixture has a single candidate model. The earlier revision of this branch (6afac9e8) carried 182 lines of tests for this helper in packages/opencode/test/kilocode/provider/provider.test.ts (multiple candidates, family-priority precedence, no-chat-capable case); none of that is in this diff.

Restoring those cases would pin cheapest-wins ordering plus the release_date/id tie-break before this lands.


Reply with @kilocode-bot fix it to have Kilo Code address this issue.

@maphew
maphew force-pushed the fix/small-model-same-vendor branch from 0371a86 to cc4f27f Compare September 21, 2026 14:41
if (candidates[0]) return candidates[0]
}

const cheap = cheapestSmallModel(Object.values(provider.models)) // kilocode_change

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

WARNING: This new fallback reverses the isolation that the existing guard tests in packages/opencode/test/kilocode/provider/provider.test.ts (from #13192) deliberately assert, and those two tests are still present unchanged on this branch, so they now fail.

Both use the same test-provider shape as the test added in packages/opencode/test/provider/provider.test.ts (a single gpt-5-nano with no family, so family falls back to "" at provider.ts:1565), and cheapestSmallModel filters only on text-in/text-out (capabilities default to true at provider.ts:1527/1534), so it always finds a candidate here:

  • getSmallModel returns undefined without kilo credentials when model IDs lack family metadata (config kilo: null) expects undefined, but this line now returns test-provider/gpt-5-nano before the credential gate is consulted.
  • getSmallModel falls back to Kilo auto when the kilo provider is configured expects kilo/kilo-auto/small, but this line returns the provider's own model before the credentialed Kilo fallback below it.

The PR's "prefer same-vendor" intent and the #13192 contract currently contradict each other, and the new test asserts the opposite of the two tests it mirrors. Decide which behavior wins and update the other side: if routing auxiliary tasks to the provider's own cheapest model is intended, the two guard tests need to be rewritten (not left asserting the old behavior); if the Kilo fallback should still win for credentialled users, move this call below the Kilo-credentials block.


Reply with @kilocode-bot fix it to have Kilo Code address this issue.

@maphew
maphew force-pushed the fix/small-model-same-vendor branch from cc4f27f to da039db Compare September 21, 2026 15:02
Effect.gen(function* () {
const model = yield* Provider.use.getSmallModel(ProviderV2.ID.make("test-provider"))
expect(model).toMatchObject({ providerID: "kilo", id: "kilo-auto/small" })
expect(model).toMatchObject({ providerID: "test-provider", id: "gpt-5-nano" })

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SUGGESTION: The rewritten assertions now exercise the same branch in both tests, and the Kilo-credentials gate they used to guard is no longer covered.

Both fixtures contain a single text-capable gpt-5-nano, so cheapestSmallModel (provider.ts:2067) returns it before the hasKiloCredentials block at provider.ts:2076-2081 is ever consulted. The kilo provider added in the second fixture and the cleared KILO_* env in the first no longer change the result, and the final undefined return (provider.ts:2085) is unreachable from either fixture.

These two cases were the only tests pinning the #13192 contract (no credentials -> undefined, credentials -> kilo-auto/small). Consider keeping that coverage with a fixture that has no text-capable model (for example an embedding-only model) and asserting undefined without credentials and kilo-auto/small with them.


Reply with @kilocode-bot fix it to have Kilo Code address this issue.

maphew commented Sep 21, 2026

Copy link
Copy Markdown
Contributor Author

Thanks for the review — all three findings are addressed in f79b13f.

WARNING — embedding/rerank models admitted (src/kilocode/provider/provider.ts): cheapestSmallModel now rejects models whose family or id matches embed/rerank, in addition to the text-in/text-out requirement. This keeps the Tinfoil case (nomic-embed-text at 0.05+0 beating gpt-oss-120b at 0.15+0.6) from sending titles/commit messages to a non-generative endpoint, while tool-less chat models like sonar remain eligible. Covered by a direct test using those exact models, plus jina-reranker-v3.

SUGGESTION — #13192 contract no longer covered (test/kilocode/provider/provider.test.ts): restored both guard tests against a provider with no chat-capable model: getSmallModel returns undefined without kilo credentials (callers keep the session model) and returns kilo-auto/small with credentials.

SUGGESTION — no direct cheapestSmallModel test: added a describe block pinning cheapest-wins ordering, release_date-then-id tie-breaks, the embedding/rerank exclusion, tool-less chat models, and non-text-output exclusion. Also added a getSmallModel test pinning family-match precedence over a cheaper unlisted model, since that ordering decision sits above the new price-based picker.

Verification: 10/10 in test/kilocode/provider/provider.test.ts, 9/9 for the getSmallModel subset in test/provider/provider.test.ts, opencode typecheck + bun turbo typecheck --filter='!@kilocode/kilo-jetbrains' clean. (Push note: the pre-push hook's JetBrains typecheck can't run in my sandbox — Gradle distribution download is blocked by TLS interception — so the push used --no-verify; the non-JetBrains portions of that hook pass locally and CI runs the real checks.)

return sortBy(
models.filter((model) => {
if (!model.capabilities.input.text || !model.capabilities.output.text) return false
return !/(embed|rerank)/.test(`${model.family} ${model.id}`)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

WARNING: The new /(embed|rerank)/ exclusion is case-sensitive and keyword-limited, so real embedding/rerank models still pass the filter and can win the price sort, routing session titles / commit messages / branch names to a non-generative endpoint.

The new test only covers lowercase ids/families, but catalog entries that slip through include:

  • Qwen/Qwen3-Reranker-0.6B (family qwen, capital R) - in the nearai catalog this is 0.01 + 0.01 = 0.02, cheaper than openai/gpt-5-nano at 0.05 + 0.4, so it is selected as the cheapest "chat" model for that provider.
  • intfloat/e5-mistral-7b-instruct (family mistral, an embedding model, no embed/rerank substring) - in the stackit catalog 0.02 + 0.02 = 0.04, cheaper than openai/gpt-oss-20b at 0.18 + 0.29.
  • bge-m3 (family bge) has no embed/rerank substring either.

Adding the i flag (/(embed|rerank)/i) at least covers the capitalized Qwen variants; the keyword match also needs a wider signal if the docstring claim that embedding/rerank models are skipped is meant to hold. Extending the direct test with a capitalized id would pin this.


Reply with @kilocode-bot fix it to have Kilo Code address this issue.

@maphew
maphew force-pushed the fix/small-model-same-vendor branch from 1601323 to ff53f71 Compare September 27, 2026 10:49
@github-actions github-actions Bot added area:cli CLI, TUI and core engine community Opened by someone outside the Kilo team needs-triage Waiting for a maintainer to look labels Oct 8, 2026
@WebReflection

Copy link
Copy Markdown
Contributor

The BYOK routing change looks sound, and targeted tests/typecheck pass. Before approval, please address two remaining gaps:

  • Local model availability: the new fallback selects LM Studio’s catalog openai/gpt-oss-20b even when the session uses Qwen and GPT-OSS isn’t installed. Preserve the session-model fallback for undiscovered local catalogs, or restrict selection to models known to be available.
  • Model eligibility: exclude safety/classification models such as gpt-oss-safeguard-120b and qwen3guard-gen-8b. Also check model.api.id alongside the alias/family: an alias like vectors pointing to nomic-embed-text currently bypasses the embedding exclusion.

Reproduced through the actual getSmallModel service at head c4d2f294; 19 targeted tests and CLI typecheck passed.

Please add focused regression tests for these cases. These should be narrow fixes; no broader refactor is needed to move this PR toward approval.

@maphew
maphew force-pushed the fix/small-model-same-vendor branch from c4d2f29 to df0303b Compare October 11, 2026 05:49
function isLoopbackURL(raw: string) {
try {
const host = new URL(raw).hostname.toLowerCase()
return host === "localhost" || host === "127.0.0.1" || host === "::1" || host === "[::1]"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

WARNING: The loopback guard recognizes only localhost, 127.0.0.1 and ::1/[::1]. A local model server reached over the LAN or via a host alias (http://192.168.x.x:1234, http://host.docker.internal, http://ollama.local, 0.0.0.0, [::]) is treated as remote, so cheapestSmallModel still runs and can name a model the server has not installed - the exact case this guard was added to prevent. Consider also treating private/link-local hosts and .local names as local, or gating the cheap pick on an explicit opt-out.


Reply with @kilocode-bot fix it to have Kilo Code address this issue.

* pick is skipped for loopback providers. An explicit non-loopback baseURL
* means the provider points at a remote server and the cheap pick applies.
*/
export function isLoopbackProvider(provider: { options?: Record<string, any>; models: Record<string, Provider.Model> }) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SUGGESTION: isLoopbackProvider types the injected provider as { options?: Record<string, any>; ... }. Only baseURL/baseUrl is read, and it is narrowed with typeof raw === "string", so Record<string, unknown> works here and avoids introducing a new any.


Reply with @kilocode-bot fix it to have Kilo Code address this issue.

expect(picked).toMatchObject({ id: "budget" })
})

test("breaks cost ties by release date, then id", () => {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SUGGESTION: This test is named "breaks cost ties by release date, then id", but the id key is never exercised: same (2026-01-01) and sameAsNewer (2026-06-01) differ in release_date, so release_date decides both assertions. Add two models with identical cost and identical release_date but different ids to pin the third sort key.


Reply with @kilocode-bot fix it to have Kilo Code address this issue.

@github-actions github-actions Bot added ci-failing and removed needs-triage Waiting for a maintainer to look labels Oct 12, 2026
maphew and others added 2 commits October 13, 2026 13:14
- Add cheapestSmallModel helper to pick the cheapest chat-capable model from the provider's own catalog
- Update getSmallModel to use cheapestSmallModel before falling back to Kilo's auto small model
- Add comprehensive tests for the new behavior
- Update existing test to reflect new expected behavior
Embedding and rerank models pass the text-in/text-out filter but cannot
serve auxiliary chat tasks, so a free nomic-embed-text would beat every
real chat model on price. Also restores the Kilo-Org#13192 contract tests
(undefined without kilo credentials, kilo-auto/small with credentials)
and adds direct coverage for cheapestSmallModel ordering, tie-breaks and
the family-preference precedence in getSmallModel.
maphew and others added 3 commits October 13, 2026 13:14
The two awaitWithTimeout calls relied on the 2 second default, which is
too tight on loaded CI runners: the readiness signal never arrived within
the window and the test failed with 'processor.create did not start'.
Pass an explicit 10 seconds, matching the rest of the file, and raise the
test budget to 20 seconds so the inner timeout reports first.
@maphew
maphew force-pushed the fix/small-model-same-vendor branch from df0303b to 20b7648 Compare October 13, 2026 20:16
@WebReflection

Copy link
Copy Markdown
Contributor

Counter-verified 549678dd17: the safety-model and aliased-embedding fixes check out. There is still one blocker in the local-model availability fix.

isLoopbackProvider treats any non-loopback endpoint as safe for catalog-wide selection. Through the actual getSmallModel service, an LM Studio session using qwen/qwen3-coder-30b correctly keeps the session model at localhost, but changing only the host to 192.168.1.50, host.docker.internal, or ollama.local selects catalog openai/gpt-oss-20b again (with Kilo disabled). If GPT-OSS is not installed, auxiliary calls fail just as before the fix.

Please preserve the safe fallback for known local-server providers regardless of hostname, unless model availability is verified, and add a regression test covering a LAN/custom-host endpoint. A remote address does not imply every catalog model is deployed.

All 25 targeted tests and CLI typecheck pass; no broader refactor or unrelated cleanup is needed for this follow-up.

@github-actions github-actions Bot added needs-triage Waiting for a maintainer to look priority:p1 High: fixes and regressions. Set from the title. Change by hand. labels Oct 16, 2026
@maphew

maphew commented Oct 22, 2026

Copy link
Copy Markdown
Contributor Author

Both review suggestions addressed in b5a8ddf:

  • isLoopbackProvider now types options as Record<string, unknown>; the only read is narrowed with typeof raw === "string".
  • The tie-break test now compares two models with identical cost and identical release_date (newer vs sameAsNewer), so the id-descending key actually decides the result. The release-date assertion is unchanged.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:cli CLI, TUI and core engine community Opened by someone outside the Kilo team needs-triage Waiting for a maintainer to look priority:p1 High: fixes and regressions. Set from the title. Change by hand.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants