fix(kiro): route requests through current runtime surfaces and fix 400 REQUEST_BODY_INVALID (#3776)
This commit is contained in:
@@ -259,6 +259,19 @@ export class KiroExecutor extends BaseExecutor {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CLIRO parity for the Amazon surfaces: the Kiro runtime accepts the
|
||||||
|
// SSO bearer header + agent-mode marker. Without these the deprecated
|
||||||
|
// path gateway answers REQUEST_BODY_INVALID for modern payloads.
|
||||||
|
if (credentials?.accessToken) {
|
||||||
|
headers["x-amz-sso-bearer"] = credentials.accessToken;
|
||||||
|
}
|
||||||
|
headers["x-amzn-kiro-agent-mode"] = "spec";
|
||||||
|
headers["x-amzn-codewhisperer-machine-id"] = "kiro-desktop";
|
||||||
|
const profileArn = credentials?.providerSpecificData?.profileArn;
|
||||||
|
if (profileArn) {
|
||||||
|
headers["x-amzn-codewhisperer-profile-arn"] = profileArn;
|
||||||
|
}
|
||||||
|
|
||||||
return headers;
|
return headers;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -285,9 +298,13 @@ export class KiroExecutor extends BaseExecutor {
|
|||||||
// 403 "bearer token invalid", so they must hit the CodeWhisperer
|
// 403 "bearer token invalid", so they must hit the CodeWhisperer
|
||||||
// *.amazonaws.com surface, and in the region the token was minted in
|
// *.amazonaws.com surface, and in the region the token was minted in
|
||||||
// (the baseUrls are hardcoded us-east-1).
|
// (the baseUrls are hardcoded us-east-1).
|
||||||
const isCodeWhispererSurface =
|
// Kiro deprecated the legacy path-style GenerateAssistantResponse on
|
||||||
authMethod === "api_key" || authMethod === "external_idp" || authMethod === "idc";
|
// runtime.*.kiro.dev (IDE 1.0.228+ moved to POST / + x-amz-target). The
|
||||||
if (!isCodeWhispererSurface) return baseUrls;
|
// path gateway now answers valid modern payloads with 400
|
||||||
|
// REQUEST_BODY_INVALID, and 400 is terminal in BaseExecutor, so kiro.dev
|
||||||
|
// must never be the first surface for any auth method. Amazon surfaces
|
||||||
|
// reject foreign tokens with 401/403, which DO fall through, so trying
|
||||||
|
// q/codewhisperer first is safe for every auth method (CLIRO parity).
|
||||||
|
|
||||||
const region = (credentials?.providerSpecificData?.region || "us-east-1").trim();
|
const region = (credentials?.providerSpecificData?.region || "us-east-1").trim();
|
||||||
const regionalize = (u) =>
|
const regionalize = (u) =>
|
||||||
@@ -297,20 +314,17 @@ export class KiroExecutor extends BaseExecutor {
|
|||||||
|
|
||||||
const amazon = baseUrls.filter((u) => u.includes("amazonaws.com")).map(regionalize);
|
const amazon = baseUrls.filter((u) => u.includes("amazonaws.com")).map(regionalize);
|
||||||
const others = baseUrls.filter((u) => !u.includes("amazonaws.com"));
|
const others = baseUrls.filter((u) => !u.includes("amazonaws.com"));
|
||||||
if (authMethod === "api_key") {
|
const q = amazon.filter((u) => u.includes("://q."));
|
||||||
const q = amazon.filter((u) => u.includes("://q."));
|
const remaining = amazon.filter((u) => !u.includes("://q."));
|
||||||
const remaining = amazon.filter((u) => !u.includes("://q."));
|
return q.length > 0
|
||||||
return q.length > 0
|
? [...q, ...remaining, ...others]
|
||||||
? [...q, ...remaining, ...others]
|
: [...amazon, ...others];
|
||||||
: [...amazon, ...others];
|
|
||||||
}
|
|
||||||
|
|
||||||
return amazon.length > 0 ? [...amazon, ...others] : baseUrls;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
buildUrl(model, stream, urlIndex = 0, credentials = null) {
|
buildUrl(model, stream, urlIndex = 0, credentials = null) {
|
||||||
const baseUrls = this.getOrderedBaseUrls(credentials);
|
const baseUrls = this.getOrderedBaseUrls(credentials);
|
||||||
return baseUrls[urlIndex] || baseUrls[0] || this.config.baseUrl;
|
const url = baseUrls[urlIndex] || baseUrls[0] || this.config.baseUrl;
|
||||||
|
return url;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Retry only endpoint/auth-surface failures. Payload-invalid HTTP 400 must be
|
// Retry only endpoint/auth-surface failures. Payload-invalid HTTP 400 must be
|
||||||
|
|||||||
@@ -316,14 +316,11 @@ export function claudeToKiroRequest(model, body, stream, credentials) {
|
|||||||
conversationState: {
|
conversationState: {
|
||||||
chatTriggerType: "MANUAL",
|
chatTriggerType: "MANUAL",
|
||||||
conversationId,
|
conversationId,
|
||||||
agentContinuationId: continuationId,
|
|
||||||
agentTaskType: "vibe",
|
|
||||||
currentMessage: {
|
currentMessage: {
|
||||||
userInputMessage,
|
userInputMessage,
|
||||||
},
|
},
|
||||||
history: canonical.history,
|
history: canonical.history,
|
||||||
},
|
},
|
||||||
agentMode: "vibe",
|
|
||||||
};
|
};
|
||||||
|
|
||||||
if (profileArn) payload.profileArn = profileArn;
|
if (profileArn) payload.profileArn = profileArn;
|
||||||
|
|||||||
@@ -397,8 +397,6 @@ export function openaiToKiroRequest(model, body, stream, credentials) {
|
|||||||
conversationState: {
|
conversationState: {
|
||||||
chatTriggerType: "MANUAL",
|
chatTriggerType: "MANUAL",
|
||||||
conversationId,
|
conversationId,
|
||||||
agentContinuationId: continuationId,
|
|
||||||
agentTaskType: "vibe",
|
|
||||||
currentMessage: {
|
currentMessage: {
|
||||||
userInputMessage: {
|
userInputMessage: {
|
||||||
content: replayCurrent.content || "",
|
content: replayCurrent.content || "",
|
||||||
@@ -414,7 +412,6 @@ export function openaiToKiroRequest(model, body, stream, credentials) {
|
|||||||
},
|
},
|
||||||
history: canonical.history
|
history: canonical.history
|
||||||
},
|
},
|
||||||
agentMode: "vibe",
|
|
||||||
};
|
};
|
||||||
|
|
||||||
if (profileArn) {
|
if (profileArn) {
|
||||||
|
|||||||
@@ -26,9 +26,8 @@ describe("Claude → Kiro (direct route)", () => {
|
|||||||
|
|
||||||
expect(first.conversationState.conversationId).toBe("hermes-session-123-claude-replay");
|
expect(first.conversationState.conversationId).toBe("hermes-session-123-claude-replay");
|
||||||
expect(second.conversationState.conversationId).toBe("hermes-session-123-claude-replay");
|
expect(second.conversationState.conversationId).toBe("hermes-session-123-claude-replay");
|
||||||
expect(first.conversationState.agentContinuationId).toBeTruthy();
|
expect(first.conversationState).not.toHaveProperty("agentContinuationId");
|
||||||
expect(second.conversationState.agentContinuationId).toBe(first.conversationState.agentContinuationId);
|
expect(second.conversationState).not.toHaveProperty("agentTaskType");
|
||||||
expect(first.conversationState.agentTaskType).toBe("vibe");
|
|
||||||
expect(second.conversationState.history[0].userInputMessage.content).toBe(
|
expect(second.conversationState.history[0].userInputMessage.content).toBe(
|
||||||
first.conversationState.currentMessage.userInputMessage.content
|
first.conversationState.currentMessage.userInputMessage.content
|
||||||
);
|
);
|
||||||
@@ -84,7 +83,7 @@ describe("Claude → Kiro (direct route)", () => {
|
|||||||
expect(out.systemPrompt).toContain(
|
expect(out.systemPrompt).toContain(
|
||||||
"<thinking_mode>enabled</thinking_mode>"
|
"<thinking_mode>enabled</thinking_mode>"
|
||||||
);
|
);
|
||||||
expect(out.agentMode).toBe("vibe");
|
expect(out).not.toHaveProperty("agentMode");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("does not send additionalModelRequestFields for Kiro models without effort support", () => {
|
it("does not send additionalModelRequestFields for Kiro models without effort support", () => {
|
||||||
|
|||||||
@@ -20,26 +20,26 @@ describe("Kiro auth-aware endpoint routing", () => {
|
|||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("keeps Builder ID OAuth on the Kiro runtime surface", () => {
|
it("routes Builder ID OAuth through Amazon Q first (runtime path deprecated)", () => {
|
||||||
expect(executor.getOrderedBaseUrls(credentials("builder-id"))).toEqual([
|
expect(executor.getOrderedBaseUrls(credentials("builder-id"))).toEqual([
|
||||||
RUNTIME,
|
|
||||||
CODEWHISPERER,
|
|
||||||
Q,
|
Q,
|
||||||
|
CODEWHISPERER,
|
||||||
|
RUNTIME,
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("keeps external IdP on CodeWhisperer before Amazon Q", () => {
|
it("routes external IdP through Amazon Q first", () => {
|
||||||
expect(executor.getOrderedBaseUrls(credentials("external_idp"))).toEqual([
|
expect(executor.getOrderedBaseUrls(credentials("external_idp"))).toEqual([
|
||||||
CODEWHISPERER,
|
|
||||||
Q,
|
Q,
|
||||||
|
CODEWHISPERER,
|
||||||
RUNTIME,
|
RUNTIME,
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("regionalizes AWS endpoints for IDC without changing Kiro runtime", () => {
|
it("regionalizes AWS endpoints for IDC with Q first", () => {
|
||||||
expect(executor.getOrderedBaseUrls(credentials("idc", "eu-west-1"))).toEqual([
|
expect(executor.getOrderedBaseUrls(credentials("idc", "eu-west-1"))).toEqual([
|
||||||
"https://codewhisperer.eu-west-1.amazonaws.com/generateAssistantResponse",
|
|
||||||
"https://q.eu-west-1.amazonaws.com/generateAssistantResponse",
|
"https://q.eu-west-1.amazonaws.com/generateAssistantResponse",
|
||||||
|
"https://codewhisperer.eu-west-1.amazonaws.com/generateAssistantResponse",
|
||||||
RUNTIME,
|
RUNTIME,
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|||||||
19
tests/unit/kiro-minimal-wire-payload.test.js
Normal file
19
tests/unit/kiro-minimal-wire-payload.test.js
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
import { openaiToKiroRequest } from "../../open-sse/translator/request/openai-to-kiro.js";
|
||||||
|
import { claudeToKiroRequest } from "../../open-sse/translator/request/claude-to-kiro.js";
|
||||||
|
|
||||||
|
for (const [name, translate, body] of [
|
||||||
|
["OpenAI", openaiToKiroRequest, { messages: [{ role: "user", content: "hello" }] }],
|
||||||
|
["Claude", claudeToKiroRequest, { messages: [{ role: "user", content: "hello" }] }],
|
||||||
|
]) {
|
||||||
|
describe(`${name} Kiro minimal wire payload`, () => {
|
||||||
|
it("omits unsupported agent fields", () => {
|
||||||
|
const payload = translate("kiro/claude-sonnet-4.5", body, true, {});
|
||||||
|
expect(payload).not.toHaveProperty("agentMode");
|
||||||
|
expect(payload.conversationState).not.toHaveProperty("agentContinuationId");
|
||||||
|
expect(payload.conversationState).not.toHaveProperty("agentTaskType");
|
||||||
|
expect(payload.conversationState.chatTriggerType).toBe("MANUAL");
|
||||||
|
expect(payload.conversationState.currentMessage.userInputMessage.origin).toBe("AI_EDITOR");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -606,7 +606,7 @@ describe("openaiToKiroRequest", () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
expect(second.conversationState.conversationId).toBe("hermes-session-openai-replay");
|
expect(second.conversationState.conversationId).toBe("hermes-session-openai-replay");
|
||||||
expect(second.conversationState.agentContinuationId).toBe(first.conversationState.agentContinuationId);
|
expect(second.conversationState).not.toHaveProperty("agentContinuationId");
|
||||||
expect(second.conversationState.history[0].userInputMessage.content).toBe(
|
expect(second.conversationState.history[0].userInputMessage.content).toBe(
|
||||||
first.conversationState.currentMessage.userInputMessage.content
|
first.conversationState.currentMessage.userInputMessage.content
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user