mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-01 17:00:21 +08:00
fix(kiro): 修复 Claude CLI 跨格式 Responses 流式转换 (#329)
This commit is contained in:
@@ -3,6 +3,7 @@
|
||||
use aether_provider_transport::auth::{
|
||||
resolve_local_gemini_auth, resolve_local_openai_bearer_auth, resolve_local_standard_auth,
|
||||
};
|
||||
use aether_provider_transport::kiro::local_kiro_request_transport_unsupported_reason_with_network;
|
||||
use aether_provider_transport::policy::{
|
||||
local_gemini_transport_unsupported_reason_with_network,
|
||||
local_openai_chat_transport_unsupported_reason,
|
||||
@@ -222,6 +223,20 @@ pub fn request_pair_allowed_for_transport(
|
||||
if request_conversion_kind(client_api_format.as_str(), provider_api_format.as_str()).is_none() {
|
||||
return false;
|
||||
}
|
||||
if transport
|
||||
.provider
|
||||
.provider_type
|
||||
.trim()
|
||||
.eq_ignore_ascii_case("kiro")
|
||||
&& provider_api_format.eq_ignore_ascii_case("claude:cli")
|
||||
{
|
||||
return request_conversion_enabled_for_transport(
|
||||
transport,
|
||||
client_api_format.as_str(),
|
||||
provider_api_format.as_str(),
|
||||
) && local_kiro_request_transport_unsupported_reason_with_network(transport)
|
||||
.is_none();
|
||||
}
|
||||
request_conversion_enabled_for_transport(
|
||||
transport,
|
||||
client_api_format.as_str(),
|
||||
@@ -240,6 +255,20 @@ pub fn request_conversion_transport_unsupported_reason(
|
||||
transport: &GatewayProviderTransportSnapshot,
|
||||
_kind: RequestConversionKind,
|
||||
) -> Option<&'static str> {
|
||||
if transport
|
||||
.provider
|
||||
.provider_type
|
||||
.trim()
|
||||
.eq_ignore_ascii_case("kiro")
|
||||
&& transport
|
||||
.endpoint
|
||||
.api_format
|
||||
.trim()
|
||||
.eq_ignore_ascii_case("claude:cli")
|
||||
{
|
||||
return local_kiro_request_transport_unsupported_reason_with_network(transport);
|
||||
}
|
||||
|
||||
match transport
|
||||
.endpoint
|
||||
.api_format
|
||||
@@ -898,4 +927,68 @@ mod tests {
|
||||
Some(("key".to_string(), "vertex-secret".to_string()))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn kiro_claude_cli_transport_supports_cross_format_conversion_via_envelope() {
|
||||
let transport = GatewayProviderTransportSnapshot {
|
||||
provider: GatewayProviderTransportProvider {
|
||||
id: "provider-kiro".to_string(),
|
||||
name: "kiro".to_string(),
|
||||
provider_type: "kiro".to_string(),
|
||||
website: None,
|
||||
is_active: true,
|
||||
keep_priority_on_conversion: false,
|
||||
enable_format_conversion: true,
|
||||
concurrent_limit: None,
|
||||
max_retries: None,
|
||||
proxy: None,
|
||||
request_timeout_secs: None,
|
||||
stream_first_byte_timeout_secs: None,
|
||||
config: None,
|
||||
},
|
||||
endpoint: GatewayProviderTransportEndpoint {
|
||||
id: "endpoint-kiro".to_string(),
|
||||
provider_id: "provider-kiro".to_string(),
|
||||
api_format: "claude:cli".to_string(),
|
||||
api_family: Some("claude".to_string()),
|
||||
endpoint_kind: Some("cli".to_string()),
|
||||
is_active: true,
|
||||
base_url: "https://q.{region}.amazonaws.com".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-kiro".to_string(),
|
||||
provider_id: "provider-kiro".to_string(),
|
||||
name: "key".to_string(),
|
||||
auth_type: "bearer".to_string(),
|
||||
is_active: true,
|
||||
api_formats: Some(vec!["claude:cli".to_string()]),
|
||||
allowed_models: None,
|
||||
capabilities: None,
|
||||
rate_multipliers: None,
|
||||
global_priority_by_format: None,
|
||||
expires_at_unix_secs: None,
|
||||
proxy: None,
|
||||
fingerprint: None,
|
||||
decrypted_api_key: "kiro-secret".to_string(),
|
||||
decrypted_auth_config: None,
|
||||
},
|
||||
};
|
||||
|
||||
assert!(request_pair_allowed_for_transport(
|
||||
&transport,
|
||||
"openai:chat",
|
||||
"claude:cli"
|
||||
));
|
||||
assert!(request_conversion_transport_supported(
|
||||
&transport,
|
||||
RequestConversionKind::ToClaudeStandard
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -946,6 +946,8 @@ struct OpenAICliClientToolState {
|
||||
pub struct OpenAICliClientEmitter {
|
||||
response_id: Option<String>,
|
||||
model: Option<String>,
|
||||
message_item_id: Option<String>,
|
||||
reasoning_item_id: Option<String>,
|
||||
started: bool,
|
||||
finished: bool,
|
||||
sequence_number: u64,
|
||||
@@ -1166,11 +1168,29 @@ impl OpenAICliClientEmitter {
|
||||
}
|
||||
|
||||
fn message_item_id(&self) -> String {
|
||||
format!("{}_msg", self.response_id())
|
||||
self.message_item_id
|
||||
.clone()
|
||||
.unwrap_or_else(|| format!("{}_msg", self.response_id()))
|
||||
}
|
||||
|
||||
fn reasoning_item_id(&self) -> String {
|
||||
format!("{}_rs_0", self.response_id())
|
||||
self.reasoning_item_id
|
||||
.clone()
|
||||
.unwrap_or_else(|| format!("{}_rs_0", self.response_id()))
|
||||
}
|
||||
|
||||
fn ensure_message_item_id(&mut self) -> String {
|
||||
if self.message_item_id.is_none() {
|
||||
self.message_item_id = Some(format!("{}_msg", self.response_id()));
|
||||
}
|
||||
self.message_item_id()
|
||||
}
|
||||
|
||||
fn ensure_reasoning_item_id(&mut self) -> String {
|
||||
if self.reasoning_item_id.is_none() {
|
||||
self.reasoning_item_id = Some(format!("{}_rs_0", self.response_id()));
|
||||
}
|
||||
self.reasoning_item_id()
|
||||
}
|
||||
|
||||
fn in_progress_response(&self) -> Value {
|
||||
@@ -1269,6 +1289,7 @@ impl OpenAICliClientEmitter {
|
||||
fn ensure_reasoning_item_started(&mut self) -> Result<Vec<u8>, PipelineFinalizeError> {
|
||||
let mut out = self.ensure_started()?;
|
||||
let output_index = self.ensure_reasoning_output_index();
|
||||
let item_id = self.ensure_reasoning_item_id();
|
||||
if !self.reasoning_item_started {
|
||||
out.extend(self.encode_response_event(
|
||||
"response.output_item.added",
|
||||
@@ -1278,7 +1299,7 @@ impl OpenAICliClientEmitter {
|
||||
"output_index": output_index,
|
||||
"item": {
|
||||
"type": "reasoning",
|
||||
"id": self.reasoning_item_id(),
|
||||
"id": item_id.clone(),
|
||||
"summary": [],
|
||||
}
|
||||
}),
|
||||
@@ -1291,7 +1312,7 @@ impl OpenAICliClientEmitter {
|
||||
json!({
|
||||
"type": "response.reasoning_summary_part.added",
|
||||
"response_id": self.response_id(),
|
||||
"item_id": self.reasoning_item_id(),
|
||||
"item_id": item_id,
|
||||
"output_index": output_index,
|
||||
"summary_index": 0,
|
||||
"part": {
|
||||
@@ -1308,8 +1329,8 @@ impl OpenAICliClientEmitter {
|
||||
fn ensure_text_item_started(&mut self) -> Result<Vec<u8>, PipelineFinalizeError> {
|
||||
let mut out = self.ensure_started()?;
|
||||
let output_index = self.ensure_message_output_index();
|
||||
let item_id = self.ensure_message_item_id();
|
||||
if !self.text_item_started {
|
||||
let item_id = self.message_item_id();
|
||||
out.extend(self.encode_response_event(
|
||||
"response.output_item.added",
|
||||
json!({
|
||||
@@ -1318,7 +1339,7 @@ impl OpenAICliClientEmitter {
|
||||
"output_index": output_index,
|
||||
"item": {
|
||||
"type": "message",
|
||||
"id": item_id,
|
||||
"id": item_id.clone(),
|
||||
"status": "in_progress",
|
||||
"role": "assistant",
|
||||
"content": [],
|
||||
@@ -1328,7 +1349,6 @@ impl OpenAICliClientEmitter {
|
||||
self.text_item_started = true;
|
||||
}
|
||||
if !self.text_part_started {
|
||||
let item_id = self.message_item_id();
|
||||
out.extend(self.encode_response_event(
|
||||
"response.content_part.added",
|
||||
json!({
|
||||
@@ -1849,6 +1869,31 @@ mod tests {
|
||||
assert_eq!(response_sequence_numbers(&sse), (1..=9).collect::<Vec<_>>());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn openai_cli_client_emitter_keeps_text_item_id_stable_after_text_started() {
|
||||
let mut emitter = OpenAICliClientEmitter::default();
|
||||
let mut bytes = emitter
|
||||
.emit(CanonicalStreamFrame {
|
||||
id: "msg_first".to_string(),
|
||||
model: "claude-haiku-4-5-20251001".to_string(),
|
||||
event: CanonicalStreamEvent::TextDelta("Hel".to_string()),
|
||||
})
|
||||
.expect("first text should encode");
|
||||
bytes.extend(
|
||||
emitter
|
||||
.emit(CanonicalStreamFrame {
|
||||
id: "msg_second".to_string(),
|
||||
model: "claude-haiku-4-5-20251001".to_string(),
|
||||
event: CanonicalStreamEvent::TextDelta("lo".to_string()),
|
||||
})
|
||||
.expect("second text should encode"),
|
||||
);
|
||||
|
||||
let sse = String::from_utf8(bytes).expect("sse should be utf8");
|
||||
assert!(sse.contains("\"item_id\":\"msg_first_msg\""));
|
||||
assert!(!sse.contains("\"item_id\":\"msg_second_msg\""));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn openai_cli_provider_state_accepts_done_events_without_deltas() {
|
||||
let mut state = OpenAICliProviderState::default();
|
||||
|
||||
@@ -10,6 +10,7 @@ pub enum FinalizeStreamRewriteMode {
|
||||
OpenAiImage,
|
||||
Standard,
|
||||
KiroToClaudeCli,
|
||||
KiroToClaudeCliThenStandard,
|
||||
}
|
||||
|
||||
pub fn resolve_finalize_stream_rewrite_mode(
|
||||
@@ -38,6 +39,17 @@ pub fn resolve_finalize_stream_rewrite_mode(
|
||||
.trim()
|
||||
.to_ascii_lowercase();
|
||||
|
||||
if needs_conversion
|
||||
&& envelope_name.eq_ignore_ascii_case(KIRO_ENVELOPE_NAME)
|
||||
&& provider_api_format == "claude:cli"
|
||||
{
|
||||
return supports_standard_stream_rewrite(
|
||||
provider_api_format.as_str(),
|
||||
client_api_format.as_str(),
|
||||
)
|
||||
.then_some(FinalizeStreamRewriteMode::KiroToClaudeCliThenStandard);
|
||||
}
|
||||
|
||||
if needs_conversion {
|
||||
return supports_standard_stream_rewrite(
|
||||
provider_api_format.as_str(),
|
||||
|
||||
Reference in New Issue
Block a user