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 1bc6da5318
commit 4bb31053ad
5 changed files with 97 additions and 13 deletions

View File

@@ -23,6 +23,7 @@ _Living section — bullets land here as PRs merge into `release/v3.8.47` (paral
### 🐛 Bug Fixes
- **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(cli):** per-agent AgentBridge DNS toggle was broken for 8 of the 9 supported agents, and a failed MITM startup step could orphan the spawned proxy child — `addDNSEntry`/`removeDNSEntry` (`src/mitm/dns/dnsConfig.ts`) always resolved the legacy Antigravity default hosts regardless of which agent's toggle was flipped, so enabling DNS for Cursor/Codex/Claude Code/etc. silently added only `daily-cloudcode-pa.googleapis.com` while the DB recorded `dns_enabled=true` for the selected agent. Both functions now accept an optional `agentId` and resolve hosts via `ALL_TARGETS`; `POST /api/tools/agent-bridge/agents/[id]/dns` passes the route's `id` through and now returns 404 for an id that doesn't match a known target instead of silently falling back. Separately, `startMitmInternal()` (`src/mitm/manager.ts`) now wraps `generateCert()` (log + rethrow), the `provisionDnsEntries()` call, and the PID-file write in try/catch so a mid-startup failure can't orphan the already-spawned MITM child process. On Windows, `addDNSEntries`/`removeDNSEntries` also batch every missing/present entry into a single elevated PowerShell invocation instead of one UAC prompt per host line. Regression guard: `tests/unit/dns-config-generic.test.ts` (agent-specific resolution + batching), `tests/unit/agent-bridge-dns-route-validation.test.ts` (404 for unknown agent id). ([#6338](https://github.com/diegosouzapw/OmniRoute/pull/6338) — thanks @hamsa0x7)
- **fix(guardrails):** Vision Bridge's individual-model auto-reroute (route an image-bearing request straight to a vision-capable model instead of describe-then-forward) could bypass a policy-restricted API key's model allowlist/budget ([#6640](https://github.com/diegosouzapw/OmniRoute/pull/6640)) — `VisionBridgeGuardrail.preCall()` (`src/lib/guardrails/visionBridge.ts`) swaps `body.model` to the best available vision-capable model, but that swap happens in the guardrail pipeline AFTER `chat.ts` already called `enforceApiKeyPolicy()` against the ORIGINAL model, so a key scoped to a narrow `allowedModels` list could still execute against an unvetted (and possibly costlier) vision model the reroute picked. `chat.ts` now re-validates any guardrail-driven model change against the same per-key allowlist (`isModelAllowedForKey`) before honoring it, falling back to the original already-approved model when the reroute target is not allowed. The reroute path also now honors an explicit `settings.visionBridgeModel` operator override (previously ignored, unlike the combo/describe path a few lines below it, which already respects it via `getVisionBridgeConfig`). Regression guard: `tests/unit/guardrails/visionBridge.test.ts` (22 tests). (thanks @herjarsa)
- **fix(auth):** an API key restricted via `allowedModels`/`allowedCombos` could bypass that restriction entirely over the Codex Responses-over-WebSocket bridge ([#6564](https://github.com/diegosouzapw/OmniRoute/issues/6564)) — `prepare()` in `src/app/api/internal/codex-responses-ws/route.ts` authenticated the WS bridge's API key (`authenticate()`/`authorizeWebSocketHandshake()`) and honored `allowedConnections`, but never called `enforceApiKeyPolicy()`, the same model/combo policy gate the HTTP `/v1/responses` path enforces via `handleChat()` — so a key scoped to e.g. `combo/model-1.0` could still reach a direct Codex model like `gpt-5.5` through this transport, as long as an eligible Codex OAuth connection existed. The bridge's WS auth token arrives via query params (`api_key`/`token`/`access_token`), not a normal `Authorization` header, so a new `enforceCodexWsApiKeyPolicy()` builds an equivalent `Request` carrying an explicit `Authorization: Bearer <apiKey>` header and calls `enforceApiKeyPolicy()` against the CLIENT-requested model, before any Codex-specific model remapping or credential selection. Regression guard: `tests/unit/codex-ws-policy-enforcement-6564.test.ts` (a model-restricted key is rejected 403 before reaching credential selection; a combo-restricted key is rejected 403 requesting a disallowed combo; a key that DOES allow the requested model still proceeds past policy).

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
@@ -858,15 +859,14 @@ 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.
// the Kiro executor's transformRequest allowlist — the graded effort lever,
// gated on Kiro's adaptive-thinking allowlist (#6576), not supportsReasoning().
const requestedEffort = resolveKiroEffort(body) || (modelRequestsThinking ? "high" : "");
const kiroEffort = supportsReasoning(normalizedModel) ? requestedEffort : "";
const kiroEffort = supportsKiroAdaptiveThinking(normalizedModel) ? requestedEffort : "";
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

@@ -1121,7 +1121,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, {
@@ -1149,7 +1149,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(
@@ -1180,7 +1180,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");
@@ -1212,7 +1212,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
@@ -1224,7 +1224,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
@@ -1239,7 +1239,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",