Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
37be46d
fix(cli): gate OpenAI-only reasoning effort tiers to gpt-5 family
maphew Aug 28, 2026
5bc05ff
fix(cli): extract gpt-5 tier gate to Kilo mirror, cover hyphen-prefix…
maphew Aug 28, 2026
ee7eff3
fix(cli): exempt native Azure from gpt-5 tier gate, drop stray marker
maphew Aug 29, 2026
2fd2735
test(cli): widen timeouts for flaky prompt tests on loaded CI
maphew Sep 27, 2026
143a595
test(cli): annotate test timeout changes with kilocode_change markers
maphew Sep 27, 2026
22d75a3
fix(cli): gate custom-provider fallback rollout tiers to gpt-5 family
maphew Sep 28, 2026
97db666
fix(cli): treat providerID openai as first-party, drop unrelated time…
maphew Oct 11, 2026
ff8ed58
Merge branch 'main' into fix/reasoning-effort-variants-non-openai
WebReflection Oct 16, 2026
a1e6d4a
Merge upstream/main into fix/reasoning-effort-variants-non-openai
maphew Oct 16, 2026
4ef3287
Merge remote-tracking branch 'upstream/main' into fix/reasoning-effor…
maphew Oct 17, 2026
f0b445b
fix(cli): respect effective endpoint in first-party reasoning-tier ex…
maphew Oct 17, 2026
835cb35
fix(cli): annotate variants signature for upstream check
maphew Oct 17, 2026
6ec820f
Merge upstream/main into fix/reasoning-effort-variants-non-openai
maphew Oct 18, 2026
27ee94b
Merge remote-tracking branch 'upstream/main' into fix/reasoning-effor…
maphew Oct 19, 2026
b2d7b7d
Merge upstream/main into fix/reasoning-effort-variants-non-openai
maphew Oct 22, 2026
0044cca
Merge remote-tracking branch 'upstream/main' into fix/reasoning-effor…
maphew Oct 22, 2026
7a60c95
fix(provider): gate reasoning tiers when the endpoint cannot be parsed
maphew Oct 22, 2026
5467fc4
Merge remote-tracking branch 'upstream/main' into fix/reasoning-effor…
maphew Oct 22, 2026
4a7063d
Merge remote-tracking branch 'upstream/main' into fix/reasoning-effor…
maphew Oct 22, 2026
308ff6c
Merge remote-tracking branch 'upstream/main' into fix/reasoning-effor…
maphew Oct 23, 2026
4cc27a7
Merge branch 'main' into fix/reasoning-effort-variants-non-openai
maphew Oct 23, 2026
892dd10
Merge remote-tracking branch 'upstream/main' into fix/reasoning-effor…
maphew Oct 24, 2026
fdc2e1b
Merge remote-tracking branch 'origin/fix/reasoning-effort-variants-no…
maphew Oct 24, 2026
8446c84
Merge remote-tracking branch 'upstream/main' into fix/reasoning-effor…
maphew Oct 26, 2026
d880cf7
chore(ci): retrigger checks after unrelated serve IPv6 timing failure
maphew Oct 26, 2026
d9f900c
Merge remote-tracking branch 'upstream/main' into fix/reasoning-effor…
maphew Oct 27, 2026
ce4e577
chore(ci): retrigger unit tests after runner timing failures
maphew Oct 27, 2026
7169315
Merge remote-tracking branch 'upstream/main' into fix/reasoning-effor…
maphew Oct 27, 2026
ea60807
Merge remote-tracking branch 'upstream/main' into fix/reasoning-effor…
maphew Oct 27, 2026
ed3d4e8
Merge upstream/main into fix/reasoning-effort-variants-non-openai
maphew Oct 27, 2026
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
5 changes: 5 additions & 0 deletions .changeset/grok-xhigh-effort-gate.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@kilocode/cli": patch
---

Stop offering OpenAI-only `none`/`xhigh` reasoning-effort variants for non-OpenAI models routed through the OpenAI provider npm (e.g. Grok on an OpenAI-compatible base URL), which the upstream provider rejects with a 400. Only gpt-5-family models now receive the rollout-gated tiers.
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 @@ -16,6 +16,7 @@ import { Effect, Schema } from "effect"
import type { LanguageModelV3 } from "@ai-sdk/provider"
import { mapValues, omit, pickBy } from "remeda"
import { reasoningSummary } from "./reasoning-summary"
import { gpt5FamilyReasoningTiers } from "./transform"
import type { Provider } from "@/provider/provider"
import type { Auth } from "@/auth"
import type { Config } from "@/config/config"
Expand Down Expand Up @@ -102,6 +103,7 @@ export function patchConfigModel(cfg: any, existing: any) {

const CUSTOM_PROVIDER_PACKAGES = new Set(["@ai-sdk/openai-compatible", "@ai-sdk/openai", "@ai-sdk/anthropic"])
const FALLBACK_EFFORTS = ["none", "low", "medium", "high", "xhigh", "max"]
const WIDELY_SUPPORTED_FALLBACK_EFFORTS = ["low", "medium", "high"]
type Variants = NonNullable<Provider.Model["variants"]>
type Generate = (model: Provider.Model) => Variants

Expand All @@ -113,8 +115,15 @@ export function customProviderVariants(model: Provider.Model, npm: unknown, gene
if (Object.keys(variants).length > 0) return variants
if (!model.capabilities.reasoning || !supported) return variants

// The gpt-5 rollout tiers (`none`, `xhigh`) are only accepted by gpt-5-family
// models. A custom provider id that never reaches openaiReasoningEfforts
// (e.g. qwen/deepseek/minimax/glm/kimi short-circuited to {} in variants())
// must not regain those tiers through this fallback (issue #13342).
const efforts =
npm === "@ai-sdk/openai" && !gpt5FamilyReasoningTiers(model.api.id) ? WIDELY_SUPPORTED_FALLBACK_EFFORTS : FALLBACK_EFFORTS

return Object.fromEntries(
FALLBACK_EFFORTS.map((effort) => {
efforts.map((effort) => {
if (npm === "@ai-sdk/anthropic") {
return [effort, effort === "none" ? { thinking: { type: "disabled" } } : { effort }]
}
Expand Down
60 changes: 60 additions & 0 deletions packages/opencode/src/kilocode/provider/transform.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
// Kilo-specific reasoning-effort gate for shared ProviderTransform.
//
// OpenAI rolled out the `none` and `xhigh` reasoning_effort tiers on fixed
// dates. Only gpt-5-family models accept them: non-OpenAI models routed
// through @ai-sdk/openai on a compatible base URL (e.g. Grok via api.x.ai)
// reject them, which surfaced as offering an unusable `xhigh` variant
// (issue #13342).

const GPT5_FAMILY_PREFIX_RE = /(?:^|\/|\.|-)gpt-5(?:[.-]|$)/

/**
* True when the model id is a gpt-5 family member that may receive OpenAI's
* rollout-gated reasoning tiers (`none`, `xhigh`).
*
* Matches bare ("gpt-5.5"), slash-prefixed ("openai/gpt-5.6"), dot-prefixed
* ("openai.gpt-5.5", Bedrock Mantle), and hyphen-prefixed deployment ids
* ("azure-openai--gpt-5.4", SAP AI Core) while still excluding lookalikes
* such as "gpt-50" and "gpt-5o".
*/
export function gpt5FamilyReasoningTiers(id: string) {
return GPT5_FAMILY_PREFIX_RE.test(id.toLowerCase())
}

type EndpointModel = {
providerID: string
api: { url?: string }
options?: Record<string, unknown>
}

/**
* True when providerID "openai" genuinely serves first-party OpenAI models.
* A baseURL override to a third party (e.g. xAI) under that id serves
* non-OpenAI models that reject the rollout-gated tiers (issue #13342), so the
* exemption follows the effective endpoint, not the id alone. Model options
* shadow provider options, matching resolveSDK/getModel precedence; with no
* endpoint information the first-party default is preserved for forward
* compatibility.
*/
export function isFirstPartyOpenAIEndpoint(model: EndpointModel, options: Record<string, unknown>): boolean {
if (model.providerID !== "openai") return false
const override =
model.options?.["providerEndpointOverride"] ??
model.options?.["endpoint"] ??
model.options?.["baseURL"] ??
options["providerEndpointOverride"] ??
options["endpoint"] ??
options["baseURL"]
const endpoint = typeof override === "string" && override !== "" ? override : model.api.url
if (typeof endpoint !== "string" || endpoint === "") return true
let host: string
try {
host = new URL(endpoint).hostname.toLowerCase()
} catch {
// A configured but unexpanded endpoint ({env:...}, ${...}) cannot be
// proven first-party. Gate conservatively: offering tiers the endpoint may
// reject is worse than omitting tiers a first-party endpoint would accept.
return false
}
return host === "openai.com" || host.endsWith(".openai.com")
}
27 changes: 23 additions & 4 deletions packages/opencode/src/provider/provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ import {
wrapFirstByte,
} from "@/kilocode/provider/provider"
import * as ModelsRefresh from "@/kilocode/provider/models-refresh"
import { gpt5FamilyReasoningTiers, isFirstPartyOpenAIEndpoint } from "@/kilocode/provider/transform" // kilocode_change - endpoint-aware reasoning-tier gating
import { bedrockAuth, providerKey, vertexAuth, vertexCredentials, vertexOptions } from "@/kilocode/provider/cloud-auth"
// kilocode_change end
import { ProviderError } from "./error"
Expand Down Expand Up @@ -1635,10 +1636,13 @@ const layer = Layer.effect(
...patchKiloConfigModel(model, existingModel), // kilocode_change
}
// kilocode_change start
// Effective endpoint for reasoning-tier gating: provider options shadowed by
// model options, matching resolveSDK/getModel precedence.
const endpointOptions = { ...parsed.options, ...parsedModel.options }
const baseGenerate = (m: typeof parsedModel) =>
existingModel?.api.npm === m.api.npm
? (existingModel.variants ?? ProviderTransform.variants(m))
: ProviderTransform.variants(m)
? (existingModel.variants ?? ProviderTransform.variants(m, endpointOptions))
: ProviderTransform.variants(m, endpointOptions)
const generated = customProviderVariants(
parsedModel,
model.provider?.npm ?? provider.npm,
Expand Down Expand Up @@ -1801,9 +1805,24 @@ const layer = Layer.effect(
)
delete provider.models[modelID]

if (model.variants === undefined) {
model.variants = mapValues(ProviderTransform.variants(model), (v) => v)
// kilocode_change start - effective endpoint for reasoning-tier gating
// (provider options shadowed by model options, matching resolveSDK/getModel
// precedence). Variants baked at catalog time predate endpoint overrides, so a
// rerouted provider keeps a stale first-party exemption: none/xhigh on a
// non-gpt-5 id could only come from that exemption, which the override
// invalidates (issue #13342). Regenerate under the effective endpoint;
// explicit config variants re-apply below.
const endpointOptions = { ...provider.options, ...model.options }
if (
model.variants === undefined ||
(model.api.npm === "@ai-sdk/openai" &&
(model.variants.none !== undefined || model.variants.xhigh !== undefined) &&
!gpt5FamilyReasoningTiers(model.api.id) &&
!isFirstPartyOpenAIEndpoint(model, endpointOptions))
) {
model.variants = mapValues(ProviderTransform.variants(model, endpointOptions), (v) => v)
}
// kilocode_change end

const configVariants = configProvider?.models?.[modelID]?.variants
if (configVariants && model.variants) {
Expand Down
26 changes: 22 additions & 4 deletions packages/opencode/src/provider/transform.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { iife } from "@/util/iife"
import { kiloProviderOptions } from "@/kilocode/provider-options"
import { isLing } from "@/kilocode/model-match" // kilocode_change
import { reasoningSummary } from "@/kilocode/provider/reasoning-summary" // kilocode_change
import { gpt5FamilyReasoningTiers, isFirstPartyOpenAIEndpoint } from "@/kilocode/provider/transform" // kilocode_change

type Modality = NonNullable<ModelsDev.Model["modalities"]>["input"][number]

Expand Down Expand Up @@ -715,7 +716,10 @@ function gpt5ChatReasoningEfforts(apiId: string) {
// Computes the reasoning_effort tiers an OpenAI (or OpenAI-compatible upstream
// routed through it, e.g. cf-ai-gateway) model exposes. Effort order: weakest
// to strongest.
function openaiReasoningEfforts(apiId: string, releaseDate: string) {
// kilocode_change start - isFirstPartyOpenAI marks transports that only serve first-party OpenAI
// models (native Azure), where the deployment/model key may be opaque and the family gate must not apply
function openaiReasoningEfforts(apiId: string, releaseDate: string, isFirstPartyOpenAI = false) {
// kilocode_change end
const id = apiId.toLowerCase()
if (id.includes("deep-research")) return ["medium"]
const chatEfforts = gpt5ChatReasoningEfforts(id)
Expand All @@ -727,6 +731,11 @@ function openaiReasoningEfforts(apiId: string, releaseDate: string) {
// GPT-5.1 replaced GPT-5's `minimal` effort with `none`; GPT-5.2+
// additionally accepts `xhigh`. Model pages list the supported subset.
if (versionedEfforts) return versionedEfforts
// kilocode_change start - `none`/`xhigh` are OpenAI rollout tiers only gpt-5-family
// models accept; non-OpenAI models on an OpenAI-compatible base URL (e.g. Grok
// via api.x.ai) reject them (issue #13342). See gpt5FamilyReasoningTiers.
if (!isFirstPartyOpenAI && !gpt5FamilyReasoningTiers(id)) return WIDELY_SUPPORTED_EFFORTS

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 gate is bypassed by the custom-provider fallback for non-gpt-5 ids

customProviderVariants() (packages/opencode/src/kilocode/provider/provider.ts) calls ProviderTransform.variants() and, when the heuristics return {}, falls back to FALLBACK_EFFORTS = ["none", "low", "medium", "high", "xhigh", "max"] for @ai-sdk/openai. Several ids are short-circuited by variants() before reaching this gate — qwen*, deepseek-*, minimax*, glm*, kimi*, big-pickle (the id.includes("qwen") block in variants()) — so a custom OpenAI-compatible provider using one of those ids still advertises none/xhigh, the same unsupported-tier shape as #13342. The existing packages/opencode/test/kilocode/provider-reasoning-options.test.ts cases assert exactly this fallback (["none","low","medium","high","xhigh","max"] for qwen-custom on @ai-sdk/openai). The new regression tests call ProviderTransform.variants() directly, so they do not cover this path. Consider gating the fallback for @ai-sdk/openai to the widely supported set unless the id is gpt-5 family, or documenting the exception with a regression test.


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

// kilocode_change end
const efforts = [...WIDELY_SUPPORTED_EFFORTS]
if (GPT5_FAMILY_RE.test(id)) efforts.unshift("minimal")
if (releaseDate >= OPENAI_NONE_EFFORT_RELEASE_DATE) efforts.unshift("none")
Expand Down Expand Up @@ -849,7 +858,12 @@ function googleThinkingVariants(model: Provider.Model): Record<string, Record<st
)
}

export function variants(model: Provider.Model): Record<string, Record<string, any>> {
// kilocode_change start - options carries the effective endpoint for reasoning-tier gating
export function variants(
model: Provider.Model,
options: Record<string, unknown> = {},
): Record<string, Record<string, any>> {
// kilocode_change end
// kilocode_change start
if (
["@kilocode/kilo-gateway", "@ai-sdk/openai-compatible"].includes(model.api.npm) &&
Expand Down Expand Up @@ -1077,7 +1091,7 @@ export function variants(model: Provider.Model): Record<string, Record<string, a
// https://v5.ai-sdk.dev/providers/ai-sdk-providers/azure
if (id === "o1-mini") return {}
return Object.fromEntries(
openaiReasoningEfforts(id, model.release_date).map((effort) => [
openaiReasoningEfforts(id, model.release_date, true).map((effort) => [ // kilocode_change - native Azure serves first-party OpenAI only
effort,
{
reasoningEffort: effort,
Expand All @@ -1101,7 +1115,11 @@ export function variants(model: Provider.Model): Record<string, Record<string, a
)
}
// https://v5.ai-sdk.dev/providers/ai-sdk-providers/openai
const efforts = openaiReasoningEfforts(model.api.id, model.release_date)
// kilocode_change start - providerID "openai" serves first-party OpenAI models, so a post-cutoff
// id like gpt-6 keeps rollout tiers via release-date logic instead of collapsing to low/medium/high.
// The exemption follows the effective endpoint: a baseURL override to a third party (xAI) serves
// non-OpenAI models that reject those tiers (issue #13342).
const efforts = openaiReasoningEfforts(model.api.id, model.release_date, isFirstPartyOpenAIEndpoint(model, options))
return Object.fromEntries(
efforts.map((effort) => [
effort,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -142,8 +142,9 @@ describe("ProviderTransform.reasoningVariants - models.dev reasoning_options", (

describe("custom provider fallback reasoning efforts", () => {
const efforts = ["none", "low", "medium", "high", "xhigh", "max"]
const gated = ["low", "medium", "high"]

for (const npm of ["@ai-sdk/openai-compatible", "@ai-sdk/openai", "@ai-sdk/anthropic"]) {
for (const npm of ["@ai-sdk/openai-compatible", "@ai-sdk/anthropic"]) {
test(`${npm} exposes broad efforts after heuristics fail`, () => {
const model = mockModel({ id: "qwen-custom", api: { id: "qwen-custom", url: "https://api.test.com", npm } })
const generated = ProviderTransform.variants({ ...model, variants: {} })
Expand All @@ -162,6 +163,26 @@ describe("custom provider fallback reasoning efforts", () => {
})
}

test("@ai-sdk/openai gates rollout tiers for non-gpt-5 ids (issue #13342)", () => {
const npm = "@ai-sdk/openai"
const model = mockModel({ id: "qwen-custom", api: { id: "qwen-custom", url: "https://api.test.com", npm } })
expect(ProviderTransform.variants({ ...model, variants: {} })).toEqual({})

const result = customProviderVariants(model, npm, ProviderTransform.variants)
expect(Object.keys(result)).toEqual(gated)
expect(result.low?.reasoningEffort).toBe("low")
expect(result.high?.reasoningEffort).toBe("high")
})

test("@ai-sdk/openai keeps rollout tiers for gpt-5 ids", () => {
const npm = "@ai-sdk/openai"
const model = mockModel({ id: "gpt-5.5", api: { id: "gpt-5.5", url: "https://api.test.com", npm } })
const result = customProviderVariants(model, npm, () => ({}))
expect(Object.keys(result)).toEqual(efforts)
expect(result.none?.reasoningEffort).toBe("none")
expect(result.xhigh?.reasoningEffort).toBe("xhigh")
})

for (const id of ["glm-custom", "kimi-custom", "minimax-custom"]) {
test(`openai-compatible custom provider uses broad efforts for ${id}`, () => {
const npm = "@ai-sdk/openai-compatible"
Expand Down
Loading
Loading