mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-02 01:10:23 +08:00
Fix reasoning model directive response identity
This commit is contained in:
@@ -267,6 +267,9 @@ where
|
|||||||
self.state,
|
self.state,
|
||||||
self.trace_id,
|
self.trace_id,
|
||||||
self.persistence_policy.available,
|
self.persistence_policy.available,
|
||||||
|
self.persistence_policy
|
||||||
|
.skipped
|
||||||
|
.record_runtime_miss_diagnostic,
|
||||||
candidates,
|
candidates,
|
||||||
self.sticky_session_token,
|
self.sticky_session_token,
|
||||||
self.requested_model,
|
self.requested_model,
|
||||||
@@ -528,6 +531,8 @@ where
|
|||||||
state,
|
state,
|
||||||
candidates,
|
candidates,
|
||||||
0,
|
0,
|
||||||
|
Some(trace_id),
|
||||||
|
persistence_policy.skipped.record_runtime_miss_diagnostic,
|
||||||
sticky_session_token,
|
sticky_session_token,
|
||||||
requested_model,
|
requested_model,
|
||||||
request_auth_channel,
|
request_auth_channel,
|
||||||
@@ -543,6 +548,8 @@ fn build_logical_candidate_items<'a>(
|
|||||||
state: PlannerAppState<'a>,
|
state: PlannerAppState<'a>,
|
||||||
candidates: Vec<EligibleLocalExecutionCandidate>,
|
candidates: Vec<EligibleLocalExecutionCandidate>,
|
||||||
starting_candidate_index: u32,
|
starting_candidate_index: u32,
|
||||||
|
trace_id: Option<&str>,
|
||||||
|
record_runtime_miss_diagnostic: bool,
|
||||||
sticky_session_token: Option<&str>,
|
sticky_session_token: Option<&str>,
|
||||||
requested_model: Option<&str>,
|
requested_model: Option<&str>,
|
||||||
request_auth_channel: Option<&str>,
|
request_auth_channel: Option<&str>,
|
||||||
@@ -563,14 +570,20 @@ fn build_logical_candidate_items<'a>(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
LocalExecutionCandidateKind::PoolGroup => {
|
LocalExecutionCandidateKind::PoolGroup => {
|
||||||
|
let cursor = PoolKeyCursor::new(
|
||||||
|
state,
|
||||||
|
candidate,
|
||||||
|
sticky_session_token,
|
||||||
|
requested_model,
|
||||||
|
request_auth_channel,
|
||||||
|
);
|
||||||
|
let cursor = if let Some(trace_id) = trace_id {
|
||||||
|
cursor.with_runtime_miss_diagnostic(trace_id, record_runtime_miss_diagnostic)
|
||||||
|
} else {
|
||||||
|
cursor
|
||||||
|
};
|
||||||
items.push_back(LocalExecutionCandidateAttemptSourceItem::Pool {
|
items.push_back(LocalExecutionCandidateAttemptSourceItem::Pool {
|
||||||
cursor: PoolKeyCursor::new(
|
cursor,
|
||||||
state,
|
|
||||||
candidate,
|
|
||||||
sticky_session_token,
|
|
||||||
requested_model,
|
|
||||||
request_auth_channel,
|
|
||||||
),
|
|
||||||
candidate_index,
|
candidate_index,
|
||||||
pending_attempts: VecDeque::new(),
|
pending_attempts: VecDeque::new(),
|
||||||
});
|
});
|
||||||
@@ -757,6 +770,8 @@ impl<'a> RequestedModelAttemptPageCursor<'a> {
|
|||||||
self.state,
|
self.state,
|
||||||
candidates,
|
candidates,
|
||||||
self.next_candidate_index,
|
self.next_candidate_index,
|
||||||
|
Some(&self.trace_id),
|
||||||
|
self.record_runtime_miss_diagnostic,
|
||||||
self.sticky_session_token.as_deref(),
|
self.sticky_session_token.as_deref(),
|
||||||
Some(&self.requested_model),
|
Some(&self.requested_model),
|
||||||
self.request_auth_channel.as_deref(),
|
self.request_auth_channel.as_deref(),
|
||||||
@@ -922,6 +937,7 @@ async fn materialize_logical_local_execution_candidate_attempts<F>(
|
|||||||
state: PlannerAppState<'_>,
|
state: PlannerAppState<'_>,
|
||||||
trace_id: &str,
|
trace_id: &str,
|
||||||
context: LocalAvailableCandidatePersistenceContext<'_>,
|
context: LocalAvailableCandidatePersistenceContext<'_>,
|
||||||
|
record_runtime_miss_diagnostic: bool,
|
||||||
candidates: Vec<EligibleLocalExecutionCandidate>,
|
candidates: Vec<EligibleLocalExecutionCandidate>,
|
||||||
sticky_session_token: Option<&str>,
|
sticky_session_token: Option<&str>,
|
||||||
requested_model: Option<&str>,
|
requested_model: Option<&str>,
|
||||||
@@ -956,7 +972,8 @@ where
|
|||||||
sticky_session_token,
|
sticky_session_token,
|
||||||
requested_model,
|
requested_model,
|
||||||
request_auth_channel,
|
request_auth_channel,
|
||||||
);
|
)
|
||||||
|
.with_runtime_miss_diagnostic(trace_id, record_runtime_miss_diagnostic);
|
||||||
let attempt_count_before_pool = attempts.len();
|
let attempt_count_before_pool = attempts.len();
|
||||||
while let Some(candidate) = cursor.next_key().await {
|
while let Some(candidate) = cursor.next_key().await {
|
||||||
attempts.extend(build_unpersisted_local_execution_candidate_attempts(
|
attempts.extend(build_unpersisted_local_execution_candidate_attempts(
|
||||||
@@ -1525,6 +1542,7 @@ mod tests {
|
|||||||
required_capabilities: None,
|
required_capabilities: None,
|
||||||
error_context: "persist should not fail",
|
error_context: "persist should not fail",
|
||||||
},
|
},
|
||||||
|
false,
|
||||||
vec![pool_group, sample_eligible("normal-key", None)],
|
vec![pool_group, sample_eligible("normal-key", None)],
|
||||||
None,
|
None,
|
||||||
Some("gpt-5"),
|
Some("gpt-5"),
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ use crate::ai_serving::planner::candidate_resolution::{
|
|||||||
candidate_auth_channel_skip_reason, read_candidate_transport_snapshot,
|
candidate_auth_channel_skip_reason, read_candidate_transport_snapshot,
|
||||||
EligibleLocalExecutionCandidate, LocalExecutionCandidateKind, SkippedLocalExecutionCandidate,
|
EligibleLocalExecutionCandidate, LocalExecutionCandidateKind, SkippedLocalExecutionCandidate,
|
||||||
};
|
};
|
||||||
|
use crate::ai_serving::planner::runtime_miss::record_local_runtime_candidate_skip_reason;
|
||||||
use crate::ai_serving::{
|
use crate::ai_serving::{
|
||||||
candidate_common_transport_skip_reason, CandidateTransportPolicyFacts, PlannerAppState,
|
candidate_common_transport_skip_reason, CandidateTransportPolicyFacts, PlannerAppState,
|
||||||
};
|
};
|
||||||
@@ -171,6 +172,8 @@ pub(crate) struct PoolKeyCursor<'a> {
|
|||||||
sticky_session_token: Option<String>,
|
sticky_session_token: Option<String>,
|
||||||
requested_model: Option<String>,
|
requested_model: Option<String>,
|
||||||
request_auth_channel: Option<String>,
|
request_auth_channel: Option<String>,
|
||||||
|
runtime_miss_trace_id: Option<String>,
|
||||||
|
record_runtime_miss_diagnostic: bool,
|
||||||
pool_key_order: StoredPoolKeyCandidateOrder,
|
pool_key_order: StoredPoolKeyCandidateOrder,
|
||||||
next_offset: u32,
|
next_offset: u32,
|
||||||
scanned_keys: u32,
|
scanned_keys: u32,
|
||||||
@@ -183,6 +186,8 @@ pub(crate) struct PoolKeyCursor<'a> {
|
|||||||
queued_candidates: VecDeque<EligibleLocalExecutionCandidate>,
|
queued_candidates: VecDeque<EligibleLocalExecutionCandidate>,
|
||||||
skipped_candidates: Vec<SkippedLocalExecutionCandidate>,
|
skipped_candidates: Vec<SkippedLocalExecutionCandidate>,
|
||||||
exhausted_logged: bool,
|
exhausted_logged: bool,
|
||||||
|
returned_key_count: u32,
|
||||||
|
exhaustion_skip_recorded: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> PoolKeyCursor<'a> {
|
impl<'a> PoolKeyCursor<'a> {
|
||||||
@@ -200,6 +205,8 @@ impl<'a> PoolKeyCursor<'a> {
|
|||||||
sticky_session_token: sticky_session_token.map(str::to_string),
|
sticky_session_token: sticky_session_token.map(str::to_string),
|
||||||
requested_model: requested_model.map(str::to_string),
|
requested_model: requested_model.map(str::to_string),
|
||||||
request_auth_channel: request_auth_channel.map(str::to_string),
|
request_auth_channel: request_auth_channel.map(str::to_string),
|
||||||
|
runtime_miss_trace_id: None,
|
||||||
|
record_runtime_miss_diagnostic: false,
|
||||||
pool_key_order,
|
pool_key_order,
|
||||||
next_offset: 0,
|
next_offset: 0,
|
||||||
scanned_keys: 0,
|
scanned_keys: 0,
|
||||||
@@ -212,12 +219,27 @@ impl<'a> PoolKeyCursor<'a> {
|
|||||||
queued_candidates: VecDeque::new(),
|
queued_candidates: VecDeque::new(),
|
||||||
skipped_candidates: Vec::new(),
|
skipped_candidates: Vec::new(),
|
||||||
exhausted_logged: false,
|
exhausted_logged: false,
|
||||||
|
returned_key_count: 0,
|
||||||
|
exhaustion_skip_recorded: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) fn with_runtime_miss_diagnostic(
|
||||||
|
mut self,
|
||||||
|
trace_id: &str,
|
||||||
|
record_runtime_miss_diagnostic: bool,
|
||||||
|
) -> Self {
|
||||||
|
if record_runtime_miss_diagnostic {
|
||||||
|
self.runtime_miss_trace_id = Some(trace_id.to_string());
|
||||||
|
self.record_runtime_miss_diagnostic = true;
|
||||||
|
}
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
pub(crate) async fn next_key(&mut self) -> Option<EligibleLocalExecutionCandidate> {
|
pub(crate) async fn next_key(&mut self) -> Option<EligibleLocalExecutionCandidate> {
|
||||||
loop {
|
loop {
|
||||||
if let Some(candidate) = self.next_queued_candidate().await {
|
if let Some(candidate) = self.next_queued_candidate().await {
|
||||||
|
self.returned_key_count = self.returned_key_count.saturating_add(1);
|
||||||
return Some(candidate);
|
return Some(candidate);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -254,6 +276,37 @@ impl<'a> PoolKeyCursor<'a> {
|
|||||||
skip_reason_counts = ?self.skip_reason_counts,
|
skip_reason_counts = ?self.skip_reason_counts,
|
||||||
"gateway pool scheduler exhausted pool group without a schedulable key"
|
"gateway pool scheduler exhausted pool group without a schedulable key"
|
||||||
);
|
);
|
||||||
|
self.record_runtime_miss_pool_exhaustion_skip_reason();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn record_runtime_miss_pool_exhaustion_skip_reason(&mut self) {
|
||||||
|
if self.exhaustion_skip_recorded
|
||||||
|
|| !self.record_runtime_miss_diagnostic
|
||||||
|
|| self.returned_key_count > 0
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let Some(trace_id) = self.runtime_miss_trace_id.as_deref() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
self.exhaustion_skip_recorded = true;
|
||||||
|
record_local_runtime_candidate_skip_reason(
|
||||||
|
self.state.app(),
|
||||||
|
trace_id,
|
||||||
|
self.runtime_miss_pool_exhaustion_skip_reason(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn runtime_miss_pool_exhaustion_skip_reason(&self) -> &'static str {
|
||||||
|
let mut selected_reason = "pool_group_exhausted";
|
||||||
|
let mut selected_count = 0;
|
||||||
|
for (reason, count) in &self.skip_reason_counts {
|
||||||
|
if *count > selected_count {
|
||||||
|
selected_reason = *reason;
|
||||||
|
selected_count = *count;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
selected_reason
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn next_page_candidates(&mut self) -> Option<Vec<EligibleLocalExecutionCandidate>> {
|
async fn next_page_candidates(&mut self) -> Option<Vec<EligibleLocalExecutionCandidate>> {
|
||||||
@@ -963,6 +1016,7 @@ mod tests {
|
|||||||
use crate::ai_serving::planner::candidate_resolution::{
|
use crate::ai_serving::planner::candidate_resolution::{
|
||||||
EligibleLocalExecutionCandidate, LocalExecutionCandidateKind,
|
EligibleLocalExecutionCandidate, LocalExecutionCandidateKind,
|
||||||
};
|
};
|
||||||
|
use crate::ai_serving::planner::runtime_miss::apply_local_runtime_candidate_terminal_reason;
|
||||||
use crate::ai_serving::PlannerAppState;
|
use crate::ai_serving::PlannerAppState;
|
||||||
use crate::data::GatewayDataState;
|
use crate::data::GatewayDataState;
|
||||||
use crate::handlers::shared::provider_pool::{
|
use crate::handlers::shared::provider_pool::{
|
||||||
@@ -970,7 +1024,7 @@ mod tests {
|
|||||||
try_claim_admin_provider_pool_key, AdminProviderPoolRuntimeState,
|
try_claim_admin_provider_pool_key, AdminProviderPoolRuntimeState,
|
||||||
};
|
};
|
||||||
use crate::orchestration::LocalExecutionCandidateMetadata;
|
use crate::orchestration::LocalExecutionCandidateMetadata;
|
||||||
use crate::AppState;
|
use crate::{AppState, LocalExecutionRuntimeMissDiagnostic};
|
||||||
use aether_ai_serving::{normalize_enabled_ai_pool_presets, AiPoolSchedulingPreset};
|
use aether_ai_serving::{normalize_enabled_ai_pool_presets, AiPoolSchedulingPreset};
|
||||||
use aether_data::repository::candidate_selection::InMemoryMinimalCandidateSelectionReadRepository;
|
use aether_data::repository::candidate_selection::InMemoryMinimalCandidateSelectionReadRepository;
|
||||||
use aether_data::repository::provider_catalog::InMemoryProviderCatalogReadRepository;
|
use aether_data::repository::provider_catalog::InMemoryProviderCatalogReadRepository;
|
||||||
@@ -1739,6 +1793,43 @@ mod tests {
|
|||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn pool_key_cursor_records_runtime_miss_when_exhausted_without_returning_key() {
|
||||||
|
let app = AppState::new().expect("state should build");
|
||||||
|
let trace_id = "trace-pool-exhausted-runtime-miss";
|
||||||
|
app.set_local_execution_runtime_miss_diagnostic(
|
||||||
|
trace_id,
|
||||||
|
LocalExecutionRuntimeMissDiagnostic {
|
||||||
|
reason: "candidate_evaluation_incomplete".to_string(),
|
||||||
|
requested_model: Some("gpt-5".to_string()),
|
||||||
|
candidate_count: Some(1),
|
||||||
|
..LocalExecutionRuntimeMissDiagnostic::default()
|
||||||
|
},
|
||||||
|
);
|
||||||
|
let group = sample_eligible_candidate(
|
||||||
|
"provider-pool",
|
||||||
|
"endpoint-1",
|
||||||
|
"pool-group",
|
||||||
|
10,
|
||||||
|
Some(json!({ "pool_advanced": { "lru_enabled": true } })),
|
||||||
|
);
|
||||||
|
let mut cursor = PoolKeyCursor::new(PlannerAppState::new(&app), group, None, None, None)
|
||||||
|
.with_runtime_miss_diagnostic(trace_id, true);
|
||||||
|
cursor.record_skip_reason("pool_key_lease_busy");
|
||||||
|
cursor.record_skip_reason("pool_key_lease_busy");
|
||||||
|
cursor.record_skip_reason("transport_snapshot_missing");
|
||||||
|
|
||||||
|
cursor.log_exhausted();
|
||||||
|
apply_local_runtime_candidate_terminal_reason(&app, trace_id, "no_local_sync_plans");
|
||||||
|
|
||||||
|
let diagnostic = app
|
||||||
|
.take_local_execution_runtime_miss_diagnostic(trace_id)
|
||||||
|
.expect("runtime miss diagnostic should exist");
|
||||||
|
assert_eq!(diagnostic.reason, "all_candidates_skipped");
|
||||||
|
assert_eq!(diagnostic.skipped_candidate_count, Some(1));
|
||||||
|
assert_eq!(diagnostic.skip_reasons.get("pool_key_lease_busy"), Some(&1));
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn pool_key_cursor_revalidates_queued_candidates_before_return() {
|
async fn pool_key_cursor_revalidates_queued_candidates_before_return() {
|
||||||
let app = AppState::new().expect("state should build");
|
let app = AppState::new().expect("state should build");
|
||||||
|
|||||||
@@ -1656,6 +1656,7 @@ fn local_execution_runtime_miss_skip_reasons_summary(
|
|||||||
fn local_execution_runtime_miss_skip_reason_label(reason: &str) -> &str {
|
fn local_execution_runtime_miss_skip_reason_label(reason: &str) -> &str {
|
||||||
match reason {
|
match reason {
|
||||||
"api_key_concurrency_limit_reached" => "API Key 并发已达上限",
|
"api_key_concurrency_limit_reached" => "API Key 并发已达上限",
|
||||||
|
"auth_channel_mismatch" => "认证通道不匹配",
|
||||||
"auth_snapshot_missing" => "API Key 本地执行配置缺失",
|
"auth_snapshot_missing" => "API Key 本地执行配置缺失",
|
||||||
"endpoint_api_format_changed" => "端点 API 格式已变更",
|
"endpoint_api_format_changed" => "端点 API 格式已变更",
|
||||||
"endpoint_inactive" => "端点未启用",
|
"endpoint_inactive" => "端点未启用",
|
||||||
@@ -1664,6 +1665,10 @@ fn local_execution_runtime_miss_skip_reason_label(reason: &str) -> &str {
|
|||||||
"key_inactive" => "API Key 未启用",
|
"key_inactive" => "API Key 未启用",
|
||||||
"key_model_disabled" => "API Key 未允许该模型",
|
"key_model_disabled" => "API Key 未允许该模型",
|
||||||
"mapped_model_missing" => "模型映射缺失",
|
"mapped_model_missing" => "模型映射缺失",
|
||||||
|
"pool_cooldown" => "池内账号处于冷却中",
|
||||||
|
"pool_cost_limit_reached" => "池内账号成本额度已用尽",
|
||||||
|
"pool_group_exhausted" => "池化提供商没有可调度账号",
|
||||||
|
"pool_key_lease_busy" => "池内账号正被其他请求占用",
|
||||||
"provider_inactive" => "提供商未启用",
|
"provider_inactive" => "提供商未启用",
|
||||||
"provider_request_body_missing" => "无法构建上游请求体",
|
"provider_request_body_missing" => "无法构建上游请求体",
|
||||||
"provider_request_body_build_failed" => "上游请求体转换失败",
|
"provider_request_body_build_failed" => "上游请求体转换失败",
|
||||||
|
|||||||
@@ -98,6 +98,21 @@ pub fn model_directive_base_model(model: &str) -> Option<String> {
|
|||||||
parse_model_directive(model).map(|directive| directive.base_model)
|
parse_model_directive(model).map(|directive| directive.base_model)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) fn model_directive_display_model(model: &str) -> Option<String> {
|
||||||
|
let model = model.trim();
|
||||||
|
parse_model_directive(model)?;
|
||||||
|
Some(model.to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn model_directive_display_model_from_report_context(
|
||||||
|
report_context: &Value,
|
||||||
|
) -> Option<String> {
|
||||||
|
report_context
|
||||||
|
.get("model")
|
||||||
|
.and_then(Value::as_str)
|
||||||
|
.and_then(model_directive_display_model)
|
||||||
|
}
|
||||||
|
|
||||||
pub fn normalize_model_directive_model(model: &str) -> String {
|
pub fn normalize_model_directive_model(model: &str) -> String {
|
||||||
parse_model_directive(model)
|
parse_model_directive(model)
|
||||||
.map(|directive| directive.base_model)
|
.map(|directive| directive.base_model)
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
use serde_json::{json, Map, Value};
|
use serde_json::{json, Map, Value};
|
||||||
|
|
||||||
|
use crate::formats::shared::model_directives::model_directive_display_model_from_report_context;
|
||||||
|
|
||||||
pub use aether_ai_formats::protocol::stream::{
|
pub use aether_ai_formats::protocol::stream::{
|
||||||
CanonicalContentPart, CanonicalStreamEvent, CanonicalStreamFrame, CanonicalUsage,
|
CanonicalContentPart, CanonicalStreamEvent, CanonicalStreamFrame, CanonicalUsage,
|
||||||
};
|
};
|
||||||
@@ -27,6 +29,9 @@ pub fn resolve_identity(
|
|||||||
.filter(|value| !value.is_empty())
|
.filter(|value| !value.is_empty())
|
||||||
.unwrap_or(default_id)
|
.unwrap_or(default_id)
|
||||||
.to_string();
|
.to_string();
|
||||||
|
if let Some(display_model) = model_directive_display_model_from_report_context(report_context) {
|
||||||
|
return (id, display_model);
|
||||||
|
}
|
||||||
let model = model
|
let model = model
|
||||||
.filter(|value| !value.is_empty())
|
.filter(|value| !value.is_empty())
|
||||||
.or_else(|| report_context.get("mapped_model").and_then(Value::as_str))
|
.or_else(|| report_context.get("mapped_model").and_then(Value::as_str))
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
|
|
||||||
use crate::formats::openai::image::stream::OpenAiImageStreamState;
|
use crate::formats::openai::image::stream::OpenAiImageStreamState;
|
||||||
|
use crate::formats::shared::model_directives::model_directive_display_model_from_report_context;
|
||||||
use crate::formats::shared::stream_core::StreamingStandardFormatMatrix;
|
use crate::formats::shared::stream_core::StreamingStandardFormatMatrix;
|
||||||
use crate::formats::shared::AiSurfaceFinalizeError;
|
use crate::formats::shared::AiSurfaceFinalizeError;
|
||||||
use crate::provider_compat::kiro_stream::KiroToClaudeCliStreamState;
|
use crate::provider_compat::kiro_stream::KiroToClaudeCliStreamState;
|
||||||
@@ -12,6 +13,7 @@ use crate::provider_compat::surfaces::{
|
|||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
pub enum FinalizeStreamRewriteMode {
|
pub enum FinalizeStreamRewriteMode {
|
||||||
EnvelopeUnwrap,
|
EnvelopeUnwrap,
|
||||||
|
ModelDirectiveDisplay,
|
||||||
OpenAiImage,
|
OpenAiImage,
|
||||||
Standard,
|
Standard,
|
||||||
KiroToClaudeCli,
|
KiroToClaudeCli,
|
||||||
@@ -73,6 +75,17 @@ pub fn resolve_finalize_stream_rewrite_mode(
|
|||||||
.then_some(FinalizeStreamRewriteMode::KiroToClaudeCli);
|
.then_some(FinalizeStreamRewriteMode::KiroToClaudeCli);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if model_directive_display_model_from_report_context(report_context).is_some()
|
||||||
|
&& provider_api_format == client_api_format
|
||||||
|
&& is_standard_provider_api_format(provider_api_format.as_str())
|
||||||
|
&& !provider_adaptation_should_unwrap_stream_envelope(
|
||||||
|
envelope_name.as_str(),
|
||||||
|
provider_api_format.as_str(),
|
||||||
|
)
|
||||||
|
{
|
||||||
|
return Some(FinalizeStreamRewriteMode::ModelDirectiveDisplay);
|
||||||
|
}
|
||||||
|
|
||||||
(provider_api_format == client_api_format
|
(provider_api_format == client_api_format
|
||||||
&& provider_adaptation_should_unwrap_stream_envelope(
|
&& provider_adaptation_should_unwrap_stream_envelope(
|
||||||
envelope_name.as_str(),
|
envelope_name.as_str(),
|
||||||
@@ -83,6 +96,7 @@ pub fn resolve_finalize_stream_rewrite_mode(
|
|||||||
|
|
||||||
enum AiSurfaceStreamRewriteState {
|
enum AiSurfaceStreamRewriteState {
|
||||||
EnvelopeUnwrap,
|
EnvelopeUnwrap,
|
||||||
|
ModelDirectiveDisplay,
|
||||||
OpenAiImage(Box<OpenAiImageStreamState>),
|
OpenAiImage(Box<OpenAiImageStreamState>),
|
||||||
Standard(Box<StreamingStandardFormatMatrix>),
|
Standard(Box<StreamingStandardFormatMatrix>),
|
||||||
KiroToClaudeCli(Box<KiroToClaudeCliStreamState>),
|
KiroToClaudeCli(Box<KiroToClaudeCliStreamState>),
|
||||||
@@ -104,6 +118,9 @@ pub fn maybe_build_ai_surface_stream_rewriter<'a>(
|
|||||||
let report_context = report_context?;
|
let report_context = report_context?;
|
||||||
let state = match resolve_finalize_stream_rewrite_mode(report_context)? {
|
let state = match resolve_finalize_stream_rewrite_mode(report_context)? {
|
||||||
FinalizeStreamRewriteMode::EnvelopeUnwrap => AiSurfaceStreamRewriteState::EnvelopeUnwrap,
|
FinalizeStreamRewriteMode::EnvelopeUnwrap => AiSurfaceStreamRewriteState::EnvelopeUnwrap,
|
||||||
|
FinalizeStreamRewriteMode::ModelDirectiveDisplay => {
|
||||||
|
AiSurfaceStreamRewriteState::ModelDirectiveDisplay
|
||||||
|
}
|
||||||
FinalizeStreamRewriteMode::OpenAiImage => {
|
FinalizeStreamRewriteMode::OpenAiImage => {
|
||||||
AiSurfaceStreamRewriteState::OpenAiImage(Box::<OpenAiImageStreamState>::default())
|
AiSurfaceStreamRewriteState::OpenAiImage(Box::<OpenAiImageStreamState>::default())
|
||||||
}
|
}
|
||||||
@@ -142,6 +159,7 @@ impl AiSurfaceStreamRewriter<'_> {
|
|||||||
transform_standard_bytes(standard, self.report_context, claude_bytes)
|
transform_standard_bytes(standard, self.report_context, claude_bytes)
|
||||||
}
|
}
|
||||||
AiSurfaceStreamRewriteState::EnvelopeUnwrap
|
AiSurfaceStreamRewriteState::EnvelopeUnwrap
|
||||||
|
| AiSurfaceStreamRewriteState::ModelDirectiveDisplay
|
||||||
| AiSurfaceStreamRewriteState::Standard(_) => {
|
| AiSurfaceStreamRewriteState::Standard(_) => {
|
||||||
self.buffered.extend_from_slice(chunk);
|
self.buffered.extend_from_slice(chunk);
|
||||||
let mut output = Vec::new();
|
let mut output = Vec::new();
|
||||||
@@ -170,6 +188,7 @@ impl AiSurfaceStreamRewriter<'_> {
|
|||||||
Ok(output)
|
Ok(output)
|
||||||
}
|
}
|
||||||
AiSurfaceStreamRewriteState::EnvelopeUnwrap
|
AiSurfaceStreamRewriteState::EnvelopeUnwrap
|
||||||
|
| AiSurfaceStreamRewriteState::ModelDirectiveDisplay
|
||||||
| AiSurfaceStreamRewriteState::Standard(_) => {
|
| AiSurfaceStreamRewriteState::Standard(_) => {
|
||||||
if self.buffered.is_empty() {
|
if self.buffered.is_empty() {
|
||||||
if let AiSurfaceStreamRewriteState::Standard(state) = &mut self.state {
|
if let AiSurfaceStreamRewriteState::Standard(state) = &mut self.state {
|
||||||
@@ -190,8 +209,12 @@ impl AiSurfaceStreamRewriter<'_> {
|
|||||||
fn transform_line(&mut self, line: Vec<u8>) -> Result<Vec<u8>, AiSurfaceFinalizeError> {
|
fn transform_line(&mut self, line: Vec<u8>) -> Result<Vec<u8>, AiSurfaceFinalizeError> {
|
||||||
match &mut self.state {
|
match &mut self.state {
|
||||||
AiSurfaceStreamRewriteState::EnvelopeUnwrap => {
|
AiSurfaceStreamRewriteState::EnvelopeUnwrap => {
|
||||||
transform_provider_private_stream_line(self.report_context, line)
|
let output = transform_provider_private_stream_line(self.report_context, line)
|
||||||
.map_err(AiSurfaceFinalizeError::from)
|
.map_err(AiSurfaceFinalizeError::from)?;
|
||||||
|
rewrite_model_directive_stream_line(self.report_context, output)
|
||||||
|
}
|
||||||
|
AiSurfaceStreamRewriteState::ModelDirectiveDisplay => {
|
||||||
|
rewrite_model_directive_stream_line(self.report_context, line)
|
||||||
}
|
}
|
||||||
AiSurfaceStreamRewriteState::Standard(state) => {
|
AiSurfaceStreamRewriteState::Standard(state) => {
|
||||||
transform_standard_line(state, self.report_context, line)
|
transform_standard_line(state, self.report_context, line)
|
||||||
@@ -203,6 +226,63 @@ impl AiSurfaceStreamRewriter<'_> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn rewrite_model_directive_stream_line(
|
||||||
|
report_context: &Value,
|
||||||
|
line: Vec<u8>,
|
||||||
|
) -> Result<Vec<u8>, AiSurfaceFinalizeError> {
|
||||||
|
let Some(display_model) = model_directive_display_model_from_report_context(report_context)
|
||||||
|
else {
|
||||||
|
return Ok(line);
|
||||||
|
};
|
||||||
|
let text = match std::str::from_utf8(&line) {
|
||||||
|
Ok(text) => text,
|
||||||
|
Err(_) => return Ok(line),
|
||||||
|
};
|
||||||
|
let trimmed_line_end = text.trim_end_matches(['\r', '\n']);
|
||||||
|
let trailing = &text[trimmed_line_end.len()..];
|
||||||
|
let Some((prefix, payload)) = trimmed_line_end.split_once(':') else {
|
||||||
|
return Ok(line);
|
||||||
|
};
|
||||||
|
if prefix.trim() != "data" {
|
||||||
|
return Ok(line);
|
||||||
|
}
|
||||||
|
let payload = payload.trim_start();
|
||||||
|
if payload.is_empty() || payload == "[DONE]" {
|
||||||
|
return Ok(line);
|
||||||
|
}
|
||||||
|
let mut value = match serde_json::from_str::<Value>(payload) {
|
||||||
|
Ok(value) => value,
|
||||||
|
Err(_) => return Ok(line),
|
||||||
|
};
|
||||||
|
if !rewrite_stream_payload_model(&mut value, &display_model) {
|
||||||
|
return Ok(line);
|
||||||
|
}
|
||||||
|
let mut output = Vec::new();
|
||||||
|
output.extend_from_slice(b"data: ");
|
||||||
|
output.extend(serde_json::to_vec(&value)?);
|
||||||
|
output.extend_from_slice(trailing.as_bytes());
|
||||||
|
Ok(output)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn rewrite_stream_payload_model(value: &mut Value, display_model: &str) -> bool {
|
||||||
|
let Some(object) = value.as_object_mut() else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
let mut changed = false;
|
||||||
|
for key in ["model", "modelVersion"] {
|
||||||
|
if object.get(key).and_then(Value::as_str).is_some() {
|
||||||
|
object.insert(key.to_string(), Value::String(display_model.to_string()));
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for key in ["response", "message"] {
|
||||||
|
if let Some(nested) = object.get_mut(key) {
|
||||||
|
changed |= rewrite_stream_payload_model(nested, display_model);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
changed
|
||||||
|
}
|
||||||
|
|
||||||
fn transform_standard_bytes(
|
fn transform_standard_bytes(
|
||||||
standard: &mut StreamingStandardFormatMatrix,
|
standard: &mut StreamingStandardFormatMatrix,
|
||||||
report_context: &Value,
|
report_context: &Value,
|
||||||
@@ -288,7 +368,10 @@ fn is_standard_cli_client_api_format(api_format: &str) -> bool {
|
|||||||
mod tests {
|
mod tests {
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
|
|
||||||
use super::{resolve_finalize_stream_rewrite_mode, FinalizeStreamRewriteMode};
|
use super::{
|
||||||
|
maybe_build_ai_surface_stream_rewriter, resolve_finalize_stream_rewrite_mode,
|
||||||
|
FinalizeStreamRewriteMode,
|
||||||
|
};
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn resolves_standard_mode_for_cross_format_standard_streams() {
|
fn resolves_standard_mode_for_cross_format_standard_streams() {
|
||||||
@@ -341,6 +424,84 @@ mod tests {
|
|||||||
assert_eq!(resolve_finalize_stream_rewrite_mode(&report_context), None);
|
assert_eq!(resolve_finalize_stream_rewrite_mode(&report_context), None);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn resolves_model_directive_display_mode_for_same_format_standard_streams() {
|
||||||
|
let report_context = json!({
|
||||||
|
"provider_api_format": "openai:responses",
|
||||||
|
"client_api_format": "openai:responses",
|
||||||
|
"model": "gpt-5.5-xhigh",
|
||||||
|
"mapped_model": "gpt-5.5",
|
||||||
|
"needs_conversion": false,
|
||||||
|
});
|
||||||
|
assert_eq!(
|
||||||
|
resolve_finalize_stream_rewrite_mode(&report_context),
|
||||||
|
Some(FinalizeStreamRewriteMode::ModelDirectiveDisplay)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn model_directive_display_mode_does_not_displace_kiro_stream_bridge() {
|
||||||
|
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",
|
||||||
|
"needs_conversion": false,
|
||||||
|
});
|
||||||
|
assert_eq!(
|
||||||
|
resolve_finalize_stream_rewrite_mode(&report_context),
|
||||||
|
Some(FinalizeStreamRewriteMode::KiroToClaudeCli)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn envelope_unwrap_rewriter_restores_model_directive_display_model() {
|
||||||
|
let report_context = json!({
|
||||||
|
"provider_api_format": "gemini:generate_content",
|
||||||
|
"client_api_format": "gemini:generate_content",
|
||||||
|
"envelope_name": "gemini_cli:v1internal",
|
||||||
|
"model": "gemini-2.5-pro-high",
|
||||||
|
"mapped_model": "gemini-2.5-pro",
|
||||||
|
"needs_conversion": false,
|
||||||
|
});
|
||||||
|
let mut rewriter = maybe_build_ai_surface_stream_rewriter(Some(&report_context))
|
||||||
|
.expect("rewriter should exist");
|
||||||
|
let output = rewriter
|
||||||
|
.push_chunk(
|
||||||
|
b"data: {\"response\":{\"modelVersion\":\"gemini-2.5-pro\",\"candidates\":[]}}\n\n",
|
||||||
|
)
|
||||||
|
.expect("rewrite should succeed");
|
||||||
|
let output = String::from_utf8(output).expect("output should be utf8");
|
||||||
|
|
||||||
|
assert!(output.contains("\"modelVersion\":\"gemini-2.5-pro-high\""));
|
||||||
|
assert!(!output.contains("\"modelVersion\":\"gemini-2.5-pro\""));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn model_directive_display_rewriter_restores_response_model() {
|
||||||
|
let report_context = json!({
|
||||||
|
"provider_api_format": "openai:responses",
|
||||||
|
"client_api_format": "openai:responses",
|
||||||
|
"model": "gpt-5.5-xhigh",
|
||||||
|
"mapped_model": "gpt-5.5",
|
||||||
|
"needs_conversion": false,
|
||||||
|
});
|
||||||
|
let mut rewriter = maybe_build_ai_surface_stream_rewriter(Some(&report_context))
|
||||||
|
.expect("rewriter should exist");
|
||||||
|
let output = rewriter
|
||||||
|
.push_chunk(
|
||||||
|
b"event: response.created\n\
|
||||||
|
data: {\"type\":\"response.created\",\"response\":{\"id\":\"resp_123\",\"object\":\"response\",\"model\":\"gpt-5.5\",\"status\":\"in_progress\"}}\n\n",
|
||||||
|
)
|
||||||
|
.expect("rewrite should succeed");
|
||||||
|
let output = String::from_utf8(output).expect("output should be utf8");
|
||||||
|
|
||||||
|
assert!(output.contains("event: response.created"));
|
||||||
|
assert!(output.contains("\"model\":\"gpt-5.5-xhigh\""));
|
||||||
|
assert!(!output.contains("\"model\":\"gpt-5.5\""));
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn resolves_openai_image_mode_for_same_format_image_streams() {
|
fn resolves_openai_image_mode_for_same_format_image_streams() {
|
||||||
let report_context = json!({
|
let report_context = json!({
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ use serde_json::{json, Map, Value};
|
|||||||
|
|
||||||
use super::AiSurfaceFinalizeError;
|
use super::AiSurfaceFinalizeError;
|
||||||
use crate::formats::gemini::generate_content::stream::GeminiProviderState;
|
use crate::formats::gemini::generate_content::stream::GeminiProviderState;
|
||||||
|
use crate::formats::shared::model_directives::model_directive_display_model_from_report_context;
|
||||||
use crate::formats::shared::response::remove_empty_pages_from_tool_arguments;
|
use crate::formats::shared::response::remove_empty_pages_from_tool_arguments;
|
||||||
use crate::formats::shared::stream_core::common::{
|
use crate::formats::shared::stream_core::common::{
|
||||||
map_openai_finish_reason_to_gemini, parse_json_arguments_value, CanonicalContentPart,
|
map_openai_finish_reason_to_gemini, parse_json_arguments_value, CanonicalContentPart,
|
||||||
@@ -382,7 +383,11 @@ fn maybe_build_standard_same_format_sync_body(
|
|||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
|
|
||||||
Some(body_json.clone())
|
Some(client_body_with_report_context_model(
|
||||||
|
body_json.clone(),
|
||||||
|
report_context,
|
||||||
|
&client_api_format,
|
||||||
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn maybe_build_standard_same_format_stream_sync_body(
|
fn maybe_build_standard_same_format_stream_sync_body(
|
||||||
@@ -431,10 +436,11 @@ fn maybe_build_standard_same_format_stream_sync_body(
|
|||||||
return Ok(None);
|
return Ok(None);
|
||||||
};
|
};
|
||||||
let body_bytes = base64::engine::general_purpose::STANDARD.decode(body_base64)?;
|
let body_bytes = base64::engine::general_purpose::STANDARD.decode(body_base64)?;
|
||||||
Ok(aggregate_same_format_stream_sync_response(
|
Ok(
|
||||||
expected_api_format,
|
aggregate_same_format_stream_sync_response(expected_api_format, &body_bytes).map(|body| {
|
||||||
&body_bytes,
|
client_body_with_report_context_model(body, report_context, &client_api_format)
|
||||||
))
|
}),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn maybe_build_openai_responses_same_family_sync_body(
|
fn maybe_build_openai_responses_same_family_sync_body(
|
||||||
@@ -480,7 +486,11 @@ fn maybe_build_openai_responses_same_family_sync_body(
|
|||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
|
|
||||||
Some(body_json.clone())
|
Some(client_body_with_report_context_model(
|
||||||
|
body_json.clone(),
|
||||||
|
report_context,
|
||||||
|
&client_api_format,
|
||||||
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn maybe_build_openai_responses_same_family_stream_sync_body(
|
fn maybe_build_openai_responses_same_family_stream_sync_body(
|
||||||
@@ -528,7 +538,11 @@ fn maybe_build_openai_responses_same_family_stream_sync_body(
|
|||||||
return Ok(None);
|
return Ok(None);
|
||||||
};
|
};
|
||||||
let body_bytes = base64::engine::general_purpose::STANDARD.decode(body_base64)?;
|
let body_bytes = base64::engine::general_purpose::STANDARD.decode(body_base64)?;
|
||||||
Ok(aggregate_openai_responses_stream_sync_response(&body_bytes))
|
Ok(
|
||||||
|
aggregate_openai_responses_stream_sync_response(&body_bytes).map(|body| {
|
||||||
|
client_body_with_report_context_model(body, report_context, &client_api_format)
|
||||||
|
}),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn maybe_build_openai_cross_format_provider_body_from_normalized_payload(
|
fn maybe_build_openai_cross_format_provider_body_from_normalized_payload(
|
||||||
@@ -625,6 +639,8 @@ pub fn maybe_build_standard_cross_format_sync_product(
|
|||||||
} else {
|
} else {
|
||||||
return None;
|
return None;
|
||||||
};
|
};
|
||||||
|
let client_body_json =
|
||||||
|
client_body_with_report_context_model(client_body_json, report_context, &client_api_format);
|
||||||
|
|
||||||
Some(StandardCrossFormatSyncProduct {
|
Some(StandardCrossFormatSyncProduct {
|
||||||
client_body_json,
|
client_body_json,
|
||||||
@@ -797,6 +813,30 @@ fn format_context_from_report_context(report_context: &Value) -> FormatContext {
|
|||||||
context
|
context
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn client_body_with_report_context_model(
|
||||||
|
mut body_json: Value,
|
||||||
|
report_context: &Value,
|
||||||
|
client_api_format: &str,
|
||||||
|
) -> Value {
|
||||||
|
let Some(display_model) = model_directive_display_model_from_report_context(report_context)
|
||||||
|
else {
|
||||||
|
return body_json;
|
||||||
|
};
|
||||||
|
let Some(object) = body_json.as_object_mut() else {
|
||||||
|
return body_json;
|
||||||
|
};
|
||||||
|
match normalize_openai_responses_family_api_format(client_api_format).as_str() {
|
||||||
|
"openai:chat" | "openai:responses" | "openai:responses:compact" | "claude:messages" => {
|
||||||
|
object.insert("model".to_string(), Value::String(display_model));
|
||||||
|
}
|
||||||
|
"gemini:generate_content" => {
|
||||||
|
object.insert("modelVersion".to_string(), Value::String(display_model));
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
body_json
|
||||||
|
}
|
||||||
|
|
||||||
fn convert_openai_chat_canonical_chat_response(
|
fn convert_openai_chat_canonical_chat_response(
|
||||||
body_json: &Value,
|
body_json: &Value,
|
||||||
client_api_format: &str,
|
client_api_format: &str,
|
||||||
@@ -1195,6 +1235,10 @@ fn gemini_response_can_use_single_response_canonical(body_json: &Value) -> bool
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn apply_report_context_model_fallback(model: &mut String, report_context: &Value) {
|
fn apply_report_context_model_fallback(model: &mut String, report_context: &Value) {
|
||||||
|
if let Some(display_model) = model_directive_display_model_from_report_context(report_context) {
|
||||||
|
*model = display_model;
|
||||||
|
return;
|
||||||
|
}
|
||||||
if model != "unknown" && !model.trim().is_empty() {
|
if model != "unknown" && !model.trim().is_empty() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -2881,6 +2925,7 @@ mod tests {
|
|||||||
maybe_build_openai_chat_cross_format_sync_product_from_normalized_payload,
|
maybe_build_openai_chat_cross_format_sync_product_from_normalized_payload,
|
||||||
maybe_build_openai_responses_cross_format_sync_product_from_normalized_payload,
|
maybe_build_openai_responses_cross_format_sync_product_from_normalized_payload,
|
||||||
maybe_build_openai_responses_same_family_sync_body_from_normalized_payload,
|
maybe_build_openai_responses_same_family_sync_body_from_normalized_payload,
|
||||||
|
maybe_build_standard_cross_format_sync_product,
|
||||||
maybe_build_standard_cross_format_sync_product_from_normalized_payload,
|
maybe_build_standard_cross_format_sync_product_from_normalized_payload,
|
||||||
maybe_build_standard_same_format_sync_body_from_normalized_payload,
|
maybe_build_standard_same_format_sync_body_from_normalized_payload,
|
||||||
maybe_build_standard_sync_finalize_product_from_normalized_payload,
|
maybe_build_standard_sync_finalize_product_from_normalized_payload,
|
||||||
@@ -3222,6 +3267,75 @@ mod tests {
|
|||||||
assert_eq!(body_json, provider_body_json);
|
assert_eq!(body_json, provider_body_json);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn same_format_sync_response_restores_model_directive_display_model() {
|
||||||
|
let report_context = json!({
|
||||||
|
"provider_api_format": "openai:responses",
|
||||||
|
"client_api_format": "openai:responses",
|
||||||
|
"model": "gpt-5.5-xhigh",
|
||||||
|
"mapped_model": "gpt-5.5",
|
||||||
|
"needs_conversion": false,
|
||||||
|
});
|
||||||
|
let provider_body_json = json!({
|
||||||
|
"id": "resp_123",
|
||||||
|
"object": "response",
|
||||||
|
"model": "gpt-5.5",
|
||||||
|
"status": "completed",
|
||||||
|
"output": []
|
||||||
|
});
|
||||||
|
|
||||||
|
let product = maybe_build_standard_sync_finalize_product_from_normalized_payload(
|
||||||
|
"openai_responses_sync_finalize",
|
||||||
|
200,
|
||||||
|
Some(&report_context),
|
||||||
|
Some(&provider_body_json),
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
.expect("same-format sync body should succeed")
|
||||||
|
.expect("body should exist");
|
||||||
|
let StandardSyncFinalizeNormalizedProduct::SuccessBody(body_json) = product else {
|
||||||
|
panic!("same-format response should be a success body");
|
||||||
|
};
|
||||||
|
|
||||||
|
assert_eq!(body_json["model"], "gpt-5.5-xhigh");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn cross_format_sync_response_restores_model_directive_display_model() {
|
||||||
|
let report_context = json!({
|
||||||
|
"provider_api_format": "openai:responses",
|
||||||
|
"client_api_format": "claude:messages",
|
||||||
|
"model": "gpt-5.5-xhigh",
|
||||||
|
"mapped_model": "gpt-5.5",
|
||||||
|
"needs_conversion": true,
|
||||||
|
});
|
||||||
|
let provider_body_json = json!({
|
||||||
|
"id": "resp_123",
|
||||||
|
"object": "response",
|
||||||
|
"model": "gpt-5.5",
|
||||||
|
"status": "completed",
|
||||||
|
"output": [{
|
||||||
|
"type": "message",
|
||||||
|
"id": "msg_123",
|
||||||
|
"role": "assistant",
|
||||||
|
"status": "completed",
|
||||||
|
"content": [{ "type": "output_text", "text": "done" }]
|
||||||
|
}]
|
||||||
|
});
|
||||||
|
|
||||||
|
let product = maybe_build_standard_cross_format_sync_product(
|
||||||
|
"claude_cli_sync_finalize",
|
||||||
|
"openai:responses",
|
||||||
|
"claude:messages",
|
||||||
|
&report_context,
|
||||||
|
provider_body_json,
|
||||||
|
)
|
||||||
|
.expect("cross-format product should exist");
|
||||||
|
|
||||||
|
assert_eq!(product.client_body_json["model"], "gpt-5.5-xhigh");
|
||||||
|
assert_eq!(product.provider_body_json["model"], "gpt-5.5");
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn rejects_standard_same_format_when_needs_conversion_is_true() {
|
fn rejects_standard_same_format_when_needs_conversion_is_true() {
|
||||||
let report_context = json!({
|
let report_context = json!({
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
use crate::formats::shared::model_directives::model_directive_display_model_from_report_context;
|
||||||
use crate::formats::shared::AiSurfaceFinalizeError;
|
use crate::formats::shared::AiSurfaceFinalizeError;
|
||||||
use crate::provider_compat::kiro_stream::{
|
use crate::provider_compat::kiro_stream::{
|
||||||
build_kiro_initial_sse_events, build_kiro_stream_error_sse_events, encode_kiro_sse_events,
|
build_kiro_initial_sse_events, build_kiro_stream_error_sse_events, encode_kiro_sse_events,
|
||||||
@@ -63,18 +64,22 @@ impl KiroToClaudeCliStreamState {
|
|||||||
|
|
||||||
impl KiroClaudeStreamState {
|
impl KiroClaudeStreamState {
|
||||||
pub(super) fn new(report_context: &Value) -> Self {
|
pub(super) fn new(report_context: &Value) -> Self {
|
||||||
let model = report_context
|
let model = model_directive_display_model_from_report_context(report_context)
|
||||||
.get("mapped_model")
|
.or_else(|| {
|
||||||
.and_then(Value::as_str)
|
report_context
|
||||||
.filter(|value| !value.is_empty())
|
.get("mapped_model")
|
||||||
|
.and_then(Value::as_str)
|
||||||
|
.filter(|value| !value.is_empty())
|
||||||
|
.map(ToOwned::to_owned)
|
||||||
|
})
|
||||||
.or_else(|| {
|
.or_else(|| {
|
||||||
report_context
|
report_context
|
||||||
.get("model")
|
.get("model")
|
||||||
.and_then(Value::as_str)
|
.and_then(Value::as_str)
|
||||||
.filter(|value| !value.is_empty())
|
.filter(|value| !value.is_empty())
|
||||||
|
.map(ToOwned::to_owned)
|
||||||
})
|
})
|
||||||
.unwrap_or("unknown")
|
.unwrap_or_else(|| "unknown".to_string());
|
||||||
.to_string();
|
|
||||||
let thinking_enabled = report_context
|
let thinking_enabled = report_context
|
||||||
.get("original_request_body")
|
.get("original_request_body")
|
||||||
.and_then(Value::as_object)
|
.and_then(Value::as_object)
|
||||||
|
|||||||
@@ -86,6 +86,32 @@ fn kiro_stream_rewriter_converts_text_events_to_claude_sse() {
|
|||||||
assert!(text.contains("\"input_tokens\":2000"));
|
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]
|
#[test]
|
||||||
fn kiro_stream_rewriter_converts_tool_use_to_claude_events() {
|
fn kiro_stream_rewriter_converts_tool_use_to_claude_events() {
|
||||||
let report_context = kiro_report_context(false);
|
let report_context = kiro_report_context(false);
|
||||||
|
|||||||
Reference in New Issue
Block a user