mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-02 01:10:23 +08:00
fix(kiro): 修复 Kiro WebSearch MCP 调用 (#344)
* fix(kiro): 接入 Kiro MCP web_search 工具调用 * fix(kiro): 对齐 Kiro IDE MCP 鉴权与 profileArn 头部 * fix(responses): 保留 Responses 嵌套与自定义工具参数 * test(ci): 通过 Rust Action 三项检查
This commit is contained in:
@@ -2807,7 +2807,29 @@ pub(crate) fn openai_responses_tools_to_canonical(
|
||||
.unwrap_or("function")
|
||||
.trim()
|
||||
.to_ascii_lowercase();
|
||||
if tool_type == "function" && tool_object.get("function").is_none() {
|
||||
if tool_type == "function" {
|
||||
if let Some(function) = tool_object.get("function").and_then(Value::as_object) {
|
||||
let name = function
|
||||
.get("name")
|
||||
.and_then(Value::as_str)
|
||||
.map(str::trim)
|
||||
.filter(|value| !value.is_empty())?;
|
||||
let mut extensions =
|
||||
openai_responses_extensions(tool_object, &["type", "function"]);
|
||||
let function_extensions =
|
||||
openai_responses_extensions(function, &["name", "description", "parameters"]);
|
||||
merge_tool_extensions(&mut extensions, function_extensions);
|
||||
canonical.push(CanonicalToolDefinition {
|
||||
name: name.to_string(),
|
||||
description: function
|
||||
.get("description")
|
||||
.and_then(Value::as_str)
|
||||
.map(ToOwned::to_owned),
|
||||
parameters: function.get("parameters").cloned(),
|
||||
extensions,
|
||||
});
|
||||
continue;
|
||||
}
|
||||
let name = tool_object
|
||||
.get("name")
|
||||
.and_then(Value::as_str)
|
||||
@@ -2825,6 +2847,41 @@ pub(crate) fn openai_responses_tools_to_canonical(
|
||||
&["type", "name", "description", "parameters"],
|
||||
),
|
||||
});
|
||||
} else if tool_type == "custom" {
|
||||
let custom = tool_object.get("custom").and_then(Value::as_object);
|
||||
let name = tool_object
|
||||
.get("name")
|
||||
.and_then(Value::as_str)
|
||||
.or_else(|| {
|
||||
custom
|
||||
.and_then(|value| value.get("name"))
|
||||
.and_then(Value::as_str)
|
||||
})
|
||||
.map(str::trim)
|
||||
.filter(|value| !value.is_empty())?;
|
||||
let description = tool_object
|
||||
.get("description")
|
||||
.and_then(Value::as_str)
|
||||
.or_else(|| {
|
||||
custom
|
||||
.and_then(|value| value.get("description"))
|
||||
.and_then(Value::as_str)
|
||||
})
|
||||
.map(ToOwned::to_owned);
|
||||
let parameters = tool_object
|
||||
.get("parameters")
|
||||
.or_else(|| custom.and_then(|value| value.get("parameters")))
|
||||
.filter(|value| value.is_object())
|
||||
.cloned();
|
||||
canonical.push(CanonicalToolDefinition {
|
||||
name: name.to_string(),
|
||||
description,
|
||||
parameters,
|
||||
extensions: BTreeMap::from([(
|
||||
OPENAI_RESPONSES_EXTENSION_NAMESPACE.to_string(),
|
||||
tool.clone(),
|
||||
)]),
|
||||
});
|
||||
} else if tool_type.starts_with("web_search") {
|
||||
canonical.push(CanonicalToolDefinition {
|
||||
name: tool_type,
|
||||
@@ -2840,6 +2897,19 @@ pub(crate) fn openai_responses_tools_to_canonical(
|
||||
Some(canonical)
|
||||
}
|
||||
|
||||
fn merge_tool_extensions(target: &mut BTreeMap<String, Value>, source: BTreeMap<String, Value>) {
|
||||
for (namespace, value) in source {
|
||||
match (target.get_mut(&namespace), value) {
|
||||
(Some(Value::Object(target)), Value::Object(source)) => {
|
||||
target.extend(source);
|
||||
}
|
||||
(_, value) => {
|
||||
target.insert(namespace, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn canonical_tool_to_openai(tool: &CanonicalToolDefinition) -> Value {
|
||||
let mut function = Map::new();
|
||||
function.insert("name".to_string(), Value::String(tool.name.clone()));
|
||||
@@ -2901,6 +2971,31 @@ pub(crate) fn openai_responses_tool_choice_to_canonical(
|
||||
object
|
||||
.get("name")
|
||||
.and_then(Value::as_str)
|
||||
.or_else(|| {
|
||||
object
|
||||
.get("function")
|
||||
.and_then(Value::as_object)
|
||||
.and_then(|function| function.get("name"))
|
||||
.and_then(Value::as_str)
|
||||
})
|
||||
.map(str::trim)
|
||||
.filter(|value| !value.is_empty())
|
||||
.map(|name| CanonicalToolChoice::Tool {
|
||||
name: name.to_string(),
|
||||
})
|
||||
} else if choice_type == "custom" {
|
||||
object
|
||||
.get("name")
|
||||
.and_then(Value::as_str)
|
||||
.or_else(|| {
|
||||
object
|
||||
.get("custom")
|
||||
.and_then(Value::as_object)
|
||||
.and_then(|custom| custom.get("name"))
|
||||
.and_then(Value::as_str)
|
||||
})
|
||||
.map(str::trim)
|
||||
.filter(|value| !value.is_empty())
|
||||
.map(|name| CanonicalToolChoice::Tool {
|
||||
name: name.to_string(),
|
||||
})
|
||||
@@ -4271,6 +4366,72 @@ mod tests {
|
||||
assert_eq!(rebuilt["tool_choice"]["name"], "lookup");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn openai_responses_request_adapter_accepts_nested_function_and_custom_tools() {
|
||||
let request = json!({
|
||||
"model": "gpt-5",
|
||||
"input": "Use a tool",
|
||||
"tools": [
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "lookup_weather",
|
||||
"description": "Lookup weather",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"city": {"type": "string"}
|
||||
},
|
||||
"required": ["city"]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "custom",
|
||||
"custom": {
|
||||
"name": "shell_command",
|
||||
"description": "Run a shell command"
|
||||
}
|
||||
}
|
||||
],
|
||||
"tool_choice": {
|
||||
"type": "function",
|
||||
"function": {"name": "lookup_weather"}
|
||||
}
|
||||
});
|
||||
|
||||
let canonical =
|
||||
from_openai_responses_to_canonical_request(&request).expect("canonical request");
|
||||
assert_eq!(canonical.tools.len(), 2);
|
||||
assert_eq!(canonical.tools[0].name, "lookup_weather");
|
||||
assert_eq!(
|
||||
canonical.tools[0]
|
||||
.parameters
|
||||
.as_ref()
|
||||
.and_then(|value| value.get("required"))
|
||||
.and_then(Value::as_array)
|
||||
.map(Vec::len),
|
||||
Some(1)
|
||||
);
|
||||
assert_eq!(canonical.tools[1].name, "shell_command");
|
||||
assert!(matches!(
|
||||
canonical.tool_choice,
|
||||
Some(super::CanonicalToolChoice::Tool { ref name }) if name == "lookup_weather"
|
||||
));
|
||||
|
||||
let claude = canonical_to_claude_request(&canonical, "claude-sonnet-4-upstream", false)
|
||||
.expect("claude request");
|
||||
assert_eq!(claude["tools"][0]["name"], "lookup_weather");
|
||||
assert_eq!(claude["tools"][1]["name"], "shell_command");
|
||||
assert_eq!(claude["tool_choice"]["name"], "lookup_weather");
|
||||
|
||||
let rebuilt = canonical_to_openai_responses_request(&canonical, "gpt-5-upstream", false)
|
||||
.expect("openai responses request");
|
||||
assert_eq!(rebuilt["tools"][0]["name"], "lookup_weather");
|
||||
assert_eq!(rebuilt["tools"][1]["type"], "custom");
|
||||
assert_eq!(rebuilt["tools"][1]["custom"]["name"], "shell_command");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn openai_responses_response_adapter_preserves_output_items_reasoning_tools_and_usage() {
|
||||
let response = json!({
|
||||
|
||||
@@ -447,7 +447,9 @@ fn canonical_tool_to_responses(tool: &CanonicalToolDefinition) -> Value {
|
||||
value
|
||||
.get("type")
|
||||
.and_then(Value::as_str)
|
||||
.is_some_and(|tool_type| tool_type.starts_with("web_search"))
|
||||
.is_some_and(|tool_type| {
|
||||
tool_type == "custom" || tool_type.starts_with("web_search")
|
||||
})
|
||||
})
|
||||
{
|
||||
return raw.clone();
|
||||
|
||||
@@ -1071,4 +1071,76 @@ mod tests {
|
||||
"image/jpeg"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn openai_responses_nested_tools_survive_claude_cli_and_kiro_envelope_conversion() {
|
||||
let request = json!({
|
||||
"model": "gpt-5",
|
||||
"input": "Use the weather tool for Shanghai.",
|
||||
"tools": [{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "get_weather",
|
||||
"description": "Get weather",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"city": {"type": "string"}
|
||||
},
|
||||
"required": []
|
||||
}
|
||||
}
|
||||
}],
|
||||
"tool_choice": {
|
||||
"type": "function",
|
||||
"function": {"name": "get_weather"}
|
||||
}
|
||||
});
|
||||
|
||||
let claude = build_standard_request_body(
|
||||
&request,
|
||||
"openai:responses",
|
||||
"claude-sonnet-4.6",
|
||||
"kiro",
|
||||
"claude:cli",
|
||||
"/v1/responses",
|
||||
true,
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.expect("openai responses should convert to claude cli");
|
||||
assert_eq!(claude["tools"][0]["name"], "get_weather");
|
||||
assert_eq!(claude["tool_choice"]["name"], "get_weather");
|
||||
|
||||
let auth_config = aether_provider_transport::kiro::KiroAuthConfig {
|
||||
auth_method: None,
|
||||
refresh_token: None,
|
||||
expires_at: None,
|
||||
profile_arn: Some("arn:aws:bedrock:demo".to_string()),
|
||||
region: None,
|
||||
auth_region: None,
|
||||
api_region: Some("us-east-1".to_string()),
|
||||
client_id: None,
|
||||
client_secret: None,
|
||||
machine_id: None,
|
||||
kiro_version: None,
|
||||
system_version: None,
|
||||
node_version: None,
|
||||
access_token: Some("token".to_string()),
|
||||
};
|
||||
let kiro = aether_provider_transport::kiro::build_kiro_provider_request_body(
|
||||
&claude,
|
||||
"claude-sonnet-4.6",
|
||||
&auth_config,
|
||||
None,
|
||||
)
|
||||
.expect("kiro envelope should build");
|
||||
let tool_spec = &kiro["conversationState"]["currentMessage"]["userInputMessage"]
|
||||
["userInputMessageContext"]["tools"][0]["toolSpecification"];
|
||||
assert_eq!(tool_spec["name"], "get_weather");
|
||||
assert!(
|
||||
tool_spec["inputSchema"]["json"].get("required").is_none(),
|
||||
"Kiro envelope should strip empty required arrays from tool schema"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,7 +15,10 @@ pub use auth::{
|
||||
};
|
||||
pub use converter::convert_claude_messages_to_conversation_state;
|
||||
pub use credentials::{generate_machine_id, normalize_machine_id, KiroAuthConfig};
|
||||
pub use headers::{build_generate_assistant_headers, AWS_EVENTSTREAM_CONTENT_TYPE};
|
||||
pub use headers::{
|
||||
build_generate_assistant_headers, build_mcp_headers, AWS_EVENTSTREAM_CONTENT_TYPE,
|
||||
KIRO_EXTERNAL_IDP_TOKEN_TYPE, KIRO_PROFILE_ARN_HEADER, KIRO_TOKEN_TYPE_HEADER,
|
||||
};
|
||||
pub use policy::{
|
||||
local_kiro_request_transport_unsupported_reason_with_network,
|
||||
supports_local_kiro_request_transport, supports_local_kiro_request_transport_with_network,
|
||||
@@ -28,6 +31,7 @@ pub use request::{
|
||||
KiroProviderHeadersInput,
|
||||
};
|
||||
pub use url::{
|
||||
build_kiro_generate_assistant_response_url, resolve_kiro_base_url,
|
||||
GENERATE_ASSISTANT_RESPONSE_PATH, KIRO_ENVELOPE_NAME,
|
||||
build_kiro_generate_assistant_response_url, build_kiro_mcp_url,
|
||||
build_kiro_mcp_url_from_resolved_url, resolve_kiro_base_url, GENERATE_ASSISTANT_RESPONSE_PATH,
|
||||
KIRO_ENVELOPE_NAME, MCP_PATH, MCP_STREAM_PATH,
|
||||
};
|
||||
|
||||
@@ -3,7 +3,7 @@ use sha2::{Digest, Sha256};
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
|
||||
pub const DEFAULT_REGION: &str = "us-east-1";
|
||||
pub const DEFAULT_KIRO_VERSION: &str = "0.8.0";
|
||||
pub const DEFAULT_KIRO_VERSION: &str = "0.3.210";
|
||||
pub const DEFAULT_NODE_VERSION: &str = "22.21.1";
|
||||
pub const DEFAULT_SYSTEM_VERSION: &str = "other#unknown";
|
||||
|
||||
@@ -158,7 +158,7 @@ impl KiroAuthConfig {
|
||||
.map(normalize_auth_method)
|
||||
.unwrap_or_else(|| "social".to_string());
|
||||
if explicit_method != "social" {
|
||||
return explicit_method == "idc";
|
||||
return matches!(explicit_method.as_str(), "idc" | "external_idp");
|
||||
}
|
||||
self.client_id
|
||||
.as_deref()
|
||||
@@ -173,6 +173,14 @@ impl KiroAuthConfig {
|
||||
.is_some()
|
||||
}
|
||||
|
||||
pub fn uses_external_idp_token_type(&self) -> bool {
|
||||
self.auth_method
|
||||
.as_deref()
|
||||
.map(normalize_auth_method)
|
||||
.as_deref()
|
||||
== Some("external_idp")
|
||||
}
|
||||
|
||||
pub fn profile_arn_for_payload(&self) -> Option<&str> {
|
||||
if self.is_idc_auth() {
|
||||
return None;
|
||||
@@ -183,6 +191,13 @@ impl KiroAuthConfig {
|
||||
.filter(|value| !value.is_empty())
|
||||
}
|
||||
|
||||
pub fn profile_arn_for_mcp(&self) -> Option<&str> {
|
||||
self.profile_arn
|
||||
.as_deref()
|
||||
.map(str::trim)
|
||||
.filter(|value| !value.is_empty())
|
||||
}
|
||||
|
||||
pub fn can_refresh_access_token(&self) -> bool {
|
||||
let refresh_token = self
|
||||
.refresh_token
|
||||
@@ -309,6 +324,7 @@ fn normalize_auth_method(raw: &str) -> String {
|
||||
| "identity_center"
|
||||
| "identitycenter"
|
||||
| "idc" => "idc".to_string(),
|
||||
"external-idp" | "external_idp" | "externalidp" => "external_idp".to_string(),
|
||||
_ => value,
|
||||
}
|
||||
}
|
||||
@@ -394,6 +410,29 @@ mod tests {
|
||||
assert_eq!(DEFAULT_REGION, "us-east-1");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn preserves_external_idp_auth_method_for_header_selection() {
|
||||
let auth_config = KiroAuthConfig::from_raw_json(Some(
|
||||
r#"{
|
||||
"authMethod":"external_idp",
|
||||
"refreshToken":"rt-1",
|
||||
"clientId":"cid",
|
||||
"clientSecret":"secret",
|
||||
"profileArn":"arn:aws:bedrock:demo"
|
||||
}"#,
|
||||
))
|
||||
.expect("auth config should parse");
|
||||
|
||||
assert_eq!(auth_config.auth_method.as_deref(), Some("external_idp"));
|
||||
assert!(auth_config.is_idc_auth());
|
||||
assert!(auth_config.uses_external_idp_token_type());
|
||||
assert!(auth_config.profile_arn_for_payload().is_none());
|
||||
assert_eq!(
|
||||
auth_config.profile_arn_for_mcp(),
|
||||
Some("arn:aws:bedrock:demo")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn infers_idc_when_client_credentials_exist() {
|
||||
let auth_config = KiroAuthConfig::from_raw_json(Some(
|
||||
@@ -407,7 +446,12 @@ mod tests {
|
||||
.expect("auth config should parse");
|
||||
|
||||
assert!(auth_config.is_idc_auth());
|
||||
assert!(!auth_config.uses_external_idp_token_type());
|
||||
assert!(auth_config.profile_arn_for_payload().is_none());
|
||||
assert_eq!(
|
||||
auth_config.profile_arn_for_mcp(),
|
||||
Some("arn:aws:bedrock:demo")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -5,6 +5,9 @@ use uuid::Uuid;
|
||||
use super::credentials::KiroAuthConfig;
|
||||
|
||||
pub const AWS_EVENTSTREAM_CONTENT_TYPE: &str = "application/vnd.amazon.eventstream";
|
||||
pub const KIRO_PROFILE_ARN_HEADER: &str = "x-amzn-kiro-profile-arn";
|
||||
pub const KIRO_TOKEN_TYPE_HEADER: &str = "TokenType";
|
||||
pub const KIRO_EXTERNAL_IDP_TOKEN_TYPE: &str = "EXTERNAL_IDP";
|
||||
const AWS_SDK_JS_MAIN_VERSION: &str = "1.0.27";
|
||||
const CODEWHISPERER_OPTOUT: &str = "true";
|
||||
const KIRO_AGENT_MODE: &str = "vibe";
|
||||
@@ -81,10 +84,55 @@ pub fn build_generate_assistant_headers(
|
||||
])
|
||||
}
|
||||
|
||||
pub fn build_mcp_headers(
|
||||
auth_config: &KiroAuthConfig,
|
||||
machine_id: &str,
|
||||
) -> BTreeMap<String, String> {
|
||||
let kiro_version = auth_config.effective_kiro_version();
|
||||
let system_version = auth_config.effective_system_version();
|
||||
let node_version = auth_config.effective_node_version();
|
||||
let region = auth_config.effective_api_region();
|
||||
let host = format!("q.{region}.amazonaws.com");
|
||||
|
||||
let mut headers = BTreeMap::from([
|
||||
("accept".to_string(), "application/json".to_string()),
|
||||
(
|
||||
"amz-sdk-invocation-id".to_string(),
|
||||
Uuid::new_v4().to_string(),
|
||||
),
|
||||
(
|
||||
"amz-sdk-request".to_string(),
|
||||
"attempt=1; max=3".to_string(),
|
||||
),
|
||||
("connection".to_string(), "close".to_string()),
|
||||
("content-type".to_string(), "application/json".to_string()),
|
||||
("host".to_string(), host),
|
||||
(
|
||||
"user-agent".to_string(),
|
||||
build_user_agent_main(system_version, node_version, kiro_version, machine_id),
|
||||
),
|
||||
(
|
||||
"x-amz-user-agent".to_string(),
|
||||
build_x_amz_user_agent_main(kiro_version, machine_id),
|
||||
),
|
||||
(
|
||||
"x-amzn-codewhisperer-optout".to_string(),
|
||||
CODEWHISPERER_OPTOUT.to_string(),
|
||||
),
|
||||
]);
|
||||
if let Some(profile_arn) = auth_config.profile_arn_for_mcp() {
|
||||
headers.insert(KIRO_PROFILE_ARN_HEADER.to_string(), profile_arn.to_string());
|
||||
}
|
||||
headers
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::super::credentials::KiroAuthConfig;
|
||||
use super::{build_generate_assistant_headers, AWS_EVENTSTREAM_CONTENT_TYPE};
|
||||
use super::{
|
||||
build_generate_assistant_headers, build_mcp_headers, AWS_EVENTSTREAM_CONTENT_TYPE,
|
||||
KIRO_PROFILE_ARN_HEADER, KIRO_TOKEN_TYPE_HEADER,
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn builds_generate_assistant_headers_for_region() {
|
||||
@@ -118,5 +166,96 @@ mod tests {
|
||||
headers.get("x-amzn-kiro-agent-mode").map(String::as_str),
|
||||
Some("vibe")
|
||||
);
|
||||
assert!(!headers.contains_key(KIRO_TOKEN_TYPE_HEADER));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn keeps_generate_assistant_headers_without_external_idp_token_type() {
|
||||
let auth_config = KiroAuthConfig {
|
||||
auth_method: Some("idc".to_string()),
|
||||
refresh_token: None,
|
||||
expires_at: None,
|
||||
profile_arn: Some("arn:aws:codewhisperer:us-east-1:123456789012:profile/demo".into()),
|
||||
region: None,
|
||||
auth_region: None,
|
||||
api_region: Some("us-east-1".to_string()),
|
||||
client_id: Some("client-id".to_string()),
|
||||
client_secret: Some("client-secret".to_string()),
|
||||
machine_id: None,
|
||||
kiro_version: None,
|
||||
system_version: None,
|
||||
node_version: None,
|
||||
access_token: None,
|
||||
};
|
||||
|
||||
let headers = build_generate_assistant_headers(&auth_config, "machine-123");
|
||||
|
||||
assert_eq!(
|
||||
headers.get("accept").map(String::as_str),
|
||||
Some(AWS_EVENTSTREAM_CONTENT_TYPE)
|
||||
);
|
||||
assert!(!headers.contains_key(KIRO_TOKEN_TYPE_HEADER));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn builds_mcp_headers_with_profile_arn_for_social_auth() {
|
||||
let auth_config = KiroAuthConfig {
|
||||
auth_method: Some("social".to_string()),
|
||||
refresh_token: None,
|
||||
expires_at: None,
|
||||
profile_arn: Some("arn:aws:codewhisperer:us-east-1:123456789012:profile/demo".into()),
|
||||
region: None,
|
||||
auth_region: None,
|
||||
api_region: Some("us-east-1".to_string()),
|
||||
client_id: None,
|
||||
client_secret: None,
|
||||
machine_id: None,
|
||||
kiro_version: Some("0.3.210".to_string()),
|
||||
system_version: Some("darwin#24.6.0".to_string()),
|
||||
node_version: Some("22.21.1".to_string()),
|
||||
access_token: None,
|
||||
};
|
||||
|
||||
let headers = build_mcp_headers(&auth_config, "machine-123");
|
||||
assert_eq!(
|
||||
headers.get("accept").map(String::as_str),
|
||||
Some("application/json")
|
||||
);
|
||||
assert_eq!(
|
||||
headers.get(KIRO_PROFILE_ARN_HEADER).map(String::as_str),
|
||||
Some("arn:aws:codewhisperer:us-east-1:123456789012:profile/demo")
|
||||
);
|
||||
assert!(!headers.contains_key(KIRO_TOKEN_TYPE_HEADER));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn builds_mcp_headers_with_profile_arn_for_idc_auth() {
|
||||
let auth_config = KiroAuthConfig {
|
||||
auth_method: Some("idc".to_string()),
|
||||
refresh_token: None,
|
||||
expires_at: None,
|
||||
profile_arn: Some("arn:aws:codewhisperer:us-east-1:123456789012:profile/demo".into()),
|
||||
region: None,
|
||||
auth_region: None,
|
||||
api_region: Some("us-west-2".to_string()),
|
||||
client_id: Some("client-id".to_string()),
|
||||
client_secret: Some("client-secret".to_string()),
|
||||
machine_id: None,
|
||||
kiro_version: None,
|
||||
system_version: None,
|
||||
node_version: None,
|
||||
access_token: None,
|
||||
};
|
||||
|
||||
let headers = build_mcp_headers(&auth_config, "machine-123");
|
||||
assert_eq!(
|
||||
headers.get("host").map(String::as_str),
|
||||
Some("q.us-west-2.amazonaws.com")
|
||||
);
|
||||
assert_eq!(
|
||||
headers.get(KIRO_PROFILE_ARN_HEADER).map(String::as_str),
|
||||
Some("arn:aws:codewhisperer:us-east-1:123456789012:profile/demo")
|
||||
);
|
||||
assert!(!headers.contains_key(KIRO_TOKEN_TYPE_HEADER));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,8 @@ use super::super::url::build_passthrough_path_url;
|
||||
use super::credentials::DEFAULT_REGION;
|
||||
|
||||
pub const GENERATE_ASSISTANT_RESPONSE_PATH: &str = "/generateAssistantResponse";
|
||||
pub const MCP_PATH: &str = "/mcp";
|
||||
pub const MCP_STREAM_PATH: &str = "/mcp/stream";
|
||||
pub const KIRO_ENVELOPE_NAME: &str = "kiro:generateAssistantResponse";
|
||||
|
||||
pub fn resolve_kiro_base_url(upstream_base_url: &str, api_region: Option<&str>) -> String {
|
||||
@@ -30,11 +32,24 @@ pub fn build_kiro_generate_assistant_response_url(
|
||||
)
|
||||
}
|
||||
|
||||
pub fn build_kiro_mcp_url(upstream_base_url: &str, api_region: Option<&str>) -> Option<String> {
|
||||
let upstream_base_url = resolve_kiro_base_url(upstream_base_url, api_region);
|
||||
build_passthrough_path_url(upstream_base_url.as_str(), MCP_PATH, None, &[])
|
||||
}
|
||||
|
||||
pub fn build_kiro_mcp_url_from_resolved_url(resolved_url: &str) -> Option<String> {
|
||||
let mut parsed = url::Url::parse(resolved_url).ok()?;
|
||||
parsed.set_path(MCP_PATH);
|
||||
parsed.set_query(None);
|
||||
Some(parsed.to_string())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{
|
||||
build_kiro_generate_assistant_response_url, resolve_kiro_base_url,
|
||||
GENERATE_ASSISTANT_RESPONSE_PATH, KIRO_ENVELOPE_NAME,
|
||||
build_kiro_generate_assistant_response_url, build_kiro_mcp_url,
|
||||
build_kiro_mcp_url_from_resolved_url, resolve_kiro_base_url,
|
||||
GENERATE_ASSISTANT_RESPONSE_PATH, KIRO_ENVELOPE_NAME, MCP_PATH, MCP_STREAM_PATH,
|
||||
};
|
||||
|
||||
#[test]
|
||||
@@ -43,6 +58,8 @@ mod tests {
|
||||
GENERATE_ASSISTANT_RESPONSE_PATH,
|
||||
"/generateAssistantResponse"
|
||||
);
|
||||
assert_eq!(MCP_PATH, "/mcp");
|
||||
assert_eq!(MCP_STREAM_PATH, "/mcp/stream");
|
||||
assert_eq!(KIRO_ENVELOPE_NAME, "kiro:generateAssistantResponse");
|
||||
}
|
||||
|
||||
@@ -68,4 +85,23 @@ mod tests {
|
||||
"https://kiro.us-west-2.example"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn builds_mcp_url_for_latest_kiro_endpoint() {
|
||||
assert_eq!(
|
||||
build_kiro_mcp_url("https://q.{region}.amazonaws.com", Some("eu-west-1")).as_deref(),
|
||||
Some("https://q.eu-west-1.amazonaws.com/mcp")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rewrites_generate_assistant_url_to_mcp_url() {
|
||||
assert_eq!(
|
||||
build_kiro_mcp_url_from_resolved_url(
|
||||
"https://q.us-east-1.amazonaws.com/generateAssistantResponse?beta=true"
|
||||
)
|
||||
.as_deref(),
|
||||
Some("https://q.us-east-1.amazonaws.com/mcp")
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user