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,215 @@
use serde_json::{json, Value};
pub use self::state::KiroToClaudeCliStreamState;
mod state;
pub const KIRO_CONTEXT_WINDOW_TOKENS: f64 = 200_000.0;
pub const KIRO_MAX_THINKING_BUFFER: usize = 1024 * 1024;
const KIRO_QUOTE_CHARS: &str = "`\"'\\#!@$%^&*()-_=+[]{};:<>,.?/";
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub struct KiroStreamCacheUsage {
pub cache_creation_input_tokens: usize,
pub cache_read_input_tokens: usize,
}
impl KiroStreamCacheUsage {
fn has_cache_tokens(self) -> bool {
self.cache_creation_input_tokens > 0 || self.cache_read_input_tokens > 0
}
}
pub fn encode_kiro_sse_events(events: Vec<Value>) -> Result<Vec<u8>, serde_json::Error> {
let mut output = Vec::new();
for event in events {
output.extend(encode_kiro_sse_event(&event)?);
}
Ok(output)
}
pub fn encode_kiro_sse_event(event: &Value) -> Result<Vec<u8>, serde_json::Error> {
let encoded = serde_json::to_string(event)?;
if let Some(event_type) = event.get("type").and_then(Value::as_str) {
Ok(format!("event: {event_type}\ndata: {encoded}\n\n").into_bytes())
} else {
Ok(format!("data: {encoded}\n\n").into_bytes())
}
}
pub fn build_kiro_initial_sse_events(
message_id: &str,
model: &str,
estimated_input_tokens: usize,
cache_usage: Option<KiroStreamCacheUsage>,
) -> Vec<Value> {
let usage = build_kiro_usage_payload(estimated_input_tokens, 1, cache_usage);
vec![json!({
"type": "message_start",
"message": {
"id": message_id,
"type": "message",
"role": "assistant",
"content": [],
"model": model,
"stop_reason": Value::Null,
"stop_sequence": Value::Null,
"usage": usage,
}
})]
}
pub fn build_kiro_stream_error_sse_events(error_type: &str, message: &str) -> Vec<Value> {
vec![json!({
"type": "error",
"error": {
"type": error_type,
"message": message,
}
})]
}
pub fn build_kiro_final_message_sse_events(
stop_reason: &str,
input_tokens: usize,
output_tokens: usize,
cache_usage: Option<KiroStreamCacheUsage>,
) -> Vec<Value> {
let usage = build_kiro_usage_payload(input_tokens, output_tokens, cache_usage);
vec![
json!({
"type": "message_delta",
"delta": {
"stop_reason": stop_reason,
"stop_sequence": Value::Null,
},
"usage": usage
}),
json!({"type": "message_stop"}),
]
}
fn build_kiro_usage_payload(
input_tokens: usize,
output_tokens: usize,
cache_usage: Option<KiroStreamCacheUsage>,
) -> Value {
let billed_input_tokens = cache_usage
.filter(|usage| usage.has_cache_tokens())
.map(|usage| {
input_tokens
.saturating_sub(usage.cache_creation_input_tokens)
.saturating_sub(usage.cache_read_input_tokens)
})
.unwrap_or(input_tokens);
let mut usage = json!({
"input_tokens": billed_input_tokens as u64,
"output_tokens": output_tokens as u64,
});
if let Some(cache_usage) = cache_usage.filter(|usage| usage.has_cache_tokens()) {
usage["cache_creation_input_tokens"] =
json!(cache_usage.cache_creation_input_tokens as u64);
usage["cache_read_input_tokens"] = json!(cache_usage.cache_read_input_tokens as u64);
}
usage
}
pub fn calculate_kiro_context_input_tokens(percentage: f64) -> usize {
((percentage * KIRO_CONTEXT_WINDOW_TOKENS) / 100.0) as usize
}
pub fn estimate_kiro_tokens(text: &str) -> usize {
if text.is_empty() {
return 0;
}
let mut chinese = 0usize;
let mut other = 0usize;
for ch in text.chars() {
if ('\u{4e00}'..='\u{9fff}').contains(&ch) {
chinese += 1;
} else {
other += 1;
}
}
let chinese_tokens = (chinese * 2).div_ceil(3);
let other_tokens = other.div_ceil(4);
(chinese_tokens + other_tokens).max(1)
}
pub fn find_kiro_real_thinking_start_tag(buffer: &str) -> Option<usize> {
let tag = "<thinking>";
let mut search = 0usize;
loop {
let pos = buffer[search..].find(tag).map(|value| value + search)?;
let has_before = pos > 0 && is_kiro_quote_char(buffer, pos - 1);
let after_pos = pos + tag.len();
let has_after = is_kiro_quote_char(buffer, after_pos);
if !has_before && !has_after {
return Some(pos);
}
search = pos + 1;
}
}
pub fn find_kiro_real_thinking_end_tag(buffer: &str) -> Option<usize> {
let tag = "</thinking>";
let mut search = 0usize;
loop {
let pos = buffer[search..].find(tag).map(|value| value + search)?;
let has_before = pos > 0 && is_kiro_quote_char(buffer, pos - 1);
let after_pos = pos + tag.len();
let has_after = is_kiro_quote_char(buffer, after_pos);
if has_before || has_after {
search = pos + 1;
continue;
}
let after = &buffer[after_pos..];
if after.len() < 2 {
return None;
}
if after.starts_with("\n\n") {
return Some(pos);
}
search = pos + 1;
}
}
pub fn find_kiro_real_thinking_end_tag_at_buffer_end(buffer: &str) -> Option<usize> {
let tag = "</thinking>";
let mut search = 0usize;
loop {
let pos = buffer[search..].find(tag).map(|value| value + search)?;
let has_before = pos > 0 && is_kiro_quote_char(buffer, pos - 1);
let after_pos = pos + tag.len();
let has_after = is_kiro_quote_char(buffer, after_pos);
if has_before || has_after {
search = pos + 1;
continue;
}
if buffer[after_pos..].trim().is_empty() {
return Some(pos);
}
search = pos + 1;
}
}
pub fn kiro_crc32(data: &[u8]) -> u32 {
let mut crc = 0xffff_ffffu32;
for &byte in data {
crc ^= byte as u32;
for _ in 0..8 {
let mask = if crc & 1 == 1 { 0xedb8_8320 } else { 0 };
crc = (crc >> 1) ^ mask;
}
}
!crc
}
fn is_kiro_quote_char(buffer: &str, pos: usize) -> bool {
buffer
.as_bytes()
.get(pos)
.map(|byte| KIRO_QUOTE_CHARS.as_bytes().contains(byte))
.unwrap_or(false)
}
@@ -0,0 +1,72 @@
use std::collections::BTreeMap;
const MAX_MESSAGE_SIZE: usize = 16 * 1024 * 1024;
const MAX_BUFFER_SIZE: usize = MAX_MESSAGE_SIZE;
const MAX_ERRORS: usize = 5;
#[derive(Default)]
pub struct KiroToClaudeCliStreamState {
decoder: EventStreamDecoder,
state: KiroClaudeStreamState,
started: bool,
}
#[derive(Default)]
struct KiroClaudeStreamState {
model: String,
thinking_enabled: bool,
estimated_input_tokens: usize,
cache_usage: Option<super::KiroStreamCacheUsage>,
message_id: String,
output_tokens: usize,
context_input_tokens: Option<usize>,
next_block_index: usize,
open_blocks: BTreeMap<usize, String>,
text_block_index: Option<usize>,
thinking_block_index: Option<usize>,
tool_block_indices: BTreeMap<String, usize>,
thinking_buffer: String,
in_thinking_block: bool,
thinking_extracted: bool,
strip_thinking_leading_newline: bool,
has_tool_use: bool,
stop_reason_override: Option<String>,
had_error: bool,
last_content: String,
}
#[derive(Default)]
struct EventStreamDecoder {
buffer: Vec<u8>,
error_count: usize,
stopped: bool,
}
#[derive(Default)]
struct AwsHeaders {
values: BTreeMap<String, AwsHeaderValue>,
}
enum AwsHeaderValue {
Ignored,
String(String),
}
struct AwsEventFrame {
headers: AwsHeaders,
payload: Vec<u8>,
}
enum FrameParseError {
Incomplete,
Invalid(String),
}
#[path = "stream/decoder.rs"]
mod decoder;
#[path = "stream/state.rs"]
mod stream_state;
#[cfg(test)]
#[path = "stream/tests.rs"]
mod tests;
@@ -0,0 +1,222 @@
use std::collections::BTreeMap;
use super::{
AwsEventFrame, AwsHeaderValue, AwsHeaders, EventStreamDecoder, FrameParseError,
MAX_BUFFER_SIZE, MAX_ERRORS, MAX_MESSAGE_SIZE,
};
use crate::provider_compat::kiro_stream::kiro_crc32 as crc32;
impl EventStreamDecoder {
pub(super) fn feed(&mut self, data: &[u8]) -> Result<(), String> {
if self.stopped || data.is_empty() {
return Ok(());
}
let new_size = self.buffer.len() + data.len();
if new_size > MAX_BUFFER_SIZE {
self.stopped = true;
return Err(format!(
"buffer overflow: size={new_size} max={MAX_BUFFER_SIZE}"
));
}
self.buffer.extend_from_slice(data);
Ok(())
}
pub(super) fn decode_available(&mut self) -> Result<Vec<AwsEventFrame>, String> {
let mut out = Vec::new();
if self.stopped {
return Ok(out);
}
loop {
match parse_frame(&self.buffer) {
Ok(Some((frame, consumed))) => {
if consumed == 0 {
break;
}
out.push(frame);
self.buffer.drain(..consumed);
self.error_count = 0;
}
Ok(None) => break,
Err(FrameParseError::Incomplete) => break,
Err(FrameParseError::Invalid(message)) => {
self.error_count += 1;
if self.error_count >= MAX_ERRORS {
self.stopped = true;
return Err(message);
}
if self.buffer.is_empty() {
break;
}
self.buffer.drain(..1);
}
}
}
Ok(out)
}
}
impl AwsHeaders {
fn get_string(&self, name: &str) -> Option<&str> {
match self.values.get(name) {
Some(AwsHeaderValue::String(value)) => Some(value.as_str()),
_ => None,
}
}
pub(super) fn message_type(&self) -> Option<&str> {
self.get_string(":message-type")
}
pub(super) fn event_type(&self) -> Option<&str> {
self.get_string(":event-type")
}
pub(super) fn exception_type(&self) -> Option<&str> {
self.get_string(":exception-type")
}
pub(super) fn error_code(&self) -> Option<&str> {
self.get_string(":error-code")
}
}
fn parse_frame(buffer: &[u8]) -> Result<Option<(AwsEventFrame, usize)>, FrameParseError> {
if buffer.len() < 12 {
return Ok(None);
}
let total_length = u32::from_be_bytes(buffer[0..4].try_into().expect("slice size")) as usize;
let header_length = u32::from_be_bytes(buffer[4..8].try_into().expect("slice size")) as usize;
let prelude_crc = u32::from_be_bytes(buffer[8..12].try_into().expect("slice size"));
if total_length < 16 {
return Err(FrameParseError::Invalid(format!(
"message too small: length={total_length}"
)));
}
if total_length > MAX_MESSAGE_SIZE {
return Err(FrameParseError::Invalid(format!(
"message too large: length={total_length}"
)));
}
if buffer.len() < total_length {
return Ok(None);
}
if crc32(&buffer[0..8]) != prelude_crc {
return Err(FrameParseError::Invalid("prelude crc mismatch".to_string()));
}
let message_crc = u32::from_be_bytes(
buffer[total_length - 4..total_length]
.try_into()
.expect("slice size"),
);
if crc32(&buffer[..total_length - 4]) != message_crc {
return Err(FrameParseError::Invalid("message crc mismatch".to_string()));
}
let headers_start = 12;
let headers_end = headers_start + header_length;
if headers_end > total_length - 4 {
return Err(FrameParseError::Invalid(
"header length exceeds frame boundary".to_string(),
));
}
let headers = parse_headers(&buffer[headers_start..headers_end], header_length)?;
let payload = buffer[headers_end..total_length - 4].to_vec();
Ok(Some((AwsEventFrame { headers, payload }, total_length)))
}
fn parse_headers(data: &[u8], header_length: usize) -> Result<AwsHeaders, FrameParseError> {
if data.len() < header_length {
return Err(FrameParseError::Incomplete);
}
let mut values = BTreeMap::new();
let mut offset = 0usize;
while offset < header_length {
ensure_header_bytes(data, offset, 1)?;
let name_len = data[offset] as usize;
offset += 1;
if name_len == 0 {
return Err(FrameParseError::Invalid(
"header name length cannot be 0".to_string(),
));
}
ensure_header_bytes(data, offset, name_len)?;
let name = String::from_utf8_lossy(&data[offset..offset + name_len]).to_string();
offset += name_len;
ensure_header_bytes(data, offset, 1)?;
let value_type = data[offset];
offset += 1;
let value = match value_type {
0 => AwsHeaderValue::Ignored,
1 => AwsHeaderValue::Ignored,
2 => {
ensure_header_bytes(data, offset, 1)?;
let _ = i8::from_be_bytes([data[offset]]);
offset += 1;
AwsHeaderValue::Ignored
}
3 => {
ensure_header_bytes(data, offset, 2)?;
let _ = i16::from_be_bytes(data[offset..offset + 2].try_into().expect("slice"));
offset += 2;
AwsHeaderValue::Ignored
}
4 => {
ensure_header_bytes(data, offset, 4)?;
let _ = i32::from_be_bytes(data[offset..offset + 4].try_into().expect("slice"));
offset += 4;
AwsHeaderValue::Ignored
}
5 | 8 => {
ensure_header_bytes(data, offset, 8)?;
let _ = i64::from_be_bytes(data[offset..offset + 8].try_into().expect("slice"));
offset += 8;
AwsHeaderValue::Ignored
}
6 => {
ensure_header_bytes(data, offset, 2)?;
let length = u16::from_be_bytes(data[offset..offset + 2].try_into().expect("slice"))
as usize;
offset += 2;
ensure_header_bytes(data, offset, length)?;
offset += length;
AwsHeaderValue::Ignored
}
7 => {
ensure_header_bytes(data, offset, 2)?;
let length = u16::from_be_bytes(data[offset..offset + 2].try_into().expect("slice"))
as usize;
offset += 2;
ensure_header_bytes(data, offset, length)?;
let out = String::from_utf8_lossy(&data[offset..offset + length]).to_string();
offset += length;
AwsHeaderValue::String(out)
}
9 => {
ensure_header_bytes(data, offset, 16)?;
offset += 16;
AwsHeaderValue::Ignored
}
other => {
return Err(FrameParseError::Invalid(format!(
"invalid header type: {other}"
)));
}
};
values.insert(name, value);
}
Ok(AwsHeaders { values })
}
fn ensure_header_bytes(data: &[u8], offset: usize, needed: usize) -> Result<(), FrameParseError> {
let available = data.len().saturating_sub(offset);
if available < needed {
return Err(FrameParseError::Incomplete);
}
Ok(())
}
@@ -0,0 +1,8 @@
#[path = "state/blocks.rs"]
mod blocks;
#[path = "state/events.rs"]
mod events;
#[path = "state/finalize.rs"]
mod finalize;
#[path = "state/lifecycle.rs"]
mod lifecycle;
@@ -0,0 +1,97 @@
use serde_json::{json, Value};
use super::super::KiroClaudeStreamState;
impl KiroClaudeStreamState {
pub(super) fn ensure_text_block_open(&mut self) -> Vec<Value> {
if let Some(idx) = self.text_block_index {
if self
.open_blocks
.get(&idx)
.map(|value| value == "text")
.unwrap_or(false)
{
return Vec::new();
}
}
let idx = self.next_block_index;
self.next_block_index += 1;
self.text_block_index = Some(idx);
self.open_blocks.insert(idx, "text".to_string());
vec![json!({
"type": "content_block_start",
"index": idx,
"content_block": {"type": "text", "text": ""}
})]
}
pub(super) fn ensure_thinking_block_open(&mut self) -> Vec<Value> {
if let Some(idx) = self.thinking_block_index {
if self
.open_blocks
.get(&idx)
.map(|value| value == "thinking")
.unwrap_or(false)
{
return Vec::new();
}
}
let idx = self.next_block_index;
self.next_block_index += 1;
self.thinking_block_index = Some(idx);
self.open_blocks.insert(idx, "thinking".to_string());
vec![json!({
"type": "content_block_start",
"index": idx,
"content_block": {"type": "thinking", "thinking": ""}
})]
}
pub(super) fn close_block(&mut self, idx: usize) -> Vec<Value> {
if self.open_blocks.remove(&idx).is_none() {
return Vec::new();
}
vec![json!({"type": "content_block_stop", "index": idx})]
}
pub(super) fn emit_text_delta(&mut self, text: &str) -> Vec<Value> {
if text.is_empty() {
return Vec::new();
}
let mut events = self.ensure_text_block_open();
let idx = self.text_block_index.unwrap_or_default();
events.push(json!({
"type": "content_block_delta",
"index": idx,
"delta": {"type": "text_delta", "text": text}
}));
events
}
pub(super) fn emit_thinking_delta(&mut self, thinking: &str) -> Vec<Value> {
if thinking.is_empty() {
return Vec::new();
}
let mut events = self.ensure_thinking_block_open();
let idx = self.thinking_block_index.unwrap_or_default();
events.push(json!({
"type": "content_block_delta",
"index": idx,
"delta": {"type": "thinking_delta", "thinking": thinking}
}));
events
}
pub(super) fn close_thinking_block(&mut self) -> Vec<Value> {
let Some(idx) = self.thinking_block_index else {
return Vec::new();
};
let mut events = vec![json!({
"type": "content_block_delta",
"index": idx,
"delta": {"type": "thinking_delta", "thinking": ""}
})];
events.extend(self.close_block(idx));
events
}
}
@@ -0,0 +1,331 @@
use serde_json::{json, Value};
use crate::provider_compat::kiro_stream::{
calculate_kiro_context_input_tokens, encode_kiro_sse_events, estimate_kiro_tokens,
find_kiro_real_thinking_end_tag, find_kiro_real_thinking_end_tag_at_buffer_end,
find_kiro_real_thinking_start_tag, KIRO_MAX_THINKING_BUFFER,
};
use crate::formats::shared::AiSurfaceFinalizeError;
use super::super::AwsEventFrame;
use super::super::KiroClaudeStreamState;
fn floor_char_boundary(text: &str, index: usize) -> usize {
let mut boundary = index.min(text.len());
while boundary > 0 && !text.is_char_boundary(boundary) {
boundary -= 1;
}
boundary
}
fn split_preserving_trailing_bytes(
buffer: &str,
trailing_bytes: usize,
) -> Option<(String, String)> {
if buffer.len() <= trailing_bytes {
return None;
}
let split = floor_char_boundary(buffer, buffer.len() - trailing_bytes);
if split == 0 {
return None;
}
Some((buffer[..split].to_string(), buffer[split..].to_string()))
}
impl KiroClaudeStreamState {
pub(super) fn process_frame(
&mut self,
frame: AwsEventFrame,
) -> Result<Vec<u8>, AiSurfaceFinalizeError> {
let message_type = frame.headers.message_type().unwrap_or("event");
match message_type {
"event" => self.process_event_frame(frame),
"exception" => self.process_exception_frame(frame),
"error" => self.process_error_frame(frame),
_ => Ok(Vec::new()),
}
}
pub(super) fn process_event_frame(
&mut self,
frame: AwsEventFrame,
) -> Result<Vec<u8>, AiSurfaceFinalizeError> {
let event_type = frame.headers.event_type().unwrap_or_default();
let payload: Value = if frame.payload.is_empty() {
json!({})
} else {
serde_json::from_slice(&frame.payload).unwrap_or_else(|_| json!({}))
};
let payload_object = payload.as_object();
let mut events = Vec::new();
match event_type {
"assistantResponseEvent" => {
if let Some(content) = payload_object
.and_then(|value| value.get("content"))
.and_then(Value::as_str)
{
events.extend(self.process_assistant_response(content));
}
}
"toolUseEvent" => {
if let Some(payload_object) = payload_object {
let name = payload_object
.get("name")
.and_then(Value::as_str)
.unwrap_or_default();
let tool_use_id = payload_object
.get("toolUseId")
.or_else(|| payload_object.get("tool_use_id"))
.and_then(Value::as_str)
.unwrap_or_default();
let input_json = match payload_object.get("input") {
None | Some(Value::Null) => String::new(),
Some(Value::String(text)) => text.clone(),
Some(other) => {
serde_json::to_string(other).map_err(AiSurfaceFinalizeError::from)?
}
};
let stop = payload_object
.get("stop")
.and_then(Value::as_bool)
.unwrap_or(false);
events.extend(self.process_tool_use(name, tool_use_id, &input_json, stop));
}
}
"contextUsageEvent" => {
if let Some(percentage) = payload_object
.and_then(|value| value.get("contextUsagePercentage"))
.and_then(Value::as_f64)
{
self.context_input_tokens =
Some(calculate_kiro_context_input_tokens(percentage));
}
}
_ => {}
}
encode_kiro_sse_events(events).map_err(AiSurfaceFinalizeError::from)
}
pub(super) fn process_exception_frame(
&mut self,
frame: AwsEventFrame,
) -> Result<Vec<u8>, AiSurfaceFinalizeError> {
let exception_type = frame
.headers
.exception_type()
.unwrap_or("UnknownException")
.to_string();
if exception_type == "ContentLengthExceededException" {
self.stop_reason_override = Some("max_tokens".to_string());
return Ok(Vec::new());
}
self.emit_stream_error("upstream_exception", &exception_type)
}
pub(super) fn process_error_frame(
&mut self,
frame: AwsEventFrame,
) -> Result<Vec<u8>, AiSurfaceFinalizeError> {
let error_code = frame
.headers
.error_code()
.unwrap_or("UnknownError")
.to_string();
self.emit_stream_error("upstream_error", &error_code)
}
pub(super) fn process_assistant_response(&mut self, content: &str) -> Vec<Value> {
if content.is_empty() || content == self.last_content {
return Vec::new();
}
self.last_content = content.to_string();
self.output_tokens += estimate_kiro_tokens(content);
if !self.thinking_enabled {
return self.emit_text_delta(content);
}
self.thinking_buffer.push_str(content);
if self.thinking_buffer.len() > KIRO_MAX_THINKING_BUFFER {
let overflow = std::mem::take(&mut self.thinking_buffer);
if self.in_thinking_block {
let mut events = self.emit_thinking_delta(&overflow);
events.extend(self.close_thinking_block());
self.in_thinking_block = false;
self.thinking_extracted = true;
return events;
}
return self.emit_text_delta(&overflow);
}
let mut events = Vec::new();
loop {
if !self.in_thinking_block && !self.thinking_extracted {
if let Some(start_pos) = find_kiro_real_thinking_start_tag(&self.thinking_buffer) {
let before = self.thinking_buffer[..start_pos].to_string();
if !before.trim().is_empty() {
events.extend(self.emit_text_delta(&before));
}
self.in_thinking_block = true;
self.strip_thinking_leading_newline = true;
self.thinking_buffer =
self.thinking_buffer[start_pos + "<thinking>".len()..].to_string();
events.extend(self.ensure_thinking_block_open());
continue;
}
let keep = "<thinking>".len();
if let Some((safe, remaining)) =
split_preserving_trailing_bytes(&self.thinking_buffer, keep)
{
if !safe.trim().is_empty() {
events.extend(self.emit_text_delta(&safe));
self.thinking_buffer = remaining;
}
}
break;
}
if self.in_thinking_block {
if self.strip_thinking_leading_newline {
if self.thinking_buffer.starts_with('\n') {
self.thinking_buffer.remove(0);
self.strip_thinking_leading_newline = false;
} else if !self.thinking_buffer.is_empty() {
self.strip_thinking_leading_newline = false;
}
}
if let Some(end_pos) = find_kiro_real_thinking_end_tag(&self.thinking_buffer) {
let thinking_text = self.thinking_buffer[..end_pos].to_string();
if !thinking_text.is_empty() {
events.extend(self.emit_thinking_delta(&thinking_text));
}
events.extend(self.close_thinking_block());
self.in_thinking_block = false;
self.thinking_extracted = true;
self.thinking_buffer =
self.thinking_buffer[end_pos + "</thinking>".len()..].to_string();
continue;
}
let keep = "</thinking>".len();
if let Some((safe, remaining)) =
split_preserving_trailing_bytes(&self.thinking_buffer, keep)
{
if !safe.is_empty() {
events.extend(self.emit_thinking_delta(&safe));
self.thinking_buffer = remaining;
}
}
break;
}
if !self.thinking_buffer.is_empty() {
let remaining = std::mem::take(&mut self.thinking_buffer);
events.extend(self.emit_text_delta(&remaining));
}
break;
}
events
}
pub(super) fn process_tool_use(
&mut self,
name: &str,
tool_use_id: &str,
input_json: &str,
stop: bool,
) -> Vec<Value> {
if tool_use_id.is_empty() {
return Vec::new();
}
self.has_tool_use = true;
let mut events = Vec::new();
if self.thinking_enabled && self.in_thinking_block && !self.thinking_buffer.is_empty() {
if let Some(end_pos) =
find_kiro_real_thinking_end_tag_at_buffer_end(&self.thinking_buffer)
{
let thinking_text = self.thinking_buffer[..end_pos].to_string();
if !thinking_text.is_empty() {
events.extend(self.emit_thinking_delta(&thinking_text));
}
events.extend(self.close_thinking_block());
let remaining = self.thinking_buffer[end_pos + "</thinking>".len()..].to_string();
self.thinking_buffer.clear();
self.in_thinking_block = false;
self.thinking_extracted = true;
if !remaining.is_empty() {
events.extend(self.emit_text_delta(&remaining));
}
} else {
let thinking = std::mem::take(&mut self.thinking_buffer);
events.extend(self.emit_thinking_delta(&thinking));
events.extend(self.close_thinking_block());
self.in_thinking_block = false;
self.thinking_extracted = true;
}
}
if self.thinking_enabled
&& !self.in_thinking_block
&& !self.thinking_extracted
&& !self.thinking_buffer.is_empty()
{
let buffered = std::mem::take(&mut self.thinking_buffer);
events.extend(self.emit_text_delta(&buffered));
}
if let Some(idx) = self.text_block_index.take() {
events.extend(self.close_block(idx));
}
let block_index = if let Some(block_index) = self.tool_block_indices.get(tool_use_id) {
*block_index
} else {
let block_index = self.next_block_index;
self.next_block_index += 1;
self.tool_block_indices
.insert(tool_use_id.to_string(), block_index);
block_index
};
if let std::collections::btree_map::Entry::Vacant(e) = self.open_blocks.entry(block_index) {
e.insert("tool_use".to_string());
events.push(json!({
"type": "content_block_start",
"index": block_index,
"content_block": {
"type": "tool_use",
"id": tool_use_id,
"name": name,
"input": {},
}
}));
}
if !input_json.is_empty() {
self.output_tokens += estimate_kiro_tokens(input_json);
events.push(json!({
"type": "content_block_delta",
"index": block_index,
"delta": {
"type": "input_json_delta",
"partial_json": input_json,
}
}));
}
if stop {
events.extend(self.close_block(block_index));
}
events
}
}
@@ -0,0 +1,99 @@
use crate::provider_compat::kiro_stream::{
build_kiro_final_message_sse_events, encode_kiro_sse_events,
find_kiro_real_thinking_end_tag_at_buffer_end,
};
use crate::formats::shared::AiSurfaceFinalizeError;
use super::super::KiroClaudeStreamState;
impl KiroClaudeStreamState {
pub(super) fn finalize(&mut self) -> Result<Vec<u8>, AiSurfaceFinalizeError> {
if self.thinking_enabled && !self.thinking_buffer.is_empty() {
let flush_events = if self.in_thinking_block {
if let Some(end_pos) =
find_kiro_real_thinking_end_tag_at_buffer_end(&self.thinking_buffer)
{
let thinking_text = self.thinking_buffer[..end_pos].to_string();
let mut events = Vec::new();
if !thinking_text.is_empty() {
events.extend(self.emit_thinking_delta(&thinking_text));
}
events.extend(self.close_thinking_block());
let remaining =
self.thinking_buffer[end_pos + "</thinking>".len()..].to_string();
if !remaining.is_empty() {
events.extend(self.emit_text_delta(&remaining));
}
events
} else {
let mut events = self.emit_thinking_delta(&self.thinking_buffer.clone());
events.extend(self.close_thinking_block());
events
}
} else {
self.emit_text_delta(&self.thinking_buffer.clone())
};
self.thinking_buffer.clear();
self.in_thinking_block = false;
self.thinking_extracted = true;
let mut output =
encode_kiro_sse_events(flush_events).map_err(AiSurfaceFinalizeError::from)?;
for idx in self
.open_blocks
.keys()
.cloned()
.collect::<Vec<_>>()
.into_iter()
.rev()
{
output.extend(
encode_kiro_sse_events(self.close_block(idx))
.map_err(AiSurfaceFinalizeError::from)?,
);
}
output.extend(self.final_message_bytes()?);
return Ok(output);
}
let mut output = Vec::new();
for idx in self
.open_blocks
.keys()
.cloned()
.collect::<Vec<_>>()
.into_iter()
.rev()
{
output.extend(
encode_kiro_sse_events(self.close_block(idx))
.map_err(AiSurfaceFinalizeError::from)?,
);
}
output.extend(self.final_message_bytes()?);
Ok(output)
}
pub(super) fn final_message_bytes(&self) -> Result<Vec<u8>, AiSurfaceFinalizeError> {
let stop_reason = self.stop_reason_override.clone().unwrap_or_else(|| {
if self.has_tool_use {
"tool_use"
} else {
"end_turn"
}
.to_string()
});
let input_tokens = if self.estimated_input_tokens > 0 {
self.estimated_input_tokens
} else {
self.context_input_tokens.unwrap_or_default()
};
encode_kiro_sse_events(build_kiro_final_message_sse_events(
&stop_reason,
input_tokens,
self.output_tokens,
self.cache_usage,
))
.map_err(AiSurfaceFinalizeError::from)
}
}
@@ -0,0 +1,152 @@
use serde_json::Value;
use uuid::Uuid;
use crate::formats::shared::model_directives::model_directive_display_model_from_report_context;
use crate::formats::shared::AiSurfaceFinalizeError;
use crate::provider_compat::kiro_stream::{
build_kiro_initial_sse_events, build_kiro_stream_error_sse_events, encode_kiro_sse_events,
KiroStreamCacheUsage,
};
use super::super::{EventStreamDecoder, KiroClaudeStreamState, KiroToClaudeCliStreamState};
impl KiroToClaudeCliStreamState {
pub fn new(report_context: &Value) -> Self {
Self {
decoder: EventStreamDecoder::default(),
state: KiroClaudeStreamState::new(report_context),
started: false,
}
}
pub fn push_chunk(
&mut self,
_report_context: &Value,
chunk: &[u8],
) -> Result<Vec<u8>, AiSurfaceFinalizeError> {
let mut output = Vec::new();
if !self.started {
self.started = true;
output.extend(self.state.generate_initial_bytes()?);
}
if let Err(err) = self.decoder.feed(chunk) {
output.extend(
self.state
.emit_stream_error("upstream_stream_error", &err)?,
);
return Ok(output);
}
match self.decoder.decode_available() {
Ok(frames) => {
for frame in frames {
output.extend(self.state.process_frame(frame)?);
}
}
Err(err) => {
output.extend(
self.state
.emit_stream_error("upstream_stream_error", &err)?,
);
}
}
Ok(output)
}
pub fn finish(&mut self, _report_context: &Value) -> Result<Vec<u8>, AiSurfaceFinalizeError> {
if !self.started || self.state.had_error {
return Ok(Vec::new());
}
self.state.finalize()
}
}
impl KiroClaudeStreamState {
pub(super) fn new(report_context: &Value) -> Self {
let model = model_directive_display_model_from_report_context(report_context)
.or_else(|| {
report_context
.get("mapped_model")
.and_then(Value::as_str)
.filter(|value| !value.is_empty())
.map(ToOwned::to_owned)
})
.or_else(|| {
report_context
.get("model")
.and_then(Value::as_str)
.filter(|value| !value.is_empty())
.map(ToOwned::to_owned)
})
.unwrap_or_else(|| "unknown".to_string());
let thinking_enabled = report_context
.get("original_request_body")
.and_then(Value::as_object)
.and_then(|body| body.get("thinking"))
.and_then(Value::as_object)
.and_then(|thinking| thinking.get("type"))
.and_then(Value::as_str)
.map(|value| {
value.trim().eq_ignore_ascii_case("enabled")
|| value.trim().eq_ignore_ascii_case("adaptive")
})
.unwrap_or(false);
let estimated_input_tokens = report_context
.get("input_tokens")
.and_then(Value::as_u64)
.map(|value| value as usize)
.unwrap_or(0);
let cache_creation_input_tokens = report_context
.get("cache_creation_input_tokens")
.and_then(Value::as_u64)
.map(|value| value as usize)
.unwrap_or(0);
let cache_read_input_tokens = report_context
.get("cache_read_input_tokens")
.and_then(Value::as_u64)
.map(|value| value as usize)
.unwrap_or(0);
let cache_usage = (cache_creation_input_tokens > 0 || cache_read_input_tokens > 0)
.then_some(KiroStreamCacheUsage {
cache_creation_input_tokens,
cache_read_input_tokens,
});
Self {
model,
thinking_enabled,
estimated_input_tokens,
cache_usage,
message_id: format!("msg_{}", Uuid::new_v4().simple()),
..Self::default()
}
}
pub(super) fn generate_initial_bytes(&mut self) -> Result<Vec<u8>, AiSurfaceFinalizeError> {
let events = build_kiro_initial_sse_events(
&self.message_id,
&self.model,
self.estimated_input_tokens,
self.cache_usage,
);
let mut events = events;
if !self.thinking_enabled {
events.extend(self.ensure_text_block_open());
}
encode_kiro_sse_events(events).map_err(AiSurfaceFinalizeError::from)
}
pub(super) fn emit_stream_error(
&mut self,
error_type: &str,
message: &str,
) -> Result<Vec<u8>, AiSurfaceFinalizeError> {
if self.had_error {
return Ok(Vec::new());
}
self.had_error = true;
encode_kiro_sse_events(build_kiro_stream_error_sse_events(error_type, message))
.map_err(AiSurfaceFinalizeError::from)
}
}
@@ -0,0 +1,270 @@
use crate::provider_compat::kiro_stream::kiro_crc32 as crc32;
use serde_json::{json, Value};
use super::KiroToClaudeCliStreamState;
fn encode_string_header(name: &str, value: &str) -> Vec<u8> {
let mut out = Vec::new();
out.push(name.len() as u8);
out.extend_from_slice(name.as_bytes());
out.push(7);
out.extend_from_slice(&(value.len() as u16).to_be_bytes());
out.extend_from_slice(value.as_bytes());
out
}
fn encode_event_frame(message_type: &str, event_type: Option<&str>, payload: &Value) -> Vec<u8> {
let mut headers = encode_string_header(":message-type", message_type);
if let Some(event_type) = event_type {
headers.extend_from_slice(&encode_string_header(":event-type", event_type));
}
let payload_bytes = serde_json::to_vec(payload).expect("payload should encode");
encode_frame(headers, payload_bytes)
}
fn encode_frame(headers: Vec<u8>, payload: Vec<u8>) -> Vec<u8> {
let total_len = 12 + headers.len() + payload.len() + 4;
let header_len = headers.len();
let mut out = Vec::with_capacity(total_len);
out.extend_from_slice(&(total_len as u32).to_be_bytes());
out.extend_from_slice(&(header_len as u32).to_be_bytes());
let prelude_crc = crc32(&out[..8]);
out.extend_from_slice(&prelude_crc.to_be_bytes());
out.extend_from_slice(&headers);
out.extend_from_slice(&payload);
let message_crc = crc32(&out);
out.extend_from_slice(&message_crc.to_be_bytes());
out
}
fn kiro_report_context(thinking_enabled: bool) -> Value {
let mut context = json!({
"provider_api_format": "claude:messages",
"client_api_format": "claude:messages",
"envelope_name": "kiro:generateAssistantResponse",
"mapped_model": "claude-sonnet-4.5"
});
if thinking_enabled {
context["original_request_body"] = json!({
"thinking": {
"type": "enabled"
}
});
}
context
}
#[test]
fn kiro_stream_rewriter_converts_text_events_to_claude_sse() {
let report_context = kiro_report_context(false);
let mut rewriter = KiroToClaudeCliStreamState::new(&report_context);
let chunk = [
encode_event_frame(
"event",
Some("assistantResponseEvent"),
&json!({"content": "Hello from Kiro"}),
),
encode_event_frame(
"event",
Some("contextUsageEvent"),
&json!({"contextUsagePercentage": 1.0}),
),
]
.concat();
let first = rewriter
.push_chunk(&report_context, &chunk)
.expect("rewrite should succeed");
let rest = rewriter
.finish(&report_context)
.expect("finish should succeed");
let text = String::from_utf8([first, rest].concat()).expect("utf8 should decode");
assert!(text.contains("event: message_start"));
assert!(text.contains("\"type\":\"content_block_delta\""));
assert!(text.contains("Hello from Kiro"));
assert!(text.contains("\"stop_reason\":\"end_turn\""));
assert!(text.contains("\"input_tokens\":2000"));
}
#[test]
fn kiro_stream_rewriter_restores_model_directive_display_model() {
let report_context = json!({
"provider_api_format": "claude:messages",
"client_api_format": "claude:messages",
"envelope_name": "kiro:generateAssistantResponse",
"model": "claude-sonnet-4.5-high",
"mapped_model": "claude-sonnet-4.5"
});
let mut rewriter = KiroToClaudeCliStreamState::new(&report_context);
let first = rewriter
.push_chunk(
&report_context,
&encode_event_frame(
"event",
Some("assistantResponseEvent"),
&json!({"content": "Hello"}),
),
)
.expect("rewrite should succeed");
let text = String::from_utf8(first).expect("utf8 should decode");
assert!(text.contains("\"model\":\"claude-sonnet-4.5-high\""));
assert!(!text.contains("\"model\":\"claude-sonnet-4.5\""));
}
#[test]
fn kiro_stream_rewriter_emits_cache_usage_from_report_context() {
let report_context = json!({
"provider_api_format": "claude:messages",
"client_api_format": "claude:messages",
"envelope_name": "kiro:generateAssistantResponse",
"mapped_model": "claude-sonnet-4.5",
"input_tokens": 100,
"cache_creation_input_tokens": 25,
"cache_read_input_tokens": 40
});
let mut rewriter = KiroToClaudeCliStreamState::new(&report_context);
let first = rewriter
.push_chunk(
&report_context,
&encode_event_frame(
"event",
Some("assistantResponseEvent"),
&json!({"content": "Hello"}),
),
)
.expect("rewrite should succeed");
let rest = rewriter
.finish(&report_context)
.expect("finish should succeed");
let text = String::from_utf8([first, rest].concat()).expect("utf8 should decode");
assert_eq!(text.matches("\"input_tokens\":35").count(), 2);
assert_eq!(
text.matches("\"cache_creation_input_tokens\":25").count(),
2
);
assert_eq!(text.matches("\"cache_read_input_tokens\":40").count(), 2);
}
#[test]
fn kiro_stream_rewriter_keeps_estimated_input_when_context_usage_is_cache_only() {
let report_context = json!({
"provider_api_format": "claude:messages",
"client_api_format": "claude:messages",
"envelope_name": "kiro:generateAssistantResponse",
"mapped_model": "claude-sonnet-4.5",
"input_tokens": 24_344,
"cache_creation_input_tokens": 293,
"cache_read_input_tokens": 23_935
});
let mut rewriter = KiroToClaudeCliStreamState::new(&report_context);
let chunk = [
encode_event_frame(
"event",
Some("assistantResponseEvent"),
&json!({"content": "Hello"}),
),
encode_event_frame(
"event",
Some("contextUsageEvent"),
&json!({"contextUsagePercentage": 12.114}),
),
]
.concat();
let first = rewriter
.push_chunk(&report_context, &chunk)
.expect("rewrite should succeed");
let rest = rewriter
.finish(&report_context)
.expect("finish should succeed");
let text = String::from_utf8([first, rest].concat()).expect("utf8 should decode");
assert_eq!(text.matches("\"input_tokens\":116").count(), 2);
assert!(!text.contains("\"input_tokens\":0"));
assert_eq!(
text.matches("\"cache_creation_input_tokens\":293").count(),
2
);
assert_eq!(text.matches("\"cache_read_input_tokens\":23935").count(), 2);
}
#[test]
fn kiro_stream_rewriter_converts_tool_use_to_claude_events() {
let report_context = kiro_report_context(false);
let mut rewriter = KiroToClaudeCliStreamState::new(&report_context);
let chunk = [
encode_event_frame(
"event",
Some("assistantResponseEvent"),
&json!({"content": "Need a tool."}),
),
encode_event_frame(
"event",
Some("toolUseEvent"),
&json!({
"name": "get_weather",
"toolUseId": "tool_123",
"input": {"city": "SF"},
"stop": true
}),
),
]
.concat();
let first = rewriter
.push_chunk(&report_context, &chunk)
.expect("rewrite should succeed");
let rest = rewriter
.finish(&report_context)
.expect("finish should succeed");
let text = String::from_utf8([first, rest].concat()).expect("utf8 should decode");
assert!(text.contains("\"type\":\"tool_use\""));
assert!(text.contains("\"id\":\"tool_123\""));
assert!(text.contains("\"name\":\"get_weather\""));
assert!(text.contains("\"partial_json\":\"{\\\"city\\\":\\\"SF\\\"}\""));
assert!(text.contains("\"stop_reason\":\"tool_use\""));
}
#[test]
fn kiro_stream_rewriter_handles_multibyte_text_without_thinking_tag() {
let report_context = kiro_report_context(true);
let mut rewriter = KiroToClaudeCliStreamState::new(&report_context);
let chunk = encode_event_frame(
"event",
Some("assistantResponseEvent"),
&json!({"content": "\n\n你好!有"}),
);
let first = rewriter
.push_chunk(&report_context, &chunk)
.expect("rewrite should succeed");
let rest = rewriter
.finish(&report_context)
.expect("finish should succeed");
let text = String::from_utf8([first, rest].concat()).expect("utf8 should decode");
assert!(text.contains("\"type\":\"text_delta\""));
assert!(text.contains("你好!有"));
}
#[test]
fn kiro_stream_rewriter_handles_multibyte_text_inside_thinking_block() {
let report_context = kiro_report_context(true);
let mut rewriter = KiroToClaudeCliStreamState::new(&report_context);
let chunk = encode_event_frame(
"event",
Some("assistantResponseEvent"),
&json!({"content": "<thinking>\n\n你好!有"}),
);
let first = rewriter
.push_chunk(&report_context, &chunk)
.expect("rewrite should succeed");
let rest = rewriter
.finish(&report_context)
.expect("finish should succeed");
let text = String::from_utf8([first, rest].concat()).expect("utf8 should decode");
assert!(text.contains("\"type\":\"thinking_delta\""));
assert!(text.contains("你好!有"));
}
@@ -0,0 +1,4 @@
pub mod kiro_stream;
pub mod private_envelope;
pub mod proxy;
pub mod surfaces;
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,6 @@
pub mod rules;
pub use rules::{
apply_local_body_rules, apply_local_header_rules, body_rules_are_locally_supported,
body_rules_handle_path, header_rules_are_locally_supported,
};
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,219 @@
pub const ANTIGRAVITY_PROVIDER_TYPE: &str = "antigravity";
pub const GEMINI_CLI_PROVIDER_TYPE: &str = "gemini_cli";
pub const KIRO_PROVIDER_TYPE: &str = "kiro";
pub const WINDSURF_PROVIDER_TYPE: &str = "windsurf";
pub const KIRO_ENVELOPE_NAME: &str = "kiro:generateAssistantResponse";
pub const ANTIGRAVITY_V1INTERNAL_ENVELOPE_NAME: &str = "antigravity:v1internal";
pub const GEMINI_CLI_V1INTERNAL_ENVELOPE_NAME: &str = "gemini_cli:v1internal";
pub const WINDSURF_ENVELOPE_NAME: &str = "windsurf:GetChatMessage";
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ProviderAdaptationSurface {
AntigravityGeminiChat,
AntigravityGeminiCli,
GeminiCliV1Internal,
KiroClaudeCli,
WindsurfCascade,
}
#[derive(Debug, Clone, Copy)]
pub struct ProviderAdaptationDescriptor {
pub surface: ProviderAdaptationSurface,
pub provider_type: Option<&'static str>,
pub envelope_name: &'static str,
pub anchor_api_format: &'static str,
pub supports_request_bridge: bool,
pub supports_sync_finalize_bridge: bool,
pub supports_stream_bridge: bool,
pub requires_eventstream_accept: bool,
pub unwraps_response_envelope: bool,
}
const PROVIDER_ADAPTATION_SURFACES: &[ProviderAdaptationDescriptor] = &[
ProviderAdaptationDescriptor {
surface: ProviderAdaptationSurface::AntigravityGeminiChat,
provider_type: Some(ANTIGRAVITY_PROVIDER_TYPE),
envelope_name: ANTIGRAVITY_V1INTERNAL_ENVELOPE_NAME,
anchor_api_format: "gemini:generate_content",
supports_request_bridge: true,
supports_sync_finalize_bridge: true,
supports_stream_bridge: true,
requires_eventstream_accept: false,
unwraps_response_envelope: true,
},
ProviderAdaptationDescriptor {
surface: ProviderAdaptationSurface::AntigravityGeminiCli,
provider_type: Some(ANTIGRAVITY_PROVIDER_TYPE),
envelope_name: ANTIGRAVITY_V1INTERNAL_ENVELOPE_NAME,
anchor_api_format: "gemini:generate_content",
supports_request_bridge: true,
supports_sync_finalize_bridge: true,
supports_stream_bridge: true,
requires_eventstream_accept: false,
unwraps_response_envelope: true,
},
ProviderAdaptationDescriptor {
surface: ProviderAdaptationSurface::GeminiCliV1Internal,
provider_type: Some(GEMINI_CLI_PROVIDER_TYPE),
envelope_name: GEMINI_CLI_V1INTERNAL_ENVELOPE_NAME,
anchor_api_format: "gemini:generate_content",
supports_request_bridge: true,
supports_sync_finalize_bridge: true,
supports_stream_bridge: true,
requires_eventstream_accept: false,
unwraps_response_envelope: true,
},
ProviderAdaptationDescriptor {
surface: ProviderAdaptationSurface::KiroClaudeCli,
provider_type: Some(KIRO_PROVIDER_TYPE),
envelope_name: KIRO_ENVELOPE_NAME,
anchor_api_format: "claude:messages",
supports_request_bridge: true,
supports_sync_finalize_bridge: true,
supports_stream_bridge: true,
requires_eventstream_accept: true,
unwraps_response_envelope: false,
},
ProviderAdaptationDescriptor {
surface: ProviderAdaptationSurface::WindsurfCascade,
provider_type: Some(WINDSURF_PROVIDER_TYPE),
envelope_name: WINDSURF_ENVELOPE_NAME,
anchor_api_format: "openai:chat",
supports_request_bridge: true,
supports_sync_finalize_bridge: true,
supports_stream_bridge: true,
requires_eventstream_accept: false,
unwraps_response_envelope: true,
},
];
pub fn provider_adaptation_descriptor_for_envelope(
envelope_name: &str,
provider_api_format: &str,
) -> Option<&'static ProviderAdaptationDescriptor> {
let envelope_name = envelope_name.trim();
let provider_api_format = provider_api_format.trim().to_ascii_lowercase();
PROVIDER_ADAPTATION_SURFACES.iter().find(|descriptor| {
descriptor.envelope_name.eq_ignore_ascii_case(envelope_name)
&& descriptor
.anchor_api_format
.eq_ignore_ascii_case(provider_api_format.as_str())
})
}
pub fn provider_adaptation_descriptor_for_provider_type(
provider_type: &str,
provider_api_format: &str,
) -> Option<&'static ProviderAdaptationDescriptor> {
let provider_type = provider_type.trim();
let provider_api_format = provider_api_format.trim().to_ascii_lowercase();
PROVIDER_ADAPTATION_SURFACES.iter().find(|descriptor| {
descriptor
.provider_type
.is_some_and(|value| value.eq_ignore_ascii_case(provider_type))
&& descriptor
.anchor_api_format
.eq_ignore_ascii_case(provider_api_format.as_str())
})
}
pub fn provider_adaptation_anchor_api_format(
envelope_name: &str,
provider_api_format: &str,
) -> Option<&'static str> {
provider_adaptation_descriptor_for_envelope(envelope_name, provider_api_format)
.map(|descriptor| descriptor.anchor_api_format)
}
pub fn provider_adaptation_allows_sync_finalize_envelope(
envelope_name: &str,
provider_api_format: &str,
) -> bool {
provider_adaptation_descriptor_for_envelope(envelope_name, provider_api_format)
.is_some_and(|descriptor| descriptor.supports_sync_finalize_bridge)
}
pub fn provider_adaptation_requires_eventstream_accept(
envelope_name: Option<&str>,
provider_api_format: &str,
) -> bool {
envelope_name
.and_then(|value| provider_adaptation_descriptor_for_envelope(value, provider_api_format))
.is_some_and(|descriptor| descriptor.requires_eventstream_accept)
}
pub fn provider_adaptation_should_unwrap_stream_envelope(
envelope_name: &str,
provider_api_format: &str,
) -> bool {
provider_adaptation_descriptor_for_envelope(envelope_name, provider_api_format)
.is_some_and(|descriptor| descriptor.unwraps_response_envelope)
}
#[cfg(test)]
mod tests {
use super::{
provider_adaptation_allows_sync_finalize_envelope, provider_adaptation_anchor_api_format,
provider_adaptation_descriptor_for_provider_type,
provider_adaptation_requires_eventstream_accept,
provider_adaptation_should_unwrap_stream_envelope, ANTIGRAVITY_V1INTERNAL_ENVELOPE_NAME,
GEMINI_CLI_PROVIDER_TYPE, GEMINI_CLI_V1INTERNAL_ENVELOPE_NAME, KIRO_ENVELOPE_NAME,
WINDSURF_ENVELOPE_NAME,
};
#[test]
fn resolves_private_surface_anchor_contracts() {
assert_eq!(
provider_adaptation_anchor_api_format(
ANTIGRAVITY_V1INTERNAL_ENVELOPE_NAME,
"gemini:generate_content"
),
Some("gemini:generate_content")
);
assert_eq!(
provider_adaptation_anchor_api_format(
GEMINI_CLI_V1INTERNAL_ENVELOPE_NAME,
"gemini:generate_content"
),
Some("gemini:generate_content")
);
assert_eq!(
provider_adaptation_anchor_api_format(KIRO_ENVELOPE_NAME, "claude:messages"),
Some("claude:messages")
);
assert_eq!(
provider_adaptation_anchor_api_format(WINDSURF_ENVELOPE_NAME, "openai:chat"),
Some("openai:chat")
);
}
#[test]
fn exposes_private_surface_capabilities() {
assert!(provider_adaptation_allows_sync_finalize_envelope(
ANTIGRAVITY_V1INTERNAL_ENVELOPE_NAME,
"gemini:generate_content"
));
assert!(provider_adaptation_should_unwrap_stream_envelope(
GEMINI_CLI_V1INTERNAL_ENVELOPE_NAME,
"gemini:generate_content"
));
let gemini_cli_descriptor = provider_adaptation_descriptor_for_provider_type(
GEMINI_CLI_PROVIDER_TYPE,
"gemini:generate_content",
)
.expect("gemini cli descriptor should resolve by provider type");
assert!(gemini_cli_descriptor.supports_request_bridge);
assert!(provider_adaptation_requires_eventstream_accept(
Some(KIRO_ENVELOPE_NAME),
"claude:messages"
));
assert!(!provider_adaptation_requires_eventstream_accept(
Some(ANTIGRAVITY_V1INTERNAL_ENVELOPE_NAME),
"gemini:generate_content"
));
assert!(provider_adaptation_should_unwrap_stream_envelope(
WINDSURF_ENVELOPE_NAME,
"openai:chat"
));
}
}