mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-03 09:50:21 +08:00
fix(kiro): 修复 Claude CLI 跨格式 Responses 流式转换 (#329)
This commit is contained in:
@@ -12,6 +12,10 @@ enum RewriteMode {
|
|||||||
OpenAiImage(OpenAiImageStreamState),
|
OpenAiImage(OpenAiImageStreamState),
|
||||||
Standard(StreamingStandardConversionState),
|
Standard(StreamingStandardConversionState),
|
||||||
KiroToClaudeCli(KiroToClaudeCliStreamState),
|
KiroToClaudeCli(KiroToClaudeCliStreamState),
|
||||||
|
KiroToClaudeCliThenStandard {
|
||||||
|
kiro: KiroToClaudeCliStreamState,
|
||||||
|
standard: StreamingStandardConversionState,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) struct LocalStreamRewriter<'a> {
|
pub(crate) struct LocalStreamRewriter<'a> {
|
||||||
@@ -35,6 +39,12 @@ pub(crate) fn maybe_build_local_stream_rewriter<'a>(
|
|||||||
FinalizeStreamRewriteMode::KiroToClaudeCli => {
|
FinalizeStreamRewriteMode::KiroToClaudeCli => {
|
||||||
RewriteMode::KiroToClaudeCli(KiroToClaudeCliStreamState::new(report_context))
|
RewriteMode::KiroToClaudeCli(KiroToClaudeCliStreamState::new(report_context))
|
||||||
}
|
}
|
||||||
|
FinalizeStreamRewriteMode::KiroToClaudeCliThenStandard => {
|
||||||
|
RewriteMode::KiroToClaudeCliThenStandard {
|
||||||
|
kiro: KiroToClaudeCliStreamState::new(report_context),
|
||||||
|
standard: StreamingStandardConversionState::default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
Some(LocalStreamRewriter {
|
Some(LocalStreamRewriter {
|
||||||
@@ -52,6 +62,10 @@ impl LocalStreamRewriter<'_> {
|
|||||||
if let RewriteMode::KiroToClaudeCli(state) = &mut self.mode {
|
if let RewriteMode::KiroToClaudeCli(state) = &mut self.mode {
|
||||||
return state.push_chunk(self.report_context, chunk);
|
return state.push_chunk(self.report_context, chunk);
|
||||||
}
|
}
|
||||||
|
if let RewriteMode::KiroToClaudeCliThenStandard { kiro, standard } = &mut self.mode {
|
||||||
|
let claude_bytes = kiro.push_chunk(self.report_context, chunk)?;
|
||||||
|
return transform_standard_bytes(standard, self.report_context, claude_bytes);
|
||||||
|
}
|
||||||
self.buffered.extend_from_slice(chunk);
|
self.buffered.extend_from_slice(chunk);
|
||||||
let mut output = Vec::new();
|
let mut output = Vec::new();
|
||||||
while let Some(line_end) = self.buffered.iter().position(|byte| *byte == b'\n') {
|
while let Some(line_end) = self.buffered.iter().position(|byte| *byte == b'\n') {
|
||||||
@@ -68,11 +82,21 @@ impl LocalStreamRewriter<'_> {
|
|||||||
if let RewriteMode::KiroToClaudeCli(state) = &mut self.mode {
|
if let RewriteMode::KiroToClaudeCli(state) = &mut self.mode {
|
||||||
return state.finish(self.report_context);
|
return state.finish(self.report_context);
|
||||||
}
|
}
|
||||||
|
if let RewriteMode::KiroToClaudeCliThenStandard { kiro, standard } = &mut self.mode {
|
||||||
|
let mut output = transform_standard_bytes(
|
||||||
|
standard,
|
||||||
|
self.report_context,
|
||||||
|
kiro.finish(self.report_context)?,
|
||||||
|
)?;
|
||||||
|
output.extend(standard.finish(self.report_context)?);
|
||||||
|
return Ok(output);
|
||||||
|
}
|
||||||
if self.buffered.is_empty() {
|
if self.buffered.is_empty() {
|
||||||
match &mut self.mode {
|
match &mut self.mode {
|
||||||
RewriteMode::Standard(state) => return state.finish(self.report_context),
|
RewriteMode::Standard(state) => return state.finish(self.report_context),
|
||||||
RewriteMode::OpenAiImage(_) => {}
|
RewriteMode::OpenAiImage(_) => {}
|
||||||
RewriteMode::KiroToClaudeCli(_) => {}
|
RewriteMode::KiroToClaudeCli(_) => {}
|
||||||
|
RewriteMode::KiroToClaudeCliThenStandard { .. } => {}
|
||||||
RewriteMode::EnvelopeUnwrap => {}
|
RewriteMode::EnvelopeUnwrap => {}
|
||||||
}
|
}
|
||||||
return Ok(Vec::new());
|
return Ok(Vec::new());
|
||||||
@@ -85,6 +109,7 @@ impl LocalStreamRewriter<'_> {
|
|||||||
}
|
}
|
||||||
RewriteMode::OpenAiImage(_) => {}
|
RewriteMode::OpenAiImage(_) => {}
|
||||||
RewriteMode::KiroToClaudeCli(_) => {}
|
RewriteMode::KiroToClaudeCli(_) => {}
|
||||||
|
RewriteMode::KiroToClaudeCliThenStandard { .. } => {}
|
||||||
RewriteMode::EnvelopeUnwrap => {}
|
RewriteMode::EnvelopeUnwrap => {}
|
||||||
}
|
}
|
||||||
Ok(output)
|
Ok(output)
|
||||||
@@ -97,10 +122,26 @@ impl LocalStreamRewriter<'_> {
|
|||||||
RewriteMode::OpenAiImage(_) => Ok(Vec::new()),
|
RewriteMode::OpenAiImage(_) => Ok(Vec::new()),
|
||||||
RewriteMode::Standard(state) => state.transform_line(self.report_context, line),
|
RewriteMode::Standard(state) => state.transform_line(self.report_context, line),
|
||||||
RewriteMode::KiroToClaudeCli(_) => Ok(Vec::new()),
|
RewriteMode::KiroToClaudeCli(_) => Ok(Vec::new()),
|
||||||
|
RewriteMode::KiroToClaudeCliThenStandard { .. } => Ok(Vec::new()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn transform_standard_bytes(
|
||||||
|
standard: &mut StreamingStandardConversionState,
|
||||||
|
report_context: &Value,
|
||||||
|
bytes: Vec<u8>,
|
||||||
|
) -> Result<Vec<u8>, GatewayError> {
|
||||||
|
if bytes.is_empty() {
|
||||||
|
return Ok(Vec::new());
|
||||||
|
}
|
||||||
|
let mut output = Vec::new();
|
||||||
|
for line in bytes.split_inclusive(|byte| *byte == b'\n') {
|
||||||
|
output.extend(standard.transform_line(report_context, line.to_vec())?);
|
||||||
|
}
|
||||||
|
Ok(output)
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
struct OpenAiImageStreamState {
|
struct OpenAiImageStreamState {
|
||||||
buffered: Vec<u8>,
|
buffered: Vec<u8>,
|
||||||
|
|||||||
@@ -52,6 +52,12 @@ pub(super) async fn maybe_build_local_standard_decision_payload_for_candidate(
|
|||||||
{
|
{
|
||||||
extra_fields.insert("proxy".to_string(), proxy_value);
|
extra_fields.insert("proxy".to_string(), proxy_value);
|
||||||
}
|
}
|
||||||
|
if let Some(envelope_name) = resolved.envelope_name {
|
||||||
|
extra_fields.insert(
|
||||||
|
"envelope_name".to_string(),
|
||||||
|
serde_json::Value::String(envelope_name.to_string()),
|
||||||
|
);
|
||||||
|
}
|
||||||
let report_context = append_local_failover_policy_to_value(
|
let report_context = append_local_failover_policy_to_value(
|
||||||
append_execution_contract_fields_to_value(
|
append_execution_contract_fields_to_value(
|
||||||
build_local_execution_report_context(LocalExecutionReportContextParts {
|
build_local_execution_report_context(LocalExecutionReportContextParts {
|
||||||
@@ -83,7 +89,7 @@ pub(super) async fn maybe_build_local_standard_decision_payload_for_candidate(
|
|||||||
.and_then(serde_json::Value::as_bool)
|
.and_then(serde_json::Value::as_bool)
|
||||||
.unwrap_or(false),
|
.unwrap_or(false),
|
||||||
upstream_is_stream: resolved.upstream_is_stream,
|
upstream_is_stream: resolved.upstream_is_stream,
|
||||||
has_envelope: false,
|
has_envelope: resolved.envelope_name.is_some(),
|
||||||
needs_conversion: true,
|
needs_conversion: true,
|
||||||
extra_fields,
|
extra_fields,
|
||||||
}),
|
}),
|
||||||
@@ -105,6 +111,7 @@ pub(super) async fn maybe_build_local_standard_decision_payload_for_candidate(
|
|||||||
provider_request_headers,
|
provider_request_headers,
|
||||||
upstream_url,
|
upstream_url,
|
||||||
upstream_is_stream,
|
upstream_is_stream,
|
||||||
|
envelope_name: _,
|
||||||
transport,
|
transport,
|
||||||
} = resolved;
|
} = resolved;
|
||||||
|
|
||||||
|
|||||||
@@ -13,8 +13,12 @@ use crate::ai_pipeline::transport::apply_local_header_rules;
|
|||||||
use crate::ai_pipeline::transport::auth::{
|
use crate::ai_pipeline::transport::auth::{
|
||||||
build_claude_passthrough_headers, build_openai_passthrough_headers, ensure_upstream_auth_header,
|
build_claude_passthrough_headers, build_openai_passthrough_headers, ensure_upstream_auth_header,
|
||||||
};
|
};
|
||||||
|
use crate::ai_pipeline::transport::kiro::{
|
||||||
|
build_kiro_provider_headers, build_kiro_provider_request_body, KiroProviderHeadersInput,
|
||||||
|
KiroRequestAuth, KIRO_ENVELOPE_NAME,
|
||||||
|
};
|
||||||
use crate::ai_pipeline::transport::vertex::uses_vertex_api_key_query_auth;
|
use crate::ai_pipeline::transport::vertex::uses_vertex_api_key_query_auth;
|
||||||
use crate::ai_pipeline::GatewayProviderTransportSnapshot;
|
use crate::ai_pipeline::{GatewayProviderTransportSnapshot, LocalResolvedOAuthRequestAuth};
|
||||||
use crate::AppState;
|
use crate::AppState;
|
||||||
|
|
||||||
use super::payload::mark_skipped_local_standard_candidate;
|
use super::payload::mark_skipped_local_standard_candidate;
|
||||||
@@ -29,6 +33,7 @@ pub(crate) struct LocalStandardCandidatePayloadParts {
|
|||||||
pub(super) provider_request_headers: BTreeMap<String, String>,
|
pub(super) provider_request_headers: BTreeMap<String, String>,
|
||||||
pub(super) upstream_url: String,
|
pub(super) upstream_url: String,
|
||||||
pub(super) upstream_is_stream: bool,
|
pub(super) upstream_is_stream: bool,
|
||||||
|
pub(super) envelope_name: Option<&'static str>,
|
||||||
pub(super) transport: Arc<GatewayProviderTransportSnapshot>,
|
pub(super) transport: Arc<GatewayProviderTransportSnapshot>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -46,6 +51,12 @@ pub(crate) async fn resolve_local_standard_candidate_payload_parts(
|
|||||||
let candidate = &attempt.eligible.candidate;
|
let candidate = &attempt.eligible.candidate;
|
||||||
let transport = &attempt.eligible.transport;
|
let transport = &attempt.eligible.transport;
|
||||||
let provider_api_format = attempt.eligible.provider_api_format.as_str();
|
let provider_api_format = attempt.eligible.provider_api_format.as_str();
|
||||||
|
let is_kiro_claude_cli = transport
|
||||||
|
.provider
|
||||||
|
.provider_type
|
||||||
|
.trim()
|
||||||
|
.eq_ignore_ascii_case("kiro")
|
||||||
|
&& provider_api_format.eq_ignore_ascii_case("claude:cli");
|
||||||
let Some(conversion_kind) = crate::ai_pipeline::conversion::request_conversion_kind(
|
let Some(conversion_kind) = crate::ai_pipeline::conversion::request_conversion_kind(
|
||||||
spec_metadata.api_format,
|
spec_metadata.api_format,
|
||||||
provider_api_format,
|
provider_api_format,
|
||||||
@@ -72,32 +83,90 @@ pub(crate) async fn resolve_local_standard_candidate_payload_parts(
|
|||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
|
|
||||||
let prepared_candidate = match prepare_header_authenticated_candidate(
|
let oauth_context = OauthPreparationContext {
|
||||||
planner_state,
|
trace_id,
|
||||||
transport,
|
api_format: provider_api_format,
|
||||||
candidate,
|
operation: "standard_family_cross_format",
|
||||||
crate::ai_pipeline::conversion::request_conversion_direct_auth(transport, conversion_kind),
|
};
|
||||||
OauthPreparationContext {
|
let kiro_auth = if is_kiro_claude_cli {
|
||||||
trace_id,
|
match crate::ai_pipeline::planner::candidate_preparation::resolve_candidate_oauth_auth(
|
||||||
api_format: provider_api_format,
|
planner_state,
|
||||||
operation: "standard_family_cross_format",
|
transport,
|
||||||
},
|
oauth_context,
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
Ok(prepared) => prepared,
|
Some(LocalResolvedOAuthRequestAuth::Kiro(auth)) => Some(auth),
|
||||||
Err(skip_reason) => {
|
_ => {
|
||||||
mark_skipped_local_standard_candidate(
|
mark_skipped_local_standard_candidate(
|
||||||
state,
|
state,
|
||||||
input,
|
input,
|
||||||
trace_id,
|
trace_id,
|
||||||
|
candidate,
|
||||||
|
attempt.candidate_index,
|
||||||
|
&attempt.candidate_id,
|
||||||
|
"transport_auth_unavailable",
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
|
let prepared_candidate = if let Some(kiro_auth) = kiro_auth.as_ref() {
|
||||||
|
let mapped_model =
|
||||||
|
match crate::ai_pipeline::planner::candidate_preparation::resolve_candidate_mapped_model(
|
||||||
candidate,
|
candidate,
|
||||||
attempt.candidate_index,
|
) {
|
||||||
&attempt.candidate_id,
|
Ok(mapped_model) => mapped_model,
|
||||||
skip_reason,
|
Err(skip_reason) => {
|
||||||
)
|
mark_skipped_local_standard_candidate(
|
||||||
.await;
|
state,
|
||||||
return None;
|
input,
|
||||||
|
trace_id,
|
||||||
|
candidate,
|
||||||
|
attempt.candidate_index,
|
||||||
|
&attempt.candidate_id,
|
||||||
|
skip_reason,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
crate::ai_pipeline::planner::candidate_preparation::PreparedHeaderAuthenticatedCandidate {
|
||||||
|
auth_header: kiro_auth.name.to_string(),
|
||||||
|
auth_value: kiro_auth.value.clone(),
|
||||||
|
mapped_model,
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
match prepare_header_authenticated_candidate(
|
||||||
|
planner_state,
|
||||||
|
transport,
|
||||||
|
candidate,
|
||||||
|
crate::ai_pipeline::conversion::request_conversion_direct_auth(
|
||||||
|
transport,
|
||||||
|
conversion_kind,
|
||||||
|
),
|
||||||
|
oauth_context,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(prepared) => prepared,
|
||||||
|
Err(skip_reason) => {
|
||||||
|
mark_skipped_local_standard_candidate(
|
||||||
|
state,
|
||||||
|
input,
|
||||||
|
trace_id,
|
||||||
|
candidate,
|
||||||
|
attempt.candidate_index,
|
||||||
|
&attempt.candidate_id,
|
||||||
|
skip_reason,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
return None;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -115,7 +184,11 @@ pub(crate) async fn resolve_local_standard_candidate_payload_parts(
|
|||||||
provider_api_format,
|
provider_api_format,
|
||||||
parts.uri.path(),
|
parts.uri.path(),
|
||||||
upstream_is_stream,
|
upstream_is_stream,
|
||||||
transport.endpoint.body_rules.as_ref(),
|
if is_kiro_claude_cli {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
transport.endpoint.body_rules.as_ref()
|
||||||
|
},
|
||||||
Some(input.auth_context.api_key_id.as_str()),
|
Some(input.auth_context.api_key_id.as_str()),
|
||||||
) {
|
) {
|
||||||
Some(body) => body,
|
Some(body) => body,
|
||||||
@@ -134,6 +207,26 @@ pub(crate) async fn resolve_local_standard_candidate_payload_parts(
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if let Some(kiro_auth) = kiro_auth.as_ref() {
|
||||||
|
return build_kiro_cross_format_payload_parts(
|
||||||
|
state,
|
||||||
|
parts,
|
||||||
|
trace_id,
|
||||||
|
body_json,
|
||||||
|
input,
|
||||||
|
attempt,
|
||||||
|
transport,
|
||||||
|
provider_api_format,
|
||||||
|
prepared_candidate.mapped_model,
|
||||||
|
prepared_candidate.auth_header,
|
||||||
|
prepared_candidate.auth_value,
|
||||||
|
provider_request_body,
|
||||||
|
upstream_is_stream,
|
||||||
|
kiro_auth,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
}
|
||||||
|
|
||||||
let upstream_url = match crate::ai_pipeline::planner::standard::build_standard_upstream_url(
|
let upstream_url = match crate::ai_pipeline::planner::standard::build_standard_upstream_url(
|
||||||
parts,
|
parts,
|
||||||
transport,
|
transport,
|
||||||
@@ -237,6 +330,109 @@ pub(crate) async fn resolve_local_standard_candidate_payload_parts(
|
|||||||
provider_request_headers,
|
provider_request_headers,
|
||||||
upstream_url,
|
upstream_url,
|
||||||
upstream_is_stream,
|
upstream_is_stream,
|
||||||
|
envelope_name: None,
|
||||||
|
transport: Arc::clone(transport),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(clippy::too_many_arguments)]
|
||||||
|
async fn build_kiro_cross_format_payload_parts(
|
||||||
|
state: &AppState,
|
||||||
|
parts: &http::request::Parts,
|
||||||
|
trace_id: &str,
|
||||||
|
original_body_json: &serde_json::Value,
|
||||||
|
input: &LocalStandardDecisionInput,
|
||||||
|
attempt: &LocalStandardCandidateAttempt,
|
||||||
|
transport: &Arc<GatewayProviderTransportSnapshot>,
|
||||||
|
provider_api_format: &str,
|
||||||
|
mapped_model: String,
|
||||||
|
auth_header: String,
|
||||||
|
auth_value: String,
|
||||||
|
claude_request_body: Value,
|
||||||
|
upstream_is_stream: bool,
|
||||||
|
kiro_auth: &KiroRequestAuth,
|
||||||
|
) -> Option<LocalStandardCandidatePayloadParts> {
|
||||||
|
let candidate = &attempt.eligible.candidate;
|
||||||
|
let provider_request_body = match build_kiro_provider_request_body(
|
||||||
|
&claude_request_body,
|
||||||
|
&mapped_model,
|
||||||
|
&kiro_auth.auth_config,
|
||||||
|
transport.endpoint.body_rules.as_ref(),
|
||||||
|
) {
|
||||||
|
Some(body) => body,
|
||||||
|
None => {
|
||||||
|
mark_skipped_local_standard_candidate(
|
||||||
|
state,
|
||||||
|
input,
|
||||||
|
trace_id,
|
||||||
|
candidate,
|
||||||
|
attempt.candidate_index,
|
||||||
|
&attempt.candidate_id,
|
||||||
|
"provider_request_body_missing",
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let upstream_url = match crate::ai_pipeline::build_provider_transport_request_url(
|
||||||
|
transport,
|
||||||
|
provider_api_format,
|
||||||
|
Some(&mapped_model),
|
||||||
|
upstream_is_stream,
|
||||||
|
parts.uri.query(),
|
||||||
|
Some(kiro_auth.auth_config.effective_api_region()),
|
||||||
|
) {
|
||||||
|
Some(url) => url,
|
||||||
|
None => {
|
||||||
|
mark_skipped_local_standard_candidate(
|
||||||
|
state,
|
||||||
|
input,
|
||||||
|
trace_id,
|
||||||
|
candidate,
|
||||||
|
attempt.candidate_index,
|
||||||
|
&attempt.candidate_id,
|
||||||
|
"upstream_url_missing",
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let provider_request_headers = match build_kiro_provider_headers(KiroProviderHeadersInput {
|
||||||
|
headers: &parts.headers,
|
||||||
|
provider_request_body: &provider_request_body,
|
||||||
|
original_request_body: original_body_json,
|
||||||
|
header_rules: transport.endpoint.header_rules.as_ref(),
|
||||||
|
auth_header: &auth_header,
|
||||||
|
auth_value: &auth_value,
|
||||||
|
auth_config: &kiro_auth.auth_config,
|
||||||
|
machine_id: kiro_auth.machine_id.as_str(),
|
||||||
|
}) {
|
||||||
|
Some(headers) => headers,
|
||||||
|
None => {
|
||||||
|
mark_skipped_local_standard_candidate(
|
||||||
|
state,
|
||||||
|
input,
|
||||||
|
trace_id,
|
||||||
|
candidate,
|
||||||
|
attempt.candidate_index,
|
||||||
|
&attempt.candidate_id,
|
||||||
|
"transport_header_rules_apply_failed",
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
Some(LocalStandardCandidatePayloadParts {
|
||||||
|
auth_header,
|
||||||
|
auth_value,
|
||||||
|
mapped_model,
|
||||||
|
provider_api_format: provider_api_format.to_string(),
|
||||||
|
provider_request_body,
|
||||||
|
provider_request_headers,
|
||||||
|
upstream_url,
|
||||||
|
upstream_is_stream,
|
||||||
|
envelope_name: Some(KIRO_ENVELOPE_NAME),
|
||||||
transport: Arc::clone(transport),
|
transport: Arc::clone(transport),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -69,6 +69,12 @@ pub(crate) async fn maybe_build_local_openai_chat_decision_payload_for_candidate
|
|||||||
{
|
{
|
||||||
extra_fields.insert("proxy".to_string(), proxy_value);
|
extra_fields.insert("proxy".to_string(), proxy_value);
|
||||||
}
|
}
|
||||||
|
if let Some(envelope_name) = resolved.envelope_name {
|
||||||
|
extra_fields.insert(
|
||||||
|
"envelope_name".to_string(),
|
||||||
|
serde_json::Value::String(envelope_name.to_string()),
|
||||||
|
);
|
||||||
|
}
|
||||||
let report_context = append_local_failover_policy_to_value(
|
let report_context = append_local_failover_policy_to_value(
|
||||||
append_execution_contract_fields_to_value(
|
append_execution_contract_fields_to_value(
|
||||||
build_local_execution_report_context(LocalExecutionReportContextParts {
|
build_local_execution_report_context(LocalExecutionReportContextParts {
|
||||||
@@ -100,7 +106,7 @@ pub(crate) async fn maybe_build_local_openai_chat_decision_payload_for_candidate
|
|||||||
.and_then(serde_json::Value::as_bool)
|
.and_then(serde_json::Value::as_bool)
|
||||||
.unwrap_or(false),
|
.unwrap_or(false),
|
||||||
upstream_is_stream,
|
upstream_is_stream,
|
||||||
has_envelope: false,
|
has_envelope: resolved.envelope_name.is_some(),
|
||||||
needs_conversion: matches!(
|
needs_conversion: matches!(
|
||||||
resolved.conversion_mode,
|
resolved.conversion_mode,
|
||||||
crate::ai_pipeline::ConversionMode::Bidirectional
|
crate::ai_pipeline::ConversionMode::Bidirectional
|
||||||
@@ -125,6 +131,7 @@ pub(crate) async fn maybe_build_local_openai_chat_decision_payload_for_candidate
|
|||||||
execution_strategy,
|
execution_strategy,
|
||||||
conversion_mode,
|
conversion_mode,
|
||||||
report_kind,
|
report_kind,
|
||||||
|
envelope_name: _,
|
||||||
transport,
|
transport,
|
||||||
} = resolved;
|
} = resolved;
|
||||||
|
|
||||||
|
|||||||
@@ -20,9 +20,16 @@ use crate::ai_pipeline::transport::auth::{
|
|||||||
build_openai_passthrough_headers, ensure_upstream_auth_header,
|
build_openai_passthrough_headers, ensure_upstream_auth_header,
|
||||||
resolve_local_openai_bearer_auth,
|
resolve_local_openai_bearer_auth,
|
||||||
};
|
};
|
||||||
|
use crate::ai_pipeline::transport::kiro::{
|
||||||
|
build_kiro_provider_headers, build_kiro_provider_request_body, KiroProviderHeadersInput,
|
||||||
|
KiroRequestAuth, KIRO_ENVELOPE_NAME,
|
||||||
|
};
|
||||||
use crate::ai_pipeline::transport::local_openai_chat_transport_unsupported_reason;
|
use crate::ai_pipeline::transport::local_openai_chat_transport_unsupported_reason;
|
||||||
use crate::ai_pipeline::transport::vertex::uses_vertex_api_key_query_auth;
|
use crate::ai_pipeline::transport::vertex::uses_vertex_api_key_query_auth;
|
||||||
use crate::ai_pipeline::{ConversionMode, ExecutionStrategy, GatewayProviderTransportSnapshot};
|
use crate::ai_pipeline::{
|
||||||
|
ConversionMode, ExecutionStrategy, GatewayProviderTransportSnapshot,
|
||||||
|
LocalResolvedOAuthRequestAuth,
|
||||||
|
};
|
||||||
use crate::AppState;
|
use crate::AppState;
|
||||||
|
|
||||||
use super::support::{mark_skipped_local_openai_chat_candidate, LocalOpenAiChatDecisionInput};
|
use super::support::{mark_skipped_local_openai_chat_candidate, LocalOpenAiChatDecisionInput};
|
||||||
@@ -38,6 +45,7 @@ pub(crate) struct LocalOpenAiChatCandidatePayloadParts {
|
|||||||
pub(super) execution_strategy: ExecutionStrategy,
|
pub(super) execution_strategy: ExecutionStrategy,
|
||||||
pub(super) conversion_mode: ConversionMode,
|
pub(super) conversion_mode: ConversionMode,
|
||||||
pub(super) report_kind: String,
|
pub(super) report_kind: String,
|
||||||
|
pub(super) envelope_name: Option<&'static str>,
|
||||||
pub(super) transport: Arc<GatewayProviderTransportSnapshot>,
|
pub(super) transport: Arc<GatewayProviderTransportSnapshot>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -194,6 +202,7 @@ pub(crate) async fn resolve_local_openai_chat_candidate_payload_parts(
|
|||||||
execution_strategy: ExecutionStrategy::LocalSameFormat,
|
execution_strategy: ExecutionStrategy::LocalSameFormat,
|
||||||
conversion_mode: ConversionMode::None,
|
conversion_mode: ConversionMode::None,
|
||||||
report_kind: report_kind.to_string(),
|
report_kind: report_kind.to_string(),
|
||||||
|
envelope_name: None,
|
||||||
transport: Arc::clone(transport),
|
transport: Arc::clone(transport),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -232,32 +241,92 @@ pub(crate) async fn resolve_local_openai_chat_candidate_payload_parts(
|
|||||||
.await;
|
.await;
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
let prepared_candidate = match prepare_header_authenticated_candidate(
|
let is_kiro_claude_cli = transport
|
||||||
planner_state,
|
.provider
|
||||||
transport,
|
.provider_type
|
||||||
candidate,
|
.trim()
|
||||||
request_conversion_direct_auth(transport, conversion_kind),
|
.eq_ignore_ascii_case("kiro")
|
||||||
OauthPreparationContext {
|
&& provider_api_format.eq_ignore_ascii_case("claude:cli");
|
||||||
trace_id,
|
let oauth_context = OauthPreparationContext {
|
||||||
api_format: provider_api_format.as_str(),
|
trace_id,
|
||||||
operation: "openai_chat_cross_format",
|
api_format: provider_api_format.as_str(),
|
||||||
},
|
operation: "openai_chat_cross_format",
|
||||||
)
|
};
|
||||||
.await
|
let kiro_auth = if is_kiro_claude_cli {
|
||||||
{
|
match crate::ai_pipeline::planner::candidate_preparation::resolve_candidate_oauth_auth(
|
||||||
Ok(prepared) => prepared,
|
planner_state,
|
||||||
Err(skip_reason) => {
|
transport,
|
||||||
mark_skipped_local_openai_chat_candidate(
|
oauth_context,
|
||||||
state,
|
)
|
||||||
input,
|
.await
|
||||||
trace_id,
|
{
|
||||||
|
Some(LocalResolvedOAuthRequestAuth::Kiro(auth)) => Some(auth),
|
||||||
|
_ => {
|
||||||
|
mark_skipped_local_openai_chat_candidate(
|
||||||
|
state,
|
||||||
|
input,
|
||||||
|
trace_id,
|
||||||
|
candidate,
|
||||||
|
candidate_index,
|
||||||
|
candidate_id,
|
||||||
|
"transport_auth_unavailable",
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
let prepared_candidate = if let Some(kiro_auth) = kiro_auth.as_ref() {
|
||||||
|
let mapped_model =
|
||||||
|
match crate::ai_pipeline::planner::candidate_preparation::resolve_candidate_mapped_model(
|
||||||
candidate,
|
candidate,
|
||||||
candidate_index,
|
) {
|
||||||
candidate_id,
|
Ok(mapped_model) => mapped_model,
|
||||||
skip_reason,
|
Err(skip_reason) => {
|
||||||
)
|
mark_skipped_local_openai_chat_candidate(
|
||||||
.await;
|
state,
|
||||||
return None;
|
input,
|
||||||
|
trace_id,
|
||||||
|
candidate,
|
||||||
|
candidate_index,
|
||||||
|
candidate_id,
|
||||||
|
skip_reason,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
crate::ai_pipeline::planner::candidate_preparation::PreparedHeaderAuthenticatedCandidate {
|
||||||
|
auth_header: kiro_auth.name.to_string(),
|
||||||
|
auth_value: kiro_auth.value.clone(),
|
||||||
|
mapped_model,
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
match prepare_header_authenticated_candidate(
|
||||||
|
planner_state,
|
||||||
|
transport,
|
||||||
|
candidate,
|
||||||
|
request_conversion_direct_auth(transport, conversion_kind),
|
||||||
|
oauth_context,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(prepared) => prepared,
|
||||||
|
Err(skip_reason) => {
|
||||||
|
mark_skipped_local_openai_chat_candidate(
|
||||||
|
state,
|
||||||
|
input,
|
||||||
|
trace_id,
|
||||||
|
candidate,
|
||||||
|
candidate_index,
|
||||||
|
candidate_id,
|
||||||
|
skip_reason,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
return None;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -267,7 +336,11 @@ pub(crate) async fn resolve_local_openai_chat_candidate_payload_parts(
|
|||||||
transport.provider.provider_type.as_str(),
|
transport.provider.provider_type.as_str(),
|
||||||
provider_api_format.as_str(),
|
provider_api_format.as_str(),
|
||||||
upstream_is_stream,
|
upstream_is_stream,
|
||||||
transport.endpoint.body_rules.as_ref(),
|
if is_kiro_claude_cli {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
transport.endpoint.body_rules.as_ref()
|
||||||
|
},
|
||||||
Some(input.auth_context.api_key_id.as_str()),
|
Some(input.auth_context.api_key_id.as_str()),
|
||||||
) else {
|
) else {
|
||||||
mark_skipped_local_openai_chat_candidate(
|
mark_skipped_local_openai_chat_candidate(
|
||||||
@@ -283,6 +356,29 @@ pub(crate) async fn resolve_local_openai_chat_candidate_payload_parts(
|
|||||||
return None;
|
return None;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if let Some(kiro_auth) = kiro_auth.as_ref() {
|
||||||
|
return build_kiro_openai_chat_cross_format_payload_parts(
|
||||||
|
state,
|
||||||
|
parts,
|
||||||
|
trace_id,
|
||||||
|
body_json,
|
||||||
|
input,
|
||||||
|
eligible,
|
||||||
|
candidate_index,
|
||||||
|
candidate_id,
|
||||||
|
decision_kind,
|
||||||
|
transport,
|
||||||
|
provider_api_format.as_str(),
|
||||||
|
prepared_candidate.mapped_model,
|
||||||
|
prepared_candidate.auth_header,
|
||||||
|
prepared_candidate.auth_value,
|
||||||
|
provider_request_body,
|
||||||
|
upstream_is_stream,
|
||||||
|
kiro_auth,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
}
|
||||||
|
|
||||||
let Some(upstream_url) = build_cross_format_openai_chat_upstream_url(
|
let Some(upstream_url) = build_cross_format_openai_chat_upstream_url(
|
||||||
parts,
|
parts,
|
||||||
transport,
|
transport,
|
||||||
@@ -392,6 +488,119 @@ pub(crate) async fn resolve_local_openai_chat_candidate_payload_parts(
|
|||||||
execution_strategy: ExecutionStrategy::LocalCrossFormat,
|
execution_strategy: ExecutionStrategy::LocalCrossFormat,
|
||||||
conversion_mode: ConversionMode::Bidirectional,
|
conversion_mode: ConversionMode::Bidirectional,
|
||||||
report_kind: resolved_report_kind,
|
report_kind: resolved_report_kind,
|
||||||
|
envelope_name: None,
|
||||||
|
transport: Arc::clone(transport),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(clippy::too_many_arguments)]
|
||||||
|
async fn build_kiro_openai_chat_cross_format_payload_parts(
|
||||||
|
state: &AppState,
|
||||||
|
parts: &http::request::Parts,
|
||||||
|
trace_id: &str,
|
||||||
|
original_body_json: &serde_json::Value,
|
||||||
|
input: &LocalOpenAiChatDecisionInput,
|
||||||
|
eligible: &EligibleLocalExecutionCandidate,
|
||||||
|
candidate_index: u32,
|
||||||
|
candidate_id: &str,
|
||||||
|
decision_kind: &str,
|
||||||
|
transport: &Arc<GatewayProviderTransportSnapshot>,
|
||||||
|
provider_api_format: &str,
|
||||||
|
mapped_model: String,
|
||||||
|
auth_header: String,
|
||||||
|
auth_value: String,
|
||||||
|
claude_request_body: Value,
|
||||||
|
upstream_is_stream: bool,
|
||||||
|
kiro_auth: &KiroRequestAuth,
|
||||||
|
) -> Option<LocalOpenAiChatCandidatePayloadParts> {
|
||||||
|
let candidate = &eligible.candidate;
|
||||||
|
let provider_request_body = match build_kiro_provider_request_body(
|
||||||
|
&claude_request_body,
|
||||||
|
&mapped_model,
|
||||||
|
&kiro_auth.auth_config,
|
||||||
|
transport.endpoint.body_rules.as_ref(),
|
||||||
|
) {
|
||||||
|
Some(body) => body,
|
||||||
|
None => {
|
||||||
|
mark_skipped_local_openai_chat_candidate(
|
||||||
|
state,
|
||||||
|
input,
|
||||||
|
trace_id,
|
||||||
|
candidate,
|
||||||
|
candidate_index,
|
||||||
|
candidate_id,
|
||||||
|
"provider_request_body_missing",
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let upstream_url = match crate::ai_pipeline::build_provider_transport_request_url(
|
||||||
|
transport,
|
||||||
|
provider_api_format,
|
||||||
|
Some(&mapped_model),
|
||||||
|
upstream_is_stream,
|
||||||
|
parts.uri.query(),
|
||||||
|
Some(kiro_auth.auth_config.effective_api_region()),
|
||||||
|
) {
|
||||||
|
Some(url) => url,
|
||||||
|
None => {
|
||||||
|
mark_skipped_local_openai_chat_candidate(
|
||||||
|
state,
|
||||||
|
input,
|
||||||
|
trace_id,
|
||||||
|
candidate,
|
||||||
|
candidate_index,
|
||||||
|
candidate_id,
|
||||||
|
"upstream_url_missing",
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let provider_request_headers = match build_kiro_provider_headers(KiroProviderHeadersInput {
|
||||||
|
headers: &parts.headers,
|
||||||
|
provider_request_body: &provider_request_body,
|
||||||
|
original_request_body: original_body_json,
|
||||||
|
header_rules: transport.endpoint.header_rules.as_ref(),
|
||||||
|
auth_header: &auth_header,
|
||||||
|
auth_value: &auth_value,
|
||||||
|
auth_config: &kiro_auth.auth_config,
|
||||||
|
machine_id: kiro_auth.machine_id.as_str(),
|
||||||
|
}) {
|
||||||
|
Some(headers) => headers,
|
||||||
|
None => {
|
||||||
|
mark_skipped_local_openai_chat_candidate(
|
||||||
|
state,
|
||||||
|
input,
|
||||||
|
trace_id,
|
||||||
|
candidate,
|
||||||
|
candidate_index,
|
||||||
|
candidate_id,
|
||||||
|
"transport_header_rules_apply_failed",
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let resolved_report_kind = if decision_kind == OPENAI_CHAT_STREAM_PLAN_KIND {
|
||||||
|
"openai_chat_stream_success".to_string()
|
||||||
|
} else {
|
||||||
|
"openai_chat_sync_finalize".to_string()
|
||||||
|
};
|
||||||
|
|
||||||
|
Some(LocalOpenAiChatCandidatePayloadParts {
|
||||||
|
auth_header,
|
||||||
|
auth_value,
|
||||||
|
mapped_model,
|
||||||
|
provider_api_format: provider_api_format.to_string(),
|
||||||
|
provider_request_body,
|
||||||
|
provider_request_headers,
|
||||||
|
upstream_url,
|
||||||
|
execution_strategy: ExecutionStrategy::LocalCrossFormat,
|
||||||
|
conversion_mode: ConversionMode::Bidirectional,
|
||||||
|
report_kind: resolved_report_kind,
|
||||||
|
envelope_name: Some(KIRO_ENVELOPE_NAME),
|
||||||
transport: Arc::clone(transport),
|
transport: Arc::clone(transport),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -70,8 +70,8 @@ pub(crate) async fn maybe_build_local_openai_cli_decision_payload_for_candidate(
|
|||||||
{
|
{
|
||||||
extra_fields.insert("proxy".to_string(), proxy_value);
|
extra_fields.insert("proxy".to_string(), proxy_value);
|
||||||
}
|
}
|
||||||
if resolved.is_antigravity {
|
if let Some(envelope_name) = resolved.envelope_name {
|
||||||
extra_fields.insert("envelope_name".to_string(), json!("antigravity:v1internal"));
|
extra_fields.insert("envelope_name".to_string(), json!(envelope_name));
|
||||||
}
|
}
|
||||||
let report_context = append_local_failover_policy_to_value(
|
let report_context = append_local_failover_policy_to_value(
|
||||||
append_execution_contract_fields_to_value(
|
append_execution_contract_fields_to_value(
|
||||||
@@ -104,7 +104,7 @@ pub(crate) async fn maybe_build_local_openai_cli_decision_payload_for_candidate(
|
|||||||
.and_then(serde_json::Value::as_bool)
|
.and_then(serde_json::Value::as_bool)
|
||||||
.unwrap_or(false),
|
.unwrap_or(false),
|
||||||
upstream_is_stream: resolved.upstream_is_stream,
|
upstream_is_stream: resolved.upstream_is_stream,
|
||||||
has_envelope: resolved.is_antigravity,
|
has_envelope: resolved.envelope_name.is_some(),
|
||||||
needs_conversion: matches!(
|
needs_conversion: matches!(
|
||||||
resolved.conversion_mode,
|
resolved.conversion_mode,
|
||||||
crate::ai_pipeline::ConversionMode::Bidirectional
|
crate::ai_pipeline::ConversionMode::Bidirectional
|
||||||
@@ -139,7 +139,7 @@ pub(crate) async fn maybe_build_local_openai_cli_decision_payload_for_candidate(
|
|||||||
upstream_base_url = %resolved.transport.endpoint.base_url,
|
upstream_base_url = %resolved.transport.endpoint.base_url,
|
||||||
upstream_url = %resolved.upstream_url,
|
upstream_url = %resolved.upstream_url,
|
||||||
upstream_is_stream = resolved.upstream_is_stream,
|
upstream_is_stream = resolved.upstream_is_stream,
|
||||||
has_envelope = resolved.is_antigravity,
|
has_envelope = resolved.envelope_name.is_some(),
|
||||||
"gateway built local openai cli decision payload"
|
"gateway built local openai cli decision payload"
|
||||||
);
|
);
|
||||||
let super::request::LocalOpenAiCliCandidatePayloadParts {
|
let super::request::LocalOpenAiCliCandidatePayloadParts {
|
||||||
@@ -153,6 +153,7 @@ pub(crate) async fn maybe_build_local_openai_cli_decision_payload_for_candidate(
|
|||||||
execution_strategy,
|
execution_strategy,
|
||||||
conversion_mode,
|
conversion_mode,
|
||||||
is_antigravity: _,
|
is_antigravity: _,
|
||||||
|
envelope_name: _,
|
||||||
upstream_is_stream,
|
upstream_is_stream,
|
||||||
transport,
|
transport,
|
||||||
} = resolved;
|
} = resolved;
|
||||||
|
|||||||
@@ -27,10 +27,17 @@ use crate::ai_pipeline::transport::auth::{
|
|||||||
build_openai_passthrough_headers, ensure_upstream_auth_header, resolve_local_gemini_auth,
|
build_openai_passthrough_headers, ensure_upstream_auth_header, resolve_local_gemini_auth,
|
||||||
resolve_local_openai_bearer_auth, resolve_local_standard_auth,
|
resolve_local_openai_bearer_auth, resolve_local_standard_auth,
|
||||||
};
|
};
|
||||||
|
use crate::ai_pipeline::transport::kiro::{
|
||||||
|
build_kiro_provider_headers, build_kiro_provider_request_body,
|
||||||
|
local_kiro_request_transport_unsupported_reason_with_network, KiroProviderHeadersInput,
|
||||||
|
KiroRequestAuth, KIRO_ENVELOPE_NAME,
|
||||||
|
};
|
||||||
use crate::ai_pipeline::transport::local_standard_transport_unsupported_reason_with_network;
|
use crate::ai_pipeline::transport::local_standard_transport_unsupported_reason_with_network;
|
||||||
use crate::ai_pipeline::transport::vertex::uses_vertex_api_key_query_auth;
|
use crate::ai_pipeline::transport::vertex::uses_vertex_api_key_query_auth;
|
||||||
use crate::ai_pipeline::{ConversionMode, ExecutionStrategy};
|
use crate::ai_pipeline::{ConversionMode, ExecutionStrategy};
|
||||||
use crate::ai_pipeline::{GatewayProviderTransportSnapshot, PlannerAppState};
|
use crate::ai_pipeline::{
|
||||||
|
GatewayProviderTransportSnapshot, LocalResolvedOAuthRequestAuth, PlannerAppState,
|
||||||
|
};
|
||||||
use crate::AppState;
|
use crate::AppState;
|
||||||
|
|
||||||
use super::support::{mark_skipped_local_openai_cli_candidate, LocalOpenAiCliDecisionInput};
|
use super::support::{mark_skipped_local_openai_cli_candidate, LocalOpenAiCliDecisionInput};
|
||||||
@@ -49,6 +56,7 @@ pub(crate) struct LocalOpenAiCliCandidatePayloadParts {
|
|||||||
pub(super) execution_strategy: ExecutionStrategy,
|
pub(super) execution_strategy: ExecutionStrategy,
|
||||||
pub(super) conversion_mode: ConversionMode,
|
pub(super) conversion_mode: ConversionMode,
|
||||||
pub(super) is_antigravity: bool,
|
pub(super) is_antigravity: bool,
|
||||||
|
pub(super) envelope_name: Option<&'static str>,
|
||||||
pub(super) upstream_is_stream: bool,
|
pub(super) upstream_is_stream: bool,
|
||||||
pub(super) transport: Arc<GatewayProviderTransportSnapshot>,
|
pub(super) transport: Arc<GatewayProviderTransportSnapshot>,
|
||||||
}
|
}
|
||||||
@@ -76,10 +84,18 @@ pub(crate) async fn resolve_local_openai_cli_candidate_payload_parts(
|
|||||||
.provider_type
|
.provider_type
|
||||||
.trim()
|
.trim()
|
||||||
.eq_ignore_ascii_case("antigravity");
|
.eq_ignore_ascii_case("antigravity");
|
||||||
|
let is_kiro_claude_cli = transport
|
||||||
|
.provider
|
||||||
|
.provider_type
|
||||||
|
.trim()
|
||||||
|
.eq_ignore_ascii_case("kiro")
|
||||||
|
&& provider_api_format.eq_ignore_ascii_case("claude:cli");
|
||||||
|
|
||||||
let same_format = provider_api_format == client_api_format;
|
let same_format = provider_api_format == client_api_format;
|
||||||
let conversion_kind = request_conversion_kind(spec_metadata.api_format, provider_api_format);
|
let conversion_kind = request_conversion_kind(spec_metadata.api_format, provider_api_format);
|
||||||
let transport_unsupported_reason = if same_format {
|
let transport_unsupported_reason = if same_format && is_kiro_claude_cli {
|
||||||
|
local_kiro_request_transport_unsupported_reason_with_network(transport)
|
||||||
|
} else if same_format {
|
||||||
local_standard_transport_unsupported_reason_with_network(transport, provider_api_format)
|
local_standard_transport_unsupported_reason_with_network(transport, provider_api_format)
|
||||||
} else {
|
} else {
|
||||||
match conversion_kind {
|
match conversion_kind {
|
||||||
@@ -106,7 +122,41 @@ pub(crate) async fn resolve_local_openai_cli_candidate_payload_parts(
|
|||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
|
|
||||||
let direct_auth = if same_format {
|
let oauth_context = OauthPreparationContext {
|
||||||
|
trace_id,
|
||||||
|
api_format: provider_api_format,
|
||||||
|
operation: "openai_cli_candidate_request",
|
||||||
|
};
|
||||||
|
let kiro_auth = if is_kiro_claude_cli {
|
||||||
|
match crate::ai_pipeline::planner::candidate_preparation::resolve_candidate_oauth_auth(
|
||||||
|
planner_state,
|
||||||
|
transport,
|
||||||
|
oauth_context,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Some(LocalResolvedOAuthRequestAuth::Kiro(auth)) => Some(auth),
|
||||||
|
_ => {
|
||||||
|
mark_skipped_local_openai_cli_candidate(
|
||||||
|
state,
|
||||||
|
input,
|
||||||
|
trace_id,
|
||||||
|
candidate,
|
||||||
|
candidate_index,
|
||||||
|
candidate_id,
|
||||||
|
"transport_auth_unavailable",
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
|
let direct_auth = if kiro_auth.is_some() {
|
||||||
|
None
|
||||||
|
} else if same_format {
|
||||||
match provider_api_format {
|
match provider_api_format {
|
||||||
"gemini:cli" => resolve_local_gemini_auth(transport),
|
"gemini:cli" => resolve_local_gemini_auth(transport),
|
||||||
"claude:cli" => resolve_local_standard_auth(transport),
|
"claude:cli" => resolve_local_standard_auth(transport),
|
||||||
@@ -116,32 +166,55 @@ pub(crate) async fn resolve_local_openai_cli_candidate_payload_parts(
|
|||||||
} else {
|
} else {
|
||||||
conversion_kind.and_then(|kind| request_conversion_direct_auth(transport, kind))
|
conversion_kind.and_then(|kind| request_conversion_direct_auth(transport, kind))
|
||||||
};
|
};
|
||||||
let prepared_candidate = match prepare_header_authenticated_candidate(
|
let prepared_candidate = if let Some(kiro_auth) = kiro_auth.as_ref() {
|
||||||
planner_state,
|
let mapped_model =
|
||||||
transport,
|
match crate::ai_pipeline::planner::candidate_preparation::resolve_candidate_mapped_model(
|
||||||
candidate,
|
|
||||||
direct_auth,
|
|
||||||
OauthPreparationContext {
|
|
||||||
trace_id,
|
|
||||||
api_format: provider_api_format,
|
|
||||||
operation: "openai_cli_candidate_request",
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
{
|
|
||||||
Ok(prepared) => prepared,
|
|
||||||
Err(skip_reason) => {
|
|
||||||
mark_skipped_local_openai_cli_candidate(
|
|
||||||
state,
|
|
||||||
input,
|
|
||||||
trace_id,
|
|
||||||
candidate,
|
candidate,
|
||||||
candidate_index,
|
) {
|
||||||
candidate_id,
|
Ok(mapped_model) => mapped_model,
|
||||||
skip_reason,
|
Err(skip_reason) => {
|
||||||
)
|
mark_skipped_local_openai_cli_candidate(
|
||||||
.await;
|
state,
|
||||||
return None;
|
input,
|
||||||
|
trace_id,
|
||||||
|
candidate,
|
||||||
|
candidate_index,
|
||||||
|
candidate_id,
|
||||||
|
skip_reason,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
crate::ai_pipeline::planner::candidate_preparation::PreparedHeaderAuthenticatedCandidate {
|
||||||
|
auth_header: kiro_auth.name.to_string(),
|
||||||
|
auth_value: kiro_auth.value.clone(),
|
||||||
|
mapped_model,
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
match prepare_header_authenticated_candidate(
|
||||||
|
planner_state,
|
||||||
|
transport,
|
||||||
|
candidate,
|
||||||
|
direct_auth,
|
||||||
|
oauth_context,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(prepared) => prepared,
|
||||||
|
Err(skip_reason) => {
|
||||||
|
mark_skipped_local_openai_cli_candidate(
|
||||||
|
state,
|
||||||
|
input,
|
||||||
|
trace_id,
|
||||||
|
candidate,
|
||||||
|
candidate_index,
|
||||||
|
candidate_id,
|
||||||
|
skip_reason,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
return None;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
let auth_header = prepared_candidate.auth_header;
|
let auth_header = prepared_candidate.auth_header;
|
||||||
@@ -163,7 +236,11 @@ pub(crate) async fn resolve_local_openai_cli_candidate_payload_parts(
|
|||||||
provider_api_format,
|
provider_api_format,
|
||||||
upstream_is_stream,
|
upstream_is_stream,
|
||||||
transport.provider.provider_type.as_str(),
|
transport.provider.provider_type.as_str(),
|
||||||
transport.endpoint.body_rules.as_ref(),
|
if is_kiro_claude_cli {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
transport.endpoint.body_rules.as_ref()
|
||||||
|
},
|
||||||
Some(input.auth_context.api_key_id.as_str()),
|
Some(input.auth_context.api_key_id.as_str()),
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
@@ -173,7 +250,11 @@ pub(crate) async fn resolve_local_openai_cli_candidate_payload_parts(
|
|||||||
upstream_is_stream,
|
upstream_is_stream,
|
||||||
transport.provider.provider_type.as_str(),
|
transport.provider.provider_type.as_str(),
|
||||||
provider_api_format,
|
provider_api_format,
|
||||||
transport.endpoint.body_rules.as_ref(),
|
if is_kiro_claude_cli {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
transport.endpoint.body_rules.as_ref()
|
||||||
|
},
|
||||||
Some(input.auth_context.api_key_id.as_str()),
|
Some(input.auth_context.api_key_id.as_str()),
|
||||||
)
|
)
|
||||||
}) else {
|
}) else {
|
||||||
@@ -240,6 +321,30 @@ pub(crate) async fn resolve_local_openai_cli_candidate_payload_parts(
|
|||||||
base_provider_request_body
|
base_provider_request_body
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if let Some(kiro_auth) = kiro_auth.as_ref() {
|
||||||
|
return build_kiro_openai_cli_payload_parts(
|
||||||
|
state,
|
||||||
|
parts,
|
||||||
|
trace_id,
|
||||||
|
body_json,
|
||||||
|
input,
|
||||||
|
eligible,
|
||||||
|
candidate_index,
|
||||||
|
candidate_id,
|
||||||
|
spec_metadata.api_format,
|
||||||
|
transport,
|
||||||
|
provider_api_format,
|
||||||
|
mapped_model,
|
||||||
|
auth_header,
|
||||||
|
auth_value,
|
||||||
|
provider_request_body,
|
||||||
|
upstream_is_stream,
|
||||||
|
needs_bidirectional_conversion,
|
||||||
|
kiro_auth,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
}
|
||||||
|
|
||||||
let Some(upstream_url) = (if needs_bidirectional_conversion {
|
let Some(upstream_url) = (if needs_bidirectional_conversion {
|
||||||
build_cross_format_openai_cli_upstream_url(
|
build_cross_format_openai_cli_upstream_url(
|
||||||
parts,
|
parts,
|
||||||
@@ -392,6 +497,149 @@ pub(crate) async fn resolve_local_openai_cli_candidate_payload_parts(
|
|||||||
conversion_mode,
|
conversion_mode,
|
||||||
is_antigravity: is_antigravity
|
is_antigravity: is_antigravity
|
||||||
|| antigravity_auth.is_some() && ANTIGRAVITY_ENVELOPE_NAME == "antigravity:v1internal",
|
|| antigravity_auth.is_some() && ANTIGRAVITY_ENVELOPE_NAME == "antigravity:v1internal",
|
||||||
|
envelope_name: if is_antigravity || antigravity_auth.is_some() {
|
||||||
|
Some(ANTIGRAVITY_ENVELOPE_NAME)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
},
|
||||||
|
upstream_is_stream,
|
||||||
|
transport: Arc::clone(transport),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(clippy::too_many_arguments)]
|
||||||
|
async fn build_kiro_openai_cli_payload_parts(
|
||||||
|
state: &AppState,
|
||||||
|
parts: &http::request::Parts,
|
||||||
|
trace_id: &str,
|
||||||
|
original_body_json: &serde_json::Value,
|
||||||
|
input: &LocalOpenAiCliDecisionInput,
|
||||||
|
eligible: &EligibleLocalExecutionCandidate,
|
||||||
|
candidate_index: u32,
|
||||||
|
candidate_id: &str,
|
||||||
|
client_api_format: &str,
|
||||||
|
transport: &Arc<GatewayProviderTransportSnapshot>,
|
||||||
|
provider_api_format: &str,
|
||||||
|
mapped_model: String,
|
||||||
|
auth_header: String,
|
||||||
|
auth_value: String,
|
||||||
|
claude_request_body: Value,
|
||||||
|
upstream_is_stream: bool,
|
||||||
|
needs_bidirectional_conversion: bool,
|
||||||
|
kiro_auth: &KiroRequestAuth,
|
||||||
|
) -> Option<LocalOpenAiCliCandidatePayloadParts> {
|
||||||
|
let candidate = &eligible.candidate;
|
||||||
|
let provider_request_body = match build_kiro_provider_request_body(
|
||||||
|
&claude_request_body,
|
||||||
|
&mapped_model,
|
||||||
|
&kiro_auth.auth_config,
|
||||||
|
transport.endpoint.body_rules.as_ref(),
|
||||||
|
) {
|
||||||
|
Some(body) => body,
|
||||||
|
None => {
|
||||||
|
mark_skipped_local_openai_cli_candidate(
|
||||||
|
state,
|
||||||
|
input,
|
||||||
|
trace_id,
|
||||||
|
candidate,
|
||||||
|
candidate_index,
|
||||||
|
candidate_id,
|
||||||
|
"provider_request_body_missing",
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let upstream_url = match crate::ai_pipeline::build_provider_transport_request_url(
|
||||||
|
transport,
|
||||||
|
provider_api_format,
|
||||||
|
Some(&mapped_model),
|
||||||
|
upstream_is_stream,
|
||||||
|
parts.uri.query(),
|
||||||
|
Some(kiro_auth.auth_config.effective_api_region()),
|
||||||
|
) {
|
||||||
|
Some(url) => url,
|
||||||
|
None => {
|
||||||
|
mark_skipped_local_openai_cli_candidate(
|
||||||
|
state,
|
||||||
|
input,
|
||||||
|
trace_id,
|
||||||
|
candidate,
|
||||||
|
candidate_index,
|
||||||
|
candidate_id,
|
||||||
|
"upstream_url_missing",
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let provider_request_headers = match build_kiro_provider_headers(KiroProviderHeadersInput {
|
||||||
|
headers: &parts.headers,
|
||||||
|
provider_request_body: &provider_request_body,
|
||||||
|
original_request_body: original_body_json,
|
||||||
|
header_rules: transport.endpoint.header_rules.as_ref(),
|
||||||
|
auth_header: &auth_header,
|
||||||
|
auth_value: &auth_value,
|
||||||
|
auth_config: &kiro_auth.auth_config,
|
||||||
|
machine_id: kiro_auth.machine_id.as_str(),
|
||||||
|
}) {
|
||||||
|
Some(headers) => headers,
|
||||||
|
None => {
|
||||||
|
mark_skipped_local_openai_cli_candidate(
|
||||||
|
state,
|
||||||
|
input,
|
||||||
|
trace_id,
|
||||||
|
candidate,
|
||||||
|
candidate_index,
|
||||||
|
candidate_id,
|
||||||
|
"transport_header_rules_apply_failed",
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let execution_strategy = if needs_bidirectional_conversion {
|
||||||
|
ExecutionStrategy::LocalCrossFormat
|
||||||
|
} else {
|
||||||
|
ExecutionStrategy::LocalSameFormat
|
||||||
|
};
|
||||||
|
let conversion_mode = if needs_bidirectional_conversion {
|
||||||
|
ConversionMode::Bidirectional
|
||||||
|
} else {
|
||||||
|
ConversionMode::None
|
||||||
|
};
|
||||||
|
|
||||||
|
debug!(
|
||||||
|
event_name = "local_openai_cli_kiro_upstream_url_resolved",
|
||||||
|
log_type = "debug",
|
||||||
|
trace_id = %trace_id,
|
||||||
|
candidate_id = %candidate_id,
|
||||||
|
candidate_index,
|
||||||
|
provider_id = %candidate.provider_id,
|
||||||
|
endpoint_id = %candidate.endpoint_id,
|
||||||
|
key_id = %candidate.key_id,
|
||||||
|
provider_type = %transport.provider.provider_type,
|
||||||
|
client_api_format = client_api_format,
|
||||||
|
provider_api_format = %provider_api_format,
|
||||||
|
execution_strategy = execution_strategy.as_str(),
|
||||||
|
conversion_mode = conversion_mode.as_str(),
|
||||||
|
upstream_url = %upstream_url,
|
||||||
|
upstream_is_stream,
|
||||||
|
"gateway resolved local openai cli kiro upstream url"
|
||||||
|
);
|
||||||
|
|
||||||
|
Some(LocalOpenAiCliCandidatePayloadParts {
|
||||||
|
auth_header,
|
||||||
|
auth_value,
|
||||||
|
mapped_model,
|
||||||
|
provider_api_format: provider_api_format.to_string(),
|
||||||
|
provider_request_body,
|
||||||
|
provider_request_headers,
|
||||||
|
upstream_url,
|
||||||
|
execution_strategy,
|
||||||
|
conversion_mode,
|
||||||
|
is_antigravity: false,
|
||||||
|
envelope_name: Some(KIRO_ENVELOPE_NAME),
|
||||||
upstream_is_stream,
|
upstream_is_stream,
|
||||||
transport: Arc::clone(transport),
|
transport: Arc::clone(transport),
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
use aether_provider_transport::auth::{
|
use aether_provider_transport::auth::{
|
||||||
resolve_local_gemini_auth, resolve_local_openai_bearer_auth, resolve_local_standard_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::{
|
use aether_provider_transport::policy::{
|
||||||
local_gemini_transport_unsupported_reason_with_network,
|
local_gemini_transport_unsupported_reason_with_network,
|
||||||
local_openai_chat_transport_unsupported_reason,
|
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() {
|
if request_conversion_kind(client_api_format.as_str(), provider_api_format.as_str()).is_none() {
|
||||||
return false;
|
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(
|
request_conversion_enabled_for_transport(
|
||||||
transport,
|
transport,
|
||||||
client_api_format.as_str(),
|
client_api_format.as_str(),
|
||||||
@@ -240,6 +255,20 @@ pub fn request_conversion_transport_unsupported_reason(
|
|||||||
transport: &GatewayProviderTransportSnapshot,
|
transport: &GatewayProviderTransportSnapshot,
|
||||||
_kind: RequestConversionKind,
|
_kind: RequestConversionKind,
|
||||||
) -> Option<&'static str> {
|
) -> 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
|
match transport
|
||||||
.endpoint
|
.endpoint
|
||||||
.api_format
|
.api_format
|
||||||
@@ -898,4 +927,68 @@ mod tests {
|
|||||||
Some(("key".to_string(), "vertex-secret".to_string()))
|
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 {
|
pub struct OpenAICliClientEmitter {
|
||||||
response_id: Option<String>,
|
response_id: Option<String>,
|
||||||
model: Option<String>,
|
model: Option<String>,
|
||||||
|
message_item_id: Option<String>,
|
||||||
|
reasoning_item_id: Option<String>,
|
||||||
started: bool,
|
started: bool,
|
||||||
finished: bool,
|
finished: bool,
|
||||||
sequence_number: u64,
|
sequence_number: u64,
|
||||||
@@ -1166,11 +1168,29 @@ impl OpenAICliClientEmitter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn message_item_id(&self) -> String {
|
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 {
|
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 {
|
fn in_progress_response(&self) -> Value {
|
||||||
@@ -1269,6 +1289,7 @@ impl OpenAICliClientEmitter {
|
|||||||
fn ensure_reasoning_item_started(&mut self) -> Result<Vec<u8>, PipelineFinalizeError> {
|
fn ensure_reasoning_item_started(&mut self) -> Result<Vec<u8>, PipelineFinalizeError> {
|
||||||
let mut out = self.ensure_started()?;
|
let mut out = self.ensure_started()?;
|
||||||
let output_index = self.ensure_reasoning_output_index();
|
let output_index = self.ensure_reasoning_output_index();
|
||||||
|
let item_id = self.ensure_reasoning_item_id();
|
||||||
if !self.reasoning_item_started {
|
if !self.reasoning_item_started {
|
||||||
out.extend(self.encode_response_event(
|
out.extend(self.encode_response_event(
|
||||||
"response.output_item.added",
|
"response.output_item.added",
|
||||||
@@ -1278,7 +1299,7 @@ impl OpenAICliClientEmitter {
|
|||||||
"output_index": output_index,
|
"output_index": output_index,
|
||||||
"item": {
|
"item": {
|
||||||
"type": "reasoning",
|
"type": "reasoning",
|
||||||
"id": self.reasoning_item_id(),
|
"id": item_id.clone(),
|
||||||
"summary": [],
|
"summary": [],
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
@@ -1291,7 +1312,7 @@ impl OpenAICliClientEmitter {
|
|||||||
json!({
|
json!({
|
||||||
"type": "response.reasoning_summary_part.added",
|
"type": "response.reasoning_summary_part.added",
|
||||||
"response_id": self.response_id(),
|
"response_id": self.response_id(),
|
||||||
"item_id": self.reasoning_item_id(),
|
"item_id": item_id,
|
||||||
"output_index": output_index,
|
"output_index": output_index,
|
||||||
"summary_index": 0,
|
"summary_index": 0,
|
||||||
"part": {
|
"part": {
|
||||||
@@ -1308,8 +1329,8 @@ impl OpenAICliClientEmitter {
|
|||||||
fn ensure_text_item_started(&mut self) -> Result<Vec<u8>, PipelineFinalizeError> {
|
fn ensure_text_item_started(&mut self) -> Result<Vec<u8>, PipelineFinalizeError> {
|
||||||
let mut out = self.ensure_started()?;
|
let mut out = self.ensure_started()?;
|
||||||
let output_index = self.ensure_message_output_index();
|
let output_index = self.ensure_message_output_index();
|
||||||
|
let item_id = self.ensure_message_item_id();
|
||||||
if !self.text_item_started {
|
if !self.text_item_started {
|
||||||
let item_id = self.message_item_id();
|
|
||||||
out.extend(self.encode_response_event(
|
out.extend(self.encode_response_event(
|
||||||
"response.output_item.added",
|
"response.output_item.added",
|
||||||
json!({
|
json!({
|
||||||
@@ -1318,7 +1339,7 @@ impl OpenAICliClientEmitter {
|
|||||||
"output_index": output_index,
|
"output_index": output_index,
|
||||||
"item": {
|
"item": {
|
||||||
"type": "message",
|
"type": "message",
|
||||||
"id": item_id,
|
"id": item_id.clone(),
|
||||||
"status": "in_progress",
|
"status": "in_progress",
|
||||||
"role": "assistant",
|
"role": "assistant",
|
||||||
"content": [],
|
"content": [],
|
||||||
@@ -1328,7 +1349,6 @@ impl OpenAICliClientEmitter {
|
|||||||
self.text_item_started = true;
|
self.text_item_started = true;
|
||||||
}
|
}
|
||||||
if !self.text_part_started {
|
if !self.text_part_started {
|
||||||
let item_id = self.message_item_id();
|
|
||||||
out.extend(self.encode_response_event(
|
out.extend(self.encode_response_event(
|
||||||
"response.content_part.added",
|
"response.content_part.added",
|
||||||
json!({
|
json!({
|
||||||
@@ -1849,6 +1869,31 @@ mod tests {
|
|||||||
assert_eq!(response_sequence_numbers(&sse), (1..=9).collect::<Vec<_>>());
|
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]
|
#[test]
|
||||||
fn openai_cli_provider_state_accepts_done_events_without_deltas() {
|
fn openai_cli_provider_state_accepts_done_events_without_deltas() {
|
||||||
let mut state = OpenAICliProviderState::default();
|
let mut state = OpenAICliProviderState::default();
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ pub enum FinalizeStreamRewriteMode {
|
|||||||
OpenAiImage,
|
OpenAiImage,
|
||||||
Standard,
|
Standard,
|
||||||
KiroToClaudeCli,
|
KiroToClaudeCli,
|
||||||
|
KiroToClaudeCliThenStandard,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn resolve_finalize_stream_rewrite_mode(
|
pub fn resolve_finalize_stream_rewrite_mode(
|
||||||
@@ -38,6 +39,17 @@ pub fn resolve_finalize_stream_rewrite_mode(
|
|||||||
.trim()
|
.trim()
|
||||||
.to_ascii_lowercase();
|
.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 {
|
if needs_conversion {
|
||||||
return supports_standard_stream_rewrite(
|
return supports_standard_stream_rewrite(
|
||||||
provider_api_format.as_str(),
|
provider_api_format.as_str(),
|
||||||
|
|||||||
Reference in New Issue
Block a user