mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-01 17:00:21 +08:00
refactor: 拆分 gateway 单体为独立 crate,新增 systemd 部署方案
将 gateway 内部的 model-fetch、provider-transport、scheduler-core、 usage-runtime、video-tasks-core 模块提取为独立 crate;重构 gateway 内部模块结构(state/router/cache/data/query 等);移除大量遗留模块 文件;新增 systemd 二进制部署骨架及相关文档;更新前端 usage 相关 API 和组件。
This commit is contained in:
@@ -13,7 +13,12 @@ aether-contracts.workspace = true
|
||||
aether-crypto.workspace = true
|
||||
aether-data.workspace = true
|
||||
aether-http.workspace = true
|
||||
aether-model-fetch.workspace = true
|
||||
aether-provider-transport.workspace = true
|
||||
aether-scheduler-core.workspace = true
|
||||
aether-runtime.workspace = true
|
||||
aether-usage-runtime.workspace = true
|
||||
aether-video-tasks-core.workspace = true
|
||||
aether-wallet.workspace = true
|
||||
async-stream.workspace = true
|
||||
async-trait.workspace = true
|
||||
@@ -48,3 +53,6 @@ tracing.workspace = true
|
||||
url.workspace = true
|
||||
uuid.workspace = true
|
||||
webpki-roots.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
tracing-subscriber.workspace = true
|
||||
|
||||
@@ -64,13 +64,13 @@ enum FrameParseError {
|
||||
Invalid(String),
|
||||
}
|
||||
|
||||
#[path = "kiro_stream/stream/decoder.rs"]
|
||||
#[path = "stream/decoder.rs"]
|
||||
mod decoder;
|
||||
#[path = "kiro_stream/stream/state.rs"]
|
||||
#[path = "stream/state.rs"]
|
||||
mod state;
|
||||
#[path = "kiro_stream/stream/util.rs"]
|
||||
#[path = "stream/util.rs"]
|
||||
mod util;
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "kiro_stream/stream/tests.rs"]
|
||||
#[path = "stream/tests.rs"]
|
||||
mod tests;
|
||||
@@ -1,7 +1,7 @@
|
||||
use serde_json::{json, Value};
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::gateway::GatewayError;
|
||||
use crate::GatewayError;
|
||||
|
||||
use super::util::{
|
||||
encode_events, estimate_tokens, find_real_thinking_end_tag,
|
||||
@@ -1,6 +1,6 @@
|
||||
use serde_json::Value;
|
||||
|
||||
use crate::gateway::GatewayError;
|
||||
use crate::GatewayError;
|
||||
|
||||
use super::QUOTE_CHARS;
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
pub(crate) mod kiro_stream;
|
||||
pub(crate) mod kiro;
|
||||
pub(crate) mod private_envelope;
|
||||
pub(crate) mod surfaces;
|
||||
|
||||
pub(crate) use kiro_stream::KiroToClaudeCliStreamState;
|
||||
pub(crate) use kiro::KiroToClaudeCliStreamState;
|
||||
pub(crate) use private_envelope::{
|
||||
maybe_build_provider_private_stream_normalizer,
|
||||
maybe_normalize_provider_private_sync_report_payload,
|
||||
|
||||
@@ -3,13 +3,15 @@ use std::collections::BTreeMap;
|
||||
use base64::Engine as _;
|
||||
use serde_json::Value;
|
||||
|
||||
use crate::gateway::ai_pipeline::adaptation::surfaces::{
|
||||
use crate::ai_pipeline::adaptation::surfaces::{
|
||||
provider_adaptation_allows_sync_finalize_envelope, provider_adaptation_descriptor_for_envelope,
|
||||
provider_adaptation_should_unwrap_stream_envelope, ANTIGRAVITY_V1INTERNAL_ENVELOPE_NAME,
|
||||
GEMINI_CLI_V1INTERNAL_ENVELOPE_NAME,
|
||||
};
|
||||
use crate::gateway::ai_pipeline::runtime::{KiroToClaudeCliStreamState, KIRO_ENVELOPE_NAME};
|
||||
use crate::gateway::{GatewayError, GatewaySyncReportRequest};
|
||||
use crate::ai_pipeline::runtime::adapters::kiro::{
|
||||
KiroToClaudeCliStreamState, KIRO_ENVELOPE_NAME,
|
||||
};
|
||||
use crate::{usage::GatewaySyncReportRequest, GatewayError};
|
||||
|
||||
enum ProviderPrivateStreamNormalizeMode {
|
||||
EnvelopeUnwrap,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use crate::gateway::ai_pipeline::runtime::adapters::{
|
||||
use crate::ai_pipeline::runtime::adapters::{
|
||||
antigravity::ANTIGRAVITY_PROVIDER_TYPE,
|
||||
kiro::{KIRO_ENVELOPE_NAME, PROVIDER_TYPE as KIRO_PROVIDER_TYPE},
|
||||
};
|
||||
@@ -140,12 +140,10 @@ pub(crate) fn provider_adaptation_should_unwrap_stream_envelope(
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{
|
||||
provider_adaptation_allows_sync_finalize_envelope,
|
||||
provider_adaptation_anchor_api_format,
|
||||
provider_adaptation_allows_sync_finalize_envelope, provider_adaptation_anchor_api_format,
|
||||
provider_adaptation_requires_eventstream_accept,
|
||||
provider_adaptation_should_unwrap_stream_envelope,
|
||||
ANTIGRAVITY_V1INTERNAL_ENVELOPE_NAME, GEMINI_CLI_V1INTERNAL_ENVELOPE_NAME,
|
||||
KIRO_ENVELOPE_NAME,
|
||||
provider_adaptation_should_unwrap_stream_envelope, ANTIGRAVITY_V1INTERNAL_ENVELOPE_NAME,
|
||||
GEMINI_CLI_V1INTERNAL_ENVELOPE_NAME, KIRO_ENVELOPE_NAME,
|
||||
};
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -4,8 +4,10 @@ use aether_contracts::{ExecutionPlan, ExecutionTimeouts, ProxySnapshot};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tracing::warn;
|
||||
|
||||
use crate::gateway::headers::collect_control_headers;
|
||||
use crate::gateway::{AppState, GatewayControlAuthContext, GatewayControlDecision, GatewayError};
|
||||
use crate::control::GatewayControlAuthContext;
|
||||
use crate::control::GatewayControlDecision;
|
||||
use crate::headers::collect_control_headers;
|
||||
use crate::{AppState, GatewayError};
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
pub(crate) struct GatewayControlPlanRequest {
|
||||
@@ -140,7 +142,7 @@ pub(crate) async fn build_gateway_plan_request(
|
||||
body_json: serde_json::Value,
|
||||
body_base64: Option<String>,
|
||||
) -> Result<GatewayControlPlanRequest, GatewayError> {
|
||||
let auth_context = crate::gateway::resolve_execution_runtime_auth_context(
|
||||
let auth_context = crate::control::resolve_execution_runtime_auth_context(
|
||||
state,
|
||||
decision,
|
||||
&parts.headers,
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
use serde_json::{Map, Value};
|
||||
|
||||
#[cfg(test)]
|
||||
use crate::gateway::ai_pipeline::contracts::core_success_background_report_kind as contract_core_success_background_report_kind;
|
||||
use crate::gateway::ai_pipeline::contracts::{
|
||||
use crate::ai_pipeline::contracts::core_success_background_report_kind as contract_core_success_background_report_kind;
|
||||
use crate::ai_pipeline::contracts::{
|
||||
core_error_background_report_kind as contract_core_error_background_report_kind,
|
||||
core_error_default_client_api_format as contract_core_error_default_client_api_format,
|
||||
};
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
use crate::gateway::provider_transport::{
|
||||
use crate::provider_transport::auth::{
|
||||
resolve_local_gemini_auth, resolve_local_openai_chat_auth, resolve_local_standard_auth,
|
||||
supports_local_gemini_transport_with_network, supports_local_openai_chat_transport,
|
||||
supports_local_standard_transport_with_network, GatewayProviderTransportSnapshot,
|
||||
};
|
||||
use crate::provider_transport::policy::{
|
||||
supports_local_openai_chat_transport, supports_local_standard_transport_with_network,
|
||||
};
|
||||
use crate::provider_transport::{
|
||||
supports_local_gemini_transport_with_network, GatewayProviderTransportSnapshot,
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
|
||||
@@ -3,7 +3,7 @@ use uuid::Uuid;
|
||||
|
||||
use super::super::to_openai_chat::{extract_openai_text_content, parse_openai_tool_result_content};
|
||||
use super::shared::parse_openai_tool_arguments;
|
||||
use crate::gateway::ai_pipeline::planner::standard::{
|
||||
use crate::ai_pipeline::planner::standard::{
|
||||
copy_request_number_field, map_openai_reasoning_effort_to_claude_output,
|
||||
parse_openai_stop_sequences, resolve_openai_chat_max_tokens,
|
||||
};
|
||||
|
||||
@@ -5,7 +5,7 @@ use uuid::Uuid;
|
||||
|
||||
use super::super::to_openai_chat::{extract_openai_text_content, parse_openai_tool_result_content};
|
||||
use super::shared::parse_openai_tool_arguments;
|
||||
use crate::gateway::ai_pipeline::planner::standard::{
|
||||
use crate::ai_pipeline::planner::standard::{
|
||||
copy_request_number_field_as, map_openai_reasoning_effort_to_gemini_budget,
|
||||
parse_openai_stop_sequences, value_as_u64,
|
||||
};
|
||||
|
||||
@@ -3,7 +3,7 @@ use std::collections::BTreeMap;
|
||||
use serde_json::{json, Map, Value};
|
||||
|
||||
use super::super::to_openai_chat::extract_openai_text_content;
|
||||
use crate::gateway::ai_pipeline::planner::standard::copy_request_number_field;
|
||||
use crate::ai_pipeline::planner::standard::copy_request_number_field;
|
||||
|
||||
pub(crate) fn convert_openai_chat_request_to_openai_cli_request(
|
||||
body_json: &Value,
|
||||
|
||||
@@ -4,13 +4,14 @@ use axum::body::Body;
|
||||
use axum::http::Response;
|
||||
use serde_json::Value;
|
||||
|
||||
use crate::gateway::ai_pipeline::contracts::core_success_background_report_kind;
|
||||
pub(crate) use crate::gateway::ai_pipeline::runtime::{
|
||||
pub(crate) use crate::ai_pipeline::adaptation::private_envelope::{
|
||||
normalize_provider_private_response_value as unwrap_local_finalize_response_value,
|
||||
provider_private_response_allows_sync_finalize as local_finalize_allows_envelope,
|
||||
};
|
||||
use crate::gateway::api::response::build_client_response_from_parts;
|
||||
use crate::gateway::{GatewayControlDecision, GatewayError, GatewaySyncReportRequest};
|
||||
use crate::ai_pipeline::contracts::core_success_background_report_kind;
|
||||
use crate::api::response::build_client_response_from_parts;
|
||||
use crate::control::GatewayControlDecision;
|
||||
use crate::{usage::GatewaySyncReportRequest, GatewayError};
|
||||
|
||||
pub(crate) struct LocalCoreSyncFinalizeOutcome {
|
||||
pub(crate) response: Response<Body>,
|
||||
|
||||
@@ -2,7 +2,8 @@ use axum::body::Body;
|
||||
use axum::http::Response;
|
||||
use serde_json::Value;
|
||||
|
||||
use crate::gateway::{GatewayControlDecision, GatewayError, GatewaySyncReportRequest};
|
||||
use crate::control::GatewayControlDecision;
|
||||
use crate::{usage::GatewaySyncReportRequest, GatewayError};
|
||||
|
||||
#[path = "stream_rewrite.rs"]
|
||||
pub(crate) mod stream;
|
||||
|
||||
@@ -1,30 +1,14 @@
|
||||
use serde_json::Value;
|
||||
|
||||
use crate::gateway::ai_pipeline::adaptation::private_envelope::transform_provider_private_stream_line as transform_envelope_line;
|
||||
use crate::gateway::ai_pipeline::finalize::sse::{encode_done_sse, encode_json_sse};
|
||||
use crate::gateway::ai_pipeline::finalize::standard::{
|
||||
BufferedCliConversionStreamState, BufferedStandardConversionStreamState,
|
||||
ClaudeToOpenAIChatStreamState, GeminiToOpenAIChatStreamState, OpenAICliToOpenAIChatStreamState,
|
||||
};
|
||||
use crate::gateway::ai_pipeline::runtime::KiroToClaudeCliStreamState;
|
||||
use crate::gateway::GatewayError;
|
||||
use crate::ai_pipeline::adaptation::private_envelope::transform_provider_private_stream_line as transform_envelope_line;
|
||||
use crate::ai_pipeline::finalize::standard::StreamingStandardConversionState;
|
||||
use crate::ai_pipeline::runtime::adapters::kiro::KiroToClaudeCliStreamState;
|
||||
use crate::GatewayError;
|
||||
|
||||
use super::sync::{
|
||||
aggregate_claude_stream_sync_response, aggregate_gemini_stream_sync_response,
|
||||
convert_claude_cli_response_to_openai_cli, convert_gemini_cli_response_to_openai_cli,
|
||||
};
|
||||
enum RewriteMode {
|
||||
EnvelopeUnwrap,
|
||||
ClaudeToOpenAIChat(ClaudeToOpenAIChatStreamState),
|
||||
GeminiToOpenAIChat(GeminiToOpenAIChatStreamState),
|
||||
OpenAICliToOpenAIChat(OpenAICliToOpenAIChatStreamState),
|
||||
ClaudeToOpenAICli(BufferedCliConversionStreamState),
|
||||
GeminiToOpenAICli(BufferedCliConversionStreamState),
|
||||
AntigravityGeminiToOpenAIChat(GeminiToOpenAIChatStreamState),
|
||||
AntigravityGeminiToOpenAICli(BufferedCliConversionStreamState),
|
||||
Standard(StreamingStandardConversionState),
|
||||
KiroToClaudeCli(KiroToClaudeCliStreamState),
|
||||
StandardChat(BufferedStandardConversionStreamState),
|
||||
StandardCli(BufferedStandardConversionStreamState),
|
||||
}
|
||||
|
||||
pub(crate) struct LocalStreamRewriter {
|
||||
@@ -67,48 +51,44 @@ pub(crate) fn maybe_build_local_stream_rewriter(
|
||||
client_api_format.as_str(),
|
||||
) {
|
||||
("", "claude:chat", "openai:chat") => {
|
||||
RewriteMode::ClaudeToOpenAIChat(ClaudeToOpenAIChatStreamState::default())
|
||||
RewriteMode::Standard(StreamingStandardConversionState::default())
|
||||
}
|
||||
("", "gemini:chat", "openai:chat") => {
|
||||
RewriteMode::GeminiToOpenAIChat(GeminiToOpenAIChatStreamState::default())
|
||||
RewriteMode::Standard(StreamingStandardConversionState::default())
|
||||
}
|
||||
("", "openai:cli", "openai:chat") | ("", "openai:compact", "openai:chat") => {
|
||||
RewriteMode::OpenAICliToOpenAIChat(OpenAICliToOpenAIChatStreamState::default())
|
||||
RewriteMode::Standard(StreamingStandardConversionState::default())
|
||||
}
|
||||
("", "claude:cli", "openai:cli") => {
|
||||
RewriteMode::ClaudeToOpenAICli(BufferedCliConversionStreamState::default())
|
||||
RewriteMode::Standard(StreamingStandardConversionState::default())
|
||||
}
|
||||
("", "claude:cli", "openai:compact") => {
|
||||
RewriteMode::ClaudeToOpenAICli(BufferedCliConversionStreamState::default())
|
||||
RewriteMode::Standard(StreamingStandardConversionState::default())
|
||||
}
|
||||
("", "gemini:cli", "openai:cli") => {
|
||||
RewriteMode::GeminiToOpenAICli(BufferedCliConversionStreamState::default())
|
||||
RewriteMode::Standard(StreamingStandardConversionState::default())
|
||||
}
|
||||
("", "gemini:cli", "openai:compact") => {
|
||||
RewriteMode::GeminiToOpenAICli(BufferedCliConversionStreamState::default())
|
||||
RewriteMode::Standard(StreamingStandardConversionState::default())
|
||||
}
|
||||
("antigravity:v1internal", "gemini:chat", "openai:chat") => {
|
||||
RewriteMode::AntigravityGeminiToOpenAIChat(GeminiToOpenAIChatStreamState::default())
|
||||
RewriteMode::Standard(StreamingStandardConversionState::default())
|
||||
}
|
||||
("antigravity:v1internal", "gemini:cli", "openai:cli") => {
|
||||
RewriteMode::AntigravityGeminiToOpenAICli(
|
||||
BufferedCliConversionStreamState::default(),
|
||||
)
|
||||
RewriteMode::Standard(StreamingStandardConversionState::default())
|
||||
}
|
||||
("antigravity:v1internal", "gemini:cli", "openai:compact") => {
|
||||
RewriteMode::AntigravityGeminiToOpenAICli(
|
||||
BufferedCliConversionStreamState::default(),
|
||||
)
|
||||
RewriteMode::Standard(StreamingStandardConversionState::default())
|
||||
}
|
||||
_ if is_standard_chat_client_api_format(client_api_format.as_str())
|
||||
&& is_standard_provider_api_format(provider_api_format.as_str()) =>
|
||||
{
|
||||
RewriteMode::StandardChat(BufferedStandardConversionStreamState::default())
|
||||
RewriteMode::Standard(StreamingStandardConversionState::default())
|
||||
}
|
||||
_ if is_standard_cli_client_api_format(client_api_format.as_str())
|
||||
&& is_standard_provider_api_format(provider_api_format.as_str()) =>
|
||||
{
|
||||
RewriteMode::StandardCli(BufferedStandardConversionStreamState::default())
|
||||
RewriteMode::Standard(StreamingStandardConversionState::default())
|
||||
}
|
||||
_ => return None,
|
||||
}
|
||||
@@ -168,44 +148,8 @@ impl LocalStreamRewriter {
|
||||
}
|
||||
if self.buffered.is_empty() {
|
||||
match &mut self.mode {
|
||||
RewriteMode::ClaudeToOpenAIChat(state) => return Ok(state.finish()),
|
||||
RewriteMode::GeminiToOpenAIChat(state) => {
|
||||
return state.finish(&self.report_context);
|
||||
}
|
||||
RewriteMode::OpenAICliToOpenAIChat(state) => {
|
||||
return state.finish(&self.report_context);
|
||||
}
|
||||
RewriteMode::ClaudeToOpenAICli(state) => {
|
||||
return state.finish(
|
||||
&self.report_context,
|
||||
aggregate_claude_stream_sync_response,
|
||||
convert_claude_cli_response_to_openai_cli,
|
||||
);
|
||||
}
|
||||
RewriteMode::GeminiToOpenAICli(state) => {
|
||||
return state.finish(
|
||||
&self.report_context,
|
||||
aggregate_gemini_stream_sync_response,
|
||||
convert_gemini_cli_response_to_openai_cli,
|
||||
);
|
||||
}
|
||||
RewriteMode::AntigravityGeminiToOpenAIChat(state) => {
|
||||
return state.finish(&self.report_context);
|
||||
}
|
||||
RewriteMode::AntigravityGeminiToOpenAICli(state) => {
|
||||
return state.finish(
|
||||
&self.report_context,
|
||||
aggregate_gemini_stream_sync_response,
|
||||
convert_gemini_cli_response_to_openai_cli,
|
||||
);
|
||||
}
|
||||
RewriteMode::Standard(state) => return state.finish(&self.report_context),
|
||||
RewriteMode::KiroToClaudeCli(_) => {}
|
||||
RewriteMode::StandardChat(state) => {
|
||||
return state.finish_as_chat(&self.report_context)
|
||||
}
|
||||
RewriteMode::StandardCli(state) => {
|
||||
return state.finish_as_cli(&self.report_context)
|
||||
}
|
||||
RewriteMode::EnvelopeUnwrap => {}
|
||||
}
|
||||
return Ok(Vec::new());
|
||||
@@ -213,46 +157,10 @@ impl LocalStreamRewriter {
|
||||
let line = std::mem::take(&mut self.buffered);
|
||||
let mut output = self.transform_line(line)?;
|
||||
match &mut self.mode {
|
||||
RewriteMode::ClaudeToOpenAIChat(state) => {
|
||||
output.extend(state.finish());
|
||||
}
|
||||
RewriteMode::GeminiToOpenAIChat(state) => {
|
||||
RewriteMode::Standard(state) => {
|
||||
output.extend(state.finish(&self.report_context)?);
|
||||
}
|
||||
RewriteMode::OpenAICliToOpenAIChat(state) => {
|
||||
output.extend(state.finish(&self.report_context)?);
|
||||
}
|
||||
RewriteMode::ClaudeToOpenAICli(state) => {
|
||||
output.extend(state.finish(
|
||||
&self.report_context,
|
||||
aggregate_claude_stream_sync_response,
|
||||
convert_claude_cli_response_to_openai_cli,
|
||||
)?);
|
||||
}
|
||||
RewriteMode::GeminiToOpenAICli(state) => {
|
||||
output.extend(state.finish(
|
||||
&self.report_context,
|
||||
aggregate_gemini_stream_sync_response,
|
||||
convert_gemini_cli_response_to_openai_cli,
|
||||
)?);
|
||||
}
|
||||
RewriteMode::AntigravityGeminiToOpenAIChat(state) => {
|
||||
output.extend(state.finish(&self.report_context)?);
|
||||
}
|
||||
RewriteMode::AntigravityGeminiToOpenAICli(state) => {
|
||||
output.extend(state.finish(
|
||||
&self.report_context,
|
||||
aggregate_gemini_stream_sync_response,
|
||||
convert_gemini_cli_response_to_openai_cli,
|
||||
)?);
|
||||
}
|
||||
RewriteMode::KiroToClaudeCli(_) => {}
|
||||
RewriteMode::StandardChat(state) => {
|
||||
output.extend(state.finish_as_chat(&self.report_context)?);
|
||||
}
|
||||
RewriteMode::StandardCli(state) => {
|
||||
output.extend(state.finish_as_cli(&self.report_context)?);
|
||||
}
|
||||
RewriteMode::EnvelopeUnwrap => {}
|
||||
}
|
||||
Ok(output)
|
||||
@@ -261,37 +169,7 @@ impl LocalStreamRewriter {
|
||||
fn transform_line(&mut self, line: Vec<u8>) -> Result<Vec<u8>, GatewayError> {
|
||||
match &mut self.mode {
|
||||
RewriteMode::EnvelopeUnwrap => transform_envelope_line(&self.report_context, line),
|
||||
RewriteMode::ClaudeToOpenAIChat(state) => {
|
||||
state.transform_line(&self.report_context, line)
|
||||
}
|
||||
RewriteMode::GeminiToOpenAIChat(state) => {
|
||||
state.transform_line(&self.report_context, line)
|
||||
}
|
||||
RewriteMode::OpenAICliToOpenAIChat(state) => {
|
||||
state.transform_line(&self.report_context, line)
|
||||
}
|
||||
RewriteMode::ClaudeToOpenAICli(state) | RewriteMode::GeminiToOpenAICli(state) => {
|
||||
state.transform_line(line)
|
||||
}
|
||||
RewriteMode::AntigravityGeminiToOpenAIChat(state) => {
|
||||
let unwrapped = transform_envelope_line(&self.report_context, line)?;
|
||||
if unwrapped.is_empty() {
|
||||
Ok(Vec::new())
|
||||
} else {
|
||||
state.transform_line(&self.report_context, unwrapped)
|
||||
}
|
||||
}
|
||||
RewriteMode::AntigravityGeminiToOpenAICli(state) => {
|
||||
let unwrapped = transform_envelope_line(&self.report_context, line)?;
|
||||
if unwrapped.is_empty() {
|
||||
Ok(Vec::new())
|
||||
} else {
|
||||
state.transform_line(unwrapped)
|
||||
}
|
||||
}
|
||||
RewriteMode::StandardChat(state) | RewriteMode::StandardCli(state) => {
|
||||
state.transform_line(&self.report_context, line)
|
||||
}
|
||||
RewriteMode::Standard(state) => state.transform_line(&self.report_context, line),
|
||||
RewriteMode::KiroToClaudeCli(_) => Ok(Vec::new()),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,19 +5,20 @@ use axum::http::Response;
|
||||
use base64::Engine as _;
|
||||
use serde_json::{json, Map, Value};
|
||||
|
||||
use crate::gateway::ai_pipeline::conversion::{
|
||||
use crate::ai_pipeline::conversion::{
|
||||
sync_chat_response_conversion_kind, sync_cli_response_conversion_kind,
|
||||
};
|
||||
use crate::gateway::api::response::build_client_response_from_parts;
|
||||
use crate::gateway::{GatewayControlDecision, GatewayError, GatewaySyncReportRequest};
|
||||
use crate::api::response::build_client_response_from_parts;
|
||||
use crate::control::GatewayControlDecision;
|
||||
use crate::{usage::GatewaySyncReportRequest, GatewayError};
|
||||
|
||||
pub(crate) use crate::gateway::ai_pipeline::finalize::common::{
|
||||
pub(crate) use crate::ai_pipeline::finalize::common::{
|
||||
build_generated_tool_call_id, build_local_success_outcome,
|
||||
build_local_success_outcome_with_conversion_report, canonicalize_tool_arguments,
|
||||
local_finalize_allows_envelope, parse_stream_json_events, unwrap_local_finalize_response_value,
|
||||
LocalCoreSyncFinalizeOutcome,
|
||||
};
|
||||
pub(crate) use crate::gateway::ai_pipeline::finalize::standard::{
|
||||
pub(crate) use crate::ai_pipeline::finalize::standard::{
|
||||
aggregate_claude_stream_sync_response, aggregate_gemini_stream_sync_response,
|
||||
aggregate_openai_chat_stream_sync_response, aggregate_openai_cli_stream_sync_response,
|
||||
aggregate_standard_chat_stream_sync_response, aggregate_standard_cli_stream_sync_response,
|
||||
@@ -45,7 +46,7 @@ pub(crate) fn maybe_build_local_core_sync_finalize_response(
|
||||
payload: &GatewaySyncReportRequest,
|
||||
) -> Result<Option<LocalCoreSyncFinalizeOutcome>, GatewayError> {
|
||||
let Some(normalized_payload) =
|
||||
crate::gateway::ai_pipeline::adaptation::private_envelope::maybe_normalize_provider_private_sync_report_payload(payload)?
|
||||
crate::ai_pipeline::adaptation::private_envelope::maybe_normalize_provider_private_sync_report_payload(payload)?
|
||||
else {
|
||||
return Ok(None);
|
||||
};
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
pub(crate) mod common;
|
||||
pub(crate) mod sse;
|
||||
pub(crate) mod standard;
|
||||
pub(crate) use crate::gateway::api::response::{
|
||||
pub(crate) use crate::api::response::{
|
||||
build_client_response, build_client_response_from_parts,
|
||||
};
|
||||
pub(crate) use crate::gateway::execution_runtime::maybe_build_local_sync_finalize_response;
|
||||
pub(crate) use crate::execution_runtime::maybe_build_local_sync_finalize_response;
|
||||
pub(crate) use common::build_local_success_outcome;
|
||||
pub(crate) use internal::{
|
||||
maybe_build_stream_response_rewriter, maybe_build_sync_finalize_outcome,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use serde_json::Value;
|
||||
|
||||
use crate::gateway::GatewayError;
|
||||
use crate::GatewayError;
|
||||
|
||||
pub(crate) fn map_claude_stop_reason(
|
||||
stop_reason: Option<&str>,
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
use serde_json::Value;
|
||||
|
||||
use crate::gateway::ai_pipeline::finalize::common::LocalCoreSyncFinalizeOutcome;
|
||||
use crate::gateway::{GatewayControlDecision, GatewayError, GatewaySyncReportRequest};
|
||||
|
||||
pub(crate) fn aggregate_claude_stream_sync_response(body: &[u8]) -> Option<Value> {
|
||||
super::claude_chat::aggregate_claude_stream_sync_response(body)
|
||||
}
|
||||
|
||||
pub(crate) fn maybe_build_local_claude_stream_sync_response(
|
||||
trace_id: &str,
|
||||
decision: &GatewayControlDecision,
|
||||
payload: &GatewaySyncReportRequest,
|
||||
) -> Result<Option<LocalCoreSyncFinalizeOutcome>, GatewayError> {
|
||||
super::claude_chat::maybe_build_local_claude_stream_sync_response(trace_id, decision, payload)
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
use crate::gateway::ai_pipeline::finalize::common::LocalCoreSyncFinalizeOutcome;
|
||||
use crate::gateway::{GatewayControlDecision, GatewayError, GatewaySyncReportRequest};
|
||||
|
||||
pub(crate) fn maybe_build_local_claude_cli_stream_sync_response(
|
||||
trace_id: &str,
|
||||
decision: &GatewayControlDecision,
|
||||
payload: &GatewaySyncReportRequest,
|
||||
) -> Result<Option<LocalCoreSyncFinalizeOutcome>, GatewayError> {
|
||||
super::claude_cli::maybe_build_local_claude_cli_stream_sync_response(
|
||||
trace_id, decision, payload,
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
pub(super) mod stream;
|
||||
pub(super) mod sync;
|
||||
|
||||
pub(crate) use sync::{
|
||||
aggregate_claude_stream_sync_response, convert_claude_chat_response_to_openai_chat,
|
||||
convert_claude_cli_response_to_openai_cli, convert_openai_chat_response_to_claude_chat,
|
||||
maybe_build_local_claude_cli_stream_sync_response,
|
||||
maybe_build_local_claude_stream_sync_response, maybe_build_local_claude_sync_response,
|
||||
};
|
||||
@@ -0,0 +1,531 @@
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use serde_json::{json, Map, Value};
|
||||
|
||||
use crate::ai_pipeline::finalize::common::{
|
||||
build_generated_tool_call_id, canonicalize_tool_arguments,
|
||||
};
|
||||
use crate::ai_pipeline::finalize::sse::{
|
||||
encode_done_sse, encode_json_sse, map_claude_stop_reason,
|
||||
};
|
||||
use crate::GatewayError;
|
||||
|
||||
use crate::ai_pipeline::finalize::standard::stream::common::*;
|
||||
|
||||
#[derive(Default)]
|
||||
struct ClaudeProviderToolState {
|
||||
call_id: String,
|
||||
name: String,
|
||||
started_emitted: bool,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub(crate) struct ClaudeProviderState {
|
||||
message_id: Option<String>,
|
||||
model: Option<String>,
|
||||
started: bool,
|
||||
finished: bool,
|
||||
tool_calls: BTreeMap<usize, ClaudeProviderToolState>,
|
||||
}
|
||||
|
||||
impl ClaudeProviderState {
|
||||
fn identity(&self, report_context: &Value) -> (String, String) {
|
||||
resolve_identity(
|
||||
self.message_id.as_deref(),
|
||||
self.model.as_deref(),
|
||||
report_context,
|
||||
"msg-local-stream",
|
||||
)
|
||||
}
|
||||
|
||||
fn ensure_started(&mut self, report_context: &Value, out: &mut Vec<CanonicalStreamFrame>) {
|
||||
if self.started {
|
||||
return;
|
||||
}
|
||||
let (id, model) = self.identity(report_context);
|
||||
out.push(CanonicalStreamFrame {
|
||||
id,
|
||||
model,
|
||||
event: CanonicalStreamEvent::Start,
|
||||
});
|
||||
self.started = true;
|
||||
}
|
||||
|
||||
pub(crate) fn push_line(
|
||||
&mut self,
|
||||
report_context: &Value,
|
||||
line: Vec<u8>,
|
||||
) -> Result<Vec<CanonicalStreamFrame>, GatewayError> {
|
||||
let Some(value) = decode_json_data_line(&line) else {
|
||||
return Ok(Vec::new());
|
||||
};
|
||||
let Some(event_object) = value.as_object() else {
|
||||
return Ok(Vec::new());
|
||||
};
|
||||
let mut out = Vec::new();
|
||||
match event_object
|
||||
.get("type")
|
||||
.and_then(Value::as_str)
|
||||
.unwrap_or_default()
|
||||
{
|
||||
"message_start" => {
|
||||
if let Some(message) = event_object.get("message").and_then(Value::as_object) {
|
||||
self.message_id = message
|
||||
.get("id")
|
||||
.and_then(Value::as_str)
|
||||
.map(ToOwned::to_owned);
|
||||
self.model = message
|
||||
.get("model")
|
||||
.and_then(Value::as_str)
|
||||
.map(ToOwned::to_owned);
|
||||
}
|
||||
self.ensure_started(report_context, &mut out);
|
||||
}
|
||||
"content_block_delta" => {
|
||||
let index = event_object
|
||||
.get("index")
|
||||
.and_then(Value::as_u64)
|
||||
.map(|value| value as usize)
|
||||
.unwrap_or(0);
|
||||
let Some(delta) = event_object.get("delta").and_then(Value::as_object) else {
|
||||
return Ok(out);
|
||||
};
|
||||
match delta
|
||||
.get("type")
|
||||
.and_then(Value::as_str)
|
||||
.unwrap_or_default()
|
||||
{
|
||||
"text_delta" => {
|
||||
let Some(piece) = delta.get("text").and_then(Value::as_str) else {
|
||||
return Ok(out);
|
||||
};
|
||||
if piece.is_empty() {
|
||||
return Ok(out);
|
||||
}
|
||||
self.ensure_started(report_context, &mut out);
|
||||
let (id, model) = self.identity(report_context);
|
||||
out.push(CanonicalStreamFrame {
|
||||
id,
|
||||
model,
|
||||
event: CanonicalStreamEvent::TextDelta(piece.to_string()),
|
||||
});
|
||||
}
|
||||
"input_json_delta" => {
|
||||
let Some(partial_json) = delta.get("partial_json").and_then(Value::as_str)
|
||||
else {
|
||||
return Ok(out);
|
||||
};
|
||||
if partial_json.is_empty() {
|
||||
return Ok(out);
|
||||
}
|
||||
self.ensure_started(report_context, &mut out);
|
||||
let (id, model) = self.identity(report_context);
|
||||
let tool_state = self.tool_calls.entry(index).or_default();
|
||||
if !tool_state.started_emitted {
|
||||
out.push(CanonicalStreamFrame {
|
||||
id: id.clone(),
|
||||
model: model.clone(),
|
||||
event: CanonicalStreamEvent::ToolCallStart {
|
||||
index,
|
||||
call_id: if tool_state.call_id.is_empty() {
|
||||
build_generated_tool_call_id(index)
|
||||
} else {
|
||||
tool_state.call_id.clone()
|
||||
},
|
||||
name: if tool_state.name.is_empty() {
|
||||
"unknown".to_string()
|
||||
} else {
|
||||
tool_state.name.clone()
|
||||
},
|
||||
},
|
||||
});
|
||||
tool_state.started_emitted = true;
|
||||
}
|
||||
out.push(CanonicalStreamFrame {
|
||||
id,
|
||||
model,
|
||||
event: CanonicalStreamEvent::ToolCallArgumentsDelta {
|
||||
index,
|
||||
arguments: partial_json.to_string(),
|
||||
},
|
||||
});
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
"content_block_start" => {
|
||||
let index = event_object
|
||||
.get("index")
|
||||
.and_then(Value::as_u64)
|
||||
.map(|value| value as usize)
|
||||
.unwrap_or(0);
|
||||
let Some(block) = event_object.get("content_block").and_then(Value::as_object)
|
||||
else {
|
||||
return Ok(out);
|
||||
};
|
||||
let block_type = block
|
||||
.get("type")
|
||||
.and_then(Value::as_str)
|
||||
.unwrap_or_default();
|
||||
if block_type == "text" {
|
||||
let Some(text) = block.get("text").and_then(Value::as_str) else {
|
||||
return Ok(out);
|
||||
};
|
||||
if text.is_empty() {
|
||||
return Ok(out);
|
||||
}
|
||||
self.ensure_started(report_context, &mut out);
|
||||
let (id, model) = self.identity(report_context);
|
||||
out.push(CanonicalStreamFrame {
|
||||
id,
|
||||
model,
|
||||
event: CanonicalStreamEvent::TextDelta(text.to_string()),
|
||||
});
|
||||
return Ok(out);
|
||||
}
|
||||
if block_type != "tool_use" {
|
||||
return Ok(out);
|
||||
}
|
||||
self.ensure_started(report_context, &mut out);
|
||||
let (id, model) = self.identity(report_context);
|
||||
let tool_state = self.tool_calls.entry(index).or_default();
|
||||
tool_state.call_id = block
|
||||
.get("id")
|
||||
.and_then(Value::as_str)
|
||||
.unwrap_or_else(|| tool_state.call_id.as_str())
|
||||
.to_string();
|
||||
tool_state.name = block
|
||||
.get("name")
|
||||
.and_then(Value::as_str)
|
||||
.unwrap_or_else(|| tool_state.name.as_str())
|
||||
.to_string();
|
||||
if !tool_state.started_emitted {
|
||||
out.push(CanonicalStreamFrame {
|
||||
id: id.clone(),
|
||||
model: model.clone(),
|
||||
event: CanonicalStreamEvent::ToolCallStart {
|
||||
index,
|
||||
call_id: if tool_state.call_id.is_empty() {
|
||||
build_generated_tool_call_id(index)
|
||||
} else {
|
||||
tool_state.call_id.clone()
|
||||
},
|
||||
name: if tool_state.name.is_empty() {
|
||||
"unknown".to_string()
|
||||
} else {
|
||||
tool_state.name.clone()
|
||||
},
|
||||
},
|
||||
});
|
||||
tool_state.started_emitted = true;
|
||||
}
|
||||
let arguments = canonicalize_tool_arguments(block.get("input").cloned());
|
||||
if !arguments.is_empty() && arguments != "{}" {
|
||||
out.push(CanonicalStreamFrame {
|
||||
id,
|
||||
model,
|
||||
event: CanonicalStreamEvent::ToolCallArgumentsDelta { index, arguments },
|
||||
});
|
||||
}
|
||||
}
|
||||
"message_delta" => {
|
||||
self.ensure_started(report_context, &mut out);
|
||||
let Some(delta) = event_object.get("delta").and_then(Value::as_object) else {
|
||||
return Ok(out);
|
||||
};
|
||||
let finish_reason = map_claude_stop_reason(
|
||||
delta.get("stop_reason").and_then(Value::as_str),
|
||||
delta.get("stop_reason").and_then(Value::as_str) == Some("tool_use"),
|
||||
)
|
||||
.map(ToOwned::to_owned);
|
||||
let (id, model) = self.identity(report_context);
|
||||
out.push(CanonicalStreamFrame {
|
||||
id,
|
||||
model,
|
||||
event: CanonicalStreamEvent::Finish {
|
||||
finish_reason,
|
||||
usage: canonical_usage_from_claude_usage(event_object.get("usage")),
|
||||
},
|
||||
});
|
||||
self.finished = true;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
pub(crate) fn finish(
|
||||
&mut self,
|
||||
report_context: &Value,
|
||||
) -> Result<Vec<CanonicalStreamFrame>, GatewayError> {
|
||||
if !self.started || self.finished {
|
||||
return Ok(Vec::new());
|
||||
}
|
||||
self.finished = true;
|
||||
let (id, model) = self.identity(report_context);
|
||||
Ok(vec![CanonicalStreamFrame {
|
||||
id,
|
||||
model,
|
||||
event: CanonicalStreamEvent::Finish {
|
||||
finish_reason: None,
|
||||
usage: None,
|
||||
},
|
||||
}])
|
||||
}
|
||||
}
|
||||
|
||||
enum ClaudeOpenBlock {
|
||||
Text {
|
||||
block_index: usize,
|
||||
},
|
||||
Tool {
|
||||
tool_index: usize,
|
||||
block_index: usize,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub(crate) struct ClaudeClientEmitter {
|
||||
message_id: Option<String>,
|
||||
model: Option<String>,
|
||||
started: bool,
|
||||
finished: bool,
|
||||
next_block_index: usize,
|
||||
open_block: Option<ClaudeOpenBlock>,
|
||||
tool_block_indices: BTreeMap<usize, usize>,
|
||||
}
|
||||
|
||||
impl ClaudeClientEmitter {
|
||||
fn update_identity(&mut self, frame: &CanonicalStreamFrame) {
|
||||
self.message_id = Some(frame.id.clone());
|
||||
self.model = Some(frame.model.clone());
|
||||
}
|
||||
|
||||
fn ensure_started(&mut self) -> Result<Vec<u8>, GatewayError> {
|
||||
if self.started {
|
||||
return Ok(Vec::new());
|
||||
}
|
||||
self.started = true;
|
||||
encode_json_sse(
|
||||
Some("message_start"),
|
||||
&json!({
|
||||
"type": "message_start",
|
||||
"message": {
|
||||
"id": self.message_id.as_deref().unwrap_or("msg-local-stream"),
|
||||
"type": "message",
|
||||
"role": "assistant",
|
||||
"model": self.model.as_deref().unwrap_or("unknown"),
|
||||
"content": [],
|
||||
"stop_reason": Value::Null,
|
||||
"stop_sequence": Value::Null,
|
||||
}
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
fn close_open_block(&mut self) -> Result<Vec<u8>, GatewayError> {
|
||||
let Some(open_block) = self.open_block.take() else {
|
||||
return Ok(Vec::new());
|
||||
};
|
||||
let block_index = match open_block {
|
||||
ClaudeOpenBlock::Text { block_index } => block_index,
|
||||
ClaudeOpenBlock::Tool { block_index, .. } => block_index,
|
||||
};
|
||||
encode_json_sse(
|
||||
Some("content_block_stop"),
|
||||
&json!({
|
||||
"type": "content_block_stop",
|
||||
"index": block_index,
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
fn ensure_text_block(&mut self) -> Result<Vec<u8>, GatewayError> {
|
||||
let mut out = Vec::new();
|
||||
if let Some(ClaudeOpenBlock::Text { .. }) = self.open_block {
|
||||
return Ok(out);
|
||||
}
|
||||
out.extend(self.close_open_block()?);
|
||||
let block_index = self.next_block_index;
|
||||
self.next_block_index += 1;
|
||||
self.open_block = Some(ClaudeOpenBlock::Text { block_index });
|
||||
out.extend(encode_json_sse(
|
||||
Some("content_block_start"),
|
||||
&json!({
|
||||
"type": "content_block_start",
|
||||
"index": block_index,
|
||||
"content_block": {
|
||||
"type": "text",
|
||||
"text": "",
|
||||
}
|
||||
}),
|
||||
)?);
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
fn ensure_tool_block(
|
||||
&mut self,
|
||||
tool_index: usize,
|
||||
call_id: &str,
|
||||
name: &str,
|
||||
) -> Result<Vec<u8>, GatewayError> {
|
||||
let mut out = Vec::new();
|
||||
if let Some(ClaudeOpenBlock::Tool {
|
||||
tool_index: current_tool_index,
|
||||
..
|
||||
}) = self.open_block
|
||||
{
|
||||
if current_tool_index == tool_index {
|
||||
return Ok(out);
|
||||
}
|
||||
}
|
||||
out.extend(self.close_open_block()?);
|
||||
let block_index = self
|
||||
.tool_block_indices
|
||||
.get(&tool_index)
|
||||
.copied()
|
||||
.unwrap_or_else(|| {
|
||||
let block_index = self.next_block_index;
|
||||
self.next_block_index += 1;
|
||||
self.tool_block_indices.insert(tool_index, block_index);
|
||||
block_index
|
||||
});
|
||||
self.open_block = Some(ClaudeOpenBlock::Tool {
|
||||
tool_index,
|
||||
block_index,
|
||||
});
|
||||
out.extend(encode_json_sse(
|
||||
Some("content_block_start"),
|
||||
&json!({
|
||||
"type": "content_block_start",
|
||||
"index": block_index,
|
||||
"content_block": {
|
||||
"type": "tool_use",
|
||||
"id": call_id,
|
||||
"name": name,
|
||||
"input": {},
|
||||
}
|
||||
}),
|
||||
)?);
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
pub(crate) fn emit(&mut self, frame: CanonicalStreamFrame) -> Result<Vec<u8>, GatewayError> {
|
||||
self.update_identity(&frame);
|
||||
match frame.event {
|
||||
CanonicalStreamEvent::Start => self.ensure_started(),
|
||||
CanonicalStreamEvent::TextDelta(text) => {
|
||||
let mut out = self.ensure_started()?;
|
||||
out.extend(self.ensure_text_block()?);
|
||||
let block_index = match self.open_block {
|
||||
Some(ClaudeOpenBlock::Text { block_index }) => block_index,
|
||||
_ => return Ok(out),
|
||||
};
|
||||
out.extend(encode_json_sse(
|
||||
Some("content_block_delta"),
|
||||
&json!({
|
||||
"type": "content_block_delta",
|
||||
"index": block_index,
|
||||
"delta": {
|
||||
"type": "text_delta",
|
||||
"text": text,
|
||||
}
|
||||
}),
|
||||
)?);
|
||||
Ok(out)
|
||||
}
|
||||
CanonicalStreamEvent::ToolCallStart {
|
||||
index,
|
||||
call_id,
|
||||
name,
|
||||
} => {
|
||||
let mut out = self.ensure_started()?;
|
||||
out.extend(self.ensure_tool_block(index, &call_id, &name)?);
|
||||
Ok(out)
|
||||
}
|
||||
CanonicalStreamEvent::ToolCallArgumentsDelta { index, arguments } => {
|
||||
let mut out = self.ensure_started()?;
|
||||
let call_id = format!("tool_{index}");
|
||||
out.extend(self.ensure_tool_block(index, &call_id, "unknown")?);
|
||||
let block_index = match self.open_block {
|
||||
Some(ClaudeOpenBlock::Tool { block_index, .. }) => block_index,
|
||||
_ => return Ok(out),
|
||||
};
|
||||
out.extend(encode_json_sse(
|
||||
Some("content_block_delta"),
|
||||
&json!({
|
||||
"type": "content_block_delta",
|
||||
"index": block_index,
|
||||
"delta": {
|
||||
"type": "input_json_delta",
|
||||
"partial_json": arguments,
|
||||
}
|
||||
}),
|
||||
)?);
|
||||
Ok(out)
|
||||
}
|
||||
CanonicalStreamEvent::Finish {
|
||||
finish_reason,
|
||||
usage,
|
||||
} => {
|
||||
if self.finished {
|
||||
return Ok(Vec::new());
|
||||
}
|
||||
let mut out = self.ensure_started()?;
|
||||
out.extend(self.close_open_block()?);
|
||||
let mut payload = Map::new();
|
||||
payload.insert(
|
||||
"type".to_string(),
|
||||
Value::String("message_delta".to_string()),
|
||||
);
|
||||
payload.insert(
|
||||
"delta".to_string(),
|
||||
json!({
|
||||
"stop_reason": map_openai_finish_reason_to_claude(
|
||||
finish_reason.as_deref()
|
||||
),
|
||||
"stop_sequence": Value::Null,
|
||||
}),
|
||||
);
|
||||
if let Some(usage) = usage {
|
||||
payload.insert(
|
||||
"usage".to_string(),
|
||||
json!({
|
||||
"input_tokens": usage.input_tokens,
|
||||
"output_tokens": usage.output_tokens,
|
||||
}),
|
||||
);
|
||||
}
|
||||
out.extend(encode_json_sse(
|
||||
Some("message_delta"),
|
||||
&Value::Object(payload),
|
||||
)?);
|
||||
out.extend(encode_json_sse(
|
||||
Some("message_stop"),
|
||||
&json!({
|
||||
"type": "message_stop",
|
||||
}),
|
||||
)?);
|
||||
self.finished = true;
|
||||
Ok(out)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn finish(&mut self) -> Result<Vec<u8>, GatewayError> {
|
||||
if !self.started || self.finished {
|
||||
return Ok(Vec::new());
|
||||
}
|
||||
self.emit(CanonicalStreamFrame {
|
||||
id: self
|
||||
.message_id
|
||||
.clone()
|
||||
.unwrap_or_else(|| "msg-local-stream".to_string()),
|
||||
model: self.model.clone().unwrap_or_else(|| "unknown".to_string()),
|
||||
event: CanonicalStreamEvent::Finish {
|
||||
finish_reason: None,
|
||||
usage: None,
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -3,12 +3,13 @@ use std::collections::BTreeMap;
|
||||
|
||||
use serde_json::{json, Map, Value};
|
||||
|
||||
use crate::gateway::ai_pipeline::finalize::common::{
|
||||
use crate::ai_pipeline::finalize::common::{
|
||||
build_generated_tool_call_id, build_local_success_outcome, canonicalize_tool_arguments,
|
||||
local_finalize_allows_envelope, parse_stream_json_events, unwrap_local_finalize_response_value,
|
||||
LocalCoreSyncFinalizeOutcome,
|
||||
};
|
||||
use crate::gateway::{GatewayControlDecision, GatewayError, GatewaySyncReportRequest};
|
||||
use crate::control::GatewayControlDecision;
|
||||
use crate::{usage::GatewaySyncReportRequest, GatewayError};
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
struct ClaudeContentBlockState {
|
||||
@@ -1,15 +1,16 @@
|
||||
use base64::Engine as _;
|
||||
|
||||
use super::aggregate_claude_stream_sync_response;
|
||||
use super::chat::aggregate_claude_stream_sync_response;
|
||||
use serde_json::{json, Value};
|
||||
|
||||
use crate::gateway::ai_pipeline::finalize::common::{
|
||||
use crate::ai_pipeline::conversion::response::build_openai_cli_response;
|
||||
use crate::ai_pipeline::finalize::common::{
|
||||
build_generated_tool_call_id, build_local_success_outcome, canonicalize_tool_arguments,
|
||||
local_finalize_allows_envelope, unwrap_local_finalize_response_value,
|
||||
LocalCoreSyncFinalizeOutcome,
|
||||
};
|
||||
use crate::gateway::ai_pipeline::finalize::standard::build_openai_cli_response;
|
||||
use crate::gateway::{GatewayControlDecision, GatewayError, GatewaySyncReportRequest};
|
||||
use crate::control::GatewayControlDecision;
|
||||
use crate::{usage::GatewaySyncReportRequest, GatewayError};
|
||||
|
||||
pub(crate) fn maybe_build_local_claude_cli_stream_sync_response(
|
||||
trace_id: &str,
|
||||
@@ -0,0 +1,11 @@
|
||||
mod chat;
|
||||
mod cli;
|
||||
|
||||
pub(crate) use chat::{
|
||||
aggregate_claude_stream_sync_response, convert_claude_chat_response_to_openai_chat,
|
||||
convert_openai_chat_response_to_claude_chat, maybe_build_local_claude_stream_sync_response,
|
||||
maybe_build_local_claude_sync_response,
|
||||
};
|
||||
pub(crate) use cli::{
|
||||
convert_claude_cli_response_to_openai_cli, maybe_build_local_claude_cli_stream_sync_response,
|
||||
};
|
||||
@@ -1,16 +0,0 @@
|
||||
use serde_json::Value;
|
||||
|
||||
use crate::gateway::ai_pipeline::finalize::common::LocalCoreSyncFinalizeOutcome;
|
||||
use crate::gateway::{GatewayControlDecision, GatewayError, GatewaySyncReportRequest};
|
||||
|
||||
pub(crate) fn aggregate_gemini_stream_sync_response(body: &[u8]) -> Option<Value> {
|
||||
super::gemini_chat::aggregate_gemini_stream_sync_response(body)
|
||||
}
|
||||
|
||||
pub(crate) fn maybe_build_local_gemini_stream_sync_response(
|
||||
trace_id: &str,
|
||||
decision: &GatewayControlDecision,
|
||||
payload: &GatewaySyncReportRequest,
|
||||
) -> Result<Option<LocalCoreSyncFinalizeOutcome>, GatewayError> {
|
||||
super::gemini_chat::maybe_build_local_gemini_stream_sync_response(trace_id, decision, payload)
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
use crate::gateway::ai_pipeline::finalize::common::LocalCoreSyncFinalizeOutcome;
|
||||
use crate::gateway::{GatewayControlDecision, GatewayError, GatewaySyncReportRequest};
|
||||
|
||||
pub(crate) fn maybe_build_local_gemini_cli_stream_sync_response(
|
||||
trace_id: &str,
|
||||
decision: &GatewayControlDecision,
|
||||
payload: &GatewaySyncReportRequest,
|
||||
) -> Result<Option<LocalCoreSyncFinalizeOutcome>, GatewayError> {
|
||||
super::gemini_cli::maybe_build_local_gemini_cli_stream_sync_response(
|
||||
trace_id, decision, payload,
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
pub(super) mod stream;
|
||||
pub(super) mod sync;
|
||||
|
||||
pub(crate) use sync::{
|
||||
aggregate_gemini_stream_sync_response, convert_gemini_chat_response_to_openai_chat,
|
||||
convert_gemini_cli_response_to_openai_cli, convert_openai_chat_response_to_gemini_chat,
|
||||
maybe_build_local_gemini_cli_stream_sync_response,
|
||||
maybe_build_local_gemini_stream_sync_response, maybe_build_local_gemini_sync_response,
|
||||
};
|
||||
@@ -0,0 +1,404 @@
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use serde_json::{json, Map, Value};
|
||||
|
||||
use crate::ai_pipeline::finalize::common::{
|
||||
build_generated_tool_call_id, canonicalize_tool_arguments,
|
||||
};
|
||||
use crate::ai_pipeline::finalize::sse::encode_json_sse;
|
||||
use crate::GatewayError;
|
||||
|
||||
use crate::ai_pipeline::finalize::standard::stream::common::*;
|
||||
|
||||
#[derive(Default)]
|
||||
struct GeminiProviderToolState {
|
||||
call_id: String,
|
||||
name: String,
|
||||
arguments: String,
|
||||
started_emitted: bool,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub(crate) struct GeminiProviderState {
|
||||
response_id: Option<String>,
|
||||
model: Option<String>,
|
||||
started: bool,
|
||||
finished: bool,
|
||||
text_parts: BTreeMap<usize, String>,
|
||||
tool_calls: BTreeMap<usize, GeminiProviderToolState>,
|
||||
}
|
||||
|
||||
impl GeminiProviderState {
|
||||
fn identity(&self, report_context: &Value) -> (String, String) {
|
||||
resolve_identity(
|
||||
self.response_id.as_deref(),
|
||||
self.model.as_deref(),
|
||||
report_context,
|
||||
"resp-local-stream",
|
||||
)
|
||||
}
|
||||
|
||||
fn ensure_started(&mut self, report_context: &Value, out: &mut Vec<CanonicalStreamFrame>) {
|
||||
if self.started {
|
||||
return;
|
||||
}
|
||||
let (id, model) = self.identity(report_context);
|
||||
out.push(CanonicalStreamFrame {
|
||||
id,
|
||||
model,
|
||||
event: CanonicalStreamEvent::Start,
|
||||
});
|
||||
self.started = true;
|
||||
}
|
||||
|
||||
pub(crate) fn push_line(
|
||||
&mut self,
|
||||
report_context: &Value,
|
||||
line: Vec<u8>,
|
||||
) -> Result<Vec<CanonicalStreamFrame>, GatewayError> {
|
||||
let Some(value) = decode_json_data_line(&line) else {
|
||||
return Ok(Vec::new());
|
||||
};
|
||||
let Some(raw_event_object) = value.as_object() else {
|
||||
return Ok(Vec::new());
|
||||
};
|
||||
if let Some(id) = raw_event_object.get("responseId").and_then(Value::as_str) {
|
||||
self.response_id = Some(id.to_string());
|
||||
}
|
||||
let event_object = raw_event_object
|
||||
.get("response")
|
||||
.and_then(Value::as_object)
|
||||
.filter(|response| response.contains_key("candidates"))
|
||||
.unwrap_or(raw_event_object);
|
||||
if let Some(id) = event_object.get("responseId").and_then(Value::as_str) {
|
||||
self.response_id = Some(id.to_string());
|
||||
}
|
||||
if let Some(version) = event_object.get("modelVersion").and_then(Value::as_str) {
|
||||
self.model = Some(version.to_string());
|
||||
}
|
||||
|
||||
let mut out = Vec::new();
|
||||
let Some(candidates) = event_object.get("candidates").and_then(Value::as_array) else {
|
||||
return Ok(out);
|
||||
};
|
||||
|
||||
for candidate in candidates {
|
||||
let Some(candidate_object) = candidate.as_object() else {
|
||||
continue;
|
||||
};
|
||||
let Some(content) = candidate_object.get("content").and_then(Value::as_object) else {
|
||||
continue;
|
||||
};
|
||||
let Some(parts) = content.get("parts").and_then(Value::as_array) else {
|
||||
continue;
|
||||
};
|
||||
if !parts.is_empty() {
|
||||
self.ensure_started(report_context, &mut out);
|
||||
}
|
||||
let (id, model) = self.identity(report_context);
|
||||
for (index, part) in parts.iter().enumerate() {
|
||||
let Some(part_object) = part.as_object() else {
|
||||
continue;
|
||||
};
|
||||
if let Some(text) = part_object.get("text").and_then(Value::as_str) {
|
||||
let previous = self.text_parts.entry(index).or_default();
|
||||
let delta = if text.starts_with(previous.as_str()) {
|
||||
text[previous.len()..].to_string()
|
||||
} else if previous.as_str() == text {
|
||||
String::new()
|
||||
} else {
|
||||
text.to_string()
|
||||
};
|
||||
*previous = text.to_string();
|
||||
if !delta.is_empty() {
|
||||
out.push(CanonicalStreamFrame {
|
||||
id: id.clone(),
|
||||
model: model.clone(),
|
||||
event: CanonicalStreamEvent::TextDelta(delta),
|
||||
});
|
||||
}
|
||||
continue;
|
||||
}
|
||||
let Some(function_call) =
|
||||
part_object.get("functionCall").and_then(Value::as_object)
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
let tool_state = self.tool_calls.entry(index).or_default();
|
||||
tool_state.call_id = function_call
|
||||
.get("id")
|
||||
.and_then(Value::as_str)
|
||||
.unwrap_or_else(|| tool_state.call_id.as_str())
|
||||
.to_string();
|
||||
tool_state.name = function_call
|
||||
.get("name")
|
||||
.and_then(Value::as_str)
|
||||
.unwrap_or_else(|| tool_state.name.as_str())
|
||||
.to_string();
|
||||
if !tool_state.started_emitted {
|
||||
out.push(CanonicalStreamFrame {
|
||||
id: id.clone(),
|
||||
model: model.clone(),
|
||||
event: CanonicalStreamEvent::ToolCallStart {
|
||||
index,
|
||||
call_id: if tool_state.call_id.is_empty() {
|
||||
build_generated_tool_call_id(index)
|
||||
} else {
|
||||
tool_state.call_id.clone()
|
||||
},
|
||||
name: if tool_state.name.is_empty() {
|
||||
"unknown".to_string()
|
||||
} else {
|
||||
tool_state.name.clone()
|
||||
},
|
||||
},
|
||||
});
|
||||
tool_state.started_emitted = true;
|
||||
}
|
||||
let arguments = canonicalize_tool_arguments(function_call.get("args").cloned());
|
||||
let delta = if arguments.starts_with(&tool_state.arguments) {
|
||||
arguments[tool_state.arguments.len()..].to_string()
|
||||
} else if tool_state.arguments == arguments {
|
||||
String::new()
|
||||
} else {
|
||||
arguments.clone()
|
||||
};
|
||||
tool_state.arguments = arguments;
|
||||
if !delta.is_empty() {
|
||||
out.push(CanonicalStreamFrame {
|
||||
id: id.clone(),
|
||||
model: model.clone(),
|
||||
event: CanonicalStreamEvent::ToolCallArgumentsDelta {
|
||||
index,
|
||||
arguments: delta,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
if let Some(finish_reason) =
|
||||
candidate_object.get("finishReason").and_then(Value::as_str)
|
||||
{
|
||||
let has_tool_calls = !self.tool_calls.is_empty();
|
||||
let mut finish_reason = normalize_openai_finish_reason(match finish_reason {
|
||||
"STOP" => Some("stop"),
|
||||
"MAX_TOKENS" => Some("length"),
|
||||
"SAFETY" => Some("content_filter"),
|
||||
other => Some(other),
|
||||
});
|
||||
if has_tool_calls && finish_reason.as_deref().is_none_or(|value| value == "stop") {
|
||||
finish_reason = Some("tool_calls".to_string());
|
||||
}
|
||||
out.push(CanonicalStreamFrame {
|
||||
id,
|
||||
model,
|
||||
event: CanonicalStreamEvent::Finish {
|
||||
finish_reason,
|
||||
usage: canonical_usage_from_gemini_usage(event_object.get("usageMetadata")),
|
||||
},
|
||||
});
|
||||
self.finished = true;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
pub(crate) fn finish(
|
||||
&mut self,
|
||||
report_context: &Value,
|
||||
) -> Result<Vec<CanonicalStreamFrame>, GatewayError> {
|
||||
if !self.started || self.finished {
|
||||
return Ok(Vec::new());
|
||||
}
|
||||
self.finished = true;
|
||||
let (id, model) = self.identity(report_context);
|
||||
Ok(vec![CanonicalStreamFrame {
|
||||
id,
|
||||
model,
|
||||
event: CanonicalStreamEvent::Finish {
|
||||
finish_reason: None,
|
||||
usage: None,
|
||||
},
|
||||
}])
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct GeminiClientToolState {
|
||||
call_id: String,
|
||||
name: String,
|
||||
arguments: String,
|
||||
emitted: bool,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub(crate) struct GeminiClientEmitter {
|
||||
response_id: Option<String>,
|
||||
model: Option<String>,
|
||||
finished: bool,
|
||||
tool_calls: BTreeMap<usize, GeminiClientToolState>,
|
||||
}
|
||||
|
||||
impl GeminiClientEmitter {
|
||||
fn update_identity(&mut self, frame: &CanonicalStreamFrame) {
|
||||
self.response_id = Some(frame.id.clone());
|
||||
self.model = Some(frame.model.clone());
|
||||
}
|
||||
|
||||
fn emit_candidate(
|
||||
&self,
|
||||
parts: Vec<Value>,
|
||||
finish_reason: Option<&str>,
|
||||
usage: Option<CanonicalUsage>,
|
||||
) -> Result<Vec<u8>, GatewayError> {
|
||||
let mut candidate = Map::new();
|
||||
candidate.insert(
|
||||
"content".to_string(),
|
||||
json!({
|
||||
"role": "model",
|
||||
"parts": parts,
|
||||
}),
|
||||
);
|
||||
candidate.insert("index".to_string(), Value::from(0_u64));
|
||||
if let Some(finish_reason) = finish_reason {
|
||||
candidate.insert(
|
||||
"finishReason".to_string(),
|
||||
Value::String(map_openai_finish_reason_to_gemini(Some(finish_reason)).to_string()),
|
||||
);
|
||||
}
|
||||
let mut response = Map::new();
|
||||
response.insert(
|
||||
"responseId".to_string(),
|
||||
Value::String(
|
||||
self.response_id
|
||||
.clone()
|
||||
.unwrap_or_else(|| "resp-local-stream".to_string()),
|
||||
),
|
||||
);
|
||||
response.insert(
|
||||
"modelVersion".to_string(),
|
||||
Value::String(self.model.clone().unwrap_or_else(|| "unknown".to_string())),
|
||||
);
|
||||
response.insert(
|
||||
"candidates".to_string(),
|
||||
Value::Array(vec![Value::Object(candidate)]),
|
||||
);
|
||||
if let Some(usage) = usage {
|
||||
response.insert(
|
||||
"usageMetadata".to_string(),
|
||||
json!({
|
||||
"promptTokenCount": usage.input_tokens,
|
||||
"candidatesTokenCount": usage.output_tokens,
|
||||
"totalTokenCount": usage.total_tokens,
|
||||
}),
|
||||
);
|
||||
}
|
||||
encode_json_sse(None, &Value::Object(response))
|
||||
}
|
||||
|
||||
fn flush_pending_tool_calls(&mut self) -> Result<Vec<u8>, GatewayError> {
|
||||
let mut out = Vec::new();
|
||||
let mut pending = Vec::new();
|
||||
for (index, tool_call) in &mut self.tool_calls {
|
||||
if tool_call.emitted {
|
||||
continue;
|
||||
}
|
||||
let args_value = parse_json_arguments_value(&tool_call.arguments)
|
||||
.unwrap_or_else(|| Value::Object(Map::new()));
|
||||
tool_call.emitted = true;
|
||||
pending.push(json!({
|
||||
"functionCall": {
|
||||
"id": if tool_call.call_id.is_empty() {
|
||||
build_generated_tool_call_id(*index)
|
||||
} else {
|
||||
tool_call.call_id.clone()
|
||||
},
|
||||
"name": if tool_call.name.is_empty() {
|
||||
"unknown".to_string()
|
||||
} else {
|
||||
tool_call.name.clone()
|
||||
},
|
||||
"args": args_value,
|
||||
}
|
||||
}));
|
||||
}
|
||||
for part in pending {
|
||||
out.extend(self.emit_candidate(vec![part], None, None)?);
|
||||
}
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
pub(crate) fn emit(&mut self, frame: CanonicalStreamFrame) -> Result<Vec<u8>, GatewayError> {
|
||||
self.update_identity(&frame);
|
||||
match frame.event {
|
||||
CanonicalStreamEvent::Start => Ok(Vec::new()),
|
||||
CanonicalStreamEvent::TextDelta(text) => {
|
||||
self.emit_candidate(vec![json!({ "text": text })], None, None)
|
||||
}
|
||||
CanonicalStreamEvent::ToolCallStart {
|
||||
index,
|
||||
call_id,
|
||||
name,
|
||||
} => {
|
||||
let state = self.tool_calls.entry(index).or_default();
|
||||
state.call_id = call_id;
|
||||
state.name = name;
|
||||
Ok(Vec::new())
|
||||
}
|
||||
CanonicalStreamEvent::ToolCallArgumentsDelta { index, arguments } => {
|
||||
let emitted_part = {
|
||||
let state = self.tool_calls.entry(index).or_default();
|
||||
state.arguments.push_str(&arguments);
|
||||
if state.emitted {
|
||||
None
|
||||
} else {
|
||||
let args_value = parse_json_arguments_value(&state.arguments);
|
||||
args_value.map(|args_value| {
|
||||
state.emitted = true;
|
||||
json!({
|
||||
"functionCall": {
|
||||
"id": if state.call_id.is_empty() {
|
||||
build_generated_tool_call_id(index)
|
||||
} else {
|
||||
state.call_id.clone()
|
||||
},
|
||||
"name": if state.name.is_empty() {
|
||||
"unknown".to_string()
|
||||
} else {
|
||||
state.name.clone()
|
||||
},
|
||||
"args": args_value,
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
};
|
||||
let Some(part) = emitted_part else {
|
||||
return Ok(Vec::new());
|
||||
};
|
||||
self.emit_candidate(vec![part], None, None)
|
||||
}
|
||||
CanonicalStreamEvent::Finish {
|
||||
finish_reason,
|
||||
usage,
|
||||
} => {
|
||||
if self.finished {
|
||||
return Ok(Vec::new());
|
||||
}
|
||||
let mut out = self.flush_pending_tool_calls()?;
|
||||
out.extend(self.emit_candidate(vec![], finish_reason.as_deref(), usage)?);
|
||||
self.finished = true;
|
||||
Ok(out)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn finish(&mut self) -> Result<Vec<u8>, GatewayError> {
|
||||
if self.finished {
|
||||
return Ok(Vec::new());
|
||||
}
|
||||
let out = self.flush_pending_tool_calls()?;
|
||||
self.finished = true;
|
||||
Ok(out)
|
||||
}
|
||||
}
|
||||
@@ -3,12 +3,13 @@ use std::collections::BTreeMap;
|
||||
|
||||
use serde_json::{json, Map, Value};
|
||||
|
||||
use crate::gateway::ai_pipeline::finalize::common::{
|
||||
use crate::ai_pipeline::finalize::common::{
|
||||
build_generated_tool_call_id, build_local_success_outcome, canonicalize_tool_arguments,
|
||||
local_finalize_allows_envelope, parse_stream_json_events, unwrap_local_finalize_response_value,
|
||||
LocalCoreSyncFinalizeOutcome,
|
||||
};
|
||||
use crate::gateway::{GatewayControlDecision, GatewayError, GatewaySyncReportRequest};
|
||||
use crate::control::GatewayControlDecision;
|
||||
use crate::{usage::GatewaySyncReportRequest, GatewayError};
|
||||
|
||||
pub(crate) fn maybe_build_local_gemini_stream_sync_response(
|
||||
trace_id: &str,
|
||||
@@ -1,15 +1,16 @@
|
||||
use base64::Engine as _;
|
||||
|
||||
use super::aggregate_gemini_stream_sync_response;
|
||||
use super::chat::aggregate_gemini_stream_sync_response;
|
||||
use serde_json::{json, Value};
|
||||
|
||||
use crate::gateway::ai_pipeline::finalize::common::{
|
||||
use crate::ai_pipeline::conversion::response::build_openai_cli_response;
|
||||
use crate::ai_pipeline::finalize::common::{
|
||||
build_generated_tool_call_id, build_local_success_outcome, canonicalize_tool_arguments,
|
||||
local_finalize_allows_envelope, unwrap_local_finalize_response_value,
|
||||
LocalCoreSyncFinalizeOutcome,
|
||||
};
|
||||
use crate::gateway::ai_pipeline::finalize::standard::build_openai_cli_response;
|
||||
use crate::gateway::{GatewayControlDecision, GatewayError, GatewaySyncReportRequest};
|
||||
use crate::control::GatewayControlDecision;
|
||||
use crate::{usage::GatewaySyncReportRequest, GatewayError};
|
||||
|
||||
pub(crate) fn maybe_build_local_gemini_cli_stream_sync_response(
|
||||
trace_id: &str,
|
||||
@@ -0,0 +1,11 @@
|
||||
mod chat;
|
||||
mod cli;
|
||||
|
||||
pub(crate) use chat::{
|
||||
aggregate_gemini_stream_sync_response, convert_gemini_chat_response_to_openai_chat,
|
||||
convert_openai_chat_response_to_gemini_chat, maybe_build_local_gemini_stream_sync_response,
|
||||
maybe_build_local_gemini_sync_response,
|
||||
};
|
||||
pub(crate) use cli::{
|
||||
convert_gemini_cli_response_to_openai_cli, maybe_build_local_gemini_cli_stream_sync_response,
|
||||
};
|
||||
@@ -1,75 +1,20 @@
|
||||
//! Standard finalize surface for standard contract sync/stream compilation.
|
||||
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use serde_json::Value;
|
||||
use serde_json::{json, Map};
|
||||
|
||||
use crate::gateway::{GatewayControlDecision, GatewayError, GatewaySyncReportRequest};
|
||||
mod claude;
|
||||
mod gemini;
|
||||
mod openai;
|
||||
#[path = "stream_core/mod.rs"]
|
||||
mod stream;
|
||||
|
||||
#[path = "claude/chat.rs"]
|
||||
mod claude_chat;
|
||||
#[path = "claude/chat_stream.rs"]
|
||||
mod claude_chat_stream;
|
||||
#[path = "claude/cli.rs"]
|
||||
mod claude_cli;
|
||||
#[path = "claude/cli_stream.rs"]
|
||||
mod claude_cli_stream;
|
||||
#[path = "gemini/chat.rs"]
|
||||
mod gemini_chat;
|
||||
#[path = "gemini/chat_stream.rs"]
|
||||
mod gemini_chat_stream;
|
||||
#[path = "gemini/cli.rs"]
|
||||
mod gemini_cli;
|
||||
#[path = "gemini/cli_stream.rs"]
|
||||
mod gemini_cli_stream;
|
||||
#[path = "openai/chat.rs"]
|
||||
mod openai_chat;
|
||||
#[path = "openai/chat_stream.rs"]
|
||||
mod openai_chat_stream;
|
||||
#[path = "openai/cli.rs"]
|
||||
mod openai_cli;
|
||||
#[path = "openai/cli_stream.rs"]
|
||||
mod openai_cli_stream;
|
||||
|
||||
#[path = "stream_common.rs"]
|
||||
mod stream_common;
|
||||
|
||||
pub(crate) use crate::gateway::ai_pipeline::conversion::response::{
|
||||
build_openai_cli_response, convert_claude_chat_response_to_openai_chat,
|
||||
convert_claude_cli_response_to_openai_cli, convert_gemini_chat_response_to_openai_chat,
|
||||
convert_gemini_cli_response_to_openai_cli, convert_openai_chat_response_to_claude_chat,
|
||||
convert_openai_chat_response_to_gemini_chat, convert_openai_chat_response_to_openai_cli,
|
||||
convert_openai_cli_response_to_openai_chat,
|
||||
pub(crate) use crate::ai_pipeline::conversion::response::{
|
||||
build_openai_cli_response, convert_openai_chat_response_to_openai_cli,
|
||||
};
|
||||
pub(crate) use claude_chat::maybe_build_local_claude_sync_response;
|
||||
pub(crate) use claude_chat_stream::{
|
||||
aggregate_claude_stream_sync_response, maybe_build_local_claude_stream_sync_response,
|
||||
};
|
||||
pub(crate) use claude_cli_stream::maybe_build_local_claude_cli_stream_sync_response;
|
||||
pub(crate) use gemini_chat::maybe_build_local_gemini_sync_response;
|
||||
pub(crate) use gemini_chat_stream::{
|
||||
aggregate_gemini_stream_sync_response, maybe_build_local_gemini_stream_sync_response,
|
||||
};
|
||||
pub(crate) use gemini_cli_stream::maybe_build_local_gemini_cli_stream_sync_response;
|
||||
pub(crate) use openai_chat::{
|
||||
aggregate_openai_chat_stream_sync_response,
|
||||
maybe_build_local_openai_chat_cross_format_stream_sync_response,
|
||||
maybe_build_local_openai_chat_cross_format_sync_response,
|
||||
maybe_build_local_openai_chat_stream_sync_response,
|
||||
maybe_build_local_openai_chat_sync_response,
|
||||
};
|
||||
pub(crate) use openai_chat_stream::{
|
||||
ClaudeToOpenAIChatStreamState, GeminiToOpenAIChatStreamState, OpenAICliToOpenAIChatStreamState,
|
||||
};
|
||||
pub(crate) use openai_cli::{
|
||||
aggregate_openai_cli_stream_sync_response,
|
||||
maybe_build_local_openai_cli_cross_format_stream_sync_response,
|
||||
maybe_build_local_openai_cli_cross_format_sync_response,
|
||||
maybe_build_local_openai_cli_stream_sync_response,
|
||||
};
|
||||
pub(crate) use openai_cli_stream::BufferedCliConversionStreamState;
|
||||
pub(crate) use stream_common::BufferedStandardConversionStreamState;
|
||||
pub(crate) use claude::*;
|
||||
pub(crate) use gemini::*;
|
||||
pub(crate) use openai::*;
|
||||
pub(crate) use stream::*;
|
||||
|
||||
pub(crate) fn aggregate_standard_chat_stream_sync_response(
|
||||
body: &[u8],
|
||||
|
||||
@@ -1,499 +0,0 @@
|
||||
use serde_json::{json, Map, Value};
|
||||
|
||||
use crate::gateway::ai_pipeline::finalize::sse::{
|
||||
encode_done_sse, encode_json_sse, map_claude_stop_reason,
|
||||
};
|
||||
use crate::gateway::GatewayError;
|
||||
|
||||
use super::{
|
||||
aggregate_claude_stream_sync_response, aggregate_gemini_stream_sync_response,
|
||||
convert_openai_cli_response_to_openai_chat,
|
||||
};
|
||||
|
||||
#[derive(Default)]
|
||||
pub(crate) struct ClaudeToOpenAIChatStreamState {
|
||||
raw: Vec<u8>,
|
||||
message_id: Option<String>,
|
||||
model: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub(crate) struct GeminiToOpenAIChatStreamState {
|
||||
raw: Vec<u8>,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub(crate) struct OpenAICliToOpenAIChatStreamState {
|
||||
raw: Vec<u8>,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct ClaudeToolCallState {
|
||||
id: String,
|
||||
name: String,
|
||||
arguments: String,
|
||||
}
|
||||
|
||||
fn canonicalize_arguments(value: Option<Value>) -> String {
|
||||
match value {
|
||||
Some(Value::String(text)) => text,
|
||||
Some(other) => serde_json::to_string(&other).unwrap_or_else(|_| "null".to_string()),
|
||||
None => "{}".to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
fn claude_tool_calls(content: &[Value]) -> Option<Vec<Value>> {
|
||||
let mut tool_calls = Vec::new();
|
||||
for (index, block) in content.iter().enumerate() {
|
||||
let Some(block) = block.as_object() else {
|
||||
continue;
|
||||
};
|
||||
if block.get("type").and_then(Value::as_str).unwrap_or("text") != "tool_use" {
|
||||
continue;
|
||||
}
|
||||
let state = ClaudeToolCallState {
|
||||
id: block
|
||||
.get("id")
|
||||
.and_then(Value::as_str)
|
||||
.filter(|value| !value.is_empty())
|
||||
.unwrap_or("tool_call")
|
||||
.to_string(),
|
||||
name: block
|
||||
.get("name")
|
||||
.and_then(Value::as_str)
|
||||
.unwrap_or("unknown")
|
||||
.to_string(),
|
||||
arguments: canonicalize_arguments(block.get("input").cloned()),
|
||||
};
|
||||
tool_calls.push(json!({
|
||||
"index": index,
|
||||
"id": state.id,
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": state.name,
|
||||
"arguments": state.arguments,
|
||||
}
|
||||
}));
|
||||
}
|
||||
if tool_calls.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(tool_calls)
|
||||
}
|
||||
}
|
||||
|
||||
fn gemini_tool_calls(parts: &[Value]) -> Option<Vec<Value>> {
|
||||
let mut tool_calls = Vec::new();
|
||||
for (index, part) in parts.iter().enumerate() {
|
||||
let Some(part) = part.as_object() else {
|
||||
continue;
|
||||
};
|
||||
let Some(function_call) = part.get("functionCall").and_then(Value::as_object) else {
|
||||
continue;
|
||||
};
|
||||
let name = function_call
|
||||
.get("name")
|
||||
.and_then(Value::as_str)
|
||||
.unwrap_or("unknown");
|
||||
let id = function_call
|
||||
.get("id")
|
||||
.and_then(Value::as_str)
|
||||
.filter(|value| !value.is_empty())
|
||||
.map(ToOwned::to_owned)
|
||||
.unwrap_or_else(|| format!("call_{name}_{index}"));
|
||||
tool_calls.push(json!({
|
||||
"index": index,
|
||||
"id": id,
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": name,
|
||||
"arguments": canonicalize_arguments(function_call.get("args").cloned()),
|
||||
}
|
||||
}));
|
||||
}
|
||||
if tool_calls.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(tool_calls)
|
||||
}
|
||||
}
|
||||
|
||||
fn build_openai_chat_chunk(
|
||||
id: &str,
|
||||
model: &str,
|
||||
text: String,
|
||||
tool_calls: Option<Vec<Value>>,
|
||||
finish_reason: Option<&str>,
|
||||
) -> Value {
|
||||
let mut delta = Map::new();
|
||||
delta.insert("role".to_string(), Value::String("assistant".to_string()));
|
||||
if !text.is_empty() {
|
||||
delta.insert("content".to_string(), Value::String(text));
|
||||
} else if tool_calls.is_none() {
|
||||
delta.insert("content".to_string(), Value::String(String::new()));
|
||||
}
|
||||
if let Some(tool_calls) = tool_calls {
|
||||
delta.insert("tool_calls".to_string(), Value::Array(tool_calls));
|
||||
}
|
||||
|
||||
json!({
|
||||
"id": id,
|
||||
"object": "chat.completion.chunk",
|
||||
"model": model,
|
||||
"choices": [{
|
||||
"index": 0,
|
||||
"delta": Value::Object(delta),
|
||||
"finish_reason": finish_reason,
|
||||
}]
|
||||
})
|
||||
}
|
||||
|
||||
fn claude_identity<'a>(
|
||||
state: &'a ClaudeToOpenAIChatStreamState,
|
||||
report_context: &'a Value,
|
||||
) -> (&'a str, &'a str) {
|
||||
let id = state
|
||||
.message_id
|
||||
.as_deref()
|
||||
.unwrap_or("chatcmpl-local-stream");
|
||||
let model = state
|
||||
.model
|
||||
.as_deref()
|
||||
.or_else(|| report_context.get("mapped_model").and_then(Value::as_str))
|
||||
.or_else(|| report_context.get("model").and_then(Value::as_str))
|
||||
.unwrap_or("unknown");
|
||||
(id, model)
|
||||
}
|
||||
|
||||
fn convert_claude_aggregated_to_openai_chunk(
|
||||
body_json: &Value,
|
||||
report_context: &Value,
|
||||
) -> Option<Value> {
|
||||
let body = body_json.as_object()?;
|
||||
let content = body.get("content")?.as_array()?;
|
||||
let mut text = String::new();
|
||||
for block in content {
|
||||
let block = block.as_object()?;
|
||||
if block.get("type").and_then(Value::as_str).unwrap_or("text") == "text" {
|
||||
if let Some(piece) = block.get("text").and_then(Value::as_str) {
|
||||
text.push_str(piece);
|
||||
}
|
||||
}
|
||||
}
|
||||
let tool_calls = claude_tool_calls(content);
|
||||
let finish_reason = map_claude_stop_reason(
|
||||
body.get("stop_reason").and_then(Value::as_str),
|
||||
tool_calls.is_some(),
|
||||
);
|
||||
let model = body
|
||||
.get("model")
|
||||
.and_then(Value::as_str)
|
||||
.or_else(|| report_context.get("mapped_model").and_then(Value::as_str))
|
||||
.or_else(|| report_context.get("model").and_then(Value::as_str))
|
||||
.unwrap_or("unknown");
|
||||
let id = body
|
||||
.get("id")
|
||||
.and_then(Value::as_str)
|
||||
.unwrap_or("chatcmpl-local-stream");
|
||||
|
||||
Some(build_openai_chat_chunk(
|
||||
id,
|
||||
model,
|
||||
text,
|
||||
tool_calls,
|
||||
finish_reason,
|
||||
))
|
||||
}
|
||||
|
||||
fn convert_gemini_aggregated_to_openai_chunk(
|
||||
body_json: &Value,
|
||||
report_context: &Value,
|
||||
) -> Option<Value> {
|
||||
let body = body_json.as_object()?;
|
||||
let candidates = body.get("candidates")?.as_array()?;
|
||||
let first_candidate = candidates.first()?.as_object()?;
|
||||
let content = first_candidate.get("content")?.as_object()?;
|
||||
let parts = content.get("parts")?.as_array()?;
|
||||
let mut text = String::new();
|
||||
for part in parts {
|
||||
let part = part.as_object()?;
|
||||
if let Some(piece) = part.get("text").and_then(Value::as_str) {
|
||||
text.push_str(piece);
|
||||
}
|
||||
}
|
||||
let tool_calls = gemini_tool_calls(parts);
|
||||
let mut finish_reason = match first_candidate.get("finishReason").and_then(Value::as_str) {
|
||||
Some("STOP") => Some("stop"),
|
||||
Some("MAX_TOKENS") => Some("length"),
|
||||
Some("SAFETY") => Some("content_filter"),
|
||||
_ => None,
|
||||
};
|
||||
if tool_calls.is_some() && finish_reason.is_none_or(|value| value == "stop") {
|
||||
finish_reason = Some("tool_calls");
|
||||
}
|
||||
let model = body
|
||||
.get("modelVersion")
|
||||
.and_then(Value::as_str)
|
||||
.or_else(|| report_context.get("mapped_model").and_then(Value::as_str))
|
||||
.or_else(|| report_context.get("model").and_then(Value::as_str))
|
||||
.unwrap_or("unknown");
|
||||
let id = body
|
||||
.get("responseId")
|
||||
.and_then(Value::as_str)
|
||||
.or_else(|| body.get("_v1internal_response_id").and_then(Value::as_str))
|
||||
.unwrap_or("chatcmpl-local-stream");
|
||||
|
||||
Some(build_openai_chat_chunk(
|
||||
id,
|
||||
model,
|
||||
text,
|
||||
tool_calls,
|
||||
finish_reason,
|
||||
))
|
||||
}
|
||||
|
||||
impl ClaudeToOpenAIChatStreamState {
|
||||
pub(crate) fn transform_line(
|
||||
&mut self,
|
||||
report_context: &Value,
|
||||
line: Vec<u8>,
|
||||
) -> Result<Vec<u8>, GatewayError> {
|
||||
self.raw.extend_from_slice(&line);
|
||||
|
||||
let Ok(text) = std::str::from_utf8(&line) else {
|
||||
return Ok(Vec::new());
|
||||
};
|
||||
let trimmed = text.trim_matches('\r').trim();
|
||||
if trimmed.is_empty() {
|
||||
if self
|
||||
.raw
|
||||
.windows(b"\"type\":\"message_stop\"".len())
|
||||
.any(|window| window == b"\"type\":\"message_stop\"")
|
||||
{
|
||||
return Ok(encode_done_sse());
|
||||
}
|
||||
return Ok(Vec::new());
|
||||
}
|
||||
let Some(data_line) = trimmed.strip_prefix("data:") else {
|
||||
return Ok(Vec::new());
|
||||
};
|
||||
let data_line = data_line.trim();
|
||||
if data_line.is_empty() || data_line == "[DONE]" {
|
||||
return Ok(Vec::new());
|
||||
}
|
||||
let value: Value = match serde_json::from_str(data_line) {
|
||||
Ok(value) => value,
|
||||
Err(_) => return Ok(Vec::new()),
|
||||
};
|
||||
match value
|
||||
.get("type")
|
||||
.and_then(Value::as_str)
|
||||
.unwrap_or_default()
|
||||
{
|
||||
"message_start" => {
|
||||
if let Some(message) = value.get("message").and_then(Value::as_object) {
|
||||
self.message_id = message
|
||||
.get("id")
|
||||
.and_then(Value::as_str)
|
||||
.map(ToOwned::to_owned);
|
||||
self.model = message
|
||||
.get("model")
|
||||
.and_then(Value::as_str)
|
||||
.map(ToOwned::to_owned);
|
||||
}
|
||||
let (id, model) = claude_identity(self, report_context);
|
||||
encode_json_sse(
|
||||
None,
|
||||
&json!({
|
||||
"id": id,
|
||||
"object": "chat.completion.chunk",
|
||||
"model": model,
|
||||
"choices": [{
|
||||
"index": 0,
|
||||
"delta": {
|
||||
"role": "assistant"
|
||||
},
|
||||
"finish_reason": Value::Null
|
||||
}]
|
||||
}),
|
||||
)
|
||||
}
|
||||
"content_block_delta" => {
|
||||
let Some(delta) = value.get("delta").and_then(Value::as_object) else {
|
||||
return Ok(Vec::new());
|
||||
};
|
||||
if delta.get("type").and_then(Value::as_str) != Some("text_delta") {
|
||||
return Ok(Vec::new());
|
||||
}
|
||||
let Some(piece) = delta.get("text").and_then(Value::as_str) else {
|
||||
return Ok(Vec::new());
|
||||
};
|
||||
let (id, model) = claude_identity(self, report_context);
|
||||
encode_json_sse(
|
||||
None,
|
||||
&build_openai_chat_chunk(id, model, piece.to_string(), None, None),
|
||||
)
|
||||
}
|
||||
"content_block_start" => {
|
||||
let Some(block) = value.get("content_block").and_then(Value::as_object) else {
|
||||
return Ok(Vec::new());
|
||||
};
|
||||
if block.get("type").and_then(Value::as_str) != Some("tool_use") {
|
||||
return Ok(Vec::new());
|
||||
}
|
||||
let call = json!({
|
||||
"index": value.get("index").and_then(Value::as_u64).unwrap_or(0),
|
||||
"id": block
|
||||
.get("id")
|
||||
.and_then(Value::as_str)
|
||||
.filter(|value| !value.is_empty())
|
||||
.unwrap_or("tool_call"),
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": block.get("name").and_then(Value::as_str).unwrap_or("unknown"),
|
||||
"arguments": canonicalize_arguments(block.get("input").cloned()),
|
||||
}
|
||||
});
|
||||
let (id, model) = claude_identity(self, report_context);
|
||||
encode_json_sse(
|
||||
None,
|
||||
&build_openai_chat_chunk(id, model, String::new(), Some(vec![call]), None),
|
||||
)
|
||||
}
|
||||
"message_delta" => {
|
||||
let Some(delta) = value.get("delta").and_then(Value::as_object) else {
|
||||
return Ok(Vec::new());
|
||||
};
|
||||
let Some(finish_reason) = map_claude_stop_reason(
|
||||
delta.get("stop_reason").and_then(Value::as_str),
|
||||
delta.get("stop_reason").and_then(Value::as_str) == Some("tool_use"),
|
||||
) else {
|
||||
return Ok(Vec::new());
|
||||
};
|
||||
let (id, model) = claude_identity(self, report_context);
|
||||
encode_json_sse(
|
||||
None,
|
||||
&json!({
|
||||
"id": id,
|
||||
"object": "chat.completion.chunk",
|
||||
"model": model,
|
||||
"choices": [{
|
||||
"index": 0,
|
||||
"delta": {},
|
||||
"finish_reason": finish_reason
|
||||
}]
|
||||
}),
|
||||
)
|
||||
}
|
||||
_ => Ok(Vec::new()),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn finish(&mut self) -> Vec<u8> {
|
||||
if self.raw.is_empty() {
|
||||
return Vec::new();
|
||||
}
|
||||
let aggregated = aggregate_claude_stream_sync_response(&self.raw);
|
||||
self.raw.clear();
|
||||
let Some(aggregated) = aggregated else {
|
||||
return Vec::new();
|
||||
};
|
||||
let Some(chunk) = convert_claude_aggregated_to_openai_chunk(&aggregated, &Value::Null)
|
||||
else {
|
||||
return Vec::new();
|
||||
};
|
||||
let mut out = encode_json_sse(None, &chunk).unwrap_or_default();
|
||||
out.extend(encode_done_sse());
|
||||
out
|
||||
}
|
||||
}
|
||||
|
||||
impl GeminiToOpenAIChatStreamState {
|
||||
pub(crate) fn transform_line(
|
||||
&mut self,
|
||||
_report_context: &Value,
|
||||
line: Vec<u8>,
|
||||
) -> Result<Vec<u8>, GatewayError> {
|
||||
self.raw.extend_from_slice(&line);
|
||||
Ok(Vec::new())
|
||||
}
|
||||
|
||||
pub(crate) fn finish(&mut self, report_context: &Value) -> Result<Vec<u8>, GatewayError> {
|
||||
if self.raw.is_empty() {
|
||||
return Ok(Vec::new());
|
||||
}
|
||||
let aggregated = aggregate_gemini_stream_sync_response(&self.raw);
|
||||
self.raw.clear();
|
||||
let Some(aggregated) = aggregated else {
|
||||
return Ok(Vec::new());
|
||||
};
|
||||
let Some(chunk) = convert_gemini_aggregated_to_openai_chunk(&aggregated, report_context)
|
||||
else {
|
||||
return Ok(Vec::new());
|
||||
};
|
||||
let mut out = encode_json_sse(None, &chunk)?;
|
||||
out.extend(encode_done_sse());
|
||||
Ok(out)
|
||||
}
|
||||
}
|
||||
|
||||
impl OpenAICliToOpenAIChatStreamState {
|
||||
pub(crate) fn transform_line(
|
||||
&mut self,
|
||||
_report_context: &Value,
|
||||
line: Vec<u8>,
|
||||
) -> Result<Vec<u8>, GatewayError> {
|
||||
self.raw.extend_from_slice(&line);
|
||||
Ok(Vec::new())
|
||||
}
|
||||
|
||||
pub(crate) fn finish(&mut self, report_context: &Value) -> Result<Vec<u8>, GatewayError> {
|
||||
if self.raw.is_empty() {
|
||||
return Ok(Vec::new());
|
||||
}
|
||||
let aggregated = crate::gateway::ai_pipeline::finalize::standard::aggregate_openai_cli_stream_sync_response(&self.raw);
|
||||
self.raw.clear();
|
||||
let Some(aggregated) = aggregated else {
|
||||
return Ok(Vec::new());
|
||||
};
|
||||
let Some(chat_response) =
|
||||
convert_openai_cli_response_to_openai_chat(&aggregated, report_context)
|
||||
else {
|
||||
return Ok(Vec::new());
|
||||
};
|
||||
let Some(chat_object) = chat_response.as_object() else {
|
||||
return Ok(Vec::new());
|
||||
};
|
||||
let Some(choice) = chat_object
|
||||
.get("choices")
|
||||
.and_then(Value::as_array)
|
||||
.and_then(|choices| choices.first())
|
||||
.and_then(Value::as_object)
|
||||
else {
|
||||
return Ok(Vec::new());
|
||||
};
|
||||
let Some(message) = choice.get("message").and_then(Value::as_object) else {
|
||||
return Ok(Vec::new());
|
||||
};
|
||||
let content = message
|
||||
.get("content")
|
||||
.and_then(Value::as_str)
|
||||
.unwrap_or_default()
|
||||
.to_string();
|
||||
let tool_calls = message.get("tool_calls").and_then(Value::as_array).cloned();
|
||||
let finish_reason = choice.get("finish_reason").and_then(Value::as_str);
|
||||
let id = chat_object
|
||||
.get("id")
|
||||
.and_then(Value::as_str)
|
||||
.unwrap_or("chatcmpl-local-stream");
|
||||
let model = chat_object
|
||||
.get("model")
|
||||
.and_then(Value::as_str)
|
||||
.unwrap_or("unknown");
|
||||
|
||||
let chunk = build_openai_chat_chunk(id, model, content, tool_calls, finish_reason);
|
||||
let mut out = encode_json_sse(None, &chunk)?;
|
||||
out.extend(encode_done_sse());
|
||||
Ok(out)
|
||||
}
|
||||
}
|
||||
@@ -1,44 +0,0 @@
|
||||
use serde_json::{json, Value};
|
||||
|
||||
use crate::gateway::ai_pipeline::finalize::sse::encode_json_sse;
|
||||
use crate::gateway::GatewayError;
|
||||
|
||||
#[derive(Default)]
|
||||
pub(crate) struct BufferedCliConversionStreamState {
|
||||
raw: Vec<u8>,
|
||||
}
|
||||
|
||||
impl BufferedCliConversionStreamState {
|
||||
pub(crate) fn transform_line(&mut self, line: Vec<u8>) -> Result<Vec<u8>, GatewayError> {
|
||||
self.raw.extend_from_slice(&line);
|
||||
Ok(Vec::new())
|
||||
}
|
||||
|
||||
pub(crate) fn finish<AggregateFn, ConvertFn>(
|
||||
&mut self,
|
||||
report_context: &Value,
|
||||
aggregate: AggregateFn,
|
||||
convert: ConvertFn,
|
||||
) -> Result<Vec<u8>, GatewayError>
|
||||
where
|
||||
AggregateFn: Fn(&[u8]) -> Option<Value>,
|
||||
ConvertFn: Fn(&Value, &Value) -> Option<Value>,
|
||||
{
|
||||
if self.raw.is_empty() {
|
||||
return Ok(Vec::new());
|
||||
}
|
||||
let aggregated = aggregate(&self.raw);
|
||||
self.raw.clear();
|
||||
let Some(aggregated) = aggregated else {
|
||||
return Ok(Vec::new());
|
||||
};
|
||||
let Some(response) = convert(&aggregated, report_context) else {
|
||||
return Ok(Vec::new());
|
||||
};
|
||||
let event = json!({
|
||||
"type": "response.completed",
|
||||
"response": response,
|
||||
});
|
||||
encode_json_sse(Some("response.completed"), &event)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
pub(super) mod stream;
|
||||
pub(super) mod sync;
|
||||
|
||||
pub(crate) use sync::{
|
||||
aggregate_openai_chat_stream_sync_response, aggregate_openai_cli_stream_sync_response,
|
||||
build_openai_cli_response, convert_openai_cli_response_to_openai_chat,
|
||||
maybe_build_local_openai_chat_cross_format_stream_sync_response,
|
||||
maybe_build_local_openai_chat_cross_format_sync_response,
|
||||
maybe_build_local_openai_chat_stream_sync_response,
|
||||
maybe_build_local_openai_chat_sync_response,
|
||||
maybe_build_local_openai_cli_cross_format_stream_sync_response,
|
||||
maybe_build_local_openai_cli_cross_format_sync_response,
|
||||
maybe_build_local_openai_cli_stream_sync_response,
|
||||
};
|
||||
@@ -0,0 +1,962 @@
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use serde_json::{json, Value};
|
||||
|
||||
use crate::ai_pipeline::conversion::response::build_openai_cli_response;
|
||||
use crate::ai_pipeline::finalize::common::{
|
||||
build_generated_tool_call_id, canonicalize_tool_arguments,
|
||||
};
|
||||
use crate::ai_pipeline::finalize::sse::{encode_done_sse, encode_json_sse};
|
||||
use crate::GatewayError;
|
||||
|
||||
use crate::ai_pipeline::finalize::standard::stream::common::*;
|
||||
|
||||
#[derive(Default)]
|
||||
struct OpenAIChatProviderToolState {
|
||||
id: Option<String>,
|
||||
name: Option<String>,
|
||||
started_emitted: bool,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub(crate) struct OpenAIChatProviderState {
|
||||
response_id: Option<String>,
|
||||
model: Option<String>,
|
||||
started: bool,
|
||||
finished: bool,
|
||||
tool_calls: BTreeMap<usize, OpenAIChatProviderToolState>,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct OpenAICliProviderToolState {
|
||||
call_id: String,
|
||||
name: String,
|
||||
arguments: String,
|
||||
started_emitted: bool,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub(crate) struct OpenAICliProviderState {
|
||||
response_id: Option<String>,
|
||||
model: Option<String>,
|
||||
started: bool,
|
||||
finished: bool,
|
||||
text: String,
|
||||
tool_calls: BTreeMap<usize, OpenAICliProviderToolState>,
|
||||
tool_index_by_key: BTreeMap<String, usize>,
|
||||
last_tool_index: Option<usize>,
|
||||
}
|
||||
|
||||
impl OpenAIChatProviderState {
|
||||
fn identity(&self, report_context: &Value) -> (String, String) {
|
||||
resolve_identity(
|
||||
self.response_id.as_deref(),
|
||||
self.model.as_deref(),
|
||||
report_context,
|
||||
"chatcmpl-local-stream",
|
||||
)
|
||||
}
|
||||
|
||||
fn ensure_started(&mut self, report_context: &Value, out: &mut Vec<CanonicalStreamFrame>) {
|
||||
if self.started {
|
||||
return;
|
||||
}
|
||||
let (id, model) = self.identity(report_context);
|
||||
out.push(CanonicalStreamFrame {
|
||||
id,
|
||||
model,
|
||||
event: CanonicalStreamEvent::Start,
|
||||
});
|
||||
self.started = true;
|
||||
}
|
||||
|
||||
pub(crate) fn push_line(
|
||||
&mut self,
|
||||
report_context: &Value,
|
||||
line: Vec<u8>,
|
||||
) -> Result<Vec<CanonicalStreamFrame>, GatewayError> {
|
||||
let Some(value) = decode_json_data_line(&line) else {
|
||||
return Ok(Vec::new());
|
||||
};
|
||||
let Some(chunk_object) = value.as_object() else {
|
||||
return Ok(Vec::new());
|
||||
};
|
||||
self.response_id = chunk_object
|
||||
.get("id")
|
||||
.and_then(Value::as_str)
|
||||
.map(ToOwned::to_owned)
|
||||
.or_else(|| self.response_id.clone());
|
||||
self.model = chunk_object
|
||||
.get("model")
|
||||
.and_then(Value::as_str)
|
||||
.map(ToOwned::to_owned)
|
||||
.or_else(|| self.model.clone());
|
||||
|
||||
let mut out = Vec::new();
|
||||
let Some(chunk_choices) = chunk_object.get("choices").and_then(Value::as_array) else {
|
||||
return Ok(out);
|
||||
};
|
||||
for chunk_choice in chunk_choices {
|
||||
let Some(choice_object) = chunk_choice.as_object() else {
|
||||
continue;
|
||||
};
|
||||
let Some(delta) = choice_object.get("delta").and_then(Value::as_object) else {
|
||||
if let Some(finish_reason) = normalize_openai_finish_reason(
|
||||
choice_object.get("finish_reason").and_then(Value::as_str),
|
||||
) {
|
||||
self.ensure_started(report_context, &mut out);
|
||||
let (id, model) = self.identity(report_context);
|
||||
out.push(CanonicalStreamFrame {
|
||||
id,
|
||||
model,
|
||||
event: CanonicalStreamEvent::Finish {
|
||||
finish_reason: Some(finish_reason),
|
||||
usage: canonical_usage_from_openai_usage(chunk_object.get("usage")),
|
||||
},
|
||||
});
|
||||
self.finished = true;
|
||||
}
|
||||
continue;
|
||||
};
|
||||
|
||||
if delta.get("role").and_then(Value::as_str) == Some("assistant") {
|
||||
self.ensure_started(report_context, &mut out);
|
||||
}
|
||||
|
||||
if let Some(content) = delta.get("content").and_then(Value::as_str) {
|
||||
if !content.is_empty() {
|
||||
self.ensure_started(report_context, &mut out);
|
||||
let (id, model) = self.identity(report_context);
|
||||
out.push(CanonicalStreamFrame {
|
||||
id,
|
||||
model,
|
||||
event: CanonicalStreamEvent::TextDelta(content.to_string()),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(tool_calls) = delta.get("tool_calls").and_then(Value::as_array) {
|
||||
self.ensure_started(report_context, &mut out);
|
||||
let (id, model) = self.identity(report_context);
|
||||
for tool_call in tool_calls {
|
||||
let Some(tool_call_object) = tool_call.as_object() else {
|
||||
continue;
|
||||
};
|
||||
let index = tool_call_object
|
||||
.get("index")
|
||||
.and_then(Value::as_u64)
|
||||
.map(|value| value as usize)
|
||||
.unwrap_or(0);
|
||||
let state = self.tool_calls.entry(index).or_default();
|
||||
if let Some(call_id) = tool_call_object.get("id").and_then(Value::as_str) {
|
||||
state.id = Some(call_id.to_string());
|
||||
}
|
||||
if let Some(function) =
|
||||
tool_call_object.get("function").and_then(Value::as_object)
|
||||
{
|
||||
if let Some(name) = function.get("name").and_then(Value::as_str) {
|
||||
state.name = Some(name.to_string());
|
||||
}
|
||||
if !state.started_emitted && (state.id.is_some() || state.name.is_some()) {
|
||||
out.push(CanonicalStreamFrame {
|
||||
id: id.clone(),
|
||||
model: model.clone(),
|
||||
event: CanonicalStreamEvent::ToolCallStart {
|
||||
index,
|
||||
call_id: state
|
||||
.id
|
||||
.clone()
|
||||
.unwrap_or_else(|| build_generated_tool_call_id(index)),
|
||||
name: state
|
||||
.name
|
||||
.clone()
|
||||
.unwrap_or_else(|| "unknown".to_string()),
|
||||
},
|
||||
});
|
||||
state.started_emitted = true;
|
||||
}
|
||||
if let Some(arguments) = function.get("arguments").and_then(Value::as_str) {
|
||||
if !arguments.is_empty() {
|
||||
if !state.started_emitted {
|
||||
out.push(CanonicalStreamFrame {
|
||||
id: id.clone(),
|
||||
model: model.clone(),
|
||||
event: CanonicalStreamEvent::ToolCallStart {
|
||||
index,
|
||||
call_id: state.id.clone().unwrap_or_else(|| {
|
||||
build_generated_tool_call_id(index)
|
||||
}),
|
||||
name: state
|
||||
.name
|
||||
.clone()
|
||||
.unwrap_or_else(|| "unknown".to_string()),
|
||||
},
|
||||
});
|
||||
state.started_emitted = true;
|
||||
}
|
||||
out.push(CanonicalStreamFrame {
|
||||
id: id.clone(),
|
||||
model: model.clone(),
|
||||
event: CanonicalStreamEvent::ToolCallArgumentsDelta {
|
||||
index,
|
||||
arguments: arguments.to_string(),
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(finish_reason) = normalize_openai_finish_reason(
|
||||
choice_object.get("finish_reason").and_then(Value::as_str),
|
||||
) {
|
||||
self.ensure_started(report_context, &mut out);
|
||||
let (id, model) = self.identity(report_context);
|
||||
out.push(CanonicalStreamFrame {
|
||||
id,
|
||||
model,
|
||||
event: CanonicalStreamEvent::Finish {
|
||||
finish_reason: Some(finish_reason),
|
||||
usage: canonical_usage_from_openai_usage(chunk_object.get("usage")),
|
||||
},
|
||||
});
|
||||
self.finished = true;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
pub(crate) fn finish(
|
||||
&mut self,
|
||||
report_context: &Value,
|
||||
) -> Result<Vec<CanonicalStreamFrame>, GatewayError> {
|
||||
if !self.started || self.finished {
|
||||
return Ok(Vec::new());
|
||||
}
|
||||
self.finished = true;
|
||||
let (id, model) = self.identity(report_context);
|
||||
Ok(vec![CanonicalStreamFrame {
|
||||
id,
|
||||
model,
|
||||
event: CanonicalStreamEvent::Finish {
|
||||
finish_reason: None,
|
||||
usage: None,
|
||||
},
|
||||
}])
|
||||
}
|
||||
}
|
||||
|
||||
impl OpenAICliProviderState {
|
||||
fn identity(&self, report_context: &Value) -> (String, String) {
|
||||
resolve_identity(
|
||||
self.response_id.as_deref(),
|
||||
self.model.as_deref(),
|
||||
report_context,
|
||||
"resp-local-stream",
|
||||
)
|
||||
}
|
||||
|
||||
fn ensure_started(&mut self, report_context: &Value, out: &mut Vec<CanonicalStreamFrame>) {
|
||||
if self.started {
|
||||
return;
|
||||
}
|
||||
let (id, model) = self.identity(report_context);
|
||||
out.push(CanonicalStreamFrame {
|
||||
id,
|
||||
model,
|
||||
event: CanonicalStreamEvent::Start,
|
||||
});
|
||||
self.started = true;
|
||||
}
|
||||
|
||||
fn tool_index_for_key(&mut self, key: Option<String>, output_index: Option<usize>) -> usize {
|
||||
if let Some(output_index) = output_index {
|
||||
if let Some(key) = key.as_ref() {
|
||||
self.tool_index_by_key
|
||||
.entry(key.clone())
|
||||
.or_insert(output_index);
|
||||
}
|
||||
self.last_tool_index = Some(output_index);
|
||||
return output_index;
|
||||
}
|
||||
if let Some(key) = key.as_ref() {
|
||||
if let Some(index) = self.tool_index_by_key.get(key).copied() {
|
||||
self.last_tool_index = Some(index);
|
||||
return index;
|
||||
}
|
||||
}
|
||||
let index = self.last_tool_index.unwrap_or(self.tool_calls.len());
|
||||
if let Some(key) = key {
|
||||
self.tool_index_by_key.insert(key, index);
|
||||
}
|
||||
self.last_tool_index = Some(index);
|
||||
index
|
||||
}
|
||||
|
||||
pub(crate) fn push_line(
|
||||
&mut self,
|
||||
report_context: &Value,
|
||||
line: Vec<u8>,
|
||||
) -> Result<Vec<CanonicalStreamFrame>, GatewayError> {
|
||||
let Some(value) = decode_json_data_line(&line) else {
|
||||
return Ok(Vec::new());
|
||||
};
|
||||
let mut out = Vec::new();
|
||||
if let Some(response) = value.get("response").and_then(Value::as_object) {
|
||||
self.response_id = response
|
||||
.get("id")
|
||||
.and_then(Value::as_str)
|
||||
.map(ToOwned::to_owned)
|
||||
.or_else(|| self.response_id.clone());
|
||||
self.model = response
|
||||
.get("model")
|
||||
.and_then(Value::as_str)
|
||||
.map(ToOwned::to_owned)
|
||||
.or_else(|| self.model.clone());
|
||||
}
|
||||
|
||||
match value
|
||||
.get("type")
|
||||
.and_then(Value::as_str)
|
||||
.unwrap_or_default()
|
||||
{
|
||||
"response.created" => {
|
||||
self.ensure_started(report_context, &mut out);
|
||||
}
|
||||
"response.output_text.delta" => {
|
||||
let piece = match value.get("delta") {
|
||||
Some(Value::String(text)) => text.clone(),
|
||||
Some(Value::Object(delta)) => delta
|
||||
.get("text")
|
||||
.and_then(Value::as_str)
|
||||
.unwrap_or_default()
|
||||
.to_string(),
|
||||
_ => String::new(),
|
||||
};
|
||||
if !piece.is_empty() {
|
||||
self.ensure_started(report_context, &mut out);
|
||||
self.text.push_str(&piece);
|
||||
let (id, model) = self.identity(report_context);
|
||||
out.push(CanonicalStreamFrame {
|
||||
id,
|
||||
model,
|
||||
event: CanonicalStreamEvent::TextDelta(piece),
|
||||
});
|
||||
}
|
||||
}
|
||||
"response.output_item.added" => {
|
||||
let Some(item) = value.get("item").and_then(Value::as_object) else {
|
||||
return Ok(out);
|
||||
};
|
||||
if item.get("type").and_then(Value::as_str) != Some("function_call") {
|
||||
return Ok(out);
|
||||
}
|
||||
self.ensure_started(report_context, &mut out);
|
||||
let key = item
|
||||
.get("call_id")
|
||||
.or_else(|| item.get("id"))
|
||||
.and_then(Value::as_str)
|
||||
.map(ToOwned::to_owned);
|
||||
let output_index = value
|
||||
.get("output_index")
|
||||
.and_then(Value::as_u64)
|
||||
.map(|value| value as usize);
|
||||
let index = self.tool_index_for_key(key.clone(), output_index);
|
||||
let (id, model) = self.identity(report_context);
|
||||
let state = self.tool_calls.entry(index).or_default();
|
||||
state.call_id = item
|
||||
.get("call_id")
|
||||
.or_else(|| item.get("id"))
|
||||
.and_then(Value::as_str)
|
||||
.unwrap_or_else(|| state.call_id.as_str())
|
||||
.to_string();
|
||||
state.name = item
|
||||
.get("name")
|
||||
.and_then(Value::as_str)
|
||||
.unwrap_or_else(|| state.name.as_str())
|
||||
.to_string();
|
||||
if !state.started_emitted {
|
||||
out.push(CanonicalStreamFrame {
|
||||
id: id.clone(),
|
||||
model: model.clone(),
|
||||
event: CanonicalStreamEvent::ToolCallStart {
|
||||
index,
|
||||
call_id: if state.call_id.is_empty() {
|
||||
build_generated_tool_call_id(index)
|
||||
} else {
|
||||
state.call_id.clone()
|
||||
},
|
||||
name: if state.name.is_empty() {
|
||||
"unknown".to_string()
|
||||
} else {
|
||||
state.name.clone()
|
||||
},
|
||||
},
|
||||
});
|
||||
state.started_emitted = true;
|
||||
}
|
||||
if let Some(arguments) = item.get("arguments").and_then(Value::as_str) {
|
||||
if !arguments.is_empty() {
|
||||
state.arguments.push_str(arguments);
|
||||
out.push(CanonicalStreamFrame {
|
||||
id,
|
||||
model,
|
||||
event: CanonicalStreamEvent::ToolCallArgumentsDelta {
|
||||
index,
|
||||
arguments: arguments.to_string(),
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
"response.function_call_arguments.delta" => {
|
||||
let delta = value
|
||||
.get("delta")
|
||||
.and_then(Value::as_str)
|
||||
.unwrap_or_default();
|
||||
if delta.is_empty() {
|
||||
return Ok(out);
|
||||
}
|
||||
self.ensure_started(report_context, &mut out);
|
||||
let key = value
|
||||
.get("item_id")
|
||||
.or_else(|| value.get("call_id"))
|
||||
.or_else(|| value.get("id"))
|
||||
.and_then(Value::as_str)
|
||||
.map(ToOwned::to_owned);
|
||||
let output_index = value
|
||||
.get("output_index")
|
||||
.and_then(Value::as_u64)
|
||||
.map(|value| value as usize);
|
||||
let index = self.tool_index_for_key(key, output_index);
|
||||
let (id, model) = self.identity(report_context);
|
||||
let state = self.tool_calls.entry(index).or_default();
|
||||
if !state.started_emitted {
|
||||
out.push(CanonicalStreamFrame {
|
||||
id: id.clone(),
|
||||
model: model.clone(),
|
||||
event: CanonicalStreamEvent::ToolCallStart {
|
||||
index,
|
||||
call_id: if state.call_id.is_empty() {
|
||||
build_generated_tool_call_id(index)
|
||||
} else {
|
||||
state.call_id.clone()
|
||||
},
|
||||
name: if state.name.is_empty() {
|
||||
"unknown".to_string()
|
||||
} else {
|
||||
state.name.clone()
|
||||
},
|
||||
},
|
||||
});
|
||||
state.started_emitted = true;
|
||||
}
|
||||
state.arguments.push_str(delta);
|
||||
out.push(CanonicalStreamFrame {
|
||||
id,
|
||||
model,
|
||||
event: CanonicalStreamEvent::ToolCallArgumentsDelta {
|
||||
index,
|
||||
arguments: delta.to_string(),
|
||||
},
|
||||
});
|
||||
}
|
||||
"response.completed" => {
|
||||
let Some(response) = value.get("response").and_then(Value::as_object) else {
|
||||
return Ok(out);
|
||||
};
|
||||
self.ensure_started(report_context, &mut out);
|
||||
let (id, model) = self.identity(report_context);
|
||||
|
||||
for raw_item in response
|
||||
.get("output")
|
||||
.and_then(Value::as_array)
|
||||
.into_iter()
|
||||
.flatten()
|
||||
{
|
||||
let Some(item) = raw_item.as_object() else {
|
||||
continue;
|
||||
};
|
||||
match item.get("type").and_then(Value::as_str).unwrap_or_default() {
|
||||
"message" => {
|
||||
let mut completed_text = String::new();
|
||||
for raw_content in item
|
||||
.get("content")
|
||||
.and_then(Value::as_array)
|
||||
.into_iter()
|
||||
.flatten()
|
||||
{
|
||||
let Some(content) = raw_content.as_object() else {
|
||||
continue;
|
||||
};
|
||||
if content.get("type").and_then(Value::as_str)
|
||||
== Some("output_text")
|
||||
{
|
||||
if let Some(text) = content.get("text").and_then(Value::as_str)
|
||||
{
|
||||
completed_text.push_str(text);
|
||||
}
|
||||
}
|
||||
}
|
||||
let missing = if completed_text.starts_with(&self.text) {
|
||||
completed_text[self.text.len()..].to_string()
|
||||
} else if self.text == completed_text {
|
||||
String::new()
|
||||
} else {
|
||||
completed_text.clone()
|
||||
};
|
||||
if !missing.is_empty() {
|
||||
self.text.push_str(&missing);
|
||||
out.push(CanonicalStreamFrame {
|
||||
id: id.clone(),
|
||||
model: model.clone(),
|
||||
event: CanonicalStreamEvent::TextDelta(missing),
|
||||
});
|
||||
}
|
||||
}
|
||||
"function_call" => {
|
||||
let key = item
|
||||
.get("call_id")
|
||||
.or_else(|| item.get("id"))
|
||||
.and_then(Value::as_str)
|
||||
.map(ToOwned::to_owned);
|
||||
let index = self.tool_index_for_key(key, None);
|
||||
let state = self.tool_calls.entry(index).or_default();
|
||||
state.call_id = item
|
||||
.get("call_id")
|
||||
.or_else(|| item.get("id"))
|
||||
.and_then(Value::as_str)
|
||||
.unwrap_or_else(|| state.call_id.as_str())
|
||||
.to_string();
|
||||
state.name = item
|
||||
.get("name")
|
||||
.and_then(Value::as_str)
|
||||
.unwrap_or_else(|| state.name.as_str())
|
||||
.to_string();
|
||||
if !state.started_emitted {
|
||||
out.push(CanonicalStreamFrame {
|
||||
id: id.clone(),
|
||||
model: model.clone(),
|
||||
event: CanonicalStreamEvent::ToolCallStart {
|
||||
index,
|
||||
call_id: if state.call_id.is_empty() {
|
||||
build_generated_tool_call_id(index)
|
||||
} else {
|
||||
state.call_id.clone()
|
||||
},
|
||||
name: if state.name.is_empty() {
|
||||
"unknown".to_string()
|
||||
} else {
|
||||
state.name.clone()
|
||||
},
|
||||
},
|
||||
});
|
||||
state.started_emitted = true;
|
||||
}
|
||||
let completed_arguments = item
|
||||
.get("arguments")
|
||||
.and_then(Value::as_str)
|
||||
.unwrap_or_default()
|
||||
.to_string();
|
||||
let missing = if completed_arguments.starts_with(&state.arguments) {
|
||||
completed_arguments[state.arguments.len()..].to_string()
|
||||
} else if state.arguments == completed_arguments {
|
||||
String::new()
|
||||
} else {
|
||||
completed_arguments.clone()
|
||||
};
|
||||
if !missing.is_empty() {
|
||||
state.arguments.push_str(&missing);
|
||||
out.push(CanonicalStreamFrame {
|
||||
id: id.clone(),
|
||||
model: model.clone(),
|
||||
event: CanonicalStreamEvent::ToolCallArgumentsDelta {
|
||||
index,
|
||||
arguments: missing,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
let finish_reason = if self.tool_calls.is_empty() {
|
||||
Some("stop".to_string())
|
||||
} else {
|
||||
Some("tool_calls".to_string())
|
||||
};
|
||||
out.push(CanonicalStreamFrame {
|
||||
id,
|
||||
model,
|
||||
event: CanonicalStreamEvent::Finish {
|
||||
finish_reason,
|
||||
usage: canonical_usage_from_openai_usage(response.get("usage")),
|
||||
},
|
||||
});
|
||||
self.finished = true;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
pub(crate) fn finish(
|
||||
&mut self,
|
||||
report_context: &Value,
|
||||
) -> Result<Vec<CanonicalStreamFrame>, GatewayError> {
|
||||
if !self.started || self.finished {
|
||||
return Ok(Vec::new());
|
||||
}
|
||||
self.finished = true;
|
||||
let (id, model) = self.identity(report_context);
|
||||
let finish_reason = if self.tool_calls.is_empty() {
|
||||
Some("stop".to_string())
|
||||
} else {
|
||||
Some("tool_calls".to_string())
|
||||
};
|
||||
Ok(vec![CanonicalStreamFrame {
|
||||
id,
|
||||
model,
|
||||
event: CanonicalStreamEvent::Finish {
|
||||
finish_reason,
|
||||
usage: None,
|
||||
},
|
||||
}])
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub(crate) struct OpenAIChatClientEmitter {
|
||||
response_id: Option<String>,
|
||||
model: Option<String>,
|
||||
started: bool,
|
||||
finished: bool,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct OpenAICliClientToolState {
|
||||
call_id: String,
|
||||
name: String,
|
||||
arguments: String,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub(crate) struct OpenAICliClientEmitter {
|
||||
response_id: Option<String>,
|
||||
model: Option<String>,
|
||||
started: bool,
|
||||
finished: bool,
|
||||
text: String,
|
||||
tool_calls: BTreeMap<usize, OpenAICliClientToolState>,
|
||||
}
|
||||
|
||||
impl OpenAIChatClientEmitter {
|
||||
fn update_identity(&mut self, frame: &CanonicalStreamFrame) {
|
||||
self.response_id = Some(frame.id.clone());
|
||||
self.model = Some(frame.model.clone());
|
||||
}
|
||||
|
||||
fn ensure_started(&mut self) -> Result<Vec<u8>, GatewayError> {
|
||||
if self.started {
|
||||
return Ok(Vec::new());
|
||||
}
|
||||
self.started = true;
|
||||
Ok(encode_json_sse(
|
||||
None,
|
||||
&build_openai_chat_role_chunk(
|
||||
self.response_id
|
||||
.as_deref()
|
||||
.unwrap_or("chatcmpl-local-stream"),
|
||||
self.model.as_deref().unwrap_or("unknown"),
|
||||
),
|
||||
)?)
|
||||
}
|
||||
|
||||
pub(crate) fn emit(&mut self, frame: CanonicalStreamFrame) -> Result<Vec<u8>, GatewayError> {
|
||||
self.update_identity(&frame);
|
||||
match frame.event {
|
||||
CanonicalStreamEvent::Start => self.ensure_started(),
|
||||
CanonicalStreamEvent::TextDelta(text) => {
|
||||
let mut out = self.ensure_started()?;
|
||||
out.extend(encode_json_sse(
|
||||
None,
|
||||
&build_openai_chat_chunk(
|
||||
self.response_id
|
||||
.as_deref()
|
||||
.unwrap_or("chatcmpl-local-stream"),
|
||||
self.model.as_deref().unwrap_or("unknown"),
|
||||
text,
|
||||
None,
|
||||
None,
|
||||
),
|
||||
)?);
|
||||
Ok(out)
|
||||
}
|
||||
CanonicalStreamEvent::ToolCallStart {
|
||||
index,
|
||||
call_id,
|
||||
name,
|
||||
} => {
|
||||
let mut out = self.ensure_started()?;
|
||||
out.extend(encode_json_sse(
|
||||
None,
|
||||
&build_openai_chat_chunk(
|
||||
self.response_id
|
||||
.as_deref()
|
||||
.unwrap_or("chatcmpl-local-stream"),
|
||||
self.model.as_deref().unwrap_or("unknown"),
|
||||
String::new(),
|
||||
Some(vec![json!({
|
||||
"index": index,
|
||||
"id": call_id,
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": name,
|
||||
"arguments": "",
|
||||
}
|
||||
})]),
|
||||
None,
|
||||
),
|
||||
)?);
|
||||
Ok(out)
|
||||
}
|
||||
CanonicalStreamEvent::ToolCallArgumentsDelta { index, arguments } => {
|
||||
let mut out = self.ensure_started()?;
|
||||
out.extend(encode_json_sse(
|
||||
None,
|
||||
&json!({
|
||||
"id": self.response_id
|
||||
.as_deref()
|
||||
.unwrap_or("chatcmpl-local-stream"),
|
||||
"object": "chat.completion.chunk",
|
||||
"model": self.model.as_deref().unwrap_or("unknown"),
|
||||
"choices": [{
|
||||
"index": 0,
|
||||
"delta": {
|
||||
"tool_calls": [{
|
||||
"index": index,
|
||||
"function": {
|
||||
"arguments": arguments,
|
||||
}
|
||||
}]
|
||||
},
|
||||
"finish_reason": Value::Null
|
||||
}]
|
||||
}),
|
||||
)?);
|
||||
Ok(out)
|
||||
}
|
||||
CanonicalStreamEvent::Finish { finish_reason, .. } => {
|
||||
if self.finished {
|
||||
return Ok(Vec::new());
|
||||
}
|
||||
let mut out = self.ensure_started()?;
|
||||
out.extend(encode_json_sse(
|
||||
None,
|
||||
&build_openai_chat_finish_chunk(
|
||||
self.response_id
|
||||
.as_deref()
|
||||
.unwrap_or("chatcmpl-local-stream"),
|
||||
self.model.as_deref().unwrap_or("unknown"),
|
||||
finish_reason.as_deref(),
|
||||
),
|
||||
)?);
|
||||
out.extend(encode_done_sse());
|
||||
self.finished = true;
|
||||
Ok(out)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn finish(&mut self) -> Result<Vec<u8>, GatewayError> {
|
||||
if !self.started || self.finished {
|
||||
return Ok(Vec::new());
|
||||
}
|
||||
let out = encode_json_sse(
|
||||
None,
|
||||
&build_openai_chat_finish_chunk(
|
||||
self.response_id
|
||||
.as_deref()
|
||||
.unwrap_or("chatcmpl-local-stream"),
|
||||
self.model.as_deref().unwrap_or("unknown"),
|
||||
None,
|
||||
),
|
||||
)?;
|
||||
self.finished = true;
|
||||
let mut bytes = out;
|
||||
bytes.extend(encode_done_sse());
|
||||
Ok(bytes)
|
||||
}
|
||||
}
|
||||
|
||||
impl OpenAICliClientEmitter {
|
||||
fn update_identity(&mut self, frame: &CanonicalStreamFrame) {
|
||||
self.response_id = Some(frame.id.clone().replace("chatcmpl", "resp"));
|
||||
self.model = Some(frame.model.clone());
|
||||
}
|
||||
|
||||
fn ensure_started(&mut self) -> Result<Vec<u8>, GatewayError> {
|
||||
if self.started {
|
||||
return Ok(Vec::new());
|
||||
}
|
||||
self.started = true;
|
||||
encode_json_sse(
|
||||
Some("response.created"),
|
||||
&json!({
|
||||
"type": "response.created",
|
||||
"response": {
|
||||
"id": self.response_id.as_deref().unwrap_or("resp-local-stream"),
|
||||
"object": "response",
|
||||
"model": self.model.as_deref().unwrap_or("unknown"),
|
||||
"status": "in_progress",
|
||||
"output": [],
|
||||
}
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
fn function_output_index(&self, index: usize) -> usize {
|
||||
if self.text.is_empty() {
|
||||
index
|
||||
} else {
|
||||
index + 1
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn emit(&mut self, frame: CanonicalStreamFrame) -> Result<Vec<u8>, GatewayError> {
|
||||
self.update_identity(&frame);
|
||||
match frame.event {
|
||||
CanonicalStreamEvent::Start => self.ensure_started(),
|
||||
CanonicalStreamEvent::TextDelta(text) => {
|
||||
let mut out = self.ensure_started()?;
|
||||
self.text.push_str(&text);
|
||||
out.extend(encode_json_sse(
|
||||
Some("response.output_text.delta"),
|
||||
&json!({
|
||||
"type": "response.output_text.delta",
|
||||
"output_index": 0,
|
||||
"content_index": 0,
|
||||
"delta": text,
|
||||
}),
|
||||
)?);
|
||||
Ok(out)
|
||||
}
|
||||
CanonicalStreamEvent::ToolCallStart {
|
||||
index,
|
||||
call_id,
|
||||
name,
|
||||
} => {
|
||||
let mut out = self.ensure_started()?;
|
||||
let output_index = self.function_output_index(index);
|
||||
let state = self.tool_calls.entry(index).or_default();
|
||||
state.call_id = call_id.clone();
|
||||
state.name = name.clone();
|
||||
out.extend(encode_json_sse(
|
||||
Some("response.output_item.added"),
|
||||
&json!({
|
||||
"type": "response.output_item.added",
|
||||
"output_index": output_index,
|
||||
"item": {
|
||||
"type": "function_call",
|
||||
"id": call_id,
|
||||
"call_id": state.call_id,
|
||||
"name": state.name,
|
||||
"arguments": "",
|
||||
}
|
||||
}),
|
||||
)?);
|
||||
Ok(out)
|
||||
}
|
||||
CanonicalStreamEvent::ToolCallArgumentsDelta { index, arguments } => {
|
||||
let mut out = self.ensure_started()?;
|
||||
let output_index = self.function_output_index(index);
|
||||
let state = self.tool_calls.entry(index).or_default();
|
||||
state.arguments.push_str(&arguments);
|
||||
out.extend(encode_json_sse(
|
||||
Some("response.function_call_arguments.delta"),
|
||||
&json!({
|
||||
"type": "response.function_call_arguments.delta",
|
||||
"output_index": output_index,
|
||||
"item_id": if state.call_id.is_empty() {
|
||||
build_generated_tool_call_id(index)
|
||||
} else {
|
||||
state.call_id.clone()
|
||||
},
|
||||
"delta": arguments,
|
||||
}),
|
||||
)?);
|
||||
Ok(out)
|
||||
}
|
||||
CanonicalStreamEvent::Finish { usage, .. } => {
|
||||
if self.finished {
|
||||
return Ok(Vec::new());
|
||||
}
|
||||
let mut out = self.ensure_started()?;
|
||||
let usage = usage.unwrap_or_default();
|
||||
let function_calls = self
|
||||
.tool_calls
|
||||
.iter()
|
||||
.map(|(index, state)| {
|
||||
json!({
|
||||
"type": "function_call",
|
||||
"id": if state.call_id.is_empty() {
|
||||
build_generated_tool_call_id(*index)
|
||||
} else {
|
||||
state.call_id.clone()
|
||||
},
|
||||
"call_id": if state.call_id.is_empty() {
|
||||
build_generated_tool_call_id(*index)
|
||||
} else {
|
||||
state.call_id.clone()
|
||||
},
|
||||
"name": if state.name.is_empty() {
|
||||
"unknown".to_string()
|
||||
} else {
|
||||
state.name.clone()
|
||||
},
|
||||
"arguments": state.arguments.clone(),
|
||||
})
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
out.extend(encode_json_sse(
|
||||
Some("response.completed"),
|
||||
&json!({
|
||||
"type": "response.completed",
|
||||
"response": build_openai_cli_response(
|
||||
self.response_id.as_deref().unwrap_or("resp-local-stream"),
|
||||
self.model.as_deref().unwrap_or("unknown"),
|
||||
&self.text,
|
||||
function_calls,
|
||||
usage.input_tokens,
|
||||
usage.output_tokens,
|
||||
usage.total_tokens,
|
||||
),
|
||||
}),
|
||||
)?);
|
||||
self.finished = true;
|
||||
Ok(out)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn finish(&mut self) -> Result<Vec<u8>, GatewayError> {
|
||||
if !self.started || self.finished {
|
||||
return Ok(Vec::new());
|
||||
}
|
||||
self.emit(CanonicalStreamFrame {
|
||||
id: self
|
||||
.response_id
|
||||
.clone()
|
||||
.unwrap_or_else(|| "resp-local-stream".to_string()),
|
||||
model: self.model.clone().unwrap_or_else(|| "unknown".to_string()),
|
||||
event: CanonicalStreamEvent::Finish {
|
||||
finish_reason: None,
|
||||
usage: None,
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -3,19 +3,21 @@ use std::collections::BTreeMap;
|
||||
|
||||
use serde_json::{json, Map, Value};
|
||||
|
||||
use super::{
|
||||
aggregate_claude_stream_sync_response, aggregate_gemini_stream_sync_response,
|
||||
aggregate_openai_cli_stream_sync_response, convert_claude_chat_response_to_openai_chat,
|
||||
convert_gemini_chat_response_to_openai_chat,
|
||||
use super::cli::aggregate_openai_cli_stream_sync_response;
|
||||
use crate::ai_pipeline::conversion::response::{
|
||||
convert_claude_chat_response_to_openai_chat, convert_gemini_chat_response_to_openai_chat,
|
||||
};
|
||||
use crate::gateway::ai_pipeline::conversion::sync_chat_response_conversion_kind;
|
||||
use crate::gateway::ai_pipeline::finalize::common::{
|
||||
use crate::ai_pipeline::conversion::sync_chat_response_conversion_kind;
|
||||
use crate::ai_pipeline::finalize::common::{
|
||||
build_generated_tool_call_id, build_local_success_outcome,
|
||||
build_local_success_outcome_with_conversion_report, canonicalize_tool_arguments,
|
||||
local_finalize_allows_envelope, unwrap_local_finalize_response_value,
|
||||
LocalCoreSyncFinalizeOutcome,
|
||||
};
|
||||
use crate::gateway::{GatewayControlDecision, GatewayError, GatewaySyncReportRequest};
|
||||
use crate::ai_pipeline::finalize::standard::claude::aggregate_claude_stream_sync_response;
|
||||
use crate::ai_pipeline::finalize::standard::gemini::aggregate_gemini_stream_sync_response;
|
||||
use crate::control::GatewayControlDecision;
|
||||
use crate::{usage::GatewaySyncReportRequest, GatewayError};
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
struct OpenAIChatChoiceState {
|
||||
@@ -2,17 +2,19 @@ use base64::Engine as _;
|
||||
|
||||
use serde_json::{json, Value};
|
||||
|
||||
use super::{
|
||||
aggregate_claude_stream_sync_response, aggregate_gemini_stream_sync_response,
|
||||
use crate::ai_pipeline::conversion::response::{
|
||||
convert_claude_cli_response_to_openai_cli, convert_gemini_cli_response_to_openai_cli,
|
||||
};
|
||||
use crate::gateway::ai_pipeline::conversion::sync_cli_response_conversion_kind;
|
||||
use crate::gateway::ai_pipeline::finalize::common::{
|
||||
use crate::ai_pipeline::conversion::sync_cli_response_conversion_kind;
|
||||
use crate::ai_pipeline::finalize::common::{
|
||||
build_local_success_outcome, build_local_success_outcome_with_conversion_report,
|
||||
canonicalize_tool_arguments, local_finalize_allows_envelope,
|
||||
unwrap_local_finalize_response_value, LocalCoreSyncFinalizeOutcome,
|
||||
};
|
||||
use crate::gateway::{GatewayControlDecision, GatewayError, GatewaySyncReportRequest};
|
||||
use crate::ai_pipeline::finalize::standard::claude::aggregate_claude_stream_sync_response;
|
||||
use crate::ai_pipeline::finalize::standard::gemini::aggregate_gemini_stream_sync_response;
|
||||
use crate::control::GatewayControlDecision;
|
||||
use crate::{usage::GatewaySyncReportRequest, GatewayError};
|
||||
|
||||
pub(crate) fn maybe_build_local_openai_cli_stream_sync_response(
|
||||
trace_id: &str,
|
||||
@@ -0,0 +1,16 @@
|
||||
mod chat;
|
||||
mod cli;
|
||||
|
||||
pub(crate) use chat::{
|
||||
aggregate_openai_chat_stream_sync_response, convert_openai_cli_response_to_openai_chat,
|
||||
maybe_build_local_openai_chat_cross_format_stream_sync_response,
|
||||
maybe_build_local_openai_chat_cross_format_sync_response,
|
||||
maybe_build_local_openai_chat_stream_sync_response,
|
||||
maybe_build_local_openai_chat_sync_response,
|
||||
};
|
||||
pub(crate) use cli::{
|
||||
aggregate_openai_cli_stream_sync_response, build_openai_cli_response,
|
||||
maybe_build_local_openai_cli_cross_format_stream_sync_response,
|
||||
maybe_build_local_openai_cli_cross_format_sync_response,
|
||||
maybe_build_local_openai_cli_stream_sync_response,
|
||||
};
|
||||
@@ -1,347 +0,0 @@
|
||||
use serde_json::{json, Value};
|
||||
|
||||
use crate::gateway::ai_pipeline::adaptation::private_envelope::transform_provider_private_stream_line as transform_envelope_line;
|
||||
use crate::gateway::ai_pipeline::adaptation::surfaces::provider_adaptation_should_unwrap_stream_envelope;
|
||||
use crate::gateway::ai_pipeline::finalize::sse::{encode_done_sse, encode_json_sse};
|
||||
use crate::gateway::ai_pipeline::finalize::standard::{
|
||||
aggregate_standard_chat_stream_sync_response, aggregate_standard_cli_stream_sync_response,
|
||||
convert_standard_chat_response, convert_standard_cli_response,
|
||||
};
|
||||
use crate::gateway::GatewayError;
|
||||
|
||||
#[derive(Default)]
|
||||
pub(crate) struct BufferedStandardConversionStreamState {
|
||||
raw: Vec<u8>,
|
||||
}
|
||||
|
||||
impl BufferedStandardConversionStreamState {
|
||||
pub(crate) fn transform_line(
|
||||
&mut self,
|
||||
report_context: &Value,
|
||||
line: Vec<u8>,
|
||||
) -> Result<Vec<u8>, GatewayError> {
|
||||
if should_unwrap_envelope(report_context) {
|
||||
self.raw
|
||||
.extend(transform_envelope_line(report_context, line)?);
|
||||
} else {
|
||||
self.raw.extend_from_slice(&line);
|
||||
}
|
||||
Ok(Vec::new())
|
||||
}
|
||||
|
||||
pub(crate) fn finish_as_chat(
|
||||
&mut self,
|
||||
report_context: &Value,
|
||||
) -> Result<Vec<u8>, GatewayError> {
|
||||
let provider_api_format = report_context
|
||||
.get("provider_api_format")
|
||||
.and_then(Value::as_str)
|
||||
.unwrap_or_default()
|
||||
.trim()
|
||||
.to_ascii_lowercase();
|
||||
let client_api_format = report_context
|
||||
.get("client_api_format")
|
||||
.and_then(Value::as_str)
|
||||
.unwrap_or_default()
|
||||
.trim()
|
||||
.to_ascii_lowercase();
|
||||
if self.raw.is_empty() {
|
||||
return Ok(Vec::new());
|
||||
}
|
||||
let aggregated =
|
||||
aggregate_standard_chat_stream_sync_response(&self.raw, provider_api_format.as_str());
|
||||
self.raw.clear();
|
||||
let Some(aggregated) = aggregated else {
|
||||
return Ok(Vec::new());
|
||||
};
|
||||
let Some(converted) = convert_standard_chat_response(
|
||||
&aggregated,
|
||||
provider_api_format.as_str(),
|
||||
client_api_format.as_str(),
|
||||
report_context,
|
||||
) else {
|
||||
return Ok(Vec::new());
|
||||
};
|
||||
emit_chat_stream_for_client_format(&converted, client_api_format.as_str())
|
||||
}
|
||||
|
||||
pub(crate) fn finish_as_cli(
|
||||
&mut self,
|
||||
report_context: &Value,
|
||||
) -> Result<Vec<u8>, GatewayError> {
|
||||
let provider_api_format = report_context
|
||||
.get("provider_api_format")
|
||||
.and_then(Value::as_str)
|
||||
.unwrap_or_default()
|
||||
.trim()
|
||||
.to_ascii_lowercase();
|
||||
let client_api_format = report_context
|
||||
.get("client_api_format")
|
||||
.and_then(Value::as_str)
|
||||
.unwrap_or_default()
|
||||
.trim()
|
||||
.to_ascii_lowercase();
|
||||
if self.raw.is_empty() {
|
||||
return Ok(Vec::new());
|
||||
}
|
||||
let aggregated =
|
||||
aggregate_standard_cli_stream_sync_response(&self.raw, provider_api_format.as_str());
|
||||
self.raw.clear();
|
||||
let Some(aggregated) = aggregated else {
|
||||
return Ok(Vec::new());
|
||||
};
|
||||
let Some(converted) = convert_standard_cli_response(
|
||||
&aggregated,
|
||||
provider_api_format.as_str(),
|
||||
client_api_format.as_str(),
|
||||
report_context,
|
||||
) else {
|
||||
return Ok(Vec::new());
|
||||
};
|
||||
emit_cli_stream_for_client_format(&converted, client_api_format.as_str())
|
||||
}
|
||||
}
|
||||
|
||||
fn should_unwrap_envelope(report_context: &Value) -> bool {
|
||||
let envelope_name = report_context
|
||||
.get("envelope_name")
|
||||
.and_then(Value::as_str)
|
||||
.unwrap_or_default();
|
||||
let provider_api_format = report_context
|
||||
.get("provider_api_format")
|
||||
.and_then(Value::as_str)
|
||||
.unwrap_or_default();
|
||||
provider_adaptation_should_unwrap_stream_envelope(envelope_name, provider_api_format)
|
||||
}
|
||||
|
||||
fn emit_chat_stream_for_client_format(
|
||||
response_body: &Value,
|
||||
client_api_format: &str,
|
||||
) -> Result<Vec<u8>, GatewayError> {
|
||||
match client_api_format {
|
||||
"openai:chat" => emit_openai_chat_stream(response_body),
|
||||
"claude:chat" | "claude:cli" => emit_claude_message_stream(response_body),
|
||||
"gemini:chat" | "gemini:cli" => encode_json_sse(None, response_body),
|
||||
_ => Ok(Vec::new()),
|
||||
}
|
||||
}
|
||||
|
||||
fn emit_cli_stream_for_client_format(
|
||||
response_body: &Value,
|
||||
client_api_format: &str,
|
||||
) -> Result<Vec<u8>, GatewayError> {
|
||||
match client_api_format {
|
||||
"openai:cli" | "openai:compact" => encode_json_sse(
|
||||
Some("response.completed"),
|
||||
&json!({
|
||||
"type": "response.completed",
|
||||
"response": response_body,
|
||||
}),
|
||||
),
|
||||
"claude:cli" => emit_claude_message_stream(response_body),
|
||||
"gemini:cli" => encode_json_sse(None, response_body),
|
||||
_ => Ok(Vec::new()),
|
||||
}
|
||||
}
|
||||
|
||||
fn emit_openai_chat_stream(response_body: &Value) -> Result<Vec<u8>, GatewayError> {
|
||||
let body = match response_body.as_object() {
|
||||
Some(body) => body,
|
||||
None => return Ok(Vec::new()),
|
||||
};
|
||||
let choice = match body
|
||||
.get("choices")
|
||||
.and_then(Value::as_array)
|
||||
.and_then(|choices| choices.first())
|
||||
.and_then(Value::as_object)
|
||||
{
|
||||
Some(choice) => choice,
|
||||
None => return Ok(Vec::new()),
|
||||
};
|
||||
let message = match choice.get("message").and_then(Value::as_object) {
|
||||
Some(message) => message,
|
||||
None => return Ok(Vec::new()),
|
||||
};
|
||||
let content = match extract_openai_chat_content_text(message.get("content")) {
|
||||
Some(content) => content,
|
||||
None => return Ok(Vec::new()),
|
||||
};
|
||||
let mut delta = serde_json::Map::new();
|
||||
delta.insert("role".to_string(), Value::String("assistant".to_string()));
|
||||
if !content.is_empty() {
|
||||
delta.insert("content".to_string(), Value::String(content));
|
||||
} else if message.get("tool_calls").is_none() {
|
||||
delta.insert("content".to_string(), Value::String(String::new()));
|
||||
}
|
||||
if let Some(tool_calls) = message.get("tool_calls").and_then(Value::as_array) {
|
||||
delta.insert("tool_calls".to_string(), Value::Array(tool_calls.clone()));
|
||||
}
|
||||
let chunk = json!({
|
||||
"id": body.get("id").cloned().unwrap_or_else(|| Value::String("chatcmpl-local-stream".to_string())),
|
||||
"object": "chat.completion.chunk",
|
||||
"model": body.get("model").cloned().unwrap_or_else(|| Value::String("unknown".to_string())),
|
||||
"choices": [{
|
||||
"index": choice.get("index").cloned().unwrap_or_else(|| Value::from(0_u64)),
|
||||
"delta": Value::Object(delta),
|
||||
"finish_reason": choice.get("finish_reason").cloned().unwrap_or(Value::Null),
|
||||
}]
|
||||
});
|
||||
let mut out = encode_json_sse(None, &chunk)?;
|
||||
out.extend(encode_done_sse());
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
fn emit_claude_message_stream(response_body: &Value) -> Result<Vec<u8>, GatewayError> {
|
||||
let body = match response_body.as_object() {
|
||||
Some(body) => body,
|
||||
None => return Ok(Vec::new()),
|
||||
};
|
||||
let message_id = body
|
||||
.get("id")
|
||||
.cloned()
|
||||
.unwrap_or_else(|| Value::String("msg-local-stream".to_string()));
|
||||
let model = body
|
||||
.get("model")
|
||||
.cloned()
|
||||
.unwrap_or_else(|| Value::String("unknown".to_string()));
|
||||
let content_blocks = match body.get("content").and_then(Value::as_array) {
|
||||
Some(content) => content,
|
||||
None => return Ok(Vec::new()),
|
||||
};
|
||||
|
||||
let mut out = encode_json_sse(
|
||||
Some("message_start"),
|
||||
&json!({
|
||||
"type": "message_start",
|
||||
"message": {
|
||||
"id": message_id,
|
||||
"type": "message",
|
||||
"role": "assistant",
|
||||
"model": model,
|
||||
"content": [],
|
||||
"stop_reason": Value::Null,
|
||||
"stop_sequence": Value::Null,
|
||||
}
|
||||
}),
|
||||
)?;
|
||||
|
||||
for (index, block) in content_blocks.iter().enumerate() {
|
||||
let Some(block_object) = block.as_object() else {
|
||||
continue;
|
||||
};
|
||||
match block_object
|
||||
.get("type")
|
||||
.and_then(Value::as_str)
|
||||
.unwrap_or("text")
|
||||
{
|
||||
"text" => {
|
||||
out.extend(encode_json_sse(
|
||||
Some("content_block_start"),
|
||||
&json!({
|
||||
"type": "content_block_start",
|
||||
"index": index,
|
||||
"content_block": {
|
||||
"type": "text",
|
||||
"text": "",
|
||||
}
|
||||
}),
|
||||
)?);
|
||||
if let Some(text) = block_object.get("text").and_then(Value::as_str) {
|
||||
if !text.is_empty() {
|
||||
out.extend(encode_json_sse(
|
||||
Some("content_block_delta"),
|
||||
&json!({
|
||||
"type": "content_block_delta",
|
||||
"index": index,
|
||||
"delta": {
|
||||
"type": "text_delta",
|
||||
"text": text,
|
||||
}
|
||||
}),
|
||||
)?);
|
||||
}
|
||||
}
|
||||
out.extend(encode_json_sse(
|
||||
Some("content_block_stop"),
|
||||
&json!({
|
||||
"type": "content_block_stop",
|
||||
"index": index,
|
||||
}),
|
||||
)?);
|
||||
}
|
||||
"tool_use" => {
|
||||
out.extend(encode_json_sse(
|
||||
Some("content_block_start"),
|
||||
&json!({
|
||||
"type": "content_block_start",
|
||||
"index": index,
|
||||
"content_block": block_object,
|
||||
}),
|
||||
)?);
|
||||
out.extend(encode_json_sse(
|
||||
Some("content_block_stop"),
|
||||
&json!({
|
||||
"type": "content_block_stop",
|
||||
"index": index,
|
||||
}),
|
||||
)?);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
let mut delta = serde_json::Map::new();
|
||||
delta.insert(
|
||||
"stop_reason".to_string(),
|
||||
body.get("stop_reason").cloned().unwrap_or(Value::Null),
|
||||
);
|
||||
if let Some(stop_sequence) = body.get("stop_sequence").cloned() {
|
||||
delta.insert("stop_sequence".to_string(), stop_sequence);
|
||||
}
|
||||
let mut message_delta = serde_json::Map::new();
|
||||
message_delta.insert(
|
||||
"type".to_string(),
|
||||
Value::String("message_delta".to_string()),
|
||||
);
|
||||
message_delta.insert("delta".to_string(), Value::Object(delta));
|
||||
if let Some(usage) = body.get("usage").cloned() {
|
||||
message_delta.insert("usage".to_string(), usage);
|
||||
}
|
||||
out.extend(encode_json_sse(
|
||||
Some("message_delta"),
|
||||
&Value::Object(message_delta),
|
||||
)?);
|
||||
out.extend(encode_json_sse(
|
||||
Some("message_stop"),
|
||||
&json!({
|
||||
"type": "message_stop",
|
||||
}),
|
||||
)?);
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
fn extract_openai_chat_content_text(content: Option<&Value>) -> Option<String> {
|
||||
match content? {
|
||||
Value::Null => Some(String::new()),
|
||||
Value::String(text) => Some(text.clone()),
|
||||
Value::Array(parts) => {
|
||||
let mut text = String::new();
|
||||
for part in parts {
|
||||
let part = part.as_object()?;
|
||||
let part_type = part
|
||||
.get("type")
|
||||
.and_then(Value::as_str)
|
||||
.unwrap_or_default()
|
||||
.trim()
|
||||
.to_ascii_lowercase();
|
||||
if matches!(part_type.as_str(), "text" | "output_text") {
|
||||
if let Some(piece) = part.get("text").and_then(Value::as_str) {
|
||||
text.push_str(piece);
|
||||
}
|
||||
}
|
||||
}
|
||||
Some(text)
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,222 @@
|
||||
use serde_json::{json, Map, Value};
|
||||
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub(crate) struct CanonicalUsage {
|
||||
pub(crate) input_tokens: u64,
|
||||
pub(crate) output_tokens: u64,
|
||||
pub(crate) total_tokens: u64,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub(crate) enum CanonicalStreamEvent {
|
||||
Start,
|
||||
TextDelta(String),
|
||||
ToolCallStart {
|
||||
index: usize,
|
||||
call_id: String,
|
||||
name: String,
|
||||
},
|
||||
ToolCallArgumentsDelta {
|
||||
index: usize,
|
||||
arguments: String,
|
||||
},
|
||||
Finish {
|
||||
finish_reason: Option<String>,
|
||||
usage: Option<CanonicalUsage>,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub(crate) struct CanonicalStreamFrame {
|
||||
pub(crate) id: String,
|
||||
pub(crate) model: String,
|
||||
pub(crate) event: CanonicalStreamEvent,
|
||||
}
|
||||
|
||||
pub(crate) fn decode_json_data_line(line: &[u8]) -> Option<Value> {
|
||||
let text = std::str::from_utf8(line).ok()?;
|
||||
let trimmed = text.trim_matches('\r').trim();
|
||||
if trimmed.is_empty() || trimmed.starts_with(':') || trimmed.starts_with("event:") {
|
||||
return None;
|
||||
}
|
||||
let data_line = trimmed.strip_prefix("data:")?.trim();
|
||||
if data_line.is_empty() || data_line == "[DONE]" {
|
||||
return None;
|
||||
}
|
||||
serde_json::from_str(data_line).ok()
|
||||
}
|
||||
|
||||
pub(crate) fn resolve_identity(
|
||||
response_id: Option<&str>,
|
||||
model: Option<&str>,
|
||||
report_context: &Value,
|
||||
default_id: &str,
|
||||
) -> (String, String) {
|
||||
let id = response_id
|
||||
.filter(|value| !value.is_empty())
|
||||
.unwrap_or(default_id)
|
||||
.to_string();
|
||||
let model = model
|
||||
.filter(|value| !value.is_empty())
|
||||
.or_else(|| report_context.get("mapped_model").and_then(Value::as_str))
|
||||
.or_else(|| report_context.get("model").and_then(Value::as_str))
|
||||
.unwrap_or("unknown")
|
||||
.to_string();
|
||||
(id, model)
|
||||
}
|
||||
|
||||
pub(crate) fn canonical_usage_from_openai_usage(value: Option<&Value>) -> Option<CanonicalUsage> {
|
||||
let usage = value?.as_object()?;
|
||||
let input_tokens = usage
|
||||
.get("input_tokens")
|
||||
.or_else(|| usage.get("prompt_tokens"))
|
||||
.and_then(Value::as_u64)
|
||||
.unwrap_or(0);
|
||||
let output_tokens = usage
|
||||
.get("output_tokens")
|
||||
.or_else(|| usage.get("completion_tokens"))
|
||||
.and_then(Value::as_u64)
|
||||
.unwrap_or(0);
|
||||
let total_tokens = usage
|
||||
.get("total_tokens")
|
||||
.and_then(Value::as_u64)
|
||||
.unwrap_or(input_tokens + output_tokens);
|
||||
Some(CanonicalUsage {
|
||||
input_tokens,
|
||||
output_tokens,
|
||||
total_tokens,
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn canonical_usage_from_claude_usage(value: Option<&Value>) -> Option<CanonicalUsage> {
|
||||
let usage = value?.as_object()?;
|
||||
let input_tokens = usage
|
||||
.get("input_tokens")
|
||||
.and_then(Value::as_u64)
|
||||
.unwrap_or(0);
|
||||
let output_tokens = usage
|
||||
.get("output_tokens")
|
||||
.and_then(Value::as_u64)
|
||||
.unwrap_or(0);
|
||||
Some(CanonicalUsage {
|
||||
input_tokens,
|
||||
output_tokens,
|
||||
total_tokens: input_tokens + output_tokens,
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn canonical_usage_from_gemini_usage(value: Option<&Value>) -> Option<CanonicalUsage> {
|
||||
let usage = value?.as_object()?;
|
||||
let input_tokens = usage
|
||||
.get("promptTokenCount")
|
||||
.and_then(Value::as_u64)
|
||||
.unwrap_or(0);
|
||||
let output_tokens = usage
|
||||
.get("candidatesTokenCount")
|
||||
.and_then(Value::as_u64)
|
||||
.unwrap_or(0);
|
||||
let total_tokens = usage
|
||||
.get("totalTokenCount")
|
||||
.and_then(Value::as_u64)
|
||||
.unwrap_or(input_tokens + output_tokens);
|
||||
Some(CanonicalUsage {
|
||||
input_tokens,
|
||||
output_tokens,
|
||||
total_tokens,
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn normalize_openai_finish_reason(value: Option<&str>) -> Option<String> {
|
||||
match value {
|
||||
Some("function_call") => Some("tool_calls".to_string()),
|
||||
Some(other) if !other.trim().is_empty() => Some(other.to_string()),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn map_openai_finish_reason_to_claude(value: Option<&str>) -> &'static str {
|
||||
match value {
|
||||
Some("length") => "max_tokens",
|
||||
Some("tool_calls") | Some("function_call") => "tool_use",
|
||||
Some("content_filter") => "content_filtered",
|
||||
_ => "end_turn",
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn map_openai_finish_reason_to_gemini(value: Option<&str>) -> &'static str {
|
||||
match value {
|
||||
Some("length") => "MAX_TOKENS",
|
||||
Some("content_filter") => "SAFETY",
|
||||
_ => "STOP",
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn parse_json_arguments_value(arguments: &str) -> Option<Value> {
|
||||
let trimmed = arguments.trim();
|
||||
if trimmed.is_empty() {
|
||||
return Some(Value::Object(Map::new()));
|
||||
}
|
||||
serde_json::from_str(trimmed).ok()
|
||||
}
|
||||
|
||||
pub(crate) fn build_openai_chat_chunk(
|
||||
id: &str,
|
||||
model: &str,
|
||||
text: String,
|
||||
tool_calls: Option<Vec<Value>>,
|
||||
finish_reason: Option<&str>,
|
||||
) -> Value {
|
||||
let mut delta = Map::new();
|
||||
delta.insert("role".to_string(), Value::String("assistant".to_string()));
|
||||
if !text.is_empty() {
|
||||
delta.insert("content".to_string(), Value::String(text));
|
||||
} else if tool_calls.is_none() {
|
||||
delta.insert("content".to_string(), Value::String(String::new()));
|
||||
}
|
||||
if let Some(tool_calls) = tool_calls {
|
||||
delta.insert("tool_calls".to_string(), Value::Array(tool_calls));
|
||||
}
|
||||
|
||||
json!({
|
||||
"id": id,
|
||||
"object": "chat.completion.chunk",
|
||||
"model": model,
|
||||
"choices": [{
|
||||
"index": 0,
|
||||
"delta": Value::Object(delta),
|
||||
"finish_reason": finish_reason,
|
||||
}]
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn build_openai_chat_role_chunk(id: &str, model: &str) -> Value {
|
||||
json!({
|
||||
"id": id,
|
||||
"object": "chat.completion.chunk",
|
||||
"model": model,
|
||||
"choices": [{
|
||||
"index": 0,
|
||||
"delta": {
|
||||
"role": "assistant"
|
||||
},
|
||||
"finish_reason": Value::Null
|
||||
}]
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn build_openai_chat_finish_chunk(
|
||||
id: &str,
|
||||
model: &str,
|
||||
finish_reason: Option<&str>,
|
||||
) -> Value {
|
||||
json!({
|
||||
"id": id,
|
||||
"object": "chat.completion.chunk",
|
||||
"model": model,
|
||||
"choices": [{
|
||||
"index": 0,
|
||||
"delta": {},
|
||||
"finish_reason": finish_reason,
|
||||
}]
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
//! Standard finalize streaming conversion helpers.
|
||||
|
||||
use serde_json::Value;
|
||||
|
||||
use crate::GatewayError;
|
||||
|
||||
use super::claude::stream::{ClaudeClientEmitter, ClaudeProviderState};
|
||||
use super::gemini::stream::{GeminiClientEmitter, GeminiProviderState};
|
||||
use super::openai::stream::{
|
||||
OpenAIChatClientEmitter, OpenAIChatProviderState, OpenAICliClientEmitter,
|
||||
OpenAICliProviderState,
|
||||
};
|
||||
|
||||
pub(crate) mod common;
|
||||
mod orchestrator;
|
||||
|
||||
use common::CanonicalStreamFrame;
|
||||
|
||||
pub(crate) enum ProviderStreamParser {
|
||||
OpenAIChat(OpenAIChatProviderState),
|
||||
OpenAICli(OpenAICliProviderState),
|
||||
Claude(ClaudeProviderState),
|
||||
Gemini(GeminiProviderState),
|
||||
}
|
||||
|
||||
impl ProviderStreamParser {
|
||||
pub(crate) fn for_api_format(provider_api_format: &str) -> Option<Self> {
|
||||
Some(match provider_api_format {
|
||||
"openai:chat" => Self::OpenAIChat(OpenAIChatProviderState::default()),
|
||||
"openai:cli" | "openai:compact" => Self::OpenAICli(OpenAICliProviderState::default()),
|
||||
"claude:chat" | "claude:cli" => Self::Claude(ClaudeProviderState::default()),
|
||||
"gemini:chat" | "gemini:cli" => Self::Gemini(GeminiProviderState::default()),
|
||||
_ => return None,
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn push_line(
|
||||
&mut self,
|
||||
report_context: &Value,
|
||||
line: Vec<u8>,
|
||||
) -> Result<Vec<CanonicalStreamFrame>, GatewayError> {
|
||||
match self {
|
||||
ProviderStreamParser::OpenAIChat(state) => state.push_line(report_context, line),
|
||||
ProviderStreamParser::OpenAICli(state) => state.push_line(report_context, line),
|
||||
ProviderStreamParser::Claude(state) => state.push_line(report_context, line),
|
||||
ProviderStreamParser::Gemini(state) => state.push_line(report_context, line),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn finish(
|
||||
&mut self,
|
||||
report_context: &Value,
|
||||
) -> Result<Vec<CanonicalStreamFrame>, GatewayError> {
|
||||
match self {
|
||||
ProviderStreamParser::OpenAIChat(state) => state.finish(report_context),
|
||||
ProviderStreamParser::OpenAICli(state) => state.finish(report_context),
|
||||
ProviderStreamParser::Claude(state) => state.finish(report_context),
|
||||
ProviderStreamParser::Gemini(state) => state.finish(report_context),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) enum ClientStreamEmitter {
|
||||
OpenAIChat(OpenAIChatClientEmitter),
|
||||
OpenAICli(OpenAICliClientEmitter),
|
||||
Claude(ClaudeClientEmitter),
|
||||
Gemini(GeminiClientEmitter),
|
||||
}
|
||||
|
||||
impl ClientStreamEmitter {
|
||||
pub(crate) fn for_api_format(client_api_format: &str) -> Option<Self> {
|
||||
Some(match client_api_format {
|
||||
"openai:chat" => Self::OpenAIChat(OpenAIChatClientEmitter::default()),
|
||||
"openai:cli" | "openai:compact" => Self::OpenAICli(OpenAICliClientEmitter::default()),
|
||||
"claude:chat" | "claude:cli" => Self::Claude(ClaudeClientEmitter::default()),
|
||||
"gemini:chat" | "gemini:cli" => Self::Gemini(GeminiClientEmitter::default()),
|
||||
_ => return None,
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn emit(&mut self, frame: CanonicalStreamFrame) -> Result<Vec<u8>, GatewayError> {
|
||||
match self {
|
||||
ClientStreamEmitter::OpenAIChat(state) => state.emit(frame),
|
||||
ClientStreamEmitter::OpenAICli(state) => state.emit(frame),
|
||||
ClientStreamEmitter::Claude(state) => state.emit(frame),
|
||||
ClientStreamEmitter::Gemini(state) => state.emit(frame),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn finish(&mut self) -> Result<Vec<u8>, GatewayError> {
|
||||
match self {
|
||||
ClientStreamEmitter::OpenAIChat(state) => state.finish(),
|
||||
ClientStreamEmitter::OpenAICli(state) => state.finish(),
|
||||
ClientStreamEmitter::Claude(state) => state.finish(),
|
||||
ClientStreamEmitter::Gemini(state) => state.finish(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) use orchestrator::StreamingStandardConversionState;
|
||||
@@ -0,0 +1,97 @@
|
||||
use serde_json::Value;
|
||||
|
||||
use crate::ai_pipeline::adaptation::private_envelope::transform_provider_private_stream_line as transform_envelope_line;
|
||||
use crate::ai_pipeline::adaptation::surfaces::provider_adaptation_should_unwrap_stream_envelope;
|
||||
use crate::GatewayError;
|
||||
|
||||
use super::common::CanonicalStreamFrame;
|
||||
use super::{ClientStreamEmitter, ProviderStreamParser};
|
||||
|
||||
#[derive(Default)]
|
||||
pub(crate) struct StreamingStandardConversionState {
|
||||
provider: Option<ProviderStreamParser>,
|
||||
client: Option<ClientStreamEmitter>,
|
||||
}
|
||||
|
||||
impl StreamingStandardConversionState {
|
||||
pub(crate) fn transform_line(
|
||||
&mut self,
|
||||
report_context: &Value,
|
||||
line: Vec<u8>,
|
||||
) -> Result<Vec<u8>, GatewayError> {
|
||||
self.ensure_initialized(report_context)?;
|
||||
let line = if should_unwrap_envelope(report_context) {
|
||||
transform_envelope_line(report_context, line)?
|
||||
} else {
|
||||
line
|
||||
};
|
||||
if line.is_empty() {
|
||||
return Ok(Vec::new());
|
||||
}
|
||||
let Some(provider) = self.provider.as_mut() else {
|
||||
return Ok(Vec::new());
|
||||
};
|
||||
let frames = provider.push_line(report_context, line)?;
|
||||
self.emit_frames(frames)
|
||||
}
|
||||
|
||||
pub(crate) fn finish(&mut self, report_context: &Value) -> Result<Vec<u8>, GatewayError> {
|
||||
self.ensure_initialized(report_context)?;
|
||||
let Some(provider) = self.provider.as_mut() else {
|
||||
return Ok(Vec::new());
|
||||
};
|
||||
let frames = provider.finish(report_context)?;
|
||||
let mut out = self.emit_frames(frames)?;
|
||||
if let Some(client) = self.client.as_mut() {
|
||||
out.extend(client.finish()?);
|
||||
}
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
fn ensure_initialized(&mut self, report_context: &Value) -> Result<(), GatewayError> {
|
||||
if self.provider.is_some() && self.client.is_some() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let provider_api_format = report_context
|
||||
.get("provider_api_format")
|
||||
.and_then(Value::as_str)
|
||||
.unwrap_or_default()
|
||||
.trim()
|
||||
.to_ascii_lowercase();
|
||||
let client_api_format = report_context
|
||||
.get("client_api_format")
|
||||
.and_then(Value::as_str)
|
||||
.unwrap_or_default()
|
||||
.trim()
|
||||
.to_ascii_lowercase();
|
||||
|
||||
self.provider = ProviderStreamParser::for_api_format(provider_api_format.as_str());
|
||||
self.client = ClientStreamEmitter::for_api_format(client_api_format.as_str());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn emit_frames(&mut self, frames: Vec<CanonicalStreamFrame>) -> Result<Vec<u8>, GatewayError> {
|
||||
let Some(client) = self.client.as_mut() else {
|
||||
return Ok(Vec::new());
|
||||
};
|
||||
let mut out = Vec::new();
|
||||
for frame in frames {
|
||||
out.extend(client.emit(frame)?);
|
||||
}
|
||||
Ok(out)
|
||||
}
|
||||
}
|
||||
|
||||
fn should_unwrap_envelope(report_context: &Value) -> bool {
|
||||
let envelope_name = report_context
|
||||
.get("envelope_name")
|
||||
.and_then(Value::as_str)
|
||||
.unwrap_or_default();
|
||||
let provider_api_format = report_context
|
||||
.get("provider_api_format")
|
||||
.and_then(Value::as_str)
|
||||
.unwrap_or_default();
|
||||
provider_adaptation_should_unwrap_stream_envelope(envelope_name, provider_api_format)
|
||||
}
|
||||
@@ -2,6 +2,10 @@ use serde_json::json;
|
||||
|
||||
use super::maybe_build_local_stream_rewriter;
|
||||
|
||||
fn utf8(bytes: Vec<u8>) -> String {
|
||||
String::from_utf8(bytes).expect("utf8 should decode")
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn antigravity_stream_rewriter_unwraps_and_injects_tool_ids() {
|
||||
let report_context = json!({
|
||||
@@ -136,20 +140,22 @@ fn gemini_to_openai_chat_stream_rewriter_buffers_and_converts_text() {
|
||||
b"data: {\"responseId\":\"resp_123\",\"candidates\":[{\"content\":{\"parts\":[{\"text\":\"Hello \"}],\"role\":\"model\"},\"index\":0}],\"modelVersion\":\"gemini-2.5-pro\"}\n\n",
|
||||
)
|
||||
.expect("rewrite should succeed");
|
||||
assert!(first.is_empty());
|
||||
let first_text = utf8(first);
|
||||
assert!(first_text.contains("\"object\":\"chat.completion.chunk\""));
|
||||
assert!(first_text.contains("\"role\":\"assistant\""));
|
||||
assert!(first_text.contains("\"content\":\"Hello \""));
|
||||
assert!(!first_text.contains("data: [DONE]"));
|
||||
let second = rewriter
|
||||
.push_chunk(
|
||||
b"data: {\"responseId\":\"resp_123\",\"candidates\":[{\"content\":{\"parts\":[{\"text\":\"Gemini\"}],\"role\":\"model\"},\"finishReason\":\"STOP\",\"index\":0}],\"modelVersion\":\"gemini-2.5-pro\"}\n\n",
|
||||
)
|
||||
.expect("rewrite should succeed");
|
||||
assert!(second.is_empty());
|
||||
let output_text = String::from_utf8(rewriter.finish().expect("finish should succeed"))
|
||||
.expect("utf8 should decode");
|
||||
let output_text = utf8(second);
|
||||
assert!(output_text.contains("\"object\":\"chat.completion.chunk\""));
|
||||
assert!(output_text.contains("\"role\":\"assistant\""));
|
||||
assert!(output_text.contains("\"content\":\"Gemini\""));
|
||||
assert!(output_text.contains("\"finish_reason\":\"stop\""));
|
||||
assert!(output_text.contains("data: [DONE]"));
|
||||
assert!(rewriter.finish().expect("finish should succeed").is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -167,9 +173,7 @@ fn gemini_to_openai_chat_stream_rewriter_buffers_and_converts_function_call() {
|
||||
b"data: {\"responseId\":\"resp_tool_123\",\"candidates\":[{\"content\":{\"parts\":[{\"text\":\"Need a tool.\"},{\"functionCall\":{\"name\":\"get_weather\",\"args\":{\"city\":\"SF\"}}}],\"role\":\"model\"},\"finishReason\":\"STOP\",\"index\":0}],\"modelVersion\":\"gemini-2.5-pro\"}\n\n",
|
||||
)
|
||||
.expect("rewrite should succeed");
|
||||
assert!(output.is_empty());
|
||||
let output_text = String::from_utf8(rewriter.finish().expect("finish should succeed"))
|
||||
.expect("utf8 should decode");
|
||||
let output_text = utf8(output);
|
||||
assert!(output_text.contains("\"object\":\"chat.completion.chunk\""));
|
||||
assert!(output_text.contains("\"role\":\"assistant\""));
|
||||
assert!(output_text.contains("\"content\":\"Need a tool.\""));
|
||||
@@ -178,10 +182,64 @@ fn gemini_to_openai_chat_stream_rewriter_buffers_and_converts_function_call() {
|
||||
assert!(output_text.contains("\\\"city\\\":\\\"SF\\\""));
|
||||
assert!(output_text.contains("\"finish_reason\":\"tool_calls\""));
|
||||
assert!(output_text.contains("data: [DONE]"));
|
||||
assert!(rewriter.finish().expect("finish should succeed").is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn openai_cli_to_openai_chat_stream_rewriter_buffers_and_converts_completed_event() {
|
||||
fn openai_cli_to_openai_chat_stream_rewriter_converts_text_deltas_immediately() {
|
||||
let report_context = json!({
|
||||
"provider_api_format": "openai:cli",
|
||||
"client_api_format": "openai:chat",
|
||||
"needs_conversion": true,
|
||||
"mapped_model": "gpt-5.4",
|
||||
});
|
||||
let mut rewriter =
|
||||
maybe_build_local_stream_rewriter(Some(&report_context)).expect("rewriter should exist");
|
||||
let created = rewriter
|
||||
.push_chunk(
|
||||
concat!(
|
||||
"event: response.created\n",
|
||||
"data: {\"type\":\"response.created\",\"response\":{\"id\":\"resp_cli_stream_123\",\"object\":\"response\",\"model\":\"gpt-5.4\",\"status\":\"in_progress\"}}\n\n"
|
||||
)
|
||||
.as_bytes(),
|
||||
)
|
||||
.expect("rewrite should succeed");
|
||||
let created_text = String::from_utf8(created).expect("utf8 should decode");
|
||||
assert!(created_text.contains("\"object\":\"chat.completion.chunk\""));
|
||||
assert!(created_text.contains("\"role\":\"assistant\""));
|
||||
assert!(!created_text.contains("data: [DONE]"));
|
||||
|
||||
let delta = rewriter
|
||||
.push_chunk(
|
||||
concat!(
|
||||
"event: response.output_text.delta\n",
|
||||
"data: {\"type\":\"response.output_text.delta\",\"delta\":\"Hello Codex\"}\n\n"
|
||||
)
|
||||
.as_bytes(),
|
||||
)
|
||||
.expect("rewrite should succeed");
|
||||
let delta_text = String::from_utf8(delta).expect("utf8 should decode");
|
||||
assert!(delta_text.contains("\"object\":\"chat.completion.chunk\""));
|
||||
assert!(delta_text.contains("\"content\":\"Hello Codex\""));
|
||||
assert!(!delta_text.contains("data: [DONE]"));
|
||||
|
||||
let completed = rewriter
|
||||
.push_chunk(
|
||||
concat!(
|
||||
"event: response.completed\n",
|
||||
"data: {\"type\":\"response.completed\",\"response\":{\"id\":\"resp_cli_stream_123\",\"object\":\"response\",\"model\":\"gpt-5.4\",\"status\":\"completed\",\"output\":[{\"type\":\"message\",\"id\":\"msg_cli_stream_123\",\"role\":\"assistant\",\"status\":\"completed\",\"content\":[{\"type\":\"output_text\",\"text\":\"Hello Codex\",\"annotations\":[]}]}],\"usage\":{\"input_tokens\":1,\"output_tokens\":2,\"total_tokens\":3}}}\n\n"
|
||||
)
|
||||
.as_bytes(),
|
||||
)
|
||||
.expect("rewrite should succeed");
|
||||
let completed_text = String::from_utf8(completed).expect("utf8 should decode");
|
||||
assert!(completed_text.contains("\"finish_reason\":\"stop\""));
|
||||
assert!(completed_text.contains("data: [DONE]"));
|
||||
assert!(rewriter.finish().expect("finish should succeed").is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn openai_cli_to_openai_chat_stream_rewriter_converts_completed_event_without_buffering() {
|
||||
let report_context = json!({
|
||||
"provider_api_format": "openai:cli",
|
||||
"client_api_format": "openai:chat",
|
||||
@@ -199,14 +257,13 @@ fn openai_cli_to_openai_chat_stream_rewriter_buffers_and_converts_completed_even
|
||||
.as_bytes(),
|
||||
)
|
||||
.expect("rewrite should succeed");
|
||||
assert!(output.is_empty());
|
||||
let output_text = String::from_utf8(rewriter.finish().expect("finish should succeed"))
|
||||
.expect("utf8 should decode");
|
||||
let output_text = String::from_utf8(output).expect("utf8 should decode");
|
||||
assert!(output_text.contains("\"object\":\"chat.completion.chunk\""));
|
||||
assert!(output_text.contains("\"role\":\"assistant\""));
|
||||
assert!(output_text.contains("\"content\":\"Hello Codex\""));
|
||||
assert!(output_text.contains("\"finish_reason\":\"stop\""));
|
||||
assert!(output_text.contains("data: [DONE]"));
|
||||
assert!(rewriter.finish().expect("finish should succeed").is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -226,13 +283,14 @@ fn antigravity_gemini_to_openai_chat_stream_rewriter_unwraps_and_converts_functi
|
||||
b"data: {\"response\":{\"responseId\":\"resp_antigravity_chat_tool_123\",\"candidates\":[{\"content\":{\"parts\":[{\"text\":\"Need a tool.\"},{\"functionCall\":{\"name\":\"get_weather\",\"args\":{\"city\":\"SF\"}}}],\"role\":\"model\"},\"finishReason\":\"STOP\",\"index\":0}],\"modelVersion\":\"claude-sonnet-4-5\"},\"responseId\":\"resp_antigravity_chat_tool_123\"}\n\n",
|
||||
)
|
||||
.expect("rewrite should succeed");
|
||||
assert!(output.is_empty());
|
||||
let output_text = String::from_utf8(rewriter.finish().expect("finish should succeed"))
|
||||
.expect("utf8 should decode");
|
||||
let output_text = utf8(output);
|
||||
assert!(output_text.contains("\"object\":\"chat.completion.chunk\""));
|
||||
assert!(output_text.contains("\"content\":\"Need a tool.\""));
|
||||
assert!(output_text.contains("\"tool_calls\""));
|
||||
assert!(output_text.contains("\"name\":\"get_weather\""));
|
||||
assert!(output_text.contains("\"finish_reason\":\"tool_calls\""));
|
||||
assert!(output_text.contains("data: [DONE]"));
|
||||
assert!(rewriter.finish().expect("finish should succeed").is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -251,12 +309,15 @@ fn antigravity_gemini_to_openai_cli_stream_rewriter_unwraps_and_converts_functio
|
||||
b"data: {\"response\":{\"responseId\":\"resp_antigravity_cli_tool_123\",\"candidates\":[{\"content\":{\"parts\":[{\"text\":\"Need a tool.\"},{\"functionCall\":{\"name\":\"get_weather\",\"args\":{\"city\":\"SF\"}}}],\"role\":\"model\"},\"finishReason\":\"STOP\",\"index\":0}],\"modelVersion\":\"claude-sonnet-4-5\",\"usageMetadata\":{\"promptTokenCount\":2,\"candidatesTokenCount\":3,\"totalTokenCount\":5}},\"responseId\":\"resp_antigravity_cli_tool_123\"}\n\n",
|
||||
)
|
||||
.expect("rewrite should succeed");
|
||||
assert!(output.is_empty());
|
||||
let output_text = String::from_utf8(rewriter.finish().expect("finish should succeed"))
|
||||
.expect("utf8 should decode");
|
||||
let output_text = utf8(output);
|
||||
assert!(output_text.contains("event: response.created"));
|
||||
assert!(output_text.contains("event: response.output_text.delta"));
|
||||
assert!(output_text.contains("event: response.output_item.added"));
|
||||
assert!(output_text.contains("event: response.function_call_arguments.delta"));
|
||||
assert!(output_text.contains("event: response.completed"));
|
||||
assert!(output_text.contains("\"type\":\"function_call\""));
|
||||
assert!(output_text.contains("\"name\":\"get_weather\""));
|
||||
assert!(rewriter.finish().expect("finish should succeed").is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -274,20 +335,24 @@ fn gemini_to_openai_cli_stream_rewriter_buffers_and_converts_to_completed_event(
|
||||
b"data: {\"responseId\":\"resp_123\",\"candidates\":[{\"content\":{\"parts\":[{\"text\":\"Hello \"}],\"role\":\"model\"},\"index\":0}],\"modelVersion\":\"gemini-2.5-pro\"}\n\n",
|
||||
)
|
||||
.expect("rewrite should succeed");
|
||||
assert!(first.is_empty());
|
||||
let first_text = utf8(first);
|
||||
assert!(first_text.contains("event: response.created"));
|
||||
assert!(first_text.contains("event: response.output_text.delta"));
|
||||
assert!(first_text.contains("\"delta\":\"Hello \""));
|
||||
let second = rewriter
|
||||
.push_chunk(
|
||||
b"data: {\"responseId\":\"resp_123\",\"candidates\":[{\"content\":{\"parts\":[{\"text\":\"Gemini CLI\"}],\"role\":\"model\"},\"finishReason\":\"STOP\",\"index\":0}],\"modelVersion\":\"gemini-2.5-pro\",\"usageMetadata\":{\"promptTokenCount\":2,\"candidatesTokenCount\":3,\"totalTokenCount\":5}}\n\n",
|
||||
)
|
||||
.expect("rewrite should succeed");
|
||||
assert!(second.is_empty());
|
||||
let output_text = String::from_utf8(rewriter.finish().expect("finish should succeed"))
|
||||
.expect("utf8 should decode");
|
||||
let output_text = utf8(second);
|
||||
assert!(output_text.contains("event: response.output_text.delta"));
|
||||
assert!(output_text.contains("event: response.completed"));
|
||||
assert!(output_text.contains("\"type\":\"response.completed\""));
|
||||
assert!(output_text.contains("\"object\":\"response\""));
|
||||
assert!(output_text.contains("\"text\":\"Gemini CLI\""));
|
||||
assert!(output_text.contains("\"delta\":\"Gemini CLI\""));
|
||||
assert!(output_text.contains("\"text\":\"Hello Gemini CLI\""));
|
||||
assert!(output_text.contains("\"total_tokens\":5"));
|
||||
assert!(rewriter.finish().expect("finish should succeed").is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -317,14 +382,15 @@ fn claude_to_openai_cli_stream_rewriter_buffers_and_converts_to_completed_event(
|
||||
.as_bytes(),
|
||||
)
|
||||
.expect("rewrite should succeed");
|
||||
assert!(output.is_empty());
|
||||
let output_text = String::from_utf8(rewriter.finish().expect("finish should succeed"))
|
||||
.expect("utf8 should decode");
|
||||
let output_text = utf8(output);
|
||||
assert!(output_text.contains("event: response.created"));
|
||||
assert!(output_text.contains("event: response.output_text.delta"));
|
||||
assert!(output_text.contains("event: response.completed"));
|
||||
assert!(output_text.contains("\"type\":\"response.completed\""));
|
||||
assert!(output_text.contains("\"object\":\"response\""));
|
||||
assert!(output_text.contains("\"text\":\"Hello Claude CLI\""));
|
||||
assert!(output_text.contains("\"total_tokens\":5"));
|
||||
assert!(rewriter.finish().expect("finish should succeed").is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -358,15 +424,19 @@ fn claude_to_openai_cli_stream_rewriter_converts_tool_use_to_function_call() {
|
||||
.as_bytes(),
|
||||
)
|
||||
.expect("rewrite should succeed");
|
||||
assert!(output.is_empty());
|
||||
let output_text = String::from_utf8(rewriter.finish().expect("finish should succeed"))
|
||||
.expect("utf8 should decode");
|
||||
let output_text = utf8(output);
|
||||
assert!(output_text.contains("event: response.created"));
|
||||
assert!(output_text.contains("event: response.output_text.delta"));
|
||||
assert!(output_text.contains("\"delta\":\"Running tool.\""));
|
||||
assert!(output_text.contains("event: response.output_item.added"));
|
||||
assert!(output_text.contains("event: response.function_call_arguments.delta"));
|
||||
assert!(output_text.contains("event: response.completed"));
|
||||
assert!(output_text.contains("\"type\":\"response.completed\""));
|
||||
assert!(output_text.contains("\"type\":\"function_call\""));
|
||||
assert!(output_text.contains("\"call_id\":\"tool_123\""));
|
||||
assert!(output_text.contains("\"name\":\"read_file\""));
|
||||
assert!(output_text.contains("\\\"path\\\":\\\"/tmp/test.txt\\\""));
|
||||
assert!(rewriter.finish().expect("finish should succeed").is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -384,14 +454,16 @@ fn gemini_to_openai_cli_stream_rewriter_converts_function_call_to_completed_even
|
||||
b"data: {\"responseId\":\"resp_tool_123\",\"candidates\":[{\"content\":{\"parts\":[{\"text\":\"Need a tool.\"},{\"functionCall\":{\"name\":\"get_weather\",\"args\":{\"location\":\"Tokyo\"}}}],\"role\":\"model\"},\"finishReason\":\"STOP\",\"index\":0}],\"modelVersion\":\"gemini-2.5-pro\",\"usageMetadata\":{\"promptTokenCount\":2,\"candidatesTokenCount\":3,\"totalTokenCount\":5}}\n\n",
|
||||
)
|
||||
.expect("rewrite should succeed");
|
||||
assert!(output.is_empty());
|
||||
let output_text = String::from_utf8(rewriter.finish().expect("finish should succeed"))
|
||||
.expect("utf8 should decode");
|
||||
let output_text = utf8(output);
|
||||
assert!(output_text.contains("event: response.created"));
|
||||
assert!(output_text.contains("event: response.output_item.added"));
|
||||
assert!(output_text.contains("event: response.function_call_arguments.delta"));
|
||||
assert!(output_text.contains("event: response.completed"));
|
||||
assert!(output_text.contains("\"type\":\"response.completed\""));
|
||||
assert!(output_text.contains("\"type\":\"function_call\""));
|
||||
assert!(output_text.contains("\"name\":\"get_weather\""));
|
||||
assert!(output_text.contains("\\\"location\\\":\\\"Tokyo\\\""));
|
||||
assert!(rewriter.finish().expect("finish should succeed").is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -409,14 +481,16 @@ fn gemini_to_openai_compact_stream_rewriter_converts_function_call_to_completed_
|
||||
b"data: {\"responseId\":\"resp_tool_compact_123\",\"candidates\":[{\"content\":{\"parts\":[{\"text\":\"Need a tool.\"},{\"functionCall\":{\"name\":\"get_weather\",\"args\":{\"location\":\"Tokyo\"}}}],\"role\":\"model\"},\"finishReason\":\"STOP\",\"index\":0}],\"modelVersion\":\"gemini-2.5-pro\",\"usageMetadata\":{\"promptTokenCount\":2,\"candidatesTokenCount\":3,\"totalTokenCount\":5}}\n\n",
|
||||
)
|
||||
.expect("rewrite should succeed");
|
||||
assert!(output.is_empty());
|
||||
let output_text = String::from_utf8(rewriter.finish().expect("finish should succeed"))
|
||||
.expect("utf8 should decode");
|
||||
let output_text = utf8(output);
|
||||
assert!(output_text.contains("event: response.created"));
|
||||
assert!(output_text.contains("event: response.output_item.added"));
|
||||
assert!(output_text.contains("event: response.function_call_arguments.delta"));
|
||||
assert!(output_text.contains("event: response.completed"));
|
||||
assert!(output_text.contains("\"type\":\"response.completed\""));
|
||||
assert!(output_text.contains("\"type\":\"function_call\""));
|
||||
assert!(output_text.contains("\"name\":\"get_weather\""));
|
||||
assert!(output_text.contains("\\\"location\\\":\\\"Tokyo\\\""));
|
||||
assert!(rewriter.finish().expect("finish should succeed").is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -429,23 +503,32 @@ fn openai_chat_to_claude_chat_stream_rewriter_converts_via_standard_matrix() {
|
||||
});
|
||||
let mut rewriter =
|
||||
maybe_build_local_stream_rewriter(Some(&report_context)).expect("rewriter should exist");
|
||||
let output = rewriter
|
||||
let first = rewriter
|
||||
.push_chunk(
|
||||
"data: {\"id\":\"chatcmpl_std_claude_123\",\"object\":\"chat.completion.chunk\",\"created\":1,\"model\":\"gpt-5\",\"choices\":[{\"index\":0,\"delta\":{\"role\":\"assistant\",\"content\":\"Hello Claude\"},\"finish_reason\":null}]}\n\n"
|
||||
.as_bytes(),
|
||||
)
|
||||
.expect("rewrite should succeed");
|
||||
let first_text = utf8(first);
|
||||
assert!(first_text.contains("event: message_start"));
|
||||
assert!(first_text.contains("event: content_block_start"));
|
||||
assert!(first_text.contains("event: content_block_delta"));
|
||||
assert!(first_text.contains("\"text\":\"Hello Claude\""));
|
||||
|
||||
let second = rewriter
|
||||
.push_chunk(
|
||||
concat!(
|
||||
"data: {\"id\":\"chatcmpl_std_claude_123\",\"object\":\"chat.completion.chunk\",\"created\":1,\"model\":\"gpt-5\",\"choices\":[{\"index\":0,\"delta\":{\"role\":\"assistant\",\"content\":\"Hello Claude\"},\"finish_reason\":null}]}\n\n",
|
||||
"data: {\"id\":\"chatcmpl_std_claude_123\",\"object\":\"chat.completion.chunk\",\"model\":\"gpt-5\",\"choices\":[{\"index\":0,\"delta\":{},\"finish_reason\":\"stop\"}],\"usage\":{\"prompt_tokens\":1,\"completion_tokens\":2,\"total_tokens\":3}}\n\n",
|
||||
"data: [DONE]\n\n"
|
||||
)
|
||||
.as_bytes(),
|
||||
)
|
||||
.expect("rewrite should succeed");
|
||||
assert!(output.is_empty());
|
||||
let output_text = String::from_utf8(rewriter.finish().expect("finish should succeed"))
|
||||
.expect("utf8 should decode");
|
||||
assert!(output_text.contains("event: message_start"));
|
||||
assert!(output_text.contains("event: content_block_delta"));
|
||||
assert!(output_text.contains("\"text\":\"Hello Claude\""));
|
||||
let output_text = utf8(second);
|
||||
assert!(output_text.contains("event: content_block_stop"));
|
||||
assert!(output_text.contains("event: message_delta"));
|
||||
assert!(output_text.contains("event: message_stop"));
|
||||
assert!(rewriter.finish().expect("finish should succeed").is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -458,20 +541,28 @@ fn openai_chat_to_gemini_cli_stream_rewriter_converts_via_standard_matrix() {
|
||||
});
|
||||
let mut rewriter =
|
||||
maybe_build_local_stream_rewriter(Some(&report_context)).expect("rewriter should exist");
|
||||
let output = rewriter
|
||||
let first = rewriter
|
||||
.push_chunk(
|
||||
"data: {\"id\":\"chatcmpl_std_gemini_cli_123\",\"object\":\"chat.completion.chunk\",\"created\":1,\"model\":\"gpt-5\",\"choices\":[{\"index\":0,\"delta\":{\"role\":\"assistant\",\"content\":\"Hello Gemini CLI\"},\"finish_reason\":null}]}\n\n"
|
||||
.as_bytes(),
|
||||
)
|
||||
.expect("rewrite should succeed");
|
||||
let first_text = utf8(first);
|
||||
assert!(first_text.contains("\"responseId\":\"chatcmpl_std_gemini_cli_123\""));
|
||||
assert!(first_text.contains("\"candidates\""));
|
||||
assert!(first_text.contains("\"text\":\"Hello Gemini CLI\""));
|
||||
|
||||
let second = rewriter
|
||||
.push_chunk(
|
||||
concat!(
|
||||
"data: {\"id\":\"chatcmpl_std_gemini_cli_123\",\"object\":\"chat.completion.chunk\",\"created\":1,\"model\":\"gpt-5\",\"choices\":[{\"index\":0,\"delta\":{\"role\":\"assistant\",\"content\":\"Hello Gemini CLI\"},\"finish_reason\":null}]}\n\n",
|
||||
"data: {\"id\":\"chatcmpl_std_gemini_cli_123\",\"object\":\"chat.completion.chunk\",\"model\":\"gpt-5\",\"choices\":[{\"index\":0,\"delta\":{},\"finish_reason\":\"stop\"}],\"usage\":{\"prompt_tokens\":2,\"completion_tokens\":3,\"total_tokens\":5}}\n\n",
|
||||
"data: [DONE]\n\n"
|
||||
)
|
||||
.as_bytes(),
|
||||
)
|
||||
.expect("rewrite should succeed");
|
||||
assert!(output.is_empty());
|
||||
let output_text = String::from_utf8(rewriter.finish().expect("finish should succeed"))
|
||||
.expect("utf8 should decode");
|
||||
assert!(output_text.contains("\"responseId\":\"chatcmpl_std_gemini_cli_123\""));
|
||||
assert!(output_text.contains("\"candidates\""));
|
||||
assert!(output_text.contains("\"text\":\"Hello Gemini CLI\""));
|
||||
let output_text = utf8(second);
|
||||
assert!(output_text.contains("\"finishReason\":\"STOP\""));
|
||||
assert!(output_text.contains("\"totalTokenCount\":5"));
|
||||
assert!(rewriter.finish().expect("finish should succeed").is_empty());
|
||||
}
|
||||
|
||||
@@ -10,7 +10,8 @@ use super::{
|
||||
convert_gemini_chat_response_to_openai_chat, convert_gemini_cli_response_to_openai_cli,
|
||||
maybe_build_local_core_sync_finalize_response,
|
||||
};
|
||||
use crate::gateway::{GatewayControlDecision, GatewaySyncReportRequest};
|
||||
use crate::control::GatewayControlDecision;
|
||||
use crate::usage::GatewaySyncReportRequest;
|
||||
|
||||
fn test_decision() -> GatewayControlDecision {
|
||||
GatewayControlDecision {
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
use tracing::warn;
|
||||
|
||||
use crate::gateway::provider_transport::resolve_transport_proxy_snapshot;
|
||||
use crate::gateway::scheduler::GatewayMinimalCandidateSelectionCandidate;
|
||||
use crate::gateway::AppState;
|
||||
use crate::provider_transport::resolve_transport_proxy_snapshot;
|
||||
use crate::scheduler::GatewayMinimalCandidateSelectionCandidate;
|
||||
use crate::AppState;
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
|
||||
enum TunnelOwnerAffinityBucket {
|
||||
@@ -43,6 +43,8 @@ async fn resolve_candidate_tunnel_owner_affinity(
|
||||
Ok(None) => return TunnelOwnerAffinityBucket::Neutral,
|
||||
Err(error) => {
|
||||
warn!(
|
||||
event_name = "candidate_affinity_transport_load_failed",
|
||||
log_type = "event",
|
||||
provider_id = %candidate.provider_id,
|
||||
endpoint_id = %candidate.endpoint_id,
|
||||
key_id = %candidate.key_id,
|
||||
@@ -84,6 +86,8 @@ async fn resolve_candidate_tunnel_owner_affinity(
|
||||
Ok(None) => TunnelOwnerAffinityBucket::Neutral,
|
||||
Err(error) => {
|
||||
warn!(
|
||||
event_name = "candidate_affinity_tunnel_owner_lookup_failed",
|
||||
log_type = "event",
|
||||
node_id = node_id,
|
||||
error = %error,
|
||||
"failed to load tunnel attachment owner while evaluating scheduler affinity"
|
||||
@@ -104,10 +108,10 @@ mod tests {
|
||||
use serde_json::json;
|
||||
|
||||
use super::{
|
||||
prefer_local_tunnel_owner_candidates, GatewayMinimalCandidateSelectionCandidate, AppState,
|
||||
prefer_local_tunnel_owner_candidates, AppState, GatewayMinimalCandidateSelectionCandidate,
|
||||
};
|
||||
use crate::gateway::tunnel::TunnelAttachmentRecord;
|
||||
use crate::gateway::GatewayDataState;
|
||||
use crate::data::GatewayDataState;
|
||||
use crate::tunnel::TunnelAttachmentRecord;
|
||||
|
||||
fn sample_candidate(
|
||||
endpoint_id: &str,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use axum::body::Bytes;
|
||||
use base64::Engine as _;
|
||||
|
||||
pub(crate) use crate::gateway::ai_pipeline::contracts::{
|
||||
pub(crate) use crate::ai_pipeline::contracts::{
|
||||
CLAUDE_CHAT_STREAM_PLAN_KIND, CLAUDE_CHAT_SYNC_PLAN_KIND, CLAUDE_CLI_STREAM_PLAN_KIND,
|
||||
CLAUDE_CLI_SYNC_PLAN_KIND, EXECUTION_RUNTIME_STREAM_ACTION,
|
||||
EXECUTION_RUNTIME_STREAM_DECISION_ACTION, EXECUTION_RUNTIME_SYNC_ACTION,
|
||||
@@ -15,7 +15,7 @@ pub(crate) use crate::gateway::ai_pipeline::contracts::{
|
||||
OPENAI_VIDEO_CONTENT_PLAN_KIND, OPENAI_VIDEO_CREATE_SYNC_PLAN_KIND,
|
||||
OPENAI_VIDEO_DELETE_SYNC_PLAN_KIND, OPENAI_VIDEO_REMIX_SYNC_PLAN_KIND,
|
||||
};
|
||||
use crate::gateway::headers::is_json_request;
|
||||
use crate::headers::is_json_request;
|
||||
|
||||
pub(crate) fn parse_direct_request_body(
|
||||
parts: &http::request::Parts,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use crate::gateway::ai_pipeline::planner::common::{
|
||||
use crate::ai_pipeline::planner::common::{
|
||||
CLAUDE_CHAT_STREAM_PLAN_KIND, CLAUDE_CHAT_SYNC_PLAN_KIND, CLAUDE_CLI_STREAM_PLAN_KIND,
|
||||
CLAUDE_CLI_SYNC_PLAN_KIND, EXECUTION_RUNTIME_STREAM_ACTION, EXECUTION_RUNTIME_SYNC_ACTION,
|
||||
GEMINI_CHAT_STREAM_PLAN_KIND, GEMINI_CHAT_SYNC_PLAN_KIND, GEMINI_CLI_STREAM_PLAN_KIND,
|
||||
@@ -10,7 +10,7 @@ use crate::gateway::ai_pipeline::planner::common::{
|
||||
OPENAI_VIDEO_CONTENT_PLAN_KIND, OPENAI_VIDEO_CREATE_SYNC_PLAN_KIND,
|
||||
OPENAI_VIDEO_DELETE_SYNC_PLAN_KIND, OPENAI_VIDEO_REMIX_SYNC_PLAN_KIND,
|
||||
};
|
||||
use crate::gateway::ai_pipeline::planner::plan_builders::{
|
||||
use crate::ai_pipeline::planner::plan_builders::{
|
||||
build_gemini_stream_plan_from_decision, build_gemini_sync_plan_from_decision,
|
||||
build_openai_chat_stream_plan_from_decision, build_openai_chat_sync_plan_from_decision,
|
||||
build_openai_cli_stream_plan_from_decision, build_openai_cli_sync_plan_from_decision,
|
||||
@@ -18,9 +18,10 @@ use crate::gateway::ai_pipeline::planner::plan_builders::{
|
||||
build_standard_stream_plan_from_decision, build_standard_sync_plan_from_decision,
|
||||
LocalStreamPlanAndReport, LocalSyncPlanAndReport,
|
||||
};
|
||||
use crate::gateway::{
|
||||
AppState, GatewayControlAuthContext, GatewayControlDecision, GatewayControlPlanResponse,
|
||||
GatewayControlSyncDecisionResponse, GatewayError,
|
||||
use crate::control::GatewayControlAuthContext;
|
||||
use crate::control::GatewayControlDecision;
|
||||
use crate::{
|
||||
AppState, GatewayControlPlanResponse, GatewayControlSyncDecisionResponse, GatewayError,
|
||||
};
|
||||
|
||||
pub(crate) async fn maybe_build_sync_plan_payload_impl(
|
||||
|
||||
@@ -5,18 +5,25 @@ mod sync;
|
||||
pub(crate) use self::control_plan::{
|
||||
maybe_build_stream_plan_payload_impl, maybe_build_sync_plan_payload_impl,
|
||||
};
|
||||
pub(crate) use self::stream::maybe_build_stream_decision_payload_impl as maybe_build_stream_decision_payload;
|
||||
pub(crate) use self::sync::maybe_build_sync_decision_payload_impl as maybe_build_sync_decision_payload;
|
||||
pub(crate) use super::{
|
||||
maybe_build_stream_local_decision_payload,
|
||||
maybe_build_stream_local_gemini_files_decision_payload,
|
||||
maybe_build_stream_local_openai_cli_decision_payload,
|
||||
pub(crate) use self::stream::maybe_build_stream_decision_payload;
|
||||
pub(crate) use self::sync::maybe_build_sync_decision_payload;
|
||||
pub(crate) use super::passthrough::{
|
||||
maybe_build_stream_local_same_format_provider_decision_payload,
|
||||
maybe_build_stream_local_standard_decision_payload, maybe_build_sync_local_decision_payload,
|
||||
maybe_build_sync_local_gemini_files_decision_payload,
|
||||
maybe_build_sync_local_openai_cli_decision_payload,
|
||||
maybe_build_sync_local_same_format_provider_decision_payload,
|
||||
maybe_build_sync_local_standard_decision_payload,
|
||||
maybe_build_sync_local_video_decision_payload, resolve_stream_plan_kind,
|
||||
resolve_sync_plan_kind,
|
||||
};
|
||||
pub(crate) use super::specialized::{
|
||||
maybe_build_stream_local_gemini_files_decision_payload,
|
||||
maybe_build_sync_local_gemini_files_decision_payload,
|
||||
maybe_build_sync_local_video_decision_payload,
|
||||
};
|
||||
pub(crate) use super::standard::{
|
||||
maybe_build_stream_local_decision_payload,
|
||||
maybe_build_stream_local_openai_cli_decision_payload,
|
||||
maybe_build_stream_local_standard_decision_payload, maybe_build_sync_local_decision_payload,
|
||||
maybe_build_sync_local_openai_cli_decision_payload,
|
||||
maybe_build_sync_local_standard_decision_payload,
|
||||
};
|
||||
pub(crate) use crate::scheduler::{
|
||||
resolve_execution_runtime_stream_plan_kind as resolve_stream_plan_kind,
|
||||
resolve_execution_runtime_sync_plan_kind as resolve_sync_plan_kind,
|
||||
};
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use crate::gateway::ai_pipeline::planner::common::{
|
||||
use crate::ai_pipeline::planner::common::{
|
||||
EXECUTION_RUNTIME_STREAM_DECISION_ACTION, OPENAI_VIDEO_CONTENT_PLAN_KIND,
|
||||
};
|
||||
use crate::gateway::scheduler::{
|
||||
use crate::control::GatewayControlDecision;
|
||||
use crate::execution_runtime::{ConversionMode, ExecutionStrategy};
|
||||
use crate::scheduler::{
|
||||
is_matching_stream_request, resolve_execution_runtime_stream_plan_kind,
|
||||
};
|
||||
use crate::gateway::{
|
||||
AppState, GatewayControlDecision, GatewayControlSyncDecisionResponse, GatewayError,
|
||||
};
|
||||
use crate::{AppState, GatewayControlSyncDecisionResponse, GatewayError};
|
||||
|
||||
pub(crate) async fn maybe_build_stream_decision_payload_impl(
|
||||
pub(crate) async fn maybe_build_stream_decision_payload(
|
||||
state: &AppState,
|
||||
parts: &http::request::Parts,
|
||||
trace_id: &str,
|
||||
@@ -101,20 +101,20 @@ async fn maybe_build_local_video_task_content_stream_decision_payload(
|
||||
return Ok(None);
|
||||
};
|
||||
|
||||
let crate::gateway::video_tasks::LocalVideoTaskContentAction::StreamPlan(plan) = action else {
|
||||
let crate::video_tasks::LocalVideoTaskContentAction::StreamPlan(plan) = action else {
|
||||
return Ok(None);
|
||||
};
|
||||
let provider_contract = plan.provider_api_format.clone();
|
||||
let client_contract = plan.client_api_format.clone();
|
||||
let execution_strategy = if plan.provider_api_format == plan.client_api_format {
|
||||
crate::gateway::ExecutionStrategy::LocalSameFormat
|
||||
ExecutionStrategy::LocalSameFormat
|
||||
} else {
|
||||
crate::gateway::ExecutionStrategy::LocalCrossFormat
|
||||
ExecutionStrategy::LocalCrossFormat
|
||||
};
|
||||
let conversion_mode = if plan.provider_api_format == plan.client_api_format {
|
||||
crate::gateway::ConversionMode::None
|
||||
ConversionMode::None
|
||||
} else {
|
||||
crate::gateway::ConversionMode::Bidirectional
|
||||
ConversionMode::Bidirectional
|
||||
};
|
||||
|
||||
Ok(Some(GatewayControlSyncDecisionResponse {
|
||||
|
||||
@@ -2,19 +2,19 @@ use std::collections::BTreeMap;
|
||||
|
||||
use url::Url;
|
||||
|
||||
use crate::gateway::ai_pipeline::planner::common::{
|
||||
use crate::ai_pipeline::planner::common::{
|
||||
EXECUTION_RUNTIME_SYNC_DECISION_ACTION, GEMINI_FILES_DELETE_PLAN_KIND,
|
||||
GEMINI_FILES_GET_PLAN_KIND, GEMINI_FILES_LIST_PLAN_KIND, GEMINI_VIDEO_CANCEL_SYNC_PLAN_KIND,
|
||||
OPENAI_VIDEO_CANCEL_SYNC_PLAN_KIND, OPENAI_VIDEO_DELETE_SYNC_PLAN_KIND,
|
||||
OPENAI_VIDEO_REMIX_SYNC_PLAN_KIND,
|
||||
};
|
||||
use crate::gateway::scheduler::resolve_execution_runtime_sync_plan_kind;
|
||||
use crate::gateway::{
|
||||
resolve_execution_runtime_auth_context, AppState, GatewayControlDecision,
|
||||
GatewayControlSyncDecisionResponse, GatewayError,
|
||||
};
|
||||
use crate::control::resolve_execution_runtime_auth_context;
|
||||
use crate::control::GatewayControlDecision;
|
||||
use crate::execution_runtime::{ConversionMode, ExecutionStrategy};
|
||||
use crate::scheduler::resolve_execution_runtime_sync_plan_kind;
|
||||
use crate::{AppState, GatewayControlSyncDecisionResponse, GatewayError};
|
||||
|
||||
pub(crate) async fn maybe_build_sync_decision_payload_impl(
|
||||
pub(crate) async fn maybe_build_sync_decision_payload(
|
||||
state: &AppState,
|
||||
parts: &http::request::Parts,
|
||||
trace_id: &str,
|
||||
@@ -144,15 +144,15 @@ async fn maybe_build_local_video_task_follow_up_sync_decision_payload(
|
||||
let auth_pair = extract_auth_header_pair(&follow_up.plan.headers);
|
||||
let execution_strategy =
|
||||
if follow_up.plan.provider_api_format == follow_up.plan.client_api_format {
|
||||
crate::gateway::ExecutionStrategy::LocalSameFormat
|
||||
ExecutionStrategy::LocalSameFormat
|
||||
} else {
|
||||
crate::gateway::ExecutionStrategy::LocalCrossFormat
|
||||
ExecutionStrategy::LocalCrossFormat
|
||||
};
|
||||
let conversion_mode = if follow_up.plan.provider_api_format == follow_up.plan.client_api_format
|
||||
{
|
||||
crate::gateway::ConversionMode::None
|
||||
ConversionMode::None
|
||||
} else {
|
||||
crate::gateway::ConversionMode::Bidirectional
|
||||
ConversionMode::Bidirectional
|
||||
};
|
||||
|
||||
Ok(Some(GatewayControlSyncDecisionResponse {
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
use crate::gateway::{AppState, GatewayControlDecision, GatewayError};
|
||||
use crate::ai_pipeline::contracts::{
|
||||
GatewayControlPlanResponse, GatewayControlSyncDecisionResponse,
|
||||
};
|
||||
use crate::control::GatewayControlDecision;
|
||||
use crate::{AppState, GatewayError};
|
||||
|
||||
pub(crate) mod candidate_affinity;
|
||||
pub(crate) mod common;
|
||||
@@ -8,55 +12,6 @@ pub(crate) mod plan_builders;
|
||||
pub(crate) mod specialized;
|
||||
pub(crate) mod standard;
|
||||
|
||||
pub(crate) use self::candidate_affinity::prefer_local_tunnel_owner_candidates;
|
||||
pub(crate) use self::common::{
|
||||
parse_direct_request_body, CLAUDE_CHAT_STREAM_PLAN_KIND, CLAUDE_CHAT_SYNC_PLAN_KIND,
|
||||
CLAUDE_CLI_STREAM_PLAN_KIND, CLAUDE_CLI_SYNC_PLAN_KIND, EXECUTION_RUNTIME_STREAM_ACTION,
|
||||
EXECUTION_RUNTIME_STREAM_DECISION_ACTION, EXECUTION_RUNTIME_SYNC_ACTION,
|
||||
EXECUTION_RUNTIME_SYNC_DECISION_ACTION, GEMINI_CHAT_STREAM_PLAN_KIND,
|
||||
GEMINI_CHAT_SYNC_PLAN_KIND, GEMINI_CLI_STREAM_PLAN_KIND, GEMINI_CLI_SYNC_PLAN_KIND,
|
||||
GEMINI_FILES_DELETE_PLAN_KIND, GEMINI_FILES_DOWNLOAD_PLAN_KIND, GEMINI_FILES_GET_PLAN_KIND,
|
||||
GEMINI_FILES_LIST_PLAN_KIND, GEMINI_FILES_UPLOAD_PLAN_KIND, GEMINI_VIDEO_CANCEL_SYNC_PLAN_KIND,
|
||||
GEMINI_VIDEO_CREATE_SYNC_PLAN_KIND, OPENAI_CHAT_STREAM_PLAN_KIND, OPENAI_CHAT_SYNC_PLAN_KIND,
|
||||
OPENAI_CLI_STREAM_PLAN_KIND, OPENAI_CLI_SYNC_PLAN_KIND, OPENAI_COMPACT_STREAM_PLAN_KIND,
|
||||
OPENAI_COMPACT_SYNC_PLAN_KIND, OPENAI_VIDEO_CANCEL_SYNC_PLAN_KIND,
|
||||
OPENAI_VIDEO_CONTENT_PLAN_KIND, OPENAI_VIDEO_CREATE_SYNC_PLAN_KIND,
|
||||
OPENAI_VIDEO_DELETE_SYNC_PLAN_KIND, OPENAI_VIDEO_REMIX_SYNC_PLAN_KIND,
|
||||
};
|
||||
pub(crate) use crate::gateway::ai_pipeline::contracts::{
|
||||
build_gateway_plan_request, generic_decision_missing_exact_provider_request,
|
||||
GatewayControlPlanRequest, GatewayControlPlanResponse, GatewayControlSyncDecisionResponse,
|
||||
};
|
||||
pub(crate) use crate::gateway::ai_pipeline::conversion::request::{
|
||||
convert_openai_chat_request_to_claude_request, convert_openai_chat_request_to_gemini_request,
|
||||
convert_openai_chat_request_to_openai_cli_request, extract_openai_text_content,
|
||||
normalize_openai_cli_request_to_openai_chat_request, parse_openai_tool_result_content,
|
||||
};
|
||||
pub(crate) use crate::gateway::scheduler::{
|
||||
is_matching_stream_request,
|
||||
resolve_execution_runtime_stream_plan_kind as resolve_stream_plan_kind,
|
||||
resolve_execution_runtime_sync_plan_kind as resolve_sync_plan_kind,
|
||||
};
|
||||
pub(crate) use passthrough::{
|
||||
maybe_build_stream_local_same_format_provider_decision_payload,
|
||||
maybe_build_sync_local_same_format_provider_decision_payload,
|
||||
};
|
||||
pub(crate) use specialized::{
|
||||
maybe_build_stream_local_gemini_files_decision_payload,
|
||||
maybe_build_sync_local_gemini_files_decision_payload,
|
||||
maybe_build_sync_local_video_decision_payload,
|
||||
};
|
||||
pub(crate) use standard::{
|
||||
copy_request_number_field, copy_request_number_field_as,
|
||||
map_openai_reasoning_effort_to_claude_output, map_openai_reasoning_effort_to_gemini_budget,
|
||||
maybe_build_stream_local_decision_payload,
|
||||
maybe_build_stream_local_openai_cli_decision_payload,
|
||||
maybe_build_stream_local_standard_decision_payload, maybe_build_sync_local_decision_payload,
|
||||
maybe_build_sync_local_openai_cli_decision_payload,
|
||||
maybe_build_sync_local_standard_decision_payload, parse_openai_stop_sequences,
|
||||
resolve_openai_chat_max_tokens, value_as_u64,
|
||||
};
|
||||
|
||||
pub(crate) async fn maybe_build_sync_decision_payload(
|
||||
state: &AppState,
|
||||
parts: &http::request::Parts,
|
||||
|
||||
@@ -6,4 +6,4 @@ pub(crate) use self::provider::{
|
||||
maybe_build_stream_local_same_format_provider_decision_payload,
|
||||
maybe_build_sync_local_same_format_provider_decision_payload,
|
||||
};
|
||||
pub(crate) use crate::gateway::provider_transport::provider_type_supports_local_same_format_transport;
|
||||
pub(crate) use crate::provider_transport::provider_types::provider_type_supports_local_same_format_transport;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use aether_contracts::{ExecutionPlan, RequestBody};
|
||||
|
||||
use super::{augment_sync_report_context, LocalStreamPlanAndReport, LocalSyncPlanAndReport};
|
||||
use crate::gateway::{GatewayControlSyncDecisionResponse, GatewayError};
|
||||
use crate::{GatewayControlSyncDecisionResponse, GatewayError};
|
||||
|
||||
pub(crate) fn build_passthrough_sync_plan_from_decision(
|
||||
parts: &http::request::Parts,
|
||||
|
||||
@@ -8,41 +8,57 @@ use serde_json::{json, Value};
|
||||
use tracing::warn;
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::gateway::ai_pipeline::planner::plan_builders::{
|
||||
LocalStreamPlanAndReport, LocalSyncPlanAndReport,
|
||||
};
|
||||
use crate::gateway::ai_pipeline::planner::prefer_local_tunnel_owner_candidates;
|
||||
use crate::gateway::ai_pipeline::planner::{
|
||||
use crate::ai_pipeline::planner::candidate_affinity::prefer_local_tunnel_owner_candidates;
|
||||
use crate::ai_pipeline::planner::common::{
|
||||
EXECUTION_RUNTIME_STREAM_DECISION_ACTION, EXECUTION_RUNTIME_SYNC_DECISION_ACTION,
|
||||
};
|
||||
use crate::gateway::headers::collect_control_headers;
|
||||
use crate::gateway::provider_transport::{
|
||||
apply_local_body_rules, apply_local_header_rules, build_antigravity_safe_v1internal_request,
|
||||
build_antigravity_static_identity_headers, build_antigravity_v1internal_url,
|
||||
build_claude_code_messages_url, build_claude_code_passthrough_headers,
|
||||
build_claude_messages_url, build_gemini_content_url,
|
||||
build_kiro_generate_assistant_response_url, build_kiro_provider_headers,
|
||||
build_kiro_provider_request_body, build_openai_passthrough_headers, build_passthrough_headers,
|
||||
build_passthrough_path_url, build_vertex_api_key_gemini_content_url,
|
||||
classify_local_antigravity_request_support, ensure_upstream_auth_header,
|
||||
resolve_local_gemini_auth, resolve_local_standard_auth,
|
||||
resolve_local_vertex_api_key_query_auth, resolve_transport_execution_timeouts,
|
||||
resolve_transport_proxy_snapshot_with_tunnel_affinity, resolve_transport_tls_profile,
|
||||
sanitize_claude_code_request_body, supports_local_claude_code_transport_with_network,
|
||||
supports_local_gemini_transport_with_network,
|
||||
supports_local_kiro_request_transport_with_network,
|
||||
supports_local_standard_transport_with_network,
|
||||
supports_local_vertex_api_key_gemini_transport_with_network, AntigravityEnvelopeRequestType,
|
||||
AntigravityRequestEnvelopeSupport, AntigravityRequestSideSupport, AntigravityRequestUrlAction,
|
||||
LocalResolvedOAuthRequestAuth, KIRO_ENVELOPE_NAME,
|
||||
use crate::ai_pipeline::planner::plan_builders::{
|
||||
LocalStreamPlanAndReport, LocalSyncPlanAndReport,
|
||||
};
|
||||
use crate::gateway::scheduler::{
|
||||
use crate::control::GatewayControlDecision;
|
||||
use crate::execution_runtime::{ConversionMode, ExecutionStrategy};
|
||||
use crate::headers::collect_control_headers;
|
||||
use crate::provider_transport::antigravity::{
|
||||
build_antigravity_safe_v1internal_request, build_antigravity_static_identity_headers,
|
||||
build_antigravity_v1internal_url, classify_local_antigravity_request_support,
|
||||
AntigravityEnvelopeRequestType, AntigravityRequestEnvelopeSupport,
|
||||
AntigravityRequestSideSupport, AntigravityRequestUrlAction,
|
||||
};
|
||||
use crate::provider_transport::auth::{
|
||||
build_openai_passthrough_headers, resolve_local_gemini_auth, resolve_local_standard_auth,
|
||||
};
|
||||
use crate::provider_transport::claude_code::{
|
||||
build_claude_code_messages_url, build_claude_code_passthrough_headers,
|
||||
sanitize_claude_code_request_body, supports_local_claude_code_transport_with_network,
|
||||
};
|
||||
use crate::provider_transport::kiro::{
|
||||
build_kiro_generate_assistant_response_url, build_kiro_provider_headers,
|
||||
build_kiro_provider_request_body, supports_local_kiro_request_transport_with_network,
|
||||
KIRO_ENVELOPE_NAME,
|
||||
};
|
||||
use crate::provider_transport::policy::{
|
||||
supports_local_gemini_transport_with_network, supports_local_standard_transport_with_network,
|
||||
};
|
||||
use crate::provider_transport::url::{
|
||||
build_claude_messages_url, build_gemini_content_url, build_passthrough_path_url,
|
||||
};
|
||||
use crate::provider_transport::vertex::{
|
||||
build_vertex_api_key_gemini_content_url, resolve_local_vertex_api_key_query_auth,
|
||||
supports_local_vertex_api_key_gemini_transport_with_network,
|
||||
};
|
||||
use crate::provider_transport::{
|
||||
apply_local_body_rules, apply_local_header_rules, build_passthrough_headers,
|
||||
ensure_upstream_auth_header, resolve_transport_execution_timeouts,
|
||||
resolve_transport_proxy_snapshot_with_tunnel_affinity, resolve_transport_tls_profile,
|
||||
LocalResolvedOAuthRequestAuth,
|
||||
};
|
||||
use crate::scheduler::{
|
||||
current_unix_secs, list_selectable_candidates, record_local_request_candidate_status,
|
||||
GatewayMinimalCandidateSelectionCandidate,
|
||||
};
|
||||
use crate::gateway::{
|
||||
append_execution_contract_fields_to_value, AppState, ConversionMode, ExecutionStrategy,
|
||||
GatewayControlDecision, GatewayControlSyncDecisionResponse, GatewayError,
|
||||
use crate::{
|
||||
append_execution_contract_fields_to_value, AppState, GatewayControlSyncDecisionResponse,
|
||||
GatewayError,
|
||||
};
|
||||
|
||||
pub(crate) mod family;
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
use crate::gateway::{
|
||||
AppState, GatewayControlDecision, GatewayControlSyncDecisionResponse, GatewayError,
|
||||
};
|
||||
use crate::control::GatewayControlDecision;
|
||||
use crate::{AppState, GatewayControlSyncDecisionResponse, GatewayError};
|
||||
|
||||
use super::super::plans::{resolve_stream_spec, resolve_sync_spec};
|
||||
use super::candidates::{
|
||||
|
||||
@@ -3,11 +3,11 @@ use serde_json::json;
|
||||
use tracing::warn;
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::gateway::scheduler::{current_unix_secs, list_selectable_candidates};
|
||||
use crate::gateway::{
|
||||
append_execution_contract_fields_to_value, AppState, ConversionMode, ExecutionStrategy,
|
||||
GatewayControlDecision, GatewayError,
|
||||
};
|
||||
use crate::ai_pipeline::planner::candidate_affinity::prefer_local_tunnel_owner_candidates;
|
||||
use crate::control::GatewayControlDecision;
|
||||
use crate::execution_runtime::{ConversionMode, ExecutionStrategy};
|
||||
use crate::scheduler::{current_unix_secs, list_selectable_candidates};
|
||||
use crate::{append_execution_contract_fields_to_value, AppState, GatewayError};
|
||||
|
||||
use super::types::{
|
||||
LocalSameFormatProviderCandidateAttempt, LocalSameFormatProviderDecisionInput,
|
||||
@@ -83,10 +83,7 @@ pub(crate) async fn materialize_local_same_format_provider_candidate_attempts(
|
||||
current_unix_secs(),
|
||||
)
|
||||
.await?;
|
||||
let candidates = crate::gateway::ai_pipeline::planner::prefer_local_tunnel_owner_candidates(
|
||||
state, candidates,
|
||||
)
|
||||
.await;
|
||||
let candidates = prefer_local_tunnel_owner_candidates(state, candidates).await;
|
||||
|
||||
let created_at_unix_secs = current_unix_secs();
|
||||
let mut attempts = Vec::with_capacity(candidates.len());
|
||||
|
||||
@@ -4,28 +4,39 @@ use aether_data::repository::candidates::{RequestCandidateStatus, UpsertRequestC
|
||||
use serde_json::json;
|
||||
use tracing::warn;
|
||||
|
||||
use crate::gateway::headers::collect_control_headers;
|
||||
use crate::gateway::provider_transport::{
|
||||
apply_local_header_rules, build_antigravity_safe_v1internal_request,
|
||||
build_antigravity_static_identity_headers, build_claude_code_passthrough_headers,
|
||||
build_openai_passthrough_headers, build_passthrough_headers,
|
||||
classify_local_antigravity_request_support, ensure_upstream_auth_header,
|
||||
resolve_local_gemini_auth, resolve_local_standard_auth,
|
||||
resolve_local_vertex_api_key_query_auth, resolve_transport_execution_timeouts,
|
||||
resolve_transport_proxy_snapshot_with_tunnel_affinity, resolve_transport_tls_profile,
|
||||
supports_local_claude_code_transport_with_network,
|
||||
supports_local_gemini_transport_with_network,
|
||||
supports_local_kiro_request_transport_with_network,
|
||||
supports_local_standard_transport_with_network,
|
||||
supports_local_vertex_api_key_gemini_transport_with_network, AntigravityEnvelopeRequestType,
|
||||
use crate::execution_runtime::{ConversionMode, ExecutionStrategy};
|
||||
use crate::headers::collect_control_headers;
|
||||
use crate::provider_transport::antigravity::{
|
||||
build_antigravity_safe_v1internal_request, build_antigravity_static_identity_headers,
|
||||
classify_local_antigravity_request_support, AntigravityEnvelopeRequestType,
|
||||
AntigravityRequestEnvelopeSupport, AntigravityRequestSideSupport,
|
||||
LocalResolvedOAuthRequestAuth, KIRO_ENVELOPE_NAME,
|
||||
};
|
||||
use crate::gateway::scheduler::{current_unix_secs, GatewayMinimalCandidateSelectionCandidate};
|
||||
use crate::gateway::{
|
||||
append_execution_contract_fields_to_value, AppState, ConversionMode, ExecutionStrategy,
|
||||
GatewayControlSyncDecisionResponse, EXECUTION_RUNTIME_STREAM_DECISION_ACTION,
|
||||
EXECUTION_RUNTIME_SYNC_DECISION_ACTION,
|
||||
use crate::provider_transport::auth::{
|
||||
build_openai_passthrough_headers, resolve_local_gemini_auth, resolve_local_standard_auth,
|
||||
};
|
||||
use crate::provider_transport::claude_code::{
|
||||
build_claude_code_passthrough_headers, supports_local_claude_code_transport_with_network,
|
||||
};
|
||||
use crate::provider_transport::kiro::{
|
||||
build_kiro_provider_headers, supports_local_kiro_request_transport_with_network,
|
||||
KIRO_ENVELOPE_NAME,
|
||||
};
|
||||
use crate::provider_transport::policy::{
|
||||
supports_local_gemini_transport_with_network, supports_local_standard_transport_with_network,
|
||||
};
|
||||
use crate::provider_transport::vertex::{
|
||||
resolve_local_vertex_api_key_query_auth,
|
||||
supports_local_vertex_api_key_gemini_transport_with_network,
|
||||
};
|
||||
use crate::provider_transport::{
|
||||
apply_local_header_rules, build_passthrough_headers, ensure_upstream_auth_header,
|
||||
resolve_transport_execution_timeouts, resolve_transport_proxy_snapshot_with_tunnel_affinity,
|
||||
resolve_transport_tls_profile, LocalResolvedOAuthRequestAuth,
|
||||
};
|
||||
use crate::scheduler::{current_unix_secs, GatewayMinimalCandidateSelectionCandidate};
|
||||
use crate::{
|
||||
append_execution_contract_fields_to_value, AppState, GatewayControlSyncDecisionResponse,
|
||||
EXECUTION_RUNTIME_STREAM_DECISION_ACTION, EXECUTION_RUNTIME_SYNC_DECISION_ACTION,
|
||||
};
|
||||
|
||||
use super::types::{
|
||||
@@ -351,7 +362,7 @@ pub(crate) async fn maybe_build_local_same_format_provider_decision_payload_for_
|
||||
};
|
||||
|
||||
let Some(provider_request_headers) = (if let Some(kiro_auth) = kiro_auth.as_ref() {
|
||||
crate::gateway::provider_transport::build_kiro_provider_headers(
|
||||
build_kiro_provider_headers(
|
||||
&parts.headers,
|
||||
&provider_request_body,
|
||||
body_json,
|
||||
@@ -445,6 +456,7 @@ pub(crate) async fn maybe_build_local_same_format_provider_decision_payload_for_
|
||||
"provider_id": candidate.provider_id,
|
||||
"endpoint_id": candidate.endpoint_id,
|
||||
"key_id": candidate.key_id,
|
||||
"key_name": candidate.key_name,
|
||||
"provider_api_format": spec.api_format,
|
||||
"client_api_format": spec.api_format,
|
||||
"mapped_model": mapped_model,
|
||||
|
||||
@@ -15,14 +15,14 @@ pub(crate) struct LocalSameFormatProviderSpec {
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub(crate) struct LocalSameFormatProviderDecisionInput {
|
||||
pub(crate) auth_context: crate::gateway::GatewayControlAuthContext,
|
||||
pub(crate) auth_context: crate::control::GatewayControlAuthContext,
|
||||
pub(crate) requested_model: String,
|
||||
pub(crate) auth_snapshot: crate::gateway::gateway_data::StoredGatewayAuthApiKeySnapshot,
|
||||
pub(crate) auth_snapshot: crate::data::auth::GatewayAuthApiKeySnapshot,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub(crate) struct LocalSameFormatProviderCandidateAttempt {
|
||||
pub(crate) candidate: crate::gateway::scheduler::GatewayMinimalCandidateSelectionCandidate,
|
||||
pub(crate) candidate: crate::scheduler::GatewayMinimalCandidateSelectionCandidate,
|
||||
pub(crate) candidate_index: u32,
|
||||
pub(crate) candidate_id: String,
|
||||
}
|
||||
|
||||
@@ -7,15 +7,15 @@ use super::{
|
||||
GatewayError, LocalSameFormatProviderFamily, LocalSameFormatProviderSpec,
|
||||
LocalStreamPlanAndReport, LocalSyncPlanAndReport,
|
||||
};
|
||||
use crate::gateway::ai_pipeline::planner::plan_builders::{
|
||||
build_gemini_stream_plan_from_decision, build_gemini_sync_plan_from_decision,
|
||||
build_standard_stream_plan_from_decision, build_standard_sync_plan_from_decision,
|
||||
};
|
||||
use crate::gateway::ai_pipeline::planner::{
|
||||
use crate::ai_pipeline::planner::common::{
|
||||
CLAUDE_CHAT_STREAM_PLAN_KIND, CLAUDE_CHAT_SYNC_PLAN_KIND, CLAUDE_CLI_STREAM_PLAN_KIND,
|
||||
CLAUDE_CLI_SYNC_PLAN_KIND, GEMINI_CHAT_STREAM_PLAN_KIND, GEMINI_CHAT_SYNC_PLAN_KIND,
|
||||
GEMINI_CLI_STREAM_PLAN_KIND, GEMINI_CLI_SYNC_PLAN_KIND,
|
||||
};
|
||||
use crate::ai_pipeline::planner::plan_builders::{
|
||||
build_gemini_stream_plan_from_decision, build_gemini_sync_plan_from_decision,
|
||||
build_standard_stream_plan_from_decision, build_standard_sync_plan_from_decision,
|
||||
};
|
||||
|
||||
pub(crate) fn resolve_sync_spec(plan_kind: &str) -> Option<LocalSameFormatProviderSpec> {
|
||||
match plan_kind {
|
||||
|
||||
@@ -18,7 +18,7 @@ pub(super) fn build_same_format_provider_request_body(
|
||||
spec: LocalSameFormatProviderSpec,
|
||||
body_rules: Option<&Value>,
|
||||
upstream_is_stream: bool,
|
||||
kiro_auth: Option<&crate::gateway::provider_transport::KiroRequestAuth>,
|
||||
kiro_auth: Option<&crate::provider_transport::kiro::KiroRequestAuth>,
|
||||
is_claude_code: bool,
|
||||
) -> Option<Value> {
|
||||
if let Some(kiro_auth) = kiro_auth {
|
||||
@@ -60,11 +60,11 @@ pub(super) fn build_same_format_provider_request_body(
|
||||
|
||||
pub(super) fn build_same_format_upstream_url(
|
||||
parts: &http::request::Parts,
|
||||
transport: &crate::gateway::provider_transport::GatewayProviderTransportSnapshot,
|
||||
transport: &crate::provider_transport::GatewayProviderTransportSnapshot,
|
||||
mapped_model: &str,
|
||||
spec: LocalSameFormatProviderSpec,
|
||||
upstream_is_stream: bool,
|
||||
kiro_auth: Option<&crate::gateway::provider_transport::KiroRequestAuth>,
|
||||
kiro_auth: Option<&crate::provider_transport::kiro::KiroRequestAuth>,
|
||||
) -> Option<String> {
|
||||
if let Some(kiro_auth) = kiro_auth {
|
||||
return build_kiro_generate_assistant_response_url(
|
||||
|
||||
@@ -2,8 +2,8 @@ use std::collections::BTreeMap;
|
||||
|
||||
use aether_contracts::ExecutionPlan;
|
||||
|
||||
pub(crate) use crate::gateway::ai_pipeline::planner::generic_decision_missing_exact_provider_request;
|
||||
use crate::gateway::{GatewayControlSyncDecisionResponse, GatewayError};
|
||||
pub(crate) use crate::ai_pipeline::contracts::generic_decision_missing_exact_provider_request;
|
||||
use crate::{GatewayControlSyncDecisionResponse, GatewayError};
|
||||
|
||||
pub(crate) struct LocalSyncPlanAndReport {
|
||||
pub(crate) plan: ExecutionPlan,
|
||||
|
||||
@@ -5,30 +5,32 @@ use serde_json::json;
|
||||
use tracing::warn;
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::gateway::ai_pipeline::planner::plan_builders::{
|
||||
build_passthrough_stream_plan_from_decision, build_passthrough_sync_plan_from_decision,
|
||||
LocalStreamPlanAndReport, LocalSyncPlanAndReport,
|
||||
};
|
||||
use crate::gateway::ai_pipeline::planner::prefer_local_tunnel_owner_candidates;
|
||||
use crate::gateway::ai_pipeline::planner::{
|
||||
use crate::ai_pipeline::planner::candidate_affinity::prefer_local_tunnel_owner_candidates;
|
||||
use crate::ai_pipeline::planner::common::{
|
||||
EXECUTION_RUNTIME_STREAM_DECISION_ACTION, EXECUTION_RUNTIME_SYNC_DECISION_ACTION,
|
||||
GEMINI_FILES_DELETE_PLAN_KIND, GEMINI_FILES_DOWNLOAD_PLAN_KIND, GEMINI_FILES_GET_PLAN_KIND,
|
||||
GEMINI_FILES_LIST_PLAN_KIND, GEMINI_FILES_UPLOAD_PLAN_KIND,
|
||||
};
|
||||
use crate::gateway::headers::collect_control_headers;
|
||||
use crate::gateway::provider_transport::{
|
||||
apply_local_body_rules, apply_local_header_rules, build_gemini_files_passthrough_url,
|
||||
build_passthrough_headers_with_auth, resolve_local_gemini_auth,
|
||||
resolve_transport_execution_timeouts, resolve_transport_proxy_snapshot_with_tunnel_affinity,
|
||||
resolve_transport_tls_profile, supports_local_gemini_transport_with_network,
|
||||
use crate::ai_pipeline::planner::plan_builders::{
|
||||
build_passthrough_stream_plan_from_decision, build_passthrough_sync_plan_from_decision,
|
||||
LocalStreamPlanAndReport, LocalSyncPlanAndReport,
|
||||
};
|
||||
use crate::gateway::scheduler::{
|
||||
use crate::control::GatewayControlDecision;
|
||||
use crate::headers::collect_control_headers;
|
||||
use crate::provider_transport::auth::{
|
||||
build_passthrough_headers_with_auth, resolve_local_gemini_auth,
|
||||
};
|
||||
use crate::provider_transport::policy::supports_local_gemini_transport_with_network;
|
||||
use crate::provider_transport::url::build_gemini_files_passthrough_url;
|
||||
use crate::provider_transport::{
|
||||
apply_local_body_rules, apply_local_header_rules, resolve_transport_execution_timeouts,
|
||||
resolve_transport_proxy_snapshot_with_tunnel_affinity, resolve_transport_tls_profile,
|
||||
};
|
||||
use crate::scheduler::{
|
||||
current_unix_secs, list_selectable_candidates_for_required_capability_without_requested_model,
|
||||
record_local_request_candidate_status, GatewayMinimalCandidateSelectionCandidate,
|
||||
};
|
||||
use crate::gateway::{
|
||||
AppState, GatewayControlDecision, GatewayControlSyncDecisionResponse, GatewayError,
|
||||
};
|
||||
use crate::{AppState, GatewayControlSyncDecisionResponse, GatewayError};
|
||||
|
||||
const GEMINI_FILES_CANDIDATE_API_FORMAT: &str = "gemini:chat";
|
||||
const GEMINI_FILES_CLIENT_API_FORMAT: &str = "gemini:files";
|
||||
@@ -43,8 +45,8 @@ struct LocalGeminiFilesSpec {
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
struct LocalGeminiFilesDecisionInput {
|
||||
auth_context: crate::gateway::GatewayControlAuthContext,
|
||||
auth_snapshot: crate::gateway::gateway_data::StoredGatewayAuthApiKeySnapshot,
|
||||
auth_context: crate::control::GatewayControlAuthContext,
|
||||
auth_snapshot: crate::data::auth::GatewayAuthApiKeySnapshot,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
@@ -663,11 +665,15 @@ async fn maybe_build_local_gemini_files_decision_payload_for_candidate(
|
||||
},
|
||||
decision_kind: Some(spec.decision_kind.to_string()),
|
||||
execution_strategy: Some(
|
||||
crate::gateway::ExecutionStrategy::LocalSameFormat
|
||||
crate::execution_runtime::ExecutionStrategy::LocalSameFormat
|
||||
.as_str()
|
||||
.to_string(),
|
||||
),
|
||||
conversion_mode: Some(
|
||||
crate::execution_runtime::ConversionMode::None
|
||||
.as_str()
|
||||
.to_string(),
|
||||
),
|
||||
conversion_mode: Some(crate::gateway::ConversionMode::None.as_str().to_string()),
|
||||
request_id: Some(trace_id.to_string()),
|
||||
candidate_id: Some(candidate_id.clone()),
|
||||
provider_name: Some(transport.provider.name.clone()),
|
||||
|
||||
@@ -5,29 +5,34 @@ use serde_json::{json, Value};
|
||||
use tracing::warn;
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::gateway::ai_pipeline::planner::plan_builders::{
|
||||
build_passthrough_sync_plan_from_decision, LocalSyncPlanAndReport,
|
||||
};
|
||||
use crate::gateway::ai_pipeline::planner::prefer_local_tunnel_owner_candidates;
|
||||
use crate::gateway::ai_pipeline::planner::{
|
||||
use crate::ai_pipeline::planner::candidate_affinity::prefer_local_tunnel_owner_candidates;
|
||||
use crate::ai_pipeline::planner::common::{
|
||||
EXECUTION_RUNTIME_SYNC_DECISION_ACTION, GEMINI_VIDEO_CREATE_SYNC_PLAN_KIND,
|
||||
OPENAI_VIDEO_CREATE_SYNC_PLAN_KIND,
|
||||
};
|
||||
use crate::gateway::headers::collect_control_headers;
|
||||
use crate::gateway::provider_transport::{
|
||||
apply_local_body_rules, apply_local_header_rules, build_gemini_video_predict_long_running_url,
|
||||
build_passthrough_headers_with_auth, build_passthrough_path_url, resolve_local_gemini_auth,
|
||||
resolve_local_openai_chat_auth, resolve_transport_execution_timeouts,
|
||||
resolve_transport_proxy_snapshot_with_tunnel_affinity, resolve_transport_tls_profile,
|
||||
use crate::ai_pipeline::planner::plan_builders::{
|
||||
build_passthrough_sync_plan_from_decision, LocalSyncPlanAndReport,
|
||||
};
|
||||
use crate::control::GatewayControlDecision;
|
||||
use crate::headers::collect_control_headers;
|
||||
use crate::provider_transport::auth::{
|
||||
build_passthrough_headers_with_auth, resolve_local_gemini_auth, resolve_local_openai_chat_auth,
|
||||
};
|
||||
use crate::provider_transport::policy::{
|
||||
supports_local_gemini_transport_with_network, supports_local_standard_transport_with_network,
|
||||
};
|
||||
use crate::gateway::scheduler::{
|
||||
use crate::provider_transport::url::{
|
||||
build_gemini_video_predict_long_running_url, build_passthrough_path_url,
|
||||
};
|
||||
use crate::provider_transport::{
|
||||
apply_local_body_rules, apply_local_header_rules, resolve_transport_execution_timeouts,
|
||||
resolve_transport_proxy_snapshot_with_tunnel_affinity, resolve_transport_tls_profile,
|
||||
};
|
||||
use crate::scheduler::{
|
||||
current_unix_secs, list_selectable_candidates, record_local_request_candidate_status,
|
||||
GatewayMinimalCandidateSelectionCandidate,
|
||||
};
|
||||
use crate::gateway::{
|
||||
AppState, GatewayControlDecision, GatewayControlSyncDecisionResponse, GatewayError,
|
||||
};
|
||||
use crate::{AppState, GatewayControlSyncDecisionResponse, GatewayError};
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
enum LocalVideoCreateFamily {
|
||||
@@ -45,9 +50,9 @@ struct LocalVideoCreateSpec {
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
struct LocalVideoCreateDecisionInput {
|
||||
auth_context: crate::gateway::GatewayControlAuthContext,
|
||||
auth_context: crate::control::GatewayControlAuthContext,
|
||||
requested_model: String,
|
||||
auth_snapshot: crate::gateway::gateway_data::StoredGatewayAuthApiKeySnapshot,
|
||||
auth_snapshot: crate::data::auth::GatewayAuthApiKeySnapshot,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
@@ -457,11 +462,15 @@ async fn maybe_build_local_video_create_decision_payload_for_candidate(
|
||||
action: EXECUTION_RUNTIME_SYNC_DECISION_ACTION.to_string(),
|
||||
decision_kind: Some(spec.decision_kind.to_string()),
|
||||
execution_strategy: Some(
|
||||
crate::gateway::ExecutionStrategy::LocalSameFormat
|
||||
crate::execution_runtime::ExecutionStrategy::LocalSameFormat
|
||||
.as_str()
|
||||
.to_string(),
|
||||
),
|
||||
conversion_mode: Some(
|
||||
crate::execution_runtime::ConversionMode::None
|
||||
.as_str()
|
||||
.to_string(),
|
||||
),
|
||||
conversion_mode: Some(crate::gateway::ConversionMode::None.as_str().to_string()),
|
||||
request_id: Some(trace_id.to_string()),
|
||||
candidate_id: Some(candidate_id.clone()),
|
||||
provider_name: Some(transport.provider.name.clone()),
|
||||
@@ -543,7 +552,7 @@ fn build_provider_request_body(
|
||||
|
||||
fn build_video_upstream_url(
|
||||
parts: &http::request::Parts,
|
||||
transport: &crate::gateway::provider_transport::GatewayProviderTransportSnapshot,
|
||||
transport: &crate::provider_transport::GatewayProviderTransportSnapshot,
|
||||
mapped_model: &str,
|
||||
family: LocalVideoCreateFamily,
|
||||
) -> Option<String> {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use crate::gateway::ai_pipeline::planner::{
|
||||
use crate::ai_pipeline::planner::common::{
|
||||
CLAUDE_CHAT_STREAM_PLAN_KIND, CLAUDE_CHAT_SYNC_PLAN_KIND,
|
||||
};
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use crate::gateway::ai_pipeline::planner::{
|
||||
use crate::ai_pipeline::planner::common::{
|
||||
CLAUDE_CLI_STREAM_PLAN_KIND, CLAUDE_CLI_SYNC_PLAN_KIND,
|
||||
};
|
||||
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
use crate::gateway::{
|
||||
AppState, GatewayControlDecision, GatewayControlSyncDecisionResponse, GatewayError,
|
||||
};
|
||||
use crate::control::GatewayControlDecision;
|
||||
use crate::{AppState, GatewayControlSyncDecisionResponse, GatewayError};
|
||||
|
||||
use super::family::{
|
||||
maybe_build_stream_via_standard_family_payload, maybe_build_sync_via_standard_family_payload,
|
||||
};
|
||||
pub(crate) use crate::gateway::ai_pipeline::conversion::request::normalize_claude_request_to_openai_chat_request;
|
||||
pub(crate) use crate::ai_pipeline::conversion::request::normalize_claude_request_to_openai_chat_request;
|
||||
|
||||
pub(crate) mod chat;
|
||||
pub(crate) mod cli;
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
use tracing::warn;
|
||||
|
||||
use crate::gateway::ai_pipeline::planner::plan_builders::{
|
||||
use crate::ai_pipeline::planner::plan_builders::{
|
||||
build_gemini_stream_plan_from_decision, build_gemini_sync_plan_from_decision,
|
||||
build_standard_stream_plan_from_decision, build_standard_sync_plan_from_decision,
|
||||
LocalStreamPlanAndReport, LocalSyncPlanAndReport,
|
||||
};
|
||||
use crate::gateway::{
|
||||
AppState, GatewayControlDecision, GatewayControlSyncDecisionResponse, GatewayError,
|
||||
};
|
||||
use crate::control::GatewayControlDecision;
|
||||
use crate::{AppState, GatewayControlSyncDecisionResponse, GatewayError};
|
||||
|
||||
use super::candidates::{
|
||||
materialize_local_standard_candidate_attempts, resolve_local_standard_decision_input,
|
||||
|
||||
@@ -5,13 +5,13 @@ use serde_json::json;
|
||||
use tracing::warn;
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::gateway::scheduler::{
|
||||
use crate::ai_pipeline::planner::candidate_affinity::prefer_local_tunnel_owner_candidates;
|
||||
use crate::control::GatewayControlDecision;
|
||||
use crate::execution_runtime::{ConversionMode, ExecutionStrategy};
|
||||
use crate::scheduler::{
|
||||
current_unix_secs, list_selectable_candidates, GatewayMinimalCandidateSelectionCandidate,
|
||||
};
|
||||
use crate::gateway::{
|
||||
append_execution_contract_fields_to_value, AppState, ConversionMode, ExecutionStrategy,
|
||||
GatewayControlDecision, GatewayError,
|
||||
};
|
||||
use crate::{append_execution_contract_fields_to_value, AppState, GatewayError};
|
||||
|
||||
use super::types::{
|
||||
LocalStandardCandidateAttempt, LocalStandardDecisionInput, LocalStandardSourceFamily,
|
||||
@@ -117,10 +117,7 @@ pub(super) async fn materialize_local_standard_candidate_attempts(
|
||||
}
|
||||
}
|
||||
}
|
||||
let candidates = crate::gateway::ai_pipeline::planner::prefer_local_tunnel_owner_candidates(
|
||||
state, candidates,
|
||||
)
|
||||
.await;
|
||||
let candidates = prefer_local_tunnel_owner_candidates(state, candidates).await;
|
||||
|
||||
let created_at_unix_secs = current_unix_secs();
|
||||
let mut attempts = Vec::with_capacity(candidates.len());
|
||||
@@ -132,7 +129,7 @@ pub(super) async fn materialize_local_standard_candidate_attempts(
|
||||
} else {
|
||||
ExecutionStrategy::LocalCrossFormat
|
||||
};
|
||||
let conversion_mode = if crate::gateway::ai_pipeline::conversion::request_conversion_kind(
|
||||
let conversion_mode = if crate::ai_pipeline::conversion::request_conversion_kind(
|
||||
spec.api_format,
|
||||
provider_api_format.as_str(),
|
||||
)
|
||||
@@ -213,7 +210,7 @@ pub(super) async fn materialize_local_standard_candidate_attempts(
|
||||
}
|
||||
|
||||
fn auth_snapshot_allows_cross_format_candidate(
|
||||
auth_snapshot: &crate::gateway::gateway_data::StoredGatewayAuthApiKeySnapshot,
|
||||
auth_snapshot: &crate::data::auth::GatewayAuthApiKeySnapshot,
|
||||
requested_model: &str,
|
||||
candidate: &GatewayMinimalCandidateSelectionCandidate,
|
||||
) -> bool {
|
||||
|
||||
@@ -4,17 +4,20 @@ use aether_data::repository::candidates::{RequestCandidateStatus, UpsertRequestC
|
||||
use serde_json::json;
|
||||
use tracing::warn;
|
||||
|
||||
use crate::gateway::headers::collect_control_headers;
|
||||
use crate::gateway::provider_transport::{
|
||||
apply_local_header_rules, build_openai_passthrough_headers, ensure_upstream_auth_header,
|
||||
resolve_transport_execution_timeouts, resolve_transport_proxy_snapshot_with_tunnel_affinity,
|
||||
resolve_transport_tls_profile, LocalResolvedOAuthRequestAuth,
|
||||
use crate::execution_runtime::{ConversionMode, ExecutionStrategy};
|
||||
use crate::headers::collect_control_headers;
|
||||
use crate::provider_transport::auth::{
|
||||
build_openai_passthrough_headers, ensure_upstream_auth_header,
|
||||
};
|
||||
use crate::gateway::scheduler::current_unix_secs;
|
||||
use crate::gateway::{
|
||||
append_execution_contract_fields_to_value, AppState, ConversionMode, ExecutionStrategy,
|
||||
GatewayControlSyncDecisionResponse, EXECUTION_RUNTIME_STREAM_DECISION_ACTION,
|
||||
EXECUTION_RUNTIME_SYNC_DECISION_ACTION,
|
||||
use crate::provider_transport::{
|
||||
apply_local_header_rules, resolve_transport_execution_timeouts,
|
||||
resolve_transport_proxy_snapshot_with_tunnel_affinity, resolve_transport_tls_profile,
|
||||
LocalResolvedOAuthRequestAuth,
|
||||
};
|
||||
use crate::scheduler::current_unix_secs;
|
||||
use crate::{
|
||||
append_execution_contract_fields_to_value, AppState, GatewayControlSyncDecisionResponse,
|
||||
EXECUTION_RUNTIME_STREAM_DECISION_ACTION, EXECUTION_RUNTIME_SYNC_DECISION_ACTION,
|
||||
};
|
||||
|
||||
use super::types::{LocalStandardCandidateAttempt, LocalStandardDecisionInput, LocalStandardSpec};
|
||||
@@ -34,7 +37,7 @@ pub(super) async fn maybe_build_local_standard_decision_payload_for_candidate(
|
||||
candidate_id,
|
||||
} = attempt;
|
||||
let provider_api_format = candidate.endpoint_api_format.trim().to_ascii_lowercase();
|
||||
let Some(conversion_kind) = crate::gateway::ai_pipeline::conversion::request_conversion_kind(
|
||||
let Some(conversion_kind) = crate::ai_pipeline::conversion::request_conversion_kind(
|
||||
spec.api_format,
|
||||
provider_api_format.as_str(),
|
||||
) else {
|
||||
@@ -87,7 +90,7 @@ pub(super) async fn maybe_build_local_standard_decision_payload_for_candidate(
|
||||
}
|
||||
};
|
||||
|
||||
if !crate::gateway::ai_pipeline::conversion::request_conversion_transport_supported(
|
||||
if !crate::ai_pipeline::conversion::request_conversion_transport_supported(
|
||||
&transport,
|
||||
conversion_kind,
|
||||
) {
|
||||
@@ -104,7 +107,7 @@ pub(super) async fn maybe_build_local_standard_decision_payload_for_candidate(
|
||||
return None;
|
||||
}
|
||||
|
||||
let resolved_auth = crate::gateway::ai_pipeline::conversion::request_conversion_direct_auth(
|
||||
let resolved_auth = crate::ai_pipeline::conversion::request_conversion_direct_auth(
|
||||
&transport,
|
||||
conversion_kind,
|
||||
);
|
||||
@@ -158,7 +161,7 @@ pub(super) async fn maybe_build_local_standard_decision_payload_for_candidate(
|
||||
}
|
||||
|
||||
let provider_request_body =
|
||||
match crate::gateway::ai_pipeline::planner::standard::build_standard_request_body(
|
||||
match crate::ai_pipeline::planner::standard::build_standard_request_body(
|
||||
body_json,
|
||||
spec.api_format,
|
||||
&mapped_model,
|
||||
@@ -184,7 +187,7 @@ pub(super) async fn maybe_build_local_standard_decision_payload_for_candidate(
|
||||
};
|
||||
|
||||
let upstream_url =
|
||||
match crate::gateway::ai_pipeline::planner::standard::build_standard_upstream_url(
|
||||
match crate::ai_pipeline::planner::standard::build_standard_upstream_url(
|
||||
parts,
|
||||
&transport,
|
||||
&mapped_model,
|
||||
@@ -290,6 +293,7 @@ pub(super) async fn maybe_build_local_standard_decision_payload_for_candidate(
|
||||
"provider_id": candidate.provider_id,
|
||||
"endpoint_id": candidate.endpoint_id,
|
||||
"key_id": candidate.key_id,
|
||||
"key_name": candidate.key_name,
|
||||
"provider_api_format": provider_api_format,
|
||||
"client_api_format": spec.api_format,
|
||||
"mapped_model": mapped_model,
|
||||
@@ -315,7 +319,7 @@ pub(super) async fn mark_skipped_local_standard_candidate(
|
||||
state: &AppState,
|
||||
input: &LocalStandardDecisionInput,
|
||||
trace_id: &str,
|
||||
candidate: &crate::gateway::scheduler::GatewayMinimalCandidateSelectionCandidate,
|
||||
candidate: &crate::scheduler::GatewayMinimalCandidateSelectionCandidate,
|
||||
candidate_index: u32,
|
||||
candidate_id: &str,
|
||||
skip_reason: &'static str,
|
||||
|
||||
@@ -22,14 +22,14 @@ pub(crate) struct LocalStandardSpec {
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub(super) struct LocalStandardDecisionInput {
|
||||
pub(super) auth_context: crate::gateway::GatewayControlAuthContext,
|
||||
pub(super) auth_context: crate::control::GatewayControlAuthContext,
|
||||
pub(super) requested_model: String,
|
||||
pub(super) auth_snapshot: crate::gateway::gateway_data::StoredGatewayAuthApiKeySnapshot,
|
||||
pub(super) auth_snapshot: crate::data::auth::GatewayAuthApiKeySnapshot,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub(super) struct LocalStandardCandidateAttempt {
|
||||
pub(super) candidate: crate::gateway::scheduler::GatewayMinimalCandidateSelectionCandidate,
|
||||
pub(super) candidate: crate::scheduler::GatewayMinimalCandidateSelectionCandidate,
|
||||
pub(super) candidate_index: u32,
|
||||
pub(super) candidate_id: String,
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use crate::gateway::ai_pipeline::planner::{
|
||||
use crate::ai_pipeline::planner::common::{
|
||||
GEMINI_CHAT_STREAM_PLAN_KIND, GEMINI_CHAT_SYNC_PLAN_KIND,
|
||||
};
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use crate::gateway::ai_pipeline::planner::{
|
||||
use crate::ai_pipeline::planner::common::{
|
||||
GEMINI_CLI_STREAM_PLAN_KIND, GEMINI_CLI_SYNC_PLAN_KIND,
|
||||
};
|
||||
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
use crate::gateway::{
|
||||
AppState, GatewayControlDecision, GatewayControlSyncDecisionResponse, GatewayError,
|
||||
};
|
||||
use crate::control::GatewayControlDecision;
|
||||
use crate::{AppState, GatewayControlSyncDecisionResponse, GatewayError};
|
||||
|
||||
use super::family::{
|
||||
maybe_build_stream_via_standard_family_payload, maybe_build_sync_via_standard_family_payload,
|
||||
};
|
||||
pub(crate) use crate::gateway::ai_pipeline::conversion::request::normalize_gemini_request_to_openai_chat_request;
|
||||
pub(crate) use crate::ai_pipeline::conversion::request::normalize_gemini_request_to_openai_chat_request;
|
||||
|
||||
pub(crate) mod chat;
|
||||
pub(crate) mod cli;
|
||||
|
||||
@@ -4,8 +4,8 @@ use super::{
|
||||
augment_sync_report_context, generic_decision_missing_exact_provider_request,
|
||||
LocalStreamPlanAndReport, LocalSyncPlanAndReport,
|
||||
};
|
||||
use crate::gateway::provider_transport::ensure_upstream_auth_header;
|
||||
use crate::gateway::{GatewayControlSyncDecisionResponse, GatewayError};
|
||||
use crate::provider_transport::ensure_upstream_auth_header;
|
||||
use crate::{GatewayControlSyncDecisionResponse, GatewayError};
|
||||
|
||||
pub(crate) fn build_gemini_sync_plan_from_decision(
|
||||
_parts: &http::request::Parts,
|
||||
|
||||
@@ -6,15 +6,16 @@ use super::{
|
||||
claude::normalize_claude_request_to_openai_chat_request,
|
||||
gemini::normalize_gemini_request_to_openai_chat_request,
|
||||
};
|
||||
use crate::gateway::ai_pipeline::conversion::request::{
|
||||
use crate::ai_pipeline::conversion::request::{
|
||||
convert_openai_chat_request_to_claude_request, convert_openai_chat_request_to_gemini_request,
|
||||
convert_openai_chat_request_to_openai_cli_request,
|
||||
normalize_openai_cli_request_to_openai_chat_request,
|
||||
};
|
||||
use crate::gateway::ai_pipeline::conversion::{request_conversion_kind, RequestConversionKind};
|
||||
use crate::gateway::provider_transport::{
|
||||
apply_local_body_rules, build_claude_messages_url, build_gemini_content_url,
|
||||
build_openai_chat_url, build_openai_cli_url, build_passthrough_path_url,
|
||||
use crate::ai_pipeline::conversion::{request_conversion_kind, RequestConversionKind};
|
||||
use crate::provider_transport::apply_local_body_rules;
|
||||
use crate::provider_transport::url::{
|
||||
build_claude_messages_url, build_gemini_content_url, build_openai_chat_url,
|
||||
build_openai_cli_url, build_passthrough_path_url,
|
||||
};
|
||||
|
||||
pub(crate) fn build_standard_request_body(
|
||||
@@ -72,7 +73,7 @@ pub(crate) fn build_standard_request_body(
|
||||
|
||||
pub(crate) fn build_standard_upstream_url(
|
||||
parts: &http::request::Parts,
|
||||
transport: &crate::gateway::provider_transport::GatewayProviderTransportSnapshot,
|
||||
transport: &crate::provider_transport::GatewayProviderTransportSnapshot,
|
||||
mapped_model: &str,
|
||||
provider_api_format: &str,
|
||||
upstream_is_stream: bool,
|
||||
|
||||
@@ -3,17 +3,15 @@
|
||||
//! This groups the standard planning surface in one place:
|
||||
//! request-side conversion, matrix registry, and decision payload builders.
|
||||
|
||||
use crate::gateway::{
|
||||
AppState, GatewayControlDecision, GatewayControlSyncDecisionResponse, GatewayError,
|
||||
};
|
||||
use crate::control::GatewayControlDecision;
|
||||
use crate::{AppState, GatewayControlSyncDecisionResponse, GatewayError};
|
||||
|
||||
pub(crate) mod claude;
|
||||
pub(crate) mod family;
|
||||
pub(crate) mod gemini;
|
||||
mod matrix;
|
||||
mod normalize;
|
||||
pub(crate) mod openai_chat;
|
||||
pub(crate) mod openai_cli;
|
||||
pub(crate) mod openai;
|
||||
|
||||
pub(crate) use self::matrix::{
|
||||
build_standard_request_body, build_standard_upstream_url,
|
||||
@@ -25,22 +23,20 @@ pub(crate) use self::normalize::{
|
||||
build_local_openai_chat_request_body, build_local_openai_chat_upstream_url,
|
||||
build_local_openai_cli_request_body, build_local_openai_cli_upstream_url,
|
||||
};
|
||||
pub(crate) use self::openai_chat::{
|
||||
pub(crate) use self::openai::{
|
||||
copy_request_number_field, copy_request_number_field_as,
|
||||
map_openai_reasoning_effort_to_claude_output, map_openai_reasoning_effort_to_gemini_budget,
|
||||
maybe_build_stream_local_decision_payload, maybe_build_sync_local_decision_payload,
|
||||
parse_openai_stop_sequences, resolve_openai_chat_max_tokens, value_as_u64,
|
||||
maybe_build_stream_local_decision_payload,
|
||||
maybe_build_stream_local_openai_cli_decision_payload, maybe_build_sync_local_decision_payload,
|
||||
maybe_build_sync_local_openai_cli_decision_payload, parse_openai_stop_sequences,
|
||||
resolve_openai_chat_max_tokens, value_as_u64,
|
||||
};
|
||||
pub(crate) use self::openai_cli::{
|
||||
maybe_build_stream_local_openai_cli_decision_payload,
|
||||
maybe_build_sync_local_openai_cli_decision_payload,
|
||||
};
|
||||
pub(crate) use crate::gateway::ai_pipeline::conversion::request::{
|
||||
pub(crate) use crate::ai_pipeline::conversion::request::{
|
||||
convert_openai_chat_request_to_claude_request, convert_openai_chat_request_to_gemini_request,
|
||||
convert_openai_chat_request_to_openai_cli_request, extract_openai_text_content,
|
||||
normalize_openai_cli_request_to_openai_chat_request, parse_openai_tool_result_content,
|
||||
};
|
||||
pub(crate) use crate::gateway::ai_pipeline::conversion::{
|
||||
pub(crate) use crate::ai_pipeline::conversion::{
|
||||
build_core_error_body_for_client_format, request_conversion_kind,
|
||||
request_conversion_transport_supported, sync_chat_response_conversion_kind,
|
||||
sync_cli_response_conversion_kind, RequestConversionKind, SyncChatResponseConversionKind,
|
||||
|
||||
@@ -3,16 +3,19 @@ use std::collections::BTreeMap;
|
||||
use serde_json::{json, Map, Value};
|
||||
use url::form_urlencoded;
|
||||
|
||||
use crate::gateway::ai_pipeline::conversion::request::{
|
||||
use crate::ai_pipeline::conversion::request::{
|
||||
convert_openai_chat_request_to_claude_request, convert_openai_chat_request_to_gemini_request,
|
||||
convert_openai_chat_request_to_openai_cli_request, extract_openai_text_content,
|
||||
normalize_openai_cli_request_to_openai_chat_request, parse_openai_tool_result_content,
|
||||
};
|
||||
use crate::gateway::ai_pipeline::conversion::{request_conversion_kind, RequestConversionKind};
|
||||
use crate::gateway::provider_transport::{
|
||||
apply_local_body_rules, build_antigravity_v1internal_url, build_claude_messages_url,
|
||||
build_gemini_content_url, build_openai_chat_url, build_openai_cli_url,
|
||||
build_passthrough_path_url, AntigravityRequestUrlAction,
|
||||
use crate::ai_pipeline::conversion::{request_conversion_kind, RequestConversionKind};
|
||||
use crate::provider_transport::antigravity::{
|
||||
build_antigravity_v1internal_url, AntigravityRequestUrlAction,
|
||||
};
|
||||
use crate::provider_transport::apply_local_body_rules;
|
||||
use crate::provider_transport::url::{
|
||||
build_claude_messages_url, build_gemini_content_url, build_openai_chat_url,
|
||||
build_openai_cli_url, build_passthrough_path_url,
|
||||
};
|
||||
|
||||
pub(crate) fn build_local_openai_chat_request_body(
|
||||
@@ -40,7 +43,7 @@ pub(crate) fn build_local_openai_chat_request_body(
|
||||
|
||||
pub(crate) fn build_local_openai_chat_upstream_url(
|
||||
parts: &http::request::Parts,
|
||||
transport: &crate::gateway::provider_transport::GatewayProviderTransportSnapshot,
|
||||
transport: &crate::provider_transport::GatewayProviderTransportSnapshot,
|
||||
) -> Option<String> {
|
||||
let custom_path = transport
|
||||
.endpoint
|
||||
@@ -101,7 +104,7 @@ pub(crate) fn build_cross_format_openai_chat_request_body(
|
||||
|
||||
pub(crate) fn build_cross_format_openai_chat_upstream_url(
|
||||
parts: &http::request::Parts,
|
||||
transport: &crate::gateway::provider_transport::GatewayProviderTransportSnapshot,
|
||||
transport: &crate::provider_transport::GatewayProviderTransportSnapshot,
|
||||
mapped_model: &str,
|
||||
provider_api_format: &str,
|
||||
upstream_is_stream: bool,
|
||||
@@ -214,7 +217,7 @@ pub(crate) fn build_cross_format_openai_cli_request_body(
|
||||
|
||||
pub(crate) fn build_local_openai_cli_upstream_url(
|
||||
parts: &http::request::Parts,
|
||||
transport: &crate::gateway::provider_transport::GatewayProviderTransportSnapshot,
|
||||
transport: &crate::provider_transport::GatewayProviderTransportSnapshot,
|
||||
compact: bool,
|
||||
) -> Option<String> {
|
||||
let custom_path = transport
|
||||
@@ -238,7 +241,7 @@ pub(crate) fn build_local_openai_cli_upstream_url(
|
||||
|
||||
pub(crate) fn build_cross_format_openai_cli_upstream_url(
|
||||
parts: &http::request::Parts,
|
||||
transport: &crate::gateway::provider_transport::GatewayProviderTransportSnapshot,
|
||||
transport: &crate::provider_transport::GatewayProviderTransportSnapshot,
|
||||
mapped_model: &str,
|
||||
client_api_format: &str,
|
||||
provider_api_format: &str,
|
||||
|
||||
@@ -5,43 +5,46 @@ use serde_json::json;
|
||||
use tracing::warn;
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::gateway::ai_pipeline::conversion::{
|
||||
use crate::ai_pipeline::conversion::{
|
||||
request_conversion_direct_auth, request_conversion_kind, request_conversion_transport_supported,
|
||||
};
|
||||
use crate::gateway::ai_pipeline::planner::plan_builders::{
|
||||
LocalStreamPlanAndReport, LocalSyncPlanAndReport,
|
||||
};
|
||||
use crate::gateway::ai_pipeline::planner::prefer_local_tunnel_owner_candidates;
|
||||
use crate::gateway::ai_pipeline::planner::{
|
||||
use crate::ai_pipeline::planner::candidate_affinity::prefer_local_tunnel_owner_candidates;
|
||||
use crate::ai_pipeline::planner::common::{
|
||||
EXECUTION_RUNTIME_STREAM_DECISION_ACTION, EXECUTION_RUNTIME_SYNC_DECISION_ACTION,
|
||||
OPENAI_CHAT_STREAM_PLAN_KIND,
|
||||
};
|
||||
use crate::gateway::headers::collect_control_headers;
|
||||
use crate::gateway::provider_transport::{
|
||||
apply_local_header_rules, build_openai_passthrough_headers, ensure_upstream_auth_header,
|
||||
resolve_local_openai_chat_auth, resolve_transport_execution_timeouts,
|
||||
resolve_transport_proxy_snapshot_with_tunnel_affinity, resolve_transport_tls_profile,
|
||||
supports_local_openai_chat_transport, LocalResolvedOAuthRequestAuth,
|
||||
use crate::ai_pipeline::planner::plan_builders::{
|
||||
LocalStreamPlanAndReport, LocalSyncPlanAndReport,
|
||||
};
|
||||
use crate::gateway::scheduler::{
|
||||
use crate::execution_runtime::{ConversionMode, ExecutionStrategy};
|
||||
use crate::headers::collect_control_headers;
|
||||
use crate::provider_transport::auth::{
|
||||
build_openai_passthrough_headers, ensure_upstream_auth_header, resolve_local_openai_chat_auth,
|
||||
};
|
||||
use crate::provider_transport::policy::supports_local_openai_chat_transport;
|
||||
use crate::provider_transport::{
|
||||
apply_local_header_rules, resolve_transport_execution_timeouts,
|
||||
resolve_transport_proxy_snapshot_with_tunnel_affinity, resolve_transport_tls_profile,
|
||||
LocalResolvedOAuthRequestAuth,
|
||||
};
|
||||
use crate::scheduler::{
|
||||
record_local_request_candidate_status, GatewayMinimalCandidateSelectionCandidate,
|
||||
};
|
||||
use crate::gateway::{
|
||||
append_execution_contract_fields_to_value, AppState, ConversionMode, ExecutionStrategy,
|
||||
GatewayControlSyncDecisionResponse,
|
||||
use crate::{
|
||||
append_execution_contract_fields_to_value, AppState, GatewayControlSyncDecisionResponse,
|
||||
};
|
||||
|
||||
use super::plans::current_unix_secs;
|
||||
use crate::gateway::ai_pipeline::planner::standard::{
|
||||
use crate::ai_pipeline::planner::standard::{
|
||||
build_cross_format_openai_chat_request_body, build_cross_format_openai_chat_upstream_url,
|
||||
build_local_openai_chat_request_body, build_local_openai_chat_upstream_url,
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub(super) struct LocalOpenAiChatDecisionInput {
|
||||
pub(super) auth_context: crate::gateway::GatewayControlAuthContext,
|
||||
pub(super) auth_context: crate::control::GatewayControlAuthContext,
|
||||
pub(super) requested_model: String,
|
||||
pub(super) auth_snapshot: crate::gateway::gateway_data::StoredGatewayAuthApiKeySnapshot,
|
||||
pub(super) auth_snapshot: crate::data::auth::GatewayAuthApiKeySnapshot,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
@@ -174,7 +177,7 @@ async fn build_same_format_local_openai_chat_decision_payload_for_candidate(
|
||||
decision_kind: &str,
|
||||
report_kind: &str,
|
||||
upstream_is_stream: bool,
|
||||
transport: &crate::gateway::provider_transport::GatewayProviderTransportSnapshot,
|
||||
transport: &crate::provider_transport::GatewayProviderTransportSnapshot,
|
||||
) -> Option<GatewayControlSyncDecisionResponse> {
|
||||
if !supports_local_openai_chat_transport(transport) {
|
||||
mark_skipped_local_openai_chat_candidate(
|
||||
@@ -362,6 +365,7 @@ async fn build_same_format_local_openai_chat_decision_payload_for_candidate(
|
||||
"provider_id": candidate.provider_id,
|
||||
"endpoint_id": candidate.endpoint_id,
|
||||
"key_id": candidate.key_id,
|
||||
"key_name": candidate.key_name,
|
||||
"provider_api_format": "openai:chat",
|
||||
"client_api_format": "openai:chat",
|
||||
"mapped_model": mapped_model,
|
||||
@@ -395,7 +399,7 @@ async fn build_cross_format_local_openai_chat_decision_payload_for_candidate(
|
||||
candidate_id: &str,
|
||||
decision_kind: &str,
|
||||
upstream_is_stream: bool,
|
||||
transport: &crate::gateway::provider_transport::GatewayProviderTransportSnapshot,
|
||||
transport: &crate::provider_transport::GatewayProviderTransportSnapshot,
|
||||
provider_api_format: &str,
|
||||
) -> Option<GatewayControlSyncDecisionResponse> {
|
||||
let provider_api_format = provider_api_format.trim().to_ascii_lowercase();
|
||||
@@ -606,6 +610,7 @@ async fn build_cross_format_local_openai_chat_decision_payload_for_candidate(
|
||||
"provider_id": candidate.provider_id,
|
||||
"endpoint_id": candidate.endpoint_id,
|
||||
"key_id": candidate.key_id,
|
||||
"key_name": candidate.key_name,
|
||||
"provider_api_format": provider_api_format,
|
||||
"client_api_format": "openai:chat",
|
||||
"mapped_model": mapped_model,
|
||||
|
||||
@@ -1,23 +1,15 @@
|
||||
use serde_json::{Map, Value};
|
||||
use tracing::warn;
|
||||
|
||||
use crate::gateway::ai_pipeline::planner::{
|
||||
use crate::ai_pipeline::planner::common::{
|
||||
OPENAI_CHAT_STREAM_PLAN_KIND, OPENAI_CHAT_SYNC_PLAN_KIND,
|
||||
};
|
||||
use crate::gateway::{
|
||||
AppState, GatewayControlDecision, GatewayControlSyncDecisionResponse, GatewayError,
|
||||
LocalExecutionRuntimeMissDiagnostic,
|
||||
use crate::control::GatewayControlDecision;
|
||||
use crate::{
|
||||
AppState, GatewayControlSyncDecisionResponse, GatewayError, LocalExecutionRuntimeMissDiagnostic,
|
||||
};
|
||||
|
||||
use crate::gateway::ai_pipeline::planner::standard::{
|
||||
convert_openai_chat_request_to_claude_request, convert_openai_chat_request_to_gemini_request,
|
||||
convert_openai_chat_request_to_openai_cli_request, extract_openai_text_content,
|
||||
parse_openai_tool_result_content,
|
||||
};
|
||||
|
||||
#[path = "openai/chat/decision.rs"]
|
||||
mod decision;
|
||||
#[path = "openai/chat/plans.rs"]
|
||||
mod plans;
|
||||
|
||||
use self::decision::{
|
||||
@@ -39,7 +31,7 @@ pub(crate) async fn build_local_openai_chat_sync_plan_and_reports_for_kind(
|
||||
body_json: &serde_json::Value,
|
||||
plan_kind: &str,
|
||||
) -> Result<
|
||||
Vec<crate::gateway::ai_pipeline::planner::plan_builders::LocalSyncPlanAndReport>,
|
||||
Vec<crate::ai_pipeline::planner::plan_builders::LocalSyncPlanAndReport>,
|
||||
GatewayError,
|
||||
> {
|
||||
build_local_openai_chat_sync_plan_and_reports(
|
||||
@@ -56,7 +48,7 @@ pub(crate) async fn build_local_openai_chat_stream_plan_and_reports_for_kind(
|
||||
body_json: &serde_json::Value,
|
||||
plan_kind: &str,
|
||||
) -> Result<
|
||||
Vec<crate::gateway::ai_pipeline::planner::plan_builders::LocalStreamPlanAndReport>,
|
||||
Vec<crate::ai_pipeline::planner::plan_builders::LocalStreamPlanAndReport>,
|
||||
GatewayError,
|
||||
> {
|
||||
build_local_openai_chat_stream_plan_and_reports(
|
||||
@@ -73,6 +65,17 @@ pub(crate) fn set_local_openai_chat_execution_exhausted_diagnostic(
|
||||
body_json: &serde_json::Value,
|
||||
plan_count: usize,
|
||||
) {
|
||||
warn!(
|
||||
event_name = "local_openai_chat_candidates_exhausted",
|
||||
log_type = "event",
|
||||
trace_id = %trace_id,
|
||||
plan_kind,
|
||||
route_class = decision.route_class.as_deref().unwrap_or("passthrough"),
|
||||
route_family = decision.route_family.as_deref().unwrap_or("unknown"),
|
||||
candidate_count = plan_count,
|
||||
model = body_json.get("model").and_then(|value| value.as_str()).unwrap_or(""),
|
||||
"gateway local openai chat execution exhausted all candidates"
|
||||
);
|
||||
state.set_local_execution_runtime_miss_diagnostic(
|
||||
trace_id,
|
||||
LocalExecutionRuntimeMissDiagnostic {
|
||||
@@ -111,6 +114,8 @@ pub(crate) async fn maybe_build_sync_local_decision_payload(
|
||||
Ok(candidates) => candidates,
|
||||
Err(err) => {
|
||||
warn!(
|
||||
event_name = "local_openai_chat_scheduler_selection_failed",
|
||||
log_type = "event",
|
||||
trace_id = %trace_id,
|
||||
error = ?err,
|
||||
"gateway local openai chat sync decision scheduler selection failed"
|
||||
@@ -167,6 +172,8 @@ pub(crate) async fn maybe_build_stream_local_decision_payload(
|
||||
Ok(candidates) => candidates,
|
||||
Err(err) => {
|
||||
warn!(
|
||||
event_name = "local_openai_chat_scheduler_selection_failed",
|
||||
log_type = "event",
|
||||
trace_id = %trace_id,
|
||||
error = ?err,
|
||||
"gateway local openai chat stream decision scheduler selection failed"
|
||||
@@ -198,6 +205,7 @@ pub(crate) async fn maybe_build_stream_local_decision_payload(
|
||||
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
pub(crate) fn parse_openai_stop_sequences(stop: Option<&Value>) -> Option<Vec<Value>> {
|
||||
match stop {
|
||||
Some(Value::String(value)) if !value.trim().is_empty() => {
|
||||
@@ -8,15 +8,15 @@ use super::{
|
||||
maybe_build_local_openai_chat_decision_payload_for_candidate, AppState, GatewayControlDecision,
|
||||
GatewayError, LocalExecutionRuntimeMissDiagnostic, LocalOpenAiChatDecisionInput,
|
||||
};
|
||||
use crate::gateway::ai_pipeline::planner::plan_builders::{
|
||||
use crate::ai_pipeline::planner::candidate_affinity::prefer_local_tunnel_owner_candidates;
|
||||
use crate::ai_pipeline::planner::common::{
|
||||
OPENAI_CHAT_STREAM_PLAN_KIND, OPENAI_CHAT_SYNC_PLAN_KIND,
|
||||
};
|
||||
use crate::ai_pipeline::planner::plan_builders::{
|
||||
build_openai_chat_stream_plan_from_decision, build_openai_chat_sync_plan_from_decision,
|
||||
LocalStreamPlanAndReport, LocalSyncPlanAndReport,
|
||||
};
|
||||
use crate::gateway::ai_pipeline::planner::prefer_local_tunnel_owner_candidates;
|
||||
use crate::gateway::ai_pipeline::planner::{
|
||||
OPENAI_CHAT_STREAM_PLAN_KIND, OPENAI_CHAT_SYNC_PLAN_KIND,
|
||||
};
|
||||
use crate::gateway::scheduler::{
|
||||
use crate::scheduler::{
|
||||
list_selectable_candidates, GatewayMinimalCandidateSelectionCandidate,
|
||||
};
|
||||
|
||||
@@ -352,7 +352,7 @@ pub(super) async fn list_local_openai_chat_candidates(
|
||||
}
|
||||
|
||||
fn auth_snapshot_allows_cross_format_openai_chat_candidate(
|
||||
auth_snapshot: &crate::gateway::gateway_data::StoredGatewayAuthApiKeySnapshot,
|
||||
auth_snapshot: &crate::data::auth::GatewayAuthApiKeySnapshot,
|
||||
requested_model: &str,
|
||||
candidate: &GatewayMinimalCandidateSelectionCandidate,
|
||||
) -> bool {
|
||||
|
||||
@@ -5,38 +5,45 @@ use serde_json::{json, Value};
|
||||
use tracing::warn;
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::gateway::ai_pipeline::conversion::{
|
||||
use crate::ai_pipeline::conversion::{
|
||||
request_conversion_direct_auth, request_conversion_kind, request_conversion_transport_supported,
|
||||
};
|
||||
use crate::gateway::ai_pipeline::planner::plan_builders::{
|
||||
use crate::ai_pipeline::planner::candidate_affinity::prefer_local_tunnel_owner_candidates;
|
||||
use crate::ai_pipeline::planner::common::{
|
||||
EXECUTION_RUNTIME_STREAM_DECISION_ACTION, EXECUTION_RUNTIME_SYNC_DECISION_ACTION,
|
||||
};
|
||||
use crate::ai_pipeline::planner::plan_builders::{
|
||||
LocalStreamPlanAndReport, LocalSyncPlanAndReport,
|
||||
};
|
||||
use crate::gateway::ai_pipeline::planner::prefer_local_tunnel_owner_candidates;
|
||||
use crate::gateway::ai_pipeline::planner::standard::{
|
||||
use crate::ai_pipeline::planner::standard::{
|
||||
build_cross_format_openai_cli_request_body, build_cross_format_openai_cli_upstream_url,
|
||||
build_local_openai_cli_request_body, build_local_openai_cli_upstream_url,
|
||||
};
|
||||
use crate::gateway::ai_pipeline::planner::{
|
||||
EXECUTION_RUNTIME_STREAM_DECISION_ACTION, EXECUTION_RUNTIME_SYNC_DECISION_ACTION,
|
||||
};
|
||||
use crate::gateway::headers::collect_control_headers;
|
||||
use crate::gateway::provider_transport::{
|
||||
apply_local_body_rules, apply_local_header_rules, build_antigravity_safe_v1internal_request,
|
||||
build_antigravity_static_identity_headers, build_openai_passthrough_headers,
|
||||
classify_local_antigravity_request_support, ensure_upstream_auth_header,
|
||||
resolve_local_gemini_auth, resolve_local_standard_auth, resolve_transport_execution_timeouts,
|
||||
resolve_transport_proxy_snapshot_with_tunnel_affinity, resolve_transport_tls_profile,
|
||||
supports_local_standard_transport_with_network, AntigravityEnvelopeRequestType,
|
||||
use crate::control::GatewayControlDecision;
|
||||
use crate::execution_runtime::{ConversionMode, ExecutionStrategy};
|
||||
use crate::headers::collect_control_headers;
|
||||
use crate::provider_transport::antigravity::{
|
||||
build_antigravity_safe_v1internal_request, build_antigravity_static_identity_headers,
|
||||
classify_local_antigravity_request_support, AntigravityEnvelopeRequestType,
|
||||
AntigravityRequestEnvelopeSupport, AntigravityRequestSideSupport,
|
||||
};
|
||||
use crate::provider_transport::auth::{
|
||||
build_openai_passthrough_headers, ensure_upstream_auth_header, resolve_local_gemini_auth,
|
||||
resolve_local_standard_auth,
|
||||
};
|
||||
use crate::provider_transport::policy::supports_local_standard_transport_with_network;
|
||||
use crate::provider_transport::{
|
||||
apply_local_body_rules, apply_local_header_rules, resolve_transport_execution_timeouts,
|
||||
resolve_transport_proxy_snapshot_with_tunnel_affinity, resolve_transport_tls_profile,
|
||||
LocalResolvedOAuthRequestAuth,
|
||||
};
|
||||
use crate::gateway::scheduler::{
|
||||
use crate::scheduler::{
|
||||
current_unix_secs, list_selectable_candidates, record_local_request_candidate_status,
|
||||
GatewayMinimalCandidateSelectionCandidate,
|
||||
};
|
||||
use crate::gateway::{
|
||||
append_execution_contract_fields_to_value, AppState, ConversionMode, ExecutionStrategy,
|
||||
GatewayControlDecision, GatewayControlSyncDecisionResponse, GatewayError,
|
||||
use crate::{
|
||||
append_execution_contract_fields_to_value, AppState, GatewayControlSyncDecisionResponse,
|
||||
GatewayError,
|
||||
};
|
||||
|
||||
const ANTIGRAVITY_ENVELOPE_NAME: &str = "antigravity:v1internal";
|
||||
@@ -52,9 +59,9 @@ pub(super) struct LocalOpenAiCliSpec {
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub(super) struct LocalOpenAiCliDecisionInput {
|
||||
pub(super) auth_context: crate::gateway::GatewayControlAuthContext,
|
||||
pub(super) auth_context: crate::control::GatewayControlAuthContext,
|
||||
pub(super) requested_model: String,
|
||||
pub(super) auth_snapshot: crate::gateway::gateway_data::StoredGatewayAuthApiKeySnapshot,
|
||||
pub(super) auth_snapshot: crate::data::auth::GatewayAuthApiKeySnapshot,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
@@ -247,7 +254,7 @@ pub(super) async fn materialize_local_openai_cli_candidate_attempts(
|
||||
}
|
||||
|
||||
fn auth_snapshot_allows_cross_format_openai_cli_candidate(
|
||||
auth_snapshot: &crate::gateway::gateway_data::StoredGatewayAuthApiKeySnapshot,
|
||||
auth_snapshot: &crate::data::auth::GatewayAuthApiKeySnapshot,
|
||||
requested_model: &str,
|
||||
candidate: &GatewayMinimalCandidateSelectionCandidate,
|
||||
) -> bool {
|
||||
@@ -639,6 +646,7 @@ pub(super) async fn maybe_build_local_openai_cli_decision_payload_for_candidate(
|
||||
"provider_id": candidate.provider_id,
|
||||
"endpoint_id": candidate.endpoint_id,
|
||||
"key_id": candidate.key_id,
|
||||
"key_name": candidate.key_name,
|
||||
"provider_api_format": provider_api_format,
|
||||
"client_api_format": spec.api_format,
|
||||
"mapped_model": mapped_model,
|
||||
|
||||
@@ -1,13 +1,10 @@
|
||||
use crate::gateway::ai_pipeline::planner::plan_builders::{
|
||||
use crate::ai_pipeline::planner::plan_builders::{
|
||||
LocalStreamPlanAndReport, LocalSyncPlanAndReport,
|
||||
};
|
||||
use crate::gateway::{
|
||||
AppState, GatewayControlDecision, GatewayControlSyncDecisionResponse, GatewayError,
|
||||
};
|
||||
use crate::control::GatewayControlDecision;
|
||||
use crate::{AppState, GatewayControlSyncDecisionResponse, GatewayError};
|
||||
|
||||
#[path = "openai/cli/decision.rs"]
|
||||
mod decision;
|
||||
#[path = "openai/cli/plans.rs"]
|
||||
mod plans;
|
||||
|
||||
use self::decision::{
|
||||
@@ -5,15 +5,16 @@ use super::decision::{
|
||||
maybe_build_local_openai_cli_decision_payload_for_candidate,
|
||||
resolve_local_openai_cli_decision_input, LocalOpenAiCliSpec,
|
||||
};
|
||||
use crate::gateway::ai_pipeline::planner::plan_builders::{
|
||||
build_openai_cli_stream_plan_from_decision, build_openai_cli_sync_plan_from_decision,
|
||||
LocalStreamPlanAndReport, LocalSyncPlanAndReport,
|
||||
};
|
||||
use crate::gateway::ai_pipeline::planner::{
|
||||
use crate::ai_pipeline::planner::common::{
|
||||
OPENAI_CLI_STREAM_PLAN_KIND, OPENAI_CLI_SYNC_PLAN_KIND, OPENAI_COMPACT_STREAM_PLAN_KIND,
|
||||
OPENAI_COMPACT_SYNC_PLAN_KIND,
|
||||
};
|
||||
use crate::gateway::{AppState, GatewayControlDecision, GatewayError};
|
||||
use crate::ai_pipeline::planner::plan_builders::{
|
||||
build_openai_cli_stream_plan_from_decision, build_openai_cli_sync_plan_from_decision,
|
||||
LocalStreamPlanAndReport, LocalSyncPlanAndReport,
|
||||
};
|
||||
use crate::control::GatewayControlDecision;
|
||||
use crate::{AppState, GatewayError};
|
||||
|
||||
pub(super) fn resolve_sync_spec(plan_kind: &str) -> Option<LocalOpenAiCliSpec> {
|
||||
match plan_kind {
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
pub(crate) mod chat;
|
||||
pub(crate) mod cli;
|
||||
|
||||
pub(crate) use chat::{
|
||||
copy_request_number_field, copy_request_number_field_as,
|
||||
map_openai_reasoning_effort_to_claude_output, map_openai_reasoning_effort_to_gemini_budget,
|
||||
maybe_build_stream_local_decision_payload, maybe_build_sync_local_decision_payload,
|
||||
parse_openai_stop_sequences, resolve_openai_chat_max_tokens, value_as_u64,
|
||||
};
|
||||
pub(crate) use cli::{
|
||||
maybe_build_stream_local_openai_cli_decision_payload,
|
||||
maybe_build_sync_local_openai_cli_decision_payload,
|
||||
};
|
||||
@@ -5,11 +5,11 @@ use super::{
|
||||
GatewayControlSyncDecisionResponse, GatewayError, LocalStreamPlanAndReport,
|
||||
LocalSyncPlanAndReport,
|
||||
};
|
||||
use crate::gateway::ai_pipeline::adaptation::surfaces::provider_adaptation_requires_eventstream_accept;
|
||||
use crate::gateway::provider_transport::{
|
||||
build_openai_chat_url, build_openai_cli_url, build_openai_passthrough_headers,
|
||||
ensure_upstream_auth_header,
|
||||
use crate::ai_pipeline::adaptation::surfaces::provider_adaptation_requires_eventstream_accept;
|
||||
use crate::provider_transport::auth::{
|
||||
build_openai_passthrough_headers, ensure_upstream_auth_header,
|
||||
};
|
||||
use crate::provider_transport::url::{build_openai_chat_url, build_openai_cli_url};
|
||||
|
||||
pub(crate) fn build_openai_chat_sync_plan_from_decision(
|
||||
parts: &http::request::Parts,
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
use aether_contracts::{ExecutionPlan, RequestBody};
|
||||
|
||||
use super::{augment_sync_report_context, LocalStreamPlanAndReport, LocalSyncPlanAndReport};
|
||||
use crate::gateway::ai_pipeline::adaptation::surfaces::provider_adaptation_requires_eventstream_accept;
|
||||
use crate::gateway::ai_pipeline::planner::generic_decision_missing_exact_provider_request;
|
||||
use crate::gateway::provider_transport::ensure_upstream_auth_header;
|
||||
use crate::gateway::{GatewayControlSyncDecisionResponse, GatewayError};
|
||||
use crate::ai_pipeline::adaptation::surfaces::provider_adaptation_requires_eventstream_accept;
|
||||
use crate::ai_pipeline::contracts::generic_decision_missing_exact_provider_request;
|
||||
use crate::provider_transport::ensure_upstream_auth_header;
|
||||
use crate::{GatewayControlSyncDecisionResponse, GatewayError};
|
||||
|
||||
pub(crate) fn build_standard_sync_plan_from_decision(
|
||||
_parts: &http::request::Parts,
|
||||
|
||||
@@ -2,7 +2,7 @@ use std::collections::BTreeMap;
|
||||
|
||||
use serde_json::Value;
|
||||
|
||||
use crate::gateway::provider_transport::snapshot::GatewayProviderTransportSnapshot;
|
||||
use crate::provider_transport::snapshot::GatewayProviderTransportSnapshot;
|
||||
|
||||
pub(crate) const ANTIGRAVITY_PROVIDER_TYPE: &str = "antigravity";
|
||||
pub(crate) const ANTIGRAVITY_REQUEST_USER_AGENT: &str = "antigravity";
|
||||
|
||||
@@ -1,25 +1,13 @@
|
||||
#![allow(dead_code, unused_imports)]
|
||||
|
||||
mod auth;
|
||||
mod policy;
|
||||
mod request;
|
||||
mod url;
|
||||
|
||||
pub(crate) use auth::{
|
||||
build_antigravity_static_identity_headers, resolve_local_antigravity_request_auth,
|
||||
pub(crate) use crate::provider_transport::antigravity::{
|
||||
build_antigravity_safe_v1internal_request, build_antigravity_static_identity_headers,
|
||||
build_antigravity_v1internal_url, classify_antigravity_safe_request_body,
|
||||
classify_local_antigravity_request_support, AntigravityEnvelopeRequestType,
|
||||
AntigravityRequestAuth, AntigravityRequestAuthSupport, AntigravityRequestAuthUnsupportedReason,
|
||||
AntigravityRequestEnvelopeSupport, AntigravityRequestEnvelopeUnsupportedReason,
|
||||
AntigravityRequestSideSpec, AntigravityRequestSideSupport,
|
||||
AntigravityRequestSideUnsupportedReason, AntigravityRequestUrlAction,
|
||||
ANTIGRAVITY_PROVIDER_TYPE, ANTIGRAVITY_REQUEST_USER_AGENT,
|
||||
};
|
||||
pub(crate) use policy::{
|
||||
classify_local_antigravity_request_support, AntigravityRequestSideSpec,
|
||||
AntigravityRequestSideSupport, AntigravityRequestSideUnsupportedReason,
|
||||
};
|
||||
pub(crate) use request::{
|
||||
build_antigravity_safe_v1internal_request, classify_antigravity_safe_request_body,
|
||||
AntigravityEnvelopeRequestType, AntigravityRequestEnvelopeSupport,
|
||||
AntigravityRequestEnvelopeUnsupportedReason,
|
||||
};
|
||||
pub(crate) use url::{
|
||||
build_antigravity_v1internal_url, AntigravityRequestUrlAction,
|
||||
ANTIGRAVITY_V1INTERNAL_PATH_TEMPLATE,
|
||||
};
|
||||
|
||||
@@ -8,7 +8,7 @@ use super::request::{
|
||||
classify_antigravity_safe_request_body, AntigravityEnvelopeRequestType,
|
||||
AntigravityRequestEnvelopeUnsupportedReason,
|
||||
};
|
||||
use crate::gateway::provider_transport::snapshot::GatewayProviderTransportSnapshot;
|
||||
use crate::provider_transport::snapshot::GatewayProviderTransportSnapshot;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub(crate) struct AntigravityRequestSideSpec {
|
||||
|
||||
@@ -1 +1 @@
|
||||
pub(crate) use crate::gateway::provider_transport::auth::resolve_local_standard_auth;
|
||||
pub(crate) use crate::provider_transport::auth::resolve_local_standard_auth;
|
||||
|
||||
@@ -1 +1 @@
|
||||
pub(crate) use crate::gateway::provider_transport::policy::supports_local_standard_transport_with_network;
|
||||
pub(crate) use crate::provider_transport::policy::supports_local_standard_transport_with_network;
|
||||
|
||||
@@ -1 +1 @@
|
||||
pub(crate) use crate::gateway::provider_transport::auth::build_passthrough_headers_with_auth;
|
||||
pub(crate) use crate::provider_transport::auth::build_passthrough_headers_with_auth;
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
pub(crate) use crate::gateway::provider_transport::url::{
|
||||
pub(crate) use crate::provider_transport::url::{
|
||||
build_claude_messages_url, build_passthrough_path_url,
|
||||
};
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use crate::gateway::provider_transport::auth::resolve_local_standard_auth;
|
||||
use crate::gateway::provider_transport::snapshot::GatewayProviderTransportSnapshot;
|
||||
use crate::gateway::provider_transport::supports_local_oauth_request_auth_resolution;
|
||||
use crate::provider_transport::auth::resolve_local_standard_auth;
|
||||
use crate::provider_transport::snapshot::GatewayProviderTransportSnapshot;
|
||||
use crate::provider_transport::supports_local_oauth_request_auth_resolution;
|
||||
|
||||
pub(crate) fn supports_local_claude_code_auth(
|
||||
transport: &GatewayProviderTransportSnapshot,
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
mod auth;
|
||||
mod policy;
|
||||
mod request;
|
||||
mod url;
|
||||
|
||||
pub(crate) use policy::*;
|
||||
pub(crate) use request::*;
|
||||
pub(crate) use url::*;
|
||||
pub(crate) use crate::provider_transport::claude_code::{
|
||||
build_claude_code_messages_url, build_claude_code_passthrough_headers,
|
||||
sanitize_claude_code_request_body, supports_local_claude_code_auth,
|
||||
supports_local_claude_code_transport_with_network,
|
||||
};
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user