fix(providers): Kiro adaptive-thinking allowlist excludes sonnet-4.5/haiku-4.5 (#6576)

This commit is contained in:
Diego Rodrigues de Sa e Souza 2026-07-09 05:05:26 -03:00
parent f4fb0d310b
commit 1e3ee5a0c4
5 changed files with 97 additions and 13 deletions

View file

@ -28,6 +28,7 @@ _Living section — bullets land here as PRs merge into `release/v3.8.47` (paral
- **fix(providers):** `fusion` combo strategy silently returned a panel member's raw answer instead of the configured `config.judgeModel` synthesis ([#6455](https://github.com/diegosouzapw/OmniRoute/issues/6455)) — `handleFusionChat()`'s single-survivor "degrade gracefully" path (added for #6454) returned the lone panel answer directly whenever only one panelist succeeded, regardless of whether an explicit `judgeModel` was configured; with the default `minPanel: 2` and a 2-model panel, any single flaky/rate-limited panelist forced this path on every request, so the configured judge (e.g. `auto/claude-opus`) was never invoked and the client-visible `.model` reflected whichever panelist happened to survive. The judge is now still invoked to synthesize a lone surviving answer whenever `judgeModel` is explicitly configured; the cheap direct-answer shortcut is kept only for the implicit case (no `judgeModel` set, where the "judge" is just `panel[0]`). Regression guard: `tests/unit/fusion-judge-model-6455.test.ts` + updated `tests/unit/combo-fusion-strategy.test.ts`. (thanks @chirag127)
- **feat(combo):** sanitized diagnostic trace on an auto-combo terminal failure — instead of an opaque 503, a terminal combo failure now returns a whitelist-projected trace (candidate pool size, attempted count, excluded provider/reason codes, attempt order, and a terminal-reason code) via the new `errorResponseWithComboDiagnostics()`/`sanitizeComboDiagnostics()` in `open-sse/utils/error.ts` — provider/model ids and enumerated reason codes only, never keys/tokens/bodies, length- and count-capped. A reasoning-budget-exhausted panel now returns an actionable "increase max_tokens" message rather than a blind retry-limit 503. Regression guard: `tests/unit/combo-diagnostics-trace.test.ts`. ([#6545](https://github.com/diegosouzapw/OmniRoute/pull/6545)) — see PR. (thanks @developerjillur)
- **fix(providers):** image/diffusion models discovered from an upstream catalog (e.g. HuggingFace's live `/v1/models`) are no longer advertised as chat models ([#6457](https://github.com/diegosouzapw/OmniRoute/issues/6457)) — the chat catalog builder defaulted synced models with no modality info to `endpoints: ["chat"]`, so `huggingface/stabilityai/stable-diffusion-xl-base-1.0` showed up in the chat `/v1/models` listing and returned `400 "not a chat model"` when called. `catalog.ts` now skips any synced model already registered as an image model for that provider (via the new `isRegisteredImageModel()`), leaving `getAllImageModels()` to list it with the correct `type: "image"`. Regression guard: `tests/unit/image-model-not-in-chat-catalog-6457.test.ts`.
- **fix(providers):** Kiro sent the adaptive-thinking `additionalModelRequestFields` envelope for `claude-sonnet-4.5`/`claude-haiku-4.5`, which Kiro/CodeWhisperer rejects upstream with a raw `[400]: additionalModelRequestFields is not supported for this model` ([#6576](https://github.com/diegosouzapw/OmniRoute/issues/6576)) — `buildKiroPayload()` (`open-sse/translator/request/openai-to-kiro.ts`) gated the field on the generic Anthropic-API `supportsReasoning()` capability flag, which is `true` for both models on Anthropic's direct API but does not reflect what Kiro's CodeWhisperer backend actually accepts; only `claude-sonnet-5` is confirmed adaptive-thinking-capable there. A new Kiro-specific allowlist (`supportsKiroAdaptiveThinking()` in `open-sse/translator/request/openai-to-kiro/adaptiveThinking.ts`) now gates the envelope instead. Regression guard: `tests/unit/repro-6576-kiro-thinking-unsupported-model.test.ts`.
- **fix(test):** replace the bare `expect(true).toBe(true)` tautology in `playground-api-tab.test.tsx`'s SSE test and close the `check:test-masking` gap that let it slip through for a full cycle ([#6404](https://github.com/diegosouzapw/OmniRoute/issues/6404)) — a prior pass (#6548) had already swapped the literal to `expect(sendBtn).toBeDefined()`, but that stayed just as vacuous: the test's fetch mock returned an empty `/v1/models` list, so `ApiTab`'s Send button is always `disabled` (`!selectedModel`) and the SSE branch never runs — the "SSE infra is verified" comment was never true. The test now mocks a real model, drives the model `<select>` to enable Send, asserts `sendBtn.disabled === false` before clicking, and asserts the streamed SSE delta (`"Hello!"`) actually reached the response editor. Root cause on the detector side: `check-test-masking.mjs`'s tautology subcheck only compares base-vs-HEAD counts within a PR's own diff (`headExtTaut > baseExtTaut`) and no-ops locally when `GITHUB_BASE_SHA`/`GITHUB_BASE_REF` are unset ("sem base ref — pulando") — so a tautology merged once, or checked with a bare local run, was invisible forever after. Added a new always-on, PR-independent absolute-floor scan (`scanBareTautologies` + `countBareTautologies`) over every git-tracked test file for the bare `expect(true).toBe(true)` / `assert.equal(1,1)` / `assert.strictEqual(1,1)` patterns specifically (deliberately excluding `assert.ok(true)`, which has ~15 pre-existing verified-legitimate try/catch-fallback uses repo-wide and stays governed by the lenient diff-only subcheck) — verified zero pre-existing hits repo-wide once this file was fixed, so the new floor is safe to enforce unconditionally. Regression guard: `tests/unit/check-test-masking.test.ts` (new `scanBareTautologies`/`countBareTautologies` cases) + `tests/unit/ui/playground-api-tab.test.tsx`. (thanks @chirag127)
- **fix(oauth):** Codex/ChatGPT (and every other OAuth provider) connection stays stuck showing "Auth Failed" even after a genuinely successful token refresh ([#6352](https://github.com/diegosouzapw/OmniRoute/issues/6352)) — `updateProviderCredentials()` (the shared `onPersist` callback for the manual "Refresh token" route, the reactive per-request refresh in `chat.ts`, and the Codex/Claude auth-file importers) correctly reused the stored `refresh_token`, persisted the new `access_token`, and replaced a rotated `refresh_token`, but never cleared the stale `testStatus`/`lastError*`/`errorCode` fields left over from a prior expired/invalid refresh or upstream 401/403 — only the separate background health-check sweep did that clearing. A successful refresh now resets `testStatus` to `"active"` and clears `lastError`, `lastErrorAt`, `lastErrorType`, `lastErrorSource`, and `errorCode` (an explicit `testStatus` from the caller still wins). Regression guard: `tests/unit/codex-oauth-refresh-persist-6352.test.ts`.
- **perf(health):** short-TTL (1s) cache for the frequently-polled `GET /api/monitoring/health` payload — rebuilding it every request (DB reads + status aggregation across 8 subsystems) was wasteful under rapid polling; the cache stays near-real-time and is invalidated immediately on `DELETE` (circuit-breaker reset) so a manual reset is reflected at once. Regression guard: `tests/integration/monitoring-health-cache.test.ts`. ([#6553](https://github.com/diegosouzapw/OmniRoute/pull/6553)) — see PR. (thanks @developerjillur)

View file

@ -5,12 +5,13 @@
import { register } from "../registry.ts";
import { FORMATS } from "../formats.ts";
import { v4 as uuidv4, v5 as uuidv5 } from "uuid";
import { capMaxOutputTokens, capThinkingBudget, supportsReasoning } from "@/lib/modelCapabilities";
import { capMaxOutputTokens, capThinkingBudget } from "@/lib/modelCapabilities";
import {
parseToolInput,
normalizeKiroToolSchema,
serializeToolResultContent,
} from "./openai-to-kiro/messageHelpers.ts";
import { supportsKiroAdaptiveThinking } from "./openai-to-kiro/adaptiveThinking.ts";
/**
* Anthropic's direct-provider `[1m]` context-1m beta suffix. Kiro is AWS
@ -837,14 +838,13 @@ export function buildKiroPayload(model, body, stream, credentials) {
// Thinking mode for Claude models on Kiro (ported from javargasm/pi-kiro).
// Two coordinated signals steer reasoning on the CodeWhisperer surface:
// 1. a `<thinking_mode>enabled</thinking_mode><max_thinking_length>N</...>`
// directive prepended to the current user message — makes Claude emit its
// reasoning INLINE as `<thinking>…</thinking>`, which the Kiro executor
// splits back into the OpenAI `reasoning_content` channel (kiroThinking.ts);
// directive prepended to the user message — makes Claude emit reasoning
// INLINE, split back into `reasoning_content` by the executor (kiroThinking.ts);
// 2. top-level `additionalModelRequestFields` (output_config.effort +
// thinking:{type:"adaptive"} + a clamped max_tokens), forwarded to AWS by
// the Kiro executor's transformRequest allowlist — this is the graded
// effort lever. Gated on models that advertise thinking support.
const kiroEffort = supportsReasoning(normalizedModel) ? resolveKiroEffort(body) : "";
// the Kiro executor's transformRequest allowlist — the graded effort lever,
// gated on Kiro's adaptive-thinking allowlist (#6576), not supportsReasoning().
const kiroEffort = supportsKiroAdaptiveThinking(normalizedModel) ? resolveKiroEffort(body) : "";
if (kiroEffort) {
// `<thinking_mode>` / `<max_thinking_length>` are Kiro/CodeWhisperer prompt
// conventions (NOT Anthropic API params); the length is a soft hint (the hard

View file

@ -0,0 +1,19 @@
/**
* Kiro/AWS CodeWhisperer only accepts the adaptive-thinking
* `additionalModelRequestFields` envelope for a narrow allowlist of models
* NOT the same set the generic Anthropic-API capability table
* (`supportsReasoning()` in `@/lib/modelCapabilities`) marks as
* thinking-capable. That table is correct for Anthropic's own API, but Kiro
* rejects the field for `claude-sonnet-4.5` and `claude-haiku-4.5` with a raw
* upstream 400 (`additionalModelRequestFields is not supported for this
* model`, issue #6576) even though both ARE thinking-capable on Anthropic's
* direct API. Only `claude-sonnet-5` is confirmed to accept the adaptive
* envelope on Kiro today keep this allowlist in sync with
* `open-sse/config/providers/registry/kiro/index.ts` if Kiro's catalog or
* upstream behavior changes.
*/
const KIRO_ADAPTIVE_THINKING_MODELS = new Set(["claude-sonnet-5"]);
export function supportsKiroAdaptiveThinking(normalizedModel: string): boolean {
return KIRO_ADAPTIVE_THINKING_MODELS.has(normalizedModel);
}

View file

@ -0,0 +1,64 @@
// Repro probe for GitHub issue #6576.
//
// Kiro/CodeWhisperer rejects `additionalModelRequestFields` for
// claude-sonnet-4.5 / claude-haiku-4.5 with a raw upstream 400:
// "[400]: additionalModelRequestFields is not supported for this model"
//
// buildKiroPayload() gates thinking injection on supportsReasoning(model),
// which resolves from the GENERIC Anthropic-API capability data
// (MODEL_SPECS["claude-sonnet-4-5-..."].supportsThinking === true,
// MODEL_SPECS["claude-haiku-4-5-20251001"].supportsThinking === true).
// That flag says nothing about whether the *Kiro/AWS CodeWhisperer*
// backend accepts the adaptive-thinking additionalModelRequestFields
// envelope for these specific models — only the newer adaptive-only
// models (Opus 4.7/4.8, Sonnet 5, Fable 5) are proven to accept it there
// (see the existing "drops temperature when thinking is enabled" tests).
//
// This test asserts the payload for claude-sonnet-4.5 (the reporter's own
// model) must NOT carry additionalModelRequestFields when reasoning is
// requested, matching what Kiro's upstream actually accepts. It currently
// FAILS because buildKiroPayload has no Kiro-specific allowlist/exclusion
// and blindly forwards the field whenever the generic capability flag says
// supportsThinking:true.
import test from "node:test";
import assert from "node:assert/strict";
const { buildKiroPayload } = await import(
"../../open-sse/translator/request/openai-to-kiro.ts"
);
test("[repro #6576] buildKiroPayload must not attach additionalModelRequestFields for claude-sonnet-4.5 (Kiro rejects it)", () => {
const body = {
messages: [{ role: "user", content: "Calculate 51818+62218, and reply with result only." }],
reasoning_effort: "medium",
max_tokens: 2048,
stream: false,
};
const result = buildKiroPayload("claude-sonnet-4.5", body, false, null);
assert.equal(
result.additionalModelRequestFields,
undefined,
"additionalModelRequestFields must not be sent for claude-sonnet-4.5 — " +
"Kiro/CodeWhisperer rejects it upstream with " +
"'[400]: additionalModelRequestFields is not supported for this model' (issue #6576)"
);
});
test("[repro #6576] buildKiroPayload must not attach additionalModelRequestFields for claude-haiku-4.5 (Kiro rejects it)", () => {
const body = {
messages: [{ role: "user", content: "hi" }],
thinking: { type: "adaptive" },
};
const result = buildKiroPayload("claude-haiku-4.5", body, false, null);
assert.equal(
result.additionalModelRequestFields,
undefined,
"additionalModelRequestFields must not be sent for claude-haiku-4.5 — " +
"Kiro/CodeWhisperer rejects it upstream (issue #6576 comment by fenix007: " +
"9/9 production requests with reasoning params 400'd for this exact model)"
);
});

View file

@ -1098,7 +1098,7 @@ test("buildKiroPayload enables thinking mode for Claude models via reasoning_eff
max_tokens: 64000,
};
const result = buildKiroPayload("claude-opus-4.8", body, false, null);
const result = buildKiroPayload("claude-sonnet-5", body, false, null); // only Kiro model accepting adaptive thinking (#6576)
assert.ok(result.additionalModelRequestFields, "additionalModelRequestFields must be set");
assert.deepEqual(result.additionalModelRequestFields.thinking, {
@ -1126,7 +1126,7 @@ test("buildKiroPayload drops temperature when thinking is enabled", () => {
temperature: 0.5,
};
const result = buildKiroPayload("claude-opus-4.8", body, false, null);
const result = buildKiroPayload("claude-sonnet-5", body, false, null);
assert.ok(result.additionalModelRequestFields, "thinking must be enabled");
assert.equal(
@ -1157,7 +1157,7 @@ test("buildKiroPayload maps body.thinking budget_tokens to effort level", () =>
thinking: { type: "enabled", budget_tokens: 50000 },
};
const result = buildKiroPayload("claude-opus-4.7", body, false, null);
const result = buildKiroPayload("claude-sonnet-5", body, false, null);
assert.ok(result.additionalModelRequestFields, "thinking must be enabled from budget_tokens");
assert.equal(result.additionalModelRequestFields.output_config.effort, "high");
@ -1189,7 +1189,7 @@ test("buildKiroPayload maps reasoning_effort to the same Kiro effort level (no +
test("buildKiroPayload reads effort from Anthropic output_config.effort", () => {
const result = buildKiroPayload(
"claude-opus-4.8",
"claude-sonnet-5",
{ messages: [{ role: "user", content: "hard" }], output_config: { effort: "xhigh" } },
false,
null
@ -1201,7 +1201,7 @@ test("buildKiroPayload reads effort from Anthropic output_config.effort", () =>
test("buildKiroPayload defaults adaptive thinking (no effort) to high", () => {
const result = buildKiroPayload(
"claude-opus-4.8",
"claude-sonnet-5",
{ messages: [{ role: "user", content: "hard" }], thinking: { type: "adaptive" } },
false,
null
@ -1216,7 +1216,7 @@ test("buildKiroPayload defaults adaptive thinking (no effort) to high", () => {
test("buildKiroPayload drops both temperature and top_p when thinking is enabled", () => {
const result = buildKiroPayload(
"claude-opus-4.8",
"claude-sonnet-5",
{
messages: [{ role: "user", content: "hard" }],
reasoning_effort: "high",