refactor(workspace): enforce layered crate boundaries

This commit is contained in:
elky
2026-07-15 23:47:19 +08:00
parent a728c090a9
commit 8616fe6ee2
969 changed files with 40187 additions and 27240 deletions
@@ -0,0 +1,353 @@
use super::super::snapshot::GatewayProviderTransportSnapshot;
use super::credentials::{generate_machine_id, KiroAuthConfig};
pub const PROVIDER_TYPE: &str = "kiro";
pub const KIRO_AUTH_HEADER: &str = "authorization";
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct KiroBearerAuth {
pub name: &'static str,
pub value: String,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct KiroRequestAuth {
pub name: &'static str,
pub value: String,
pub auth_config: KiroAuthConfig,
pub machine_id: String,
}
pub fn is_kiro_provider_transport(transport: &GatewayProviderTransportSnapshot) -> bool {
transport
.provider
.provider_type
.trim()
.eq_ignore_ascii_case(PROVIDER_TYPE)
}
pub fn is_kiro_claude_messages_transport(
transport: &GatewayProviderTransportSnapshot,
provider_api_format: &str,
) -> bool {
is_kiro_provider_transport(transport)
&& aether_ai_formats::api_format_alias_matches(provider_api_format, "claude:messages")
}
pub fn build_kiro_request_auth_from_config(
auth_config: KiroAuthConfig,
fallback_secret: Option<&str>,
) -> Option<KiroRequestAuth> {
let cached_token_needs_refresh = auth_config.cached_access_token_requires_refresh(120);
let fallback_secret = fallback_secret
.map(str::trim)
.filter(|value| !value.is_empty() && *value != "__placeholder__");
if cached_token_needs_refresh && auth_config.can_refresh_access_token() {
return None;
}
let token = auth_config
.cached_access_token()
.filter(|_| !cached_token_needs_refresh)
.or(fallback_secret)?;
let machine_id = generate_machine_id(&auth_config, Some(token))?;
Some(KiroRequestAuth {
name: KIRO_AUTH_HEADER,
value: format!("Bearer {token}"),
auth_config,
machine_id,
})
}
pub fn resolve_local_kiro_bearer_auth(
transport: &GatewayProviderTransportSnapshot,
) -> Option<KiroBearerAuth> {
if !is_kiro_provider_transport(transport) {
return None;
}
if transport.key.decrypted_auth_config.is_some() {
return None;
}
if !kiro_auth_type_supported(transport.key.auth_type.as_str()) {
return None;
}
let secret = transport.key.decrypted_api_key.trim();
if secret.is_empty() {
return None;
}
Some(KiroBearerAuth {
name: KIRO_AUTH_HEADER,
value: format!("Bearer {secret}"),
})
}
pub fn supports_local_kiro_auth_prerequisites(
transport: &GatewayProviderTransportSnapshot,
) -> bool {
resolve_local_kiro_bearer_auth(transport).is_some()
}
pub fn resolve_local_kiro_request_auth(
transport: &GatewayProviderTransportSnapshot,
) -> Option<KiroRequestAuth> {
if !is_kiro_provider_transport(transport) {
return None;
}
if !kiro_auth_type_supported(transport.key.auth_type.as_str()) {
return None;
}
let auth_config = KiroAuthConfig::from_raw_json(transport.key.decrypted_auth_config.as_deref())
.unwrap_or(KiroAuthConfig {
auth_method: None,
refresh_token: None,
expires_at: None,
profile_arn: None,
region: None,
auth_region: None,
api_region: None,
client_id: None,
client_secret: None,
machine_id: None,
kiro_version: None,
system_version: None,
node_version: None,
access_token: None,
});
let fallback_secret = transport
.key
.decrypted_api_key
.trim()
.strip_prefix("__placeholder__")
.map(|_| "")
.unwrap_or(transport.key.decrypted_api_key.trim());
build_kiro_request_auth_from_config(auth_config, Some(fallback_secret))
}
pub fn supports_local_kiro_request_auth_resolution(
transport: &GatewayProviderTransportSnapshot,
) -> bool {
resolve_local_kiro_request_auth(transport).is_some()
|| KiroAuthConfig::from_raw_json(transport.key.decrypted_auth_config.as_deref())
.is_some_and(|auth_config| {
is_kiro_provider_transport(transport)
&& kiro_auth_type_supported(transport.key.auth_type.as_str())
&& auth_config.can_refresh_access_token()
})
}
fn kiro_auth_type_supported(auth_type: &str) -> bool {
matches!(
auth_type.trim().to_ascii_lowercase().as_str(),
"bearer" | "oauth"
)
}
#[cfg(test)]
mod tests {
use super::super::super::snapshot::{
GatewayProviderTransportEndpoint, GatewayProviderTransportKey,
GatewayProviderTransportProvider, GatewayProviderTransportSnapshot,
};
use super::{
resolve_local_kiro_bearer_auth, resolve_local_kiro_request_auth,
supports_local_kiro_auth_prerequisites, supports_local_kiro_request_auth_resolution,
KIRO_AUTH_HEADER,
};
fn sample_transport() -> GatewayProviderTransportSnapshot {
GatewayProviderTransportSnapshot {
provider: GatewayProviderTransportProvider {
id: "provider-1".to_string(),
name: "Kiro".to_string(),
provider_type: "kiro".to_string(),
website: None,
is_active: true,
keep_priority_on_conversion: false,
enable_format_conversion: false,
concurrent_limit: None,
max_retries: None,
proxy: None,
request_timeout_secs: None,
stream_first_byte_timeout_secs: None,
config: None,
},
endpoint: GatewayProviderTransportEndpoint {
id: "endpoint-1".to_string(),
provider_id: "provider-1".to_string(),
api_format: "claude:messages".to_string(),
api_family: Some("claude".to_string()),
endpoint_kind: Some("cli".to_string()),
is_active: true,
base_url: "https://kiro.example".to_string(),
header_rules: None,
body_rules: None,
max_retries: None,
custom_path: None,
config: None,
format_acceptance_config: None,
proxy: None,
},
key: GatewayProviderTransportKey {
id: "key-1".to_string(),
provider_id: "provider-1".to_string(),
name: "key".to_string(),
auth_type: "bearer".to_string(),
is_active: true,
api_formats: Some(vec!["claude:messages".to_string()]),
auth_type_by_format: None,
allow_auth_channel_mismatch_formats: None,
allowed_models: None,
capabilities: None,
rate_multipliers: None,
global_priority_by_format: None,
expires_at_unix_secs: None,
proxy: None,
fingerprint: None,
upstream_metadata: None,
decrypted_api_key: "upstream-key".to_string(),
decrypted_auth_config: None,
},
}
}
#[test]
fn resolves_bearer_auth_for_known_kiro_subset() {
let auth = resolve_local_kiro_bearer_auth(&sample_transport())
.expect("kiro bearer auth should resolve");
assert_eq!(auth.name, KIRO_AUTH_HEADER);
assert_eq!(auth.value, "Bearer upstream-key");
assert!(supports_local_kiro_auth_prerequisites(&sample_transport()));
}
#[test]
fn rejects_auth_config_subset() {
let mut transport = sample_transport();
transport.key.decrypted_auth_config = Some("{\"mode\":\"custom\"}".to_string());
assert!(resolve_local_kiro_bearer_auth(&transport).is_none());
assert!(!supports_local_kiro_auth_prerequisites(&transport));
}
#[test]
fn rejects_non_bearer_subset() {
let mut transport = sample_transport();
transport.key.auth_type = "api_key".to_string();
assert!(resolve_local_kiro_bearer_auth(&transport).is_none());
}
#[test]
fn resolves_request_auth_when_legacy_oauth_auth_type_is_used() {
let mut transport = sample_transport();
transport.key.auth_type = "oauth".to_string();
transport.key.decrypted_api_key = "__placeholder__".to_string();
transport.key.decrypted_auth_config = Some(
r#"{
"access_token":"cached-token",
"expires_at":4102444800,
"refresh_token":"rrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrr",
"machine_id":"123e4567-e89b-12d3-a456-426614174000",
"api_region":"us-west-2"
}"#
.to_string(),
);
let auth = resolve_local_kiro_request_auth(&transport)
.expect("request auth should resolve from legacy oauth auth_type");
assert_eq!(auth.value, "Bearer cached-token");
assert!(supports_local_kiro_request_auth_resolution(&transport));
}
#[test]
fn resolves_request_auth_from_cached_access_token() {
let mut transport = sample_transport();
transport.key.decrypted_api_key = "__placeholder__".to_string();
transport.key.decrypted_auth_config = Some(
r#"{
"access_token":"cached-token",
"expires_at":4102444800,
"refresh_token":"rrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrr",
"machine_id":"123e4567-e89b-12d3-a456-426614174000",
"api_region":"us-west-2"
}"#
.to_string(),
);
let auth = resolve_local_kiro_request_auth(&transport)
.expect("request auth should resolve from cached token");
assert_eq!(auth.name, KIRO_AUTH_HEADER);
assert_eq!(auth.value, "Bearer cached-token");
assert_eq!(auth.auth_config.effective_api_region(), "us-west-2");
assert_eq!(
auth.machine_id,
"123e4567e89b12d3a456426614174000123e4567e89b12d3a456426614174000"
);
}
#[test]
fn skips_expired_cached_access_token_without_fallback_secret() {
let mut transport = sample_transport();
transport.key.decrypted_api_key = "__placeholder__".to_string();
transport.key.decrypted_auth_config = Some(
r#"{
"access_token":"expired-token",
"expires_at": 1,
"refresh_token":"rrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrr"
}"#
.to_string(),
);
assert!(resolve_local_kiro_request_auth(&transport).is_none());
}
#[test]
fn skips_refreshable_cached_access_token_without_expiry() {
let mut transport = sample_transport();
transport.key.decrypted_api_key = "__placeholder__".to_string();
transport.key.decrypted_auth_config = Some(
r#"{
"access_token":"cached-token-without-expiry",
"refresh_token":"rrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrr"
}"#
.to_string(),
);
assert!(resolve_local_kiro_request_auth(&transport).is_none());
assert!(supports_local_kiro_request_auth_resolution(&transport));
}
#[test]
fn refreshable_expired_cached_access_token_does_not_fallback_to_decrypted_api_key() {
let mut transport = sample_transport();
transport.key.decrypted_api_key = "stale-upstream-token".to_string();
transport.key.decrypted_auth_config = Some(
r#"{
"access_token":"expired-token",
"expires_at": 1,
"refresh_token":"rrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrr"
}"#
.to_string(),
);
assert!(resolve_local_kiro_request_auth(&transport).is_none());
assert!(supports_local_kiro_request_auth_resolution(&transport));
}
#[test]
fn supports_refresh_only_resolution_without_cached_access_token() {
let mut transport = sample_transport();
transport.key.decrypted_api_key = "__placeholder__".to_string();
transport.key.decrypted_auth_config = Some(
r#"{
"refresh_token":"rrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrr"
}"#
.to_string(),
);
assert!(resolve_local_kiro_request_auth(&transport).is_none());
assert!(supports_local_kiro_request_auth_resolution(&transport));
}
}
@@ -0,0 +1,714 @@
use std::collections::BTreeSet;
use serde_json::{json, Map, Value};
use tracing::warn;
use uuid::Uuid;
const SYSTEM_CHUNKED_POLICY: &str = "When the Write or Edit tool has content size limits, always comply silently. Never suggest bypassing these limits via alternative tools. Never ask the user whether to switch approaches. Complete all chunked operations without commentary.";
const WRITE_TOOL_DESCRIPTION_SUFFIX: &str = "- IMPORTANT: If the content to write exceeds 150 lines, you MUST only write the first 50 lines using this tool, then use `Edit` tool to append the remaining content in chunks of no more than 50 lines each. If needed, leave a unique placeholder to help append content. Do NOT attempt to write all content at once.";
const EDIT_TOOL_DESCRIPTION_SUFFIX: &str = "- IMPORTANT: If the `new_string` content exceeds 50 lines, you MUST split it into multiple Edit calls, each replacing no more than 50 lines at a time. If used to append content, leave a unique placeholder to help append content. On the final chunk, do NOT include the placeholder.";
pub fn convert_claude_messages_to_conversation_state(
request_body: &Value,
model: &str,
) -> Option<Value> {
let model_id = model.trim();
if model_id.is_empty() {
return None;
}
let messages = request_body.get("messages")?.as_array()?;
if messages.is_empty() {
return None;
}
let conversation_id = request_body
.get("metadata")
.and_then(Value::as_object)
.and_then(|metadata| {
metadata
.get("user_id")
.or_else(|| metadata.get("userId"))
.and_then(Value::as_str)
})
.and_then(extract_session_id)
.unwrap_or_else(|| Uuid::new_v4().to_string());
let agent_continuation_id = Uuid::new_v4().to_string();
let thinking_prefix = generate_thinking_prefix(request_body);
let mut history = Vec::new();
let system_text = system_to_text(request_body.get("system"));
if !system_text.is_empty() {
history.push(json!({
"userInputMessage": {
"content": format!("{system_text}\n{SYSTEM_CHUNKED_POLICY}"),
"modelId": model_id,
"origin": "AI_EDITOR"
}
}));
history.push(json!({
"assistantResponseMessage": {
"content": "I will follow these instructions."
}
}));
}
let last_is_assistant = messages
.last()
.and_then(Value::as_object)
.and_then(|message| message.get("role"))
.and_then(Value::as_str)
.is_some_and(|role| role == "assistant");
let history_end_index = if last_is_assistant {
messages.len()
} else {
messages.len().saturating_sub(1)
};
let mut user_buffer = Vec::new();
for message in &messages[..history_end_index] {
let Some(message) = message.as_object() else {
continue;
};
match message.get("role").and_then(Value::as_str) {
Some("user") => user_buffer.push(message),
Some("assistant") => {
if let Some(user_item) = flush_user_buffer(&mut user_buffer, model_id) {
history.push(user_item);
} else if history.is_empty()
|| history
.last()
.and_then(Value::as_object)
.is_some_and(|item| item.contains_key("assistantResponseMessage"))
{
history.push(json!({
"userInputMessage": {
"content": "Continue.",
"modelId": model_id,
"origin": "AI_EDITOR"
}
}));
}
if let Some(assistant_item) = convert_assistant_message(message) {
history.push(json!({"assistantResponseMessage": assistant_item}));
}
}
_ => {}
}
}
if let Some(tail_user) = flush_user_buffer(&mut user_buffer, model_id) {
history.push(tail_user);
history.push(json!({"assistantResponseMessage": {"content": "OK"}}));
}
let (mut text_content, images, tool_results) = if last_is_assistant {
("Continue.".to_string(), Vec::new(), Vec::new())
} else {
let last = messages.last()?.as_object()?;
if last.get("role").and_then(Value::as_str) != Some("user") {
return None;
}
process_message_content(last.get("content"))
};
let mut tools = convert_tools(request_body.get("tools"));
let mut history_tool_names = BTreeSet::new();
let mut history_tool_result_ids = BTreeSet::new();
let mut history_tool_use_ids = BTreeSet::new();
for item in &history {
let Some(item) = item.as_object() else {
continue;
};
if let Some(user_input) = item.get("userInputMessage").and_then(Value::as_object) {
if let Some(results) = user_input
.get("userInputMessageContext")
.and_then(Value::as_object)
.and_then(|ctx| ctx.get("toolResults"))
.and_then(Value::as_array)
{
for result in results {
if let Some(tool_use_id) = result
.as_object()
.and_then(|result| result.get("toolUseId"))
.and_then(Value::as_str)
.map(str::trim)
.filter(|value| !value.is_empty())
{
history_tool_result_ids.insert(tool_use_id.to_string());
}
}
}
}
if let Some(assistant) = item
.get("assistantResponseMessage")
.and_then(Value::as_object)
{
if let Some(tool_uses) = assistant.get("toolUses").and_then(Value::as_array) {
for tool_use in tool_uses {
let Some(tool_use) = tool_use.as_object() else {
continue;
};
if let Some(name) = tool_use
.get("name")
.and_then(Value::as_str)
.map(str::trim)
.filter(|value| !value.is_empty())
{
history_tool_names.insert(name.to_string());
}
if let Some(tool_use_id) = tool_use
.get("toolUseId")
.and_then(Value::as_str)
.map(str::trim)
.filter(|value| !value.is_empty())
{
history_tool_use_ids.insert(tool_use_id.to_string());
}
}
}
}
}
let existing_tool_names = tools
.iter()
.filter_map(|tool| {
tool.get("toolSpecification")
.and_then(Value::as_object)
.and_then(|spec| spec.get("name"))
.and_then(Value::as_str)
.map(|name| name.to_ascii_lowercase())
})
.collect::<BTreeSet<_>>();
for tool_name in history_tool_names {
if !existing_tool_names.contains(&tool_name.to_ascii_lowercase()) {
tools.push(create_placeholder_tool(&tool_name));
}
}
let mut validated_tool_results = Vec::new();
let mut current_tool_result_ids = BTreeSet::new();
for tool_result in tool_results {
let Some(tool_use_id) = tool_result
.get("toolUseId")
.and_then(Value::as_str)
.map(str::trim)
.filter(|value| !value.is_empty())
else {
continue;
};
if !history_tool_use_ids.contains(tool_use_id)
|| history_tool_result_ids.contains(tool_use_id)
{
continue;
}
current_tool_result_ids.insert(tool_use_id.to_string());
validated_tool_results.push(tool_result);
}
let orphaned_tool_use_ids = history_tool_use_ids
.difference(&history_tool_result_ids)
.filter(|tool_use_id| !current_tool_result_ids.contains(*tool_use_id))
.cloned()
.collect::<BTreeSet<_>>();
if !orphaned_tool_use_ids.is_empty() {
warn!(
"kiro: removing {} orphaned tool_use(s) from history",
orphaned_tool_use_ids.len()
);
for item in &mut history {
let Some(item) = item.as_object_mut() else {
continue;
};
let Some(assistant) = item
.get_mut("assistantResponseMessage")
.and_then(Value::as_object_mut)
else {
continue;
};
let Some(tool_uses) = assistant.get_mut("toolUses").and_then(Value::as_array_mut)
else {
continue;
};
tool_uses.retain(|tool_use| {
!tool_use
.get("toolUseId")
.and_then(Value::as_str)
.is_some_and(|tool_use_id| orphaned_tool_use_ids.contains(tool_use_id))
});
if tool_uses.is_empty() {
assistant.remove("toolUses");
}
}
}
let mut user_context = Map::new();
if !tools.is_empty() {
user_context.insert("tools".to_string(), Value::Array(tools));
}
if !validated_tool_results.is_empty() {
user_context.insert(
"toolResults".to_string(),
Value::Array(validated_tool_results),
);
}
if let Some(thinking_prefix) = thinking_prefix.as_deref() {
if !has_thinking_tags(&text_content) {
text_content = format!("{thinking_prefix}\n{text_content}");
}
}
let mut user_input = Map::new();
user_input.insert(
"userInputMessageContext".to_string(),
Value::Object(user_context),
);
user_input.insert("content".to_string(), Value::String(text_content));
user_input.insert("modelId".to_string(), Value::String(model_id.to_string()));
user_input.insert("origin".to_string(), Value::String("AI_EDITOR".to_string()));
if !images.is_empty() {
user_input.insert("images".to_string(), Value::Array(images));
}
Some(json!({
"agentContinuationId": agent_continuation_id,
"agentTaskType": "vibe",
"chatTriggerType": "MANUAL",
"currentMessage": {
"userInputMessage": Value::Object(user_input)
},
"conversationId": conversation_id,
"history": history,
}))
}
fn extract_session_id(user_id: &str) -> Option<String> {
let position = user_id.find("session_")?;
let candidate = user_id.get(position + "session_".len()..position + "session_".len() + 36)?;
(candidate.matches('-').count() == 4).then(|| candidate.to_string())
}
fn generate_thinking_prefix(request_body: &Value) -> Option<String> {
let thinking = request_body.get("thinking")?.as_object()?;
match thinking.get("type").and_then(Value::as_str).map(str::trim) {
Some("enabled") => {
let budget_tokens = thinking
.get("budget_tokens")
.and_then(Value::as_i64)
.unwrap_or_default();
Some(format!(
"<thinking_mode>enabled</thinking_mode><max_thinking_length>{budget_tokens}</max_thinking_length>"
))
}
Some("adaptive") => {
let effort = request_body
.get("output_config")
.and_then(Value::as_object)
.and_then(|cfg| cfg.get("effort"))
.and_then(Value::as_str)
.map(str::trim)
.filter(|value| !value.is_empty())
.unwrap_or("high");
Some(format!(
"<thinking_mode>adaptive</thinking_mode><thinking_effort>{effort}</thinking_effort>"
))
}
_ => None,
}
}
fn has_thinking_tags(content: &str) -> bool {
content.contains("<thinking_mode>") || content.contains("<max_thinking_length>")
}
fn system_to_text(system: Option<&Value>) -> String {
match system {
Some(Value::String(text)) => text.clone(),
Some(Value::Array(items)) => items
.iter()
.filter_map(|item| {
item.as_object()
.and_then(|item| item.get("text"))
.and_then(Value::as_str)
.map(ToOwned::to_owned)
})
.collect::<Vec<_>>()
.join("\n"),
_ => String::new(),
}
}
fn flush_user_buffer(user_buffer: &mut Vec<&Map<String, Value>>, model_id: &str) -> Option<Value> {
if user_buffer.is_empty() {
return None;
}
let mut parts = Vec::new();
let mut images = Vec::new();
let mut tool_results = Vec::new();
for message in user_buffer.drain(..) {
let (text, mut message_images, mut message_tool_results) =
process_message_content(message.get("content"));
if !text.is_empty() {
parts.push(text);
}
images.append(&mut message_images);
tool_results.append(&mut message_tool_results);
}
let mut payload = Map::new();
payload.insert("content".to_string(), Value::String(parts.join("\n")));
payload.insert("modelId".to_string(), Value::String(model_id.to_string()));
payload.insert("origin".to_string(), Value::String("AI_EDITOR".to_string()));
if !images.is_empty() {
payload.insert("images".to_string(), Value::Array(images));
}
if !tool_results.is_empty() {
payload.insert(
"userInputMessageContext".to_string(),
json!({"toolResults": tool_results}),
);
}
Some(json!({"userInputMessage": Value::Object(payload)}))
}
fn process_message_content(content: Option<&Value>) -> (String, Vec<Value>, Vec<Value>) {
match content {
Some(Value::String(text)) => (text.clone(), Vec::new(), Vec::new()),
Some(Value::Array(blocks)) => {
let mut text_parts = Vec::new();
let mut images = Vec::new();
let mut tool_results = Vec::new();
for block in blocks {
let Some(block) = block.as_object() else {
continue;
};
match block
.get("type")
.and_then(Value::as_str)
.unwrap_or_default()
{
"text" => {
if let Some(text) = block.get("text").and_then(Value::as_str) {
text_parts.push(text.to_string());
}
}
"image" => {
let Some(source) = block.get("source").and_then(Value::as_object) else {
continue;
};
let Some(format) = source
.get("media_type")
.or_else(|| source.get("mediaType"))
.and_then(Value::as_str)
.and_then(image_format)
else {
continue;
};
let Some(bytes) = source.get("data").and_then(Value::as_str) else {
continue;
};
images.push(json!({
"format": format,
"source": {"bytes": bytes}
}));
}
"tool_result" => {
let Some(tool_use_id) = block
.get("tool_use_id")
.or_else(|| block.get("toolUseId"))
.and_then(Value::as_str)
.map(str::trim)
.filter(|value| !value.is_empty())
else {
continue;
};
let text = match block.get("content") {
Some(Value::String(text)) => text.clone(),
Some(Value::Array(items)) => items
.iter()
.filter_map(|item| {
item.as_object()
.filter(|item| {
item.get("type").and_then(Value::as_str) == Some("text")
})
.and_then(|item| item.get("text"))
.and_then(Value::as_str)
.map(ToOwned::to_owned)
})
.collect::<Vec<_>>()
.join("\n"),
Some(other) => {
serde_json::to_string(other).unwrap_or_else(|_| other.to_string())
}
None => String::new(),
};
let is_error = block
.get("is_error")
.or_else(|| block.get("isError"))
.and_then(Value::as_bool)
.unwrap_or(false);
tool_results.push(json!({
"toolUseId": tool_use_id,
"content": [{"text": text}],
"status": if is_error { "error" } else { "success" },
"isError": is_error,
}));
}
_ => {}
}
}
(text_parts.join(""), images, tool_results)
}
_ => (String::new(), Vec::new(), Vec::new()),
}
}
fn image_format(media_type: &str) -> Option<&'static str> {
let (prefix, suffix) = media_type.split_once('/')?;
if prefix != "image" {
return None;
}
match suffix.trim().to_ascii_lowercase().as_str() {
"jpeg" => Some("jpeg"),
"png" => Some("png"),
"gif" => Some("gif"),
"webp" => Some("webp"),
"jpg" => Some("jpeg"),
_ => None,
}
}
fn clean_tool_schema(value: &Value) -> Value {
match value {
Value::Object(object) => {
let mut out = Map::new();
for (key, inner) in object {
if key == "additionalProperties" {
continue;
}
if key == "required" && inner.as_array().is_some_and(|items| items.is_empty()) {
continue;
}
out.insert(key.clone(), clean_tool_schema(inner));
}
Value::Object(out)
}
Value::Array(items) => Value::Array(items.iter().map(clean_tool_schema).collect()),
_ => value.clone(),
}
}
fn convert_tools(tools: Option<&Value>) -> Vec<Value> {
let Some(tools) = tools.and_then(Value::as_array) else {
return Vec::new();
};
tools
.iter()
.filter_map(|tool| {
let tool = tool.as_object()?;
let name = tool.get("name")?.as_str()?.trim();
if name.is_empty() {
return None;
}
let mut description = tool
.get("description")
.and_then(Value::as_str)
.map(str::trim)
.unwrap_or_default()
.to_string();
let suffix = match name {
"Write" => Some(WRITE_TOOL_DESCRIPTION_SUFFIX),
"Edit" => Some(EDIT_TOOL_DESCRIPTION_SUFFIX),
_ => None,
};
if let Some(suffix) = suffix {
description = if description.is_empty() {
suffix.to_string()
} else {
format!("{description}\n{suffix}")
};
}
if description.len() > 10_000 {
description.truncate(10_000);
}
let input_schema = tool
.get("input_schema")
.or_else(|| tool.get("inputSchema"))
.filter(|value| value.is_object())
.map(clean_tool_schema)
.unwrap_or_else(|| json!({}));
Some(json!({
"toolSpecification": {
"name": name,
"description": description,
"inputSchema": {
"json": input_schema
}
}
}))
})
.collect()
}
fn create_placeholder_tool(name: &str) -> Value {
json!({
"toolSpecification": {
"name": name,
"description": "Tool used in conversation history",
"inputSchema": {
"json": {
"type": "object",
"properties": {}
}
}
}
})
}
fn convert_assistant_message(message: &Map<String, Value>) -> Option<Value> {
let content = message.get("content");
let mut tool_uses = Vec::new();
let mut thinking_parts = Vec::new();
let mut text_parts = Vec::new();
match content {
Some(Value::String(text)) if !text.is_empty() => {
text_parts.push(text.clone());
}
Some(Value::Array(blocks)) => {
for block in blocks {
let Some(block) = block.as_object() else {
continue;
};
match block
.get("type")
.and_then(Value::as_str)
.unwrap_or_default()
{
"thinking" => {
if let Some(thinking) = block.get("thinking").and_then(Value::as_str) {
if !thinking.is_empty() {
thinking_parts.push(thinking.to_string());
}
}
}
"text" => {
if let Some(text) = block.get("text").and_then(Value::as_str) {
if !text.is_empty() {
text_parts.push(text.to_string());
}
}
}
"tool_use" => {
let Some(tool_use_id) = block
.get("id")
.and_then(Value::as_str)
.map(str::trim)
.filter(|value| !value.is_empty())
else {
continue;
};
let Some(name) = block
.get("name")
.and_then(Value::as_str)
.map(str::trim)
.filter(|value| !value.is_empty())
else {
continue;
};
let input = block
.get("input")
.filter(|value| value.is_object())
.cloned()
.unwrap_or_else(|| json!({}));
tool_uses.push(json!({
"toolUseId": tool_use_id,
"name": name,
"input": input
}));
}
_ => {}
}
}
}
_ => {}
}
let thinking_str = thinking_parts.join("");
let text_str = text_parts.join("");
let mut content_str = if thinking_str.is_empty() {
text_str
} else if text_str.is_empty() {
format!("<thinking>{thinking_str}</thinking>")
} else {
format!("<thinking>{thinking_str}</thinking>\n\n{text_str}")
};
if content_str.is_empty() && !tool_uses.is_empty() {
content_str = " ".to_string();
}
if content_str.is_empty() && tool_uses.is_empty() {
return None;
}
let mut out = Map::new();
out.insert("content".to_string(), Value::String(content_str));
if !tool_uses.is_empty() {
out.insert("toolUses".to_string(), Value::Array(tool_uses));
}
Some(Value::Object(out))
}
#[cfg(test)]
mod tests {
use serde_json::json;
use super::convert_claude_messages_to_conversation_state;
#[test]
fn converts_simple_claude_request_into_conversation_state() {
let conversation_state = convert_claude_messages_to_conversation_state(
&json!({
"messages": [
{"role":"user","content":"hello"}
],
"thinking": {"type": "enabled", "budget_tokens": 128},
"tools": [
{"name":"Write","description":"write file","input_schema":{"type":"object","properties":{},"required":[]}}
]
}),
"claude-sonnet-4-upstream",
)
.expect("conversation state should build");
assert_eq!(
conversation_state
.get("currentMessage")
.and_then(|value| value.get("userInputMessage"))
.and_then(|value| value.get("content"))
.and_then(|value| value.as_str()),
Some(
"<thinking_mode>enabled</thinking_mode><max_thinking_length>128</max_thinking_length>\nhello"
)
);
assert_eq!(
conversation_state
.get("currentMessage")
.and_then(|value| value.get("userInputMessage"))
.and_then(|value| value.get("userInputMessageContext"))
.and_then(|value| value.get("tools"))
.and_then(|value| value.as_array())
.map(Vec::len),
Some(1)
);
}
}
@@ -0,0 +1,154 @@
pub use aether_oauth::provider::providers::{
generate_kiro_machine_id as generate_machine_id,
normalize_kiro_machine_id as normalize_machine_id, KiroAuthConfig, DEFAULT_REGION,
};
#[cfg(test)]
mod tests {
use super::{generate_machine_id, normalize_machine_id, KiroAuthConfig, DEFAULT_REGION};
#[test]
fn normalizes_uuid_machine_id() {
assert_eq!(
normalize_machine_id("123e4567-e89b-12d3-a456-426614174000").as_deref(),
Some("123e4567e89b12d3a456426614174000123e4567e89b12d3a456426614174000")
);
}
#[test]
fn hashes_refresh_token_into_machine_id() {
let auth_config = KiroAuthConfig {
auth_method: None,
refresh_token: Some("r".repeat(128)),
expires_at: None,
profile_arn: None,
region: None,
auth_region: None,
api_region: None,
client_id: None,
client_secret: None,
machine_id: None,
kiro_version: None,
system_version: None,
node_version: None,
access_token: None,
};
let machine_id = generate_machine_id(&auth_config, None).expect("machine id should exist");
assert_eq!(machine_id.len(), 64);
}
#[test]
fn parses_auth_config_aliases() {
let auth_config = KiroAuthConfig::from_raw_json(Some(
r#"{
"authMethod":"identity_center",
"refreshToken":"rrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrr",
"expires_at": 4102444800,
"profileArn":"arn:aws:bedrock:demo",
"apiRegion":"us-west-2",
"clientId":"cid",
"clientSecret":"secret",
"machineId":"123e4567-e89b-12d3-a456-426614174000",
"kiroVersion":"1.2.3",
"systemVersion":"darwin#24.6.0",
"nodeVersion":"22.21.1",
"accessToken":"cached-token"
}"#,
))
.expect("auth config should parse");
assert_eq!(auth_config.auth_method.as_deref(), Some("idc"));
assert_eq!(
auth_config.refresh_token.as_deref(),
Some(
"rrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrr"
)
);
assert_eq!(auth_config.expires_at, Some(4_102_444_800));
assert_eq!(
auth_config.profile_arn.as_deref(),
Some("arn:aws:bedrock:demo")
);
assert_eq!(auth_config.client_id.as_deref(), Some("cid"));
assert_eq!(auth_config.client_secret.as_deref(), Some("secret"));
assert_eq!(auth_config.effective_api_region(), "us-west-2");
assert_eq!(auth_config.effective_kiro_version(), "1.2.3");
assert_eq!(auth_config.effective_system_version(), "darwin#24.6.0");
assert_eq!(auth_config.effective_node_version(), "22.21.1");
assert_eq!(auth_config.access_token.as_deref(), Some("cached-token"));
assert!(auth_config.is_idc_auth());
assert!(auth_config.profile_arn_for_payload().is_none());
assert_eq!(auth_config.effective_auth_region(), "us-east-1");
assert!(auth_config.can_refresh_access_token());
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(
r#"{
"refreshToken":"rt-1",
"clientId":"cid",
"clientSecret":"secret",
"profileArn":"arn:aws:bedrock:demo"
}"#,
))
.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]
fn round_trips_json_value() {
let auth_config = KiroAuthConfig::from_raw_json(Some(
r#"{
"auth_method":"social",
"refreshToken":"rt-1....................................................................................................",
"expires_at": 4102444800,
"profileArn":"arn:aws:bedrock:demo",
"region":"eu-north-1",
"apiRegion":"us-west-2",
"machineId":"123e4567-e89b-12d3-a456-426614174000",
"kiroVersion":"1.2.3",
"systemVersion":"darwin#24.6.0",
"nodeVersion":"22.21.1",
"accessToken":"cached-token"
}"#,
))
.expect("auth config should parse");
let value = auth_config.to_json_value();
let reparsed = KiroAuthConfig::from_json_value(&value).expect("auth config should reparse");
assert_eq!(reparsed, auth_config);
}
}
@@ -0,0 +1,334 @@
use std::collections::BTreeMap;
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 AWS_SDK_JS_LIST_MODELS_VERSION: &str = "1.0.0";
const CODEWHISPERER_OPTOUT: &str = "true";
const KIRO_AGENT_MODE: &str = "vibe";
fn build_kiro_ide_tag(kiro_version: &str, machine_id: &str) -> String {
if machine_id.trim().is_empty() {
format!("KiroIDE-{kiro_version}")
} else {
format!("KiroIDE-{kiro_version}-{machine_id}")
}
}
fn build_x_amz_user_agent_main(kiro_version: &str, machine_id: &str) -> String {
format!(
"aws-sdk-js/{AWS_SDK_JS_MAIN_VERSION} {}",
build_kiro_ide_tag(kiro_version, machine_id)
)
}
fn build_user_agent_main(
system_version: &str,
node_version: &str,
kiro_version: &str,
machine_id: &str,
) -> String {
format!(
"aws-sdk-js/{AWS_SDK_JS_MAIN_VERSION} ua/2.1 os/{system_version} lang/js md/nodejs#{node_version} api/codewhispererstreaming#{AWS_SDK_JS_MAIN_VERSION} m/E {}",
build_kiro_ide_tag(kiro_version, machine_id)
)
}
pub fn build_generate_assistant_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");
BTreeMap::from([
(
"accept".to_string(),
AWS_EVENTSTREAM_CONTENT_TYPE.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(),
),
(
"x-amzn-kiro-agent-mode".to_string(),
KIRO_AGENT_MODE.to_string(),
),
])
}
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
}
pub fn build_list_available_models_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 ide_tag = build_kiro_ide_tag(kiro_version, machine_id);
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=1".to_string(),
),
("connection".to_string(), "close".to_string()),
("host".to_string(), host),
(
"user-agent".to_string(),
format!(
"aws-sdk-js/{AWS_SDK_JS_LIST_MODELS_VERSION} ua/2.1 os/{system_version} lang/js md/nodejs#{node_version} api/codewhispererruntime#1.0.0 m/N,E {ide_tag}"
),
),
(
"x-amz-user-agent".to_string(),
format!("aws-sdk-js/{AWS_SDK_JS_LIST_MODELS_VERSION} {ide_tag}"),
),
])
}
#[cfg(test)]
mod tests {
use super::super::credentials::KiroAuthConfig;
use super::{
build_generate_assistant_headers, build_list_available_models_headers, build_mcp_headers,
AWS_EVENTSTREAM_CONTENT_TYPE, KIRO_PROFILE_ARN_HEADER, KIRO_TOKEN_TYPE_HEADER,
};
#[test]
fn builds_generate_assistant_headers_for_region() {
let auth_config = KiroAuthConfig {
auth_method: None,
refresh_token: None,
expires_at: None,
profile_arn: None,
region: None,
auth_region: None,
api_region: Some("us-west-2".to_string()),
client_id: None,
client_secret: None,
machine_id: None,
kiro_version: Some("1.2.3".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_generate_assistant_headers(&auth_config, "machine-123");
assert_eq!(
headers.get("accept").map(String::as_str),
Some(AWS_EVENTSTREAM_CONTENT_TYPE)
);
assert_eq!(
headers.get("host").map(String::as_str),
Some("q.us-west-2.amazonaws.com")
);
assert_eq!(
headers.get("x-amzn-kiro-agent-mode").map(String::as_str),
Some("vibe")
);
assert!(!headers.contains_key(KIRO_TOKEN_TYPE_HEADER));
}
#[test]
fn builds_list_available_models_headers() {
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.12.155".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_list_available_models_headers(&auth_config, "machine-123");
assert_eq!(
headers.get("host").map(String::as_str),
Some("q.us-east-1.amazonaws.com")
);
assert_eq!(
headers.get("amz-sdk-request").map(String::as_str),
Some("attempt=1; max=1")
);
assert_eq!(
headers.get("x-amz-user-agent").map(String::as_str),
Some("aws-sdk-js/1.0.0 KiroIDE-0.12.155-machine-123")
);
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));
}
}
@@ -0,0 +1,55 @@
mod auth;
mod converter;
mod credentials;
mod headers;
mod policy;
mod refresh;
mod request;
mod url;
use crate::provider_types::{
ProviderApiFormatInheritance, ProviderLocalEmbeddingSupport, ProviderRuntimePolicy,
};
pub const RUNTIME_POLICY: ProviderRuntimePolicy = ProviderRuntimePolicy {
fixed_provider: true,
api_format_inheritance: ProviderApiFormatInheritance::OAuthOrConfiguredBearer,
enable_format_conversion_by_default: true,
allow_auth_channel_mismatch_by_default: true,
oauth_is_bearer_like: true,
supports_model_fetch: true,
supports_local_openai_chat_transport: false,
supports_local_same_format_transport: false,
local_embedding_support: ProviderLocalEmbeddingSupport::None,
};
pub use auth::{
build_kiro_request_auth_from_config, is_kiro_claude_messages_transport,
is_kiro_provider_transport, resolve_local_kiro_bearer_auth, resolve_local_kiro_request_auth,
supports_local_kiro_auth_prerequisites, supports_local_kiro_request_auth_resolution,
KiroBearerAuth, KiroRequestAuth, KIRO_AUTH_HEADER, PROVIDER_TYPE,
};
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, build_list_available_models_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,
};
pub use refresh::KiroOAuthRefreshAdapter;
pub use request::{
apply_local_body_rules_with_request_headers, apply_local_header_rules_with_request_headers,
body_rules_are_locally_supported, build_kiro_provider_headers,
build_kiro_provider_request_body, header_rules_are_locally_supported,
supports_local_kiro_request_shape, KiroProviderHeadersInput,
};
pub use url::{
build_kiro_generate_assistant_response_url, build_kiro_list_available_models_url,
build_kiro_mcp_url, build_kiro_mcp_url_from_resolved_url, resolve_kiro_base_url,
GENERATE_ASSISTANT_RESPONSE_PATH, KIRO_ENVELOPE_NAME, LIST_AVAILABLE_MODELS_PATH, MCP_PATH,
MCP_STREAM_PATH,
};
@@ -0,0 +1,203 @@
use super::super::snapshot::GatewayProviderTransportSnapshot;
use super::super::{
resolve_transport_profile, transport_profile_is_configured,
transport_proxy_is_locally_supported,
};
use super::{
body_rules_are_locally_supported, header_rules_are_locally_supported,
supports_local_kiro_request_auth_resolution, supports_local_kiro_request_shape, PROVIDER_TYPE,
};
pub fn local_kiro_request_transport_unsupported_reason_with_network(
transport: &GatewayProviderTransportSnapshot,
) -> Option<&'static str> {
if !transport.provider.is_active || !transport.endpoint.is_active || !transport.key.is_active {
return if !transport.provider.is_active {
Some("provider_inactive")
} else if !transport.endpoint.is_active {
Some("endpoint_inactive")
} else {
Some("key_inactive")
};
}
if !transport
.provider
.provider_type
.trim()
.eq_ignore_ascii_case(PROVIDER_TYPE)
{
return Some("transport_provider_type_unsupported");
}
if !aether_ai_formats::api_format_alias_matches(
&transport.endpoint.api_format,
"claude:messages",
) {
return Some("transport_api_format_mismatch");
}
if !header_rules_are_locally_supported(transport.endpoint.header_rules.as_ref()) {
return Some("transport_header_rules_unsupported");
}
if !body_rules_are_locally_supported(transport.endpoint.body_rules.as_ref()) {
return Some("transport_body_rules_unsupported");
}
if transport.key.decrypted_auth_config.is_some()
&& !supports_local_kiro_request_auth_resolution(transport)
{
return Some("transport_oauth_resolution_unsupported");
}
if !supports_local_kiro_request_auth_resolution(transport) {
return Some("transport_auth_unavailable");
}
if !transport_proxy_is_locally_supported(transport) {
return Some("transport_proxy_unsupported");
}
if transport_profile_is_configured(transport) && resolve_transport_profile(transport).is_none()
{
return Some("transport_profile_unsupported");
}
None
}
pub fn supports_local_kiro_request_transport(transport: &GatewayProviderTransportSnapshot) -> bool {
if !transport.provider.is_active || !transport.endpoint.is_active || !transport.key.is_active {
return false;
}
if !transport
.provider
.provider_type
.trim()
.eq_ignore_ascii_case(PROVIDER_TYPE)
{
return false;
}
if !aether_ai_formats::api_format_alias_matches(
&transport.endpoint.api_format,
"claude:messages",
) {
return false;
}
supports_local_kiro_request_shape(
transport.endpoint.header_rules.as_ref(),
transport.endpoint.body_rules.as_ref(),
) && supports_local_kiro_request_auth_resolution(transport)
}
pub fn supports_local_kiro_request_transport_with_network(
transport: &GatewayProviderTransportSnapshot,
) -> bool {
local_kiro_request_transport_unsupported_reason_with_network(transport).is_none()
}
#[cfg(test)]
mod tests {
use super::super::super::snapshot::{
GatewayProviderTransportEndpoint, GatewayProviderTransportKey,
GatewayProviderTransportProvider, GatewayProviderTransportSnapshot,
};
use super::{
local_kiro_request_transport_unsupported_reason_with_network,
supports_local_kiro_request_transport, supports_local_kiro_request_transport_with_network,
};
fn sample_transport() -> GatewayProviderTransportSnapshot {
GatewayProviderTransportSnapshot {
provider: GatewayProviderTransportProvider {
id: "provider-1".to_string(),
name: "Kiro".to_string(),
provider_type: "kiro".to_string(),
website: None,
is_active: true,
keep_priority_on_conversion: false,
enable_format_conversion: false,
concurrent_limit: None,
max_retries: None,
proxy: None,
request_timeout_secs: None,
stream_first_byte_timeout_secs: None,
config: None,
},
endpoint: GatewayProviderTransportEndpoint {
id: "endpoint-1".to_string(),
provider_id: "provider-1".to_string(),
api_format: "claude:messages".to_string(),
api_family: Some("claude".to_string()),
endpoint_kind: Some("messages".to_string()),
is_active: true,
base_url: "https://kiro.example".to_string(),
header_rules: None,
body_rules: None,
max_retries: None,
custom_path: None,
config: None,
format_acceptance_config: None,
proxy: None,
},
key: GatewayProviderTransportKey {
id: "key-1".to_string(),
provider_id: "provider-1".to_string(),
name: "key".to_string(),
auth_type: "bearer".to_string(),
is_active: true,
api_formats: Some(vec!["claude:messages".to_string()]),
auth_type_by_format: None,
allow_auth_channel_mismatch_formats: None,
allowed_models: None,
capabilities: None,
rate_multipliers: None,
global_priority_by_format: None,
expires_at_unix_secs: None,
proxy: None,
fingerprint: None,
upstream_metadata: None,
decrypted_api_key: "__placeholder__".to_string(),
decrypted_auth_config: Some(
r#"{
"access_token":"cached-token",
"refresh_token":"rrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrr",
"machine_id":"123e4567-e89b-12d3-a456-426614174000"
}"#
.to_string(),
),
},
}
}
#[test]
fn supports_kiro_request_transport_when_cached_access_token_exists() {
assert!(supports_local_kiro_request_transport(&sample_transport()));
assert!(supports_local_kiro_request_transport_with_network(
&sample_transport()
));
}
#[test]
fn supports_kiro_request_transport_when_refresh_only_auth_exists() {
let mut transport = sample_transport();
transport.key.decrypted_api_key = "__placeholder__".to_string();
transport.key.decrypted_auth_config = Some(
r#"{
"refresh_token":"rrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrr"
}"#
.to_string(),
);
assert!(supports_local_kiro_request_transport(&transport));
assert!(supports_local_kiro_request_transport_with_network(
&transport
));
}
#[test]
fn reports_auth_unavailable_when_kiro_key_cannot_resolve_request_auth() {
let mut transport = sample_transport();
transport.key.auth_type = "api_key".to_string();
transport.key.decrypted_auth_config = None;
assert_eq!(
local_kiro_request_transport_unsupported_reason_with_network(&transport),
Some("transport_auth_unavailable")
);
}
}
@@ -0,0 +1,446 @@
use aether_oauth::provider::providers::KiroProviderOAuthAdapter as CoreKiroProviderOAuthAdapter;
use async_trait::async_trait;
use super::super::oauth_refresh::{
oauth_error_to_local_refresh_error, provider_oauth_transport_context_from_snapshot,
CachedOAuthEntry, LocalOAuthHttpExecutor, LocalOAuthRefreshAdapter, LocalOAuthRefreshError,
LocalResolvedOAuthRequestAuth, ProviderOAuthLocalHttpExecutor,
};
use super::super::snapshot::GatewayProviderTransportSnapshot;
use super::auth::{
build_kiro_request_auth_from_config, resolve_local_kiro_request_auth, PROVIDER_TYPE,
};
use super::credentials::KiroAuthConfig;
#[cfg(test)]
const IDC_AMZ_USER_AGENT: &str = "aws-sdk-js/3.738.0 ua/2.1 os/other lang/js md/browser#unknown_unknown api/sso-oidc#3.738.0 m/E KiroIDE";
#[derive(Debug, Clone, Default)]
pub struct KiroOAuthRefreshAdapter {
social_refresh_base_url: Option<String>,
idc_refresh_base_url: Option<String>,
}
impl KiroOAuthRefreshAdapter {
pub fn with_refresh_base_urls(
mut self,
social_refresh_base_url: Option<String>,
idc_refresh_base_url: Option<String>,
) -> Self {
self.social_refresh_base_url = social_refresh_base_url;
self.idc_refresh_base_url = idc_refresh_base_url;
self
}
pub async fn refresh_auth_config(
&self,
executor: &dyn LocalOAuthHttpExecutor,
transport: &GatewayProviderTransportSnapshot,
auth_config: &KiroAuthConfig,
) -> Result<KiroAuthConfig, LocalOAuthRefreshError> {
let adapter = CoreKiroProviderOAuthAdapter::default().with_refresh_base_urls(
self.social_refresh_base_url.clone(),
self.idc_refresh_base_url.clone(),
);
let oauth_executor =
ProviderOAuthLocalHttpExecutor::new(PROVIDER_TYPE, transport, executor);
let ctx = provider_oauth_transport_context_from_snapshot(transport);
adapter
.refresh_auth_config(&oauth_executor, &ctx, auth_config)
.await
.map_err(|error| oauth_error_to_local_refresh_error(PROVIDER_TYPE, error))
}
fn auth_config_from_entry(entry: &CachedOAuthEntry) -> Option<KiroAuthConfig> {
entry
.metadata
.as_ref()
.filter(|_| entry.provider_type.eq_ignore_ascii_case(PROVIDER_TYPE))
.and_then(KiroAuthConfig::from_json_value)
}
fn base_auth_config(
&self,
transport: &GatewayProviderTransportSnapshot,
entry: Option<&CachedOAuthEntry>,
) -> Option<KiroAuthConfig> {
entry.and_then(Self::auth_config_from_entry).or_else(|| {
KiroAuthConfig::from_raw_json(transport.key.decrypted_auth_config.as_deref())
})
}
fn build_cached_entry(auth_config: &KiroAuthConfig) -> Option<CachedOAuthEntry> {
let request_auth = build_kiro_request_auth_from_config(auth_config.clone(), None)?;
Some(CachedOAuthEntry {
provider_type: PROVIDER_TYPE.to_string(),
auth_header_name: request_auth.name.to_string(),
auth_header_value: request_auth.value,
expires_at_unix_secs: auth_config.expires_at,
metadata: Some(auth_config.to_json_value()),
})
}
fn refreshable_auth_config(
&self,
transport: &GatewayProviderTransportSnapshot,
entry: Option<&CachedOAuthEntry>,
) -> Option<KiroAuthConfig> {
let auth_config = self.base_auth_config(transport, entry)?;
auth_config
.can_refresh_access_token()
.then_some(auth_config)
}
}
#[async_trait]
impl LocalOAuthRefreshAdapter for KiroOAuthRefreshAdapter {
fn provider_type(&self) -> &'static str {
PROVIDER_TYPE
}
fn resolve_cached(
&self,
_transport: &GatewayProviderTransportSnapshot,
entry: &CachedOAuthEntry,
) -> Option<LocalResolvedOAuthRequestAuth> {
let auth_config = Self::auth_config_from_entry(entry)?;
let request_auth = build_kiro_request_auth_from_config(auth_config, None)?;
Some(LocalResolvedOAuthRequestAuth::Kiro(request_auth))
}
fn resolve_without_refresh(
&self,
transport: &GatewayProviderTransportSnapshot,
) -> Option<LocalResolvedOAuthRequestAuth> {
resolve_local_kiro_request_auth(transport).map(LocalResolvedOAuthRequestAuth::Kiro)
}
fn should_refresh(
&self,
transport: &GatewayProviderTransportSnapshot,
entry: Option<&CachedOAuthEntry>,
) -> bool {
entry
.and_then(|cached| self.resolve_cached(transport, cached))
.is_none()
&& self.resolve_without_refresh(transport).is_none()
&& self.refreshable_auth_config(transport, entry).is_some()
}
async fn refresh(
&self,
executor: &dyn LocalOAuthHttpExecutor,
transport: &GatewayProviderTransportSnapshot,
entry: Option<&CachedOAuthEntry>,
) -> Result<Option<CachedOAuthEntry>, LocalOAuthRefreshError> {
let Some(auth_config) = self.refreshable_auth_config(transport, entry) else {
return Ok(None);
};
let refreshed = self
.refresh_auth_config(executor, transport, &auth_config)
.await?;
Ok(Self::build_cached_entry(&refreshed))
}
}
#[cfg(test)]
mod tests {
use std::sync::{Arc, Mutex};
use super::super::super::oauth_refresh::{
LocalOAuthRefreshAdapter, LocalResolvedOAuthRequestAuth, ReqwestLocalOAuthHttpExecutor,
};
use super::super::super::snapshot::{
GatewayProviderTransportEndpoint, GatewayProviderTransportKey,
GatewayProviderTransportProvider, GatewayProviderTransportSnapshot,
};
use super::{KiroOAuthRefreshAdapter, IDC_AMZ_USER_AGENT};
use axum::body::to_bytes;
use axum::extract::Request;
use axum::response::IntoResponse;
use axum::routing::any;
use axum::{Json, Router};
use http::StatusCode;
use serde_json::{json, Value};
use tokio::task::JoinHandle;
#[derive(Debug, Clone)]
struct SeenRefreshRequest {
body: Value,
authorization: String,
host: String,
user_agent: String,
x_amz_user_agent: String,
}
fn sample_transport(raw_auth_config: &str) -> GatewayProviderTransportSnapshot {
GatewayProviderTransportSnapshot {
provider: GatewayProviderTransportProvider {
id: "provider-1".to_string(),
name: "Kiro".to_string(),
provider_type: "kiro".to_string(),
website: None,
is_active: true,
keep_priority_on_conversion: false,
enable_format_conversion: false,
concurrent_limit: None,
max_retries: None,
proxy: None,
request_timeout_secs: None,
stream_first_byte_timeout_secs: None,
config: None,
},
endpoint: GatewayProviderTransportEndpoint {
id: "endpoint-1".to_string(),
provider_id: "provider-1".to_string(),
api_format: "claude:messages".to_string(),
api_family: Some("claude".to_string()),
endpoint_kind: Some("cli".to_string()),
is_active: true,
base_url: "https://kiro.example".to_string(),
header_rules: None,
body_rules: None,
max_retries: None,
custom_path: None,
config: None,
format_acceptance_config: None,
proxy: None,
},
key: GatewayProviderTransportKey {
id: "key-1".to_string(),
provider_id: "provider-1".to_string(),
name: "key".to_string(),
auth_type: "bearer".to_string(),
is_active: true,
api_formats: Some(vec!["claude:messages".to_string()]),
auth_type_by_format: None,
allow_auth_channel_mismatch_formats: None,
allowed_models: None,
capabilities: None,
rate_multipliers: None,
global_priority_by_format: None,
expires_at_unix_secs: None,
proxy: None,
fingerprint: None,
upstream_metadata: None,
decrypted_api_key: "__placeholder__".to_string(),
decrypted_auth_config: Some(raw_auth_config.to_string()),
},
}
}
async fn start_server(app: Router) -> (String, JoinHandle<()>) {
let listener = tokio::net::TcpListener::bind("127.0.0.1:0")
.await
.expect("listener should bind");
let addr = listener
.local_addr()
.expect("listener should expose local addr");
let handle = tokio::spawn(async move {
axum::serve(listener, app).await.expect("server should run");
});
(format!("http://{addr}"), handle)
}
#[tokio::test]
async fn refreshes_social_token_via_adapter() {
let seen_request = Arc::new(Mutex::new(None::<SeenRefreshRequest>));
let seen_request_clone = Arc::clone(&seen_request);
let server = Router::new().route(
"/refreshToken",
any(move |request: Request| {
let seen_request_inner = Arc::clone(&seen_request_clone);
async move {
let (parts, body) = request.into_parts();
let raw_body = to_bytes(body, usize::MAX).await.expect("body should read");
let body: Value =
serde_json::from_slice(&raw_body).expect("body should parse as json");
*seen_request_inner.lock().expect("mutex should lock") =
Some(SeenRefreshRequest {
body,
authorization: parts
.headers
.get("authorization")
.and_then(|value| value.to_str().ok())
.unwrap_or_default()
.to_string(),
host: parts
.headers
.get("host")
.and_then(|value| value.to_str().ok())
.unwrap_or_default()
.to_string(),
user_agent: parts
.headers
.get("user-agent")
.and_then(|value| value.to_str().ok())
.unwrap_or_default()
.to_string(),
x_amz_user_agent: parts
.headers
.get("x-amz-user-agent")
.and_then(|value| value.to_str().ok())
.unwrap_or_default()
.to_string(),
});
(
StatusCode::OK,
Json(json!({
"accessToken": "cached-kiro-access-token",
"refreshToken": "s".repeat(120),
"expiresIn": 3600,
"profileArn": "arn:aws:bedrock:demo"
})),
)
.into_response()
}
}),
);
let (server_url, server_handle) = start_server(server).await;
let adapter =
KiroOAuthRefreshAdapter::default().with_refresh_base_urls(Some(server_url), None);
let transport = sample_transport(
r#"{
"refresh_token":"rrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrr",
"machine_id":"123e4567-e89b-12d3-a456-426614174000",
"kiro_version":"1.2.3"
}"#,
);
let executor = ReqwestLocalOAuthHttpExecutor::new(reqwest::Client::new());
let entry = adapter
.refresh(&executor, &transport, None)
.await
.expect("refresh should succeed")
.expect("cached entry should exist");
let resolved = adapter
.resolve_cached(&transport, &entry)
.expect("cached entry should resolve");
let seen_request = seen_request
.lock()
.expect("mutex should lock")
.clone()
.expect("refresh request should be captured");
assert_eq!(seen_request.body["refreshToken"], json!("r".repeat(120)));
assert_eq!(seen_request.authorization, "");
assert!(!seen_request.user_agent.is_empty());
assert_eq!(seen_request.x_amz_user_agent, "");
assert!(!seen_request.host.trim().is_empty());
match resolved {
LocalResolvedOAuthRequestAuth::Kiro(auth) => {
assert_eq!(auth.value, "Bearer cached-kiro-access-token");
assert_eq!(
auth.auth_config.profile_arn.as_deref(),
Some("arn:aws:bedrock:demo")
);
assert!(auth.auth_config.expires_at.is_some());
}
other => panic!("unexpected resolved auth: {other:?}"),
}
server_handle.abort();
}
#[tokio::test]
async fn refreshes_idc_token_via_adapter() {
let seen_request = Arc::new(Mutex::new(None::<SeenRefreshRequest>));
let seen_request_clone = Arc::clone(&seen_request);
let server = Router::new().route(
"/token",
any(move |request: Request| {
let seen_request_inner = Arc::clone(&seen_request_clone);
async move {
let (parts, body) = request.into_parts();
let raw_body = to_bytes(body, usize::MAX).await.expect("body should read");
let body: Value =
serde_json::from_slice(&raw_body).expect("body should parse as json");
*seen_request_inner.lock().expect("mutex should lock") =
Some(SeenRefreshRequest {
body,
authorization: parts
.headers
.get("authorization")
.and_then(|value| value.to_str().ok())
.unwrap_or_default()
.to_string(),
host: parts
.headers
.get("host")
.and_then(|value| value.to_str().ok())
.unwrap_or_default()
.to_string(),
user_agent: parts
.headers
.get("user-agent")
.and_then(|value| value.to_str().ok())
.unwrap_or_default()
.to_string(),
x_amz_user_agent: parts
.headers
.get("x-amz-user-agent")
.and_then(|value| value.to_str().ok())
.unwrap_or_default()
.to_string(),
});
(
StatusCode::OK,
Json(json!({
"accessToken": "cached-idc-access-token",
"refreshToken": "i".repeat(120),
"expiresIn": 1800
})),
)
.into_response()
}
}),
);
let (server_url, server_handle) = start_server(server).await;
let adapter =
KiroOAuthRefreshAdapter::default().with_refresh_base_urls(None, Some(server_url));
let transport = sample_transport(
r#"{
"auth_method":"identity_center",
"refresh_token":"rrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrr",
"client_id":"cid",
"client_secret":"secret",
"profile_arn":"arn:aws:bedrock:demo"
}"#,
);
let executor = ReqwestLocalOAuthHttpExecutor::new(reqwest::Client::new());
let entry = adapter
.refresh(&executor, &transport, None)
.await
.expect("refresh should succeed")
.expect("cached entry should exist");
let resolved = adapter
.resolve_cached(&transport, &entry)
.expect("cached entry should resolve");
let seen_request = seen_request
.lock()
.expect("mutex should lock")
.clone()
.expect("refresh request should be captured");
assert_eq!(
seen_request.body["grantType"].as_str(),
Some("refresh_token")
);
assert_eq!(seen_request.body["clientId"].as_str(), Some("cid"));
assert_eq!(seen_request.user_agent, "node");
assert_eq!(seen_request.x_amz_user_agent, IDC_AMZ_USER_AGENT);
assert!(!seen_request.host.trim().is_empty());
match resolved {
LocalResolvedOAuthRequestAuth::Kiro(auth) => {
assert_eq!(auth.value, "Bearer cached-idc-access-token");
assert!(auth.auth_config.profile_arn_for_payload().is_none());
assert!(auth.auth_config.expires_at.is_some());
}
other => panic!("unexpected resolved auth: {other:?}"),
}
server_handle.abort();
}
}
@@ -0,0 +1,309 @@
use std::collections::BTreeMap;
use serde_json::{json, Value};
pub use super::super::rules::{
apply_local_body_rules_with_request_headers, apply_local_header_rules_with_request_headers,
body_rules_are_locally_supported, header_rules_are_locally_supported,
};
use super::super::should_skip_upstream_passthrough_header;
use super::converter::convert_claude_messages_to_conversation_state;
use super::credentials::KiroAuthConfig;
use super::headers::build_generate_assistant_headers;
pub fn supports_local_kiro_request_shape(
header_rules: Option<&Value>,
body_rules: Option<&Value>,
) -> bool {
header_rules_are_locally_supported(header_rules) && body_rules_are_locally_supported(body_rules)
}
pub fn build_kiro_provider_request_body(
body_json: &Value,
mapped_model: &str,
auth_config: &KiroAuthConfig,
body_rules: Option<&Value>,
request_headers: Option<&http::HeaderMap>,
) -> Option<Value> {
let conversation_state =
convert_claude_messages_to_conversation_state(body_json, mapped_model)?;
let mut provider_request_body = json!({
"conversationState": conversation_state
});
let mut inference_config = serde_json::Map::new();
if let Some(max_tokens) = body_json
.get("max_tokens")
.and_then(|value| {
value
.as_i64()
.or_else(|| value.as_u64().map(|value| value as i64))
})
.filter(|value| *value > 0)
{
inference_config.insert("maxTokens".to_string(), Value::from(max_tokens));
}
if let Some(temperature) = body_json
.get("temperature")
.and_then(Value::as_f64)
.filter(|value| *value >= 0.0)
{
inference_config.insert("temperature".to_string(), Value::from(temperature));
}
if let Some(top_p) = body_json
.get("top_p")
.and_then(Value::as_f64)
.filter(|value| *value > 0.0)
{
inference_config.insert("topP".to_string(), Value::from(top_p));
}
if !inference_config.is_empty() {
provider_request_body.as_object_mut()?.insert(
"inferenceConfig".to_string(),
Value::Object(inference_config),
);
}
if let Some(profile_arn) = auth_config.profile_arn_for_payload() {
provider_request_body.as_object_mut()?.insert(
"profileArn".to_string(),
Value::String(profile_arn.to_string()),
);
}
if !apply_local_body_rules_with_request_headers(
&mut provider_request_body,
body_rules,
Some(body_json),
request_headers,
) {
return None;
}
Some(provider_request_body)
}
#[derive(Clone, Copy, Debug)]
pub struct KiroProviderHeadersInput<'a> {
pub headers: &'a http::HeaderMap,
pub provider_request_body: &'a Value,
pub original_request_body: &'a Value,
pub header_rules: Option<&'a Value>,
pub auth_header: &'a str,
pub auth_value: &'a str,
pub auth_config: &'a KiroAuthConfig,
pub machine_id: &'a str,
}
pub fn build_kiro_provider_headers(
input: KiroProviderHeadersInput<'_>,
) -> Option<BTreeMap<String, String>> {
let KiroProviderHeadersInput {
headers,
provider_request_body,
original_request_body,
header_rules,
auth_header,
auth_value,
auth_config,
machine_id,
} = input;
let mut out = BTreeMap::new();
for (name, value) in headers {
let Ok(value) = value.to_str() else {
continue;
};
let key = name.as_str().to_ascii_lowercase();
if should_skip_upstream_passthrough_header(&key) {
continue;
}
let value = value.trim();
if value.is_empty() {
continue;
}
out.insert(key, value.to_string());
}
if !apply_local_header_rules_with_request_headers(
&mut out,
header_rules,
&[auth_header, "content-type"],
provider_request_body,
Some(original_request_body),
Some(headers),
) {
return None;
}
for (key, value) in build_generate_assistant_headers(auth_config, machine_id) {
out.insert(key, value);
}
out.insert(
auth_header.trim().to_ascii_lowercase(),
auth_value.trim().to_string(),
);
out.entry("content-type".to_string())
.or_insert_with(|| "application/json".to_string());
out.remove("content-length");
Some(out)
}
#[cfg(test)]
mod tests {
use serde_json::json;
use super::super::credentials::KiroAuthConfig;
use super::{
build_kiro_provider_headers, build_kiro_provider_request_body,
supports_local_kiro_request_shape, KiroProviderHeadersInput,
};
#[test]
fn supports_empty_local_request_shape() {
assert!(supports_local_kiro_request_shape(None, None));
}
#[test]
fn rejects_unsupported_rule_shape() {
assert!(!supports_local_kiro_request_shape(
Some(&json!({"action":"set"})),
None
));
}
#[test]
fn supports_simple_header_and_body_rules() {
assert!(supports_local_kiro_request_shape(
Some(&json!([{"action":"set","key":"x-provider-extra","value":"1"}])),
Some(&json!([{"action":"set","path":"debugTag","value":true}]))
));
}
#[test]
fn wraps_claude_request_into_kiro_payload_before_body_rules() {
let auth_config = KiroAuthConfig {
auth_method: None,
refresh_token: Some("r".repeat(128)),
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: Some("123e4567-e89b-12d3-a456-426614174000".to_string()),
kiro_version: None,
system_version: None,
node_version: None,
access_token: Some("cached-token".to_string()),
};
let payload = build_kiro_provider_request_body(
&json!({
"messages": [{"role":"user","content":"hello"}],
"max_tokens": 64
}),
"claude-sonnet-4-upstream",
&auth_config,
Some(&json!([
{"action":"set","path":"debugTag","value":"kiro-local"}
])),
None,
)
.expect("payload should build");
assert!(payload.get("conversationState").is_some());
assert_eq!(
payload
.get("inferenceConfig")
.and_then(|value| value.get("maxTokens")),
Some(&json!(64))
);
assert_eq!(
payload.get("profileArn"),
Some(&json!("arn:aws:bedrock:demo"))
);
assert_eq!(payload.get("debugTag"), Some(&json!("kiro-local")));
}
#[test]
fn applies_header_rules_before_kiro_extra_headers() {
let auth_config = KiroAuthConfig {
auth_method: None,
refresh_token: Some("r".repeat(128)),
expires_at: None,
profile_arn: None,
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("cached-token".to_string()),
};
let headers = build_kiro_provider_headers(KiroProviderHeadersInput {
headers: &http::HeaderMap::new(),
provider_request_body: &json!({"conversationState": {}}),
original_request_body: &json!({"messages": []}),
header_rules: Some(&json!([
{"action":"set","key":"accept","value":"text/plain"},
{"action":"set","key":"x-endpoint-tag","value":"kiro-local"}
])),
auth_header: "authorization",
auth_value: "Bearer cached-token",
auth_config: &auth_config,
machine_id: "machine-123",
})
.expect("headers should build");
assert_eq!(
headers.get("accept").map(String::as_str),
Some("application/vnd.amazon.eventstream")
);
assert_eq!(
headers.get("authorization").map(String::as_str),
Some("Bearer cached-token")
);
assert_eq!(
headers.get("x-endpoint-tag").map(String::as_str),
Some("kiro-local")
);
}
#[test]
fn omits_profile_arn_for_idc_auth() {
let auth_config = KiroAuthConfig {
auth_method: Some("identity_center".to_string()),
refresh_token: Some("r".repeat(128)),
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: Some("cid".to_string()),
client_secret: Some("secret".to_string()),
machine_id: None,
kiro_version: None,
system_version: None,
node_version: None,
access_token: Some("cached-token".to_string()),
};
let payload = build_kiro_provider_request_body(
&json!({
"messages": [{"role":"user","content":"hello"}]
}),
"claude-sonnet-4-upstream",
&auth_config,
None,
None,
)
.expect("payload should build");
assert!(payload.get("profileArn").is_none());
}
}
@@ -0,0 +1,138 @@
use super::super::url::build_passthrough_path_url;
use super::credentials::DEFAULT_REGION;
pub const GENERATE_ASSISTANT_RESPONSE_PATH: &str = "/generateAssistantResponse";
pub const LIST_AVAILABLE_MODELS_PATH: &str = "/ListAvailableModels";
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 {
let region = api_region
.map(str::trim)
.filter(|value| !value.is_empty())
.unwrap_or(DEFAULT_REGION);
upstream_base_url
.trim()
.replace("{region}", region)
.trim_end_matches('/')
.to_string()
}
pub fn build_kiro_generate_assistant_response_url(
upstream_base_url: &str,
query: Option<&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(),
GENERATE_ASSISTANT_RESPONSE_PATH,
query,
&[],
)
}
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_list_available_models_url(
upstream_base_url: &str,
api_region: Option<&str>,
) -> Option<String> {
let mut serializer = url::form_urlencoded::Serializer::new(String::new());
serializer.append_pair("origin", "AI_EDITOR");
let query = serializer.finish();
let upstream_base_url = resolve_kiro_base_url(upstream_base_url, api_region);
build_passthrough_path_url(
upstream_base_url.as_str(),
LIST_AVAILABLE_MODELS_PATH,
Some(&query),
&[],
)
}
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, build_kiro_list_available_models_url,
build_kiro_mcp_url, build_kiro_mcp_url_from_resolved_url, resolve_kiro_base_url,
GENERATE_ASSISTANT_RESPONSE_PATH, KIRO_ENVELOPE_NAME, LIST_AVAILABLE_MODELS_PATH, MCP_PATH,
MCP_STREAM_PATH,
};
#[test]
fn exposes_kiro_request_constants() {
assert_eq!(
GENERATE_ASSISTANT_RESPONSE_PATH,
"/generateAssistantResponse"
);
assert_eq!(LIST_AVAILABLE_MODELS_PATH, "/ListAvailableModels");
assert_eq!(MCP_PATH, "/mcp");
assert_eq!(MCP_STREAM_PATH, "/mcp/stream");
assert_eq!(KIRO_ENVELOPE_NAME, "kiro:generateAssistantResponse");
}
#[test]
fn builds_generate_assistant_response_url() {
assert_eq!(
build_kiro_generate_assistant_response_url(
"https://kiro.{region}.example?tenant=demo",
Some("stream=true"),
Some("us-west-2")
)
.as_deref(),
Some(
"https://kiro.us-west-2.example/generateAssistantResponse?stream=true&tenant=demo"
)
);
}
#[test]
fn resolves_region_placeholder_in_base_url() {
assert_eq!(
resolve_kiro_base_url("https://kiro.{region}.example/", Some("us-west-2")),
"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 builds_list_available_models_url_for_profile() {
assert_eq!(
build_kiro_list_available_models_url(
"https://q.{region}.amazonaws.com",
Some("us-west-2")
)
.as_deref(),
Some("https://q.us-west-2.amazonaws.com/ListAvailableModels?origin=AI_EDITOR")
);
}
#[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")
);
}
}