mirror of
https://github.com/fawney19/Aether.git
synced 2026-10-04 16:37:46 +08:00
feat(gateway): harden provider request execution
Preserve exact request payloads and model client surface and API operation explicitly. Add Anthropic compatibility profiles, bounded stream commitment, and scoped OAuth retry behavior across provider transports.
This commit is contained in:
@@ -2,13 +2,14 @@ pub use crate::contracts::{
|
||||
core_error_background_report_kind, core_error_default_client_api_format,
|
||||
core_success_background_report_kind, implicit_sync_finalize_report_kind,
|
||||
is_openai_responses_stream_plan_kind, is_openai_responses_sync_plan_kind, AiControlPlanRequest,
|
||||
ExecutionRuntimeAuthContext, CLAUDE_CHAT_STREAM_PLAN_KIND,
|
||||
ApiOperation, ClientSurface, ExecutionRuntimeAuthContext, CLAUDE_CHAT_STREAM_PLAN_KIND,
|
||||
CLAUDE_CHAT_STREAM_SUCCESS_REPORT_KIND, CLAUDE_CHAT_SYNC_ERROR_REPORT_KIND,
|
||||
CLAUDE_CHAT_SYNC_FINALIZE_REPORT_KIND, CLAUDE_CHAT_SYNC_PLAN_KIND,
|
||||
CLAUDE_CHAT_SYNC_SUCCESS_REPORT_KIND, CLAUDE_CLI_STREAM_PLAN_KIND,
|
||||
CLAUDE_CLI_STREAM_SUCCESS_REPORT_KIND, CLAUDE_CLI_SYNC_ERROR_REPORT_KIND,
|
||||
CLAUDE_CLI_SYNC_FINALIZE_REPORT_KIND, CLAUDE_CLI_SYNC_PLAN_KIND,
|
||||
CLAUDE_CLI_SYNC_SUCCESS_REPORT_KIND, EXECUTION_RUNTIME_STREAM_ACTION,
|
||||
CLAUDE_CLI_SYNC_SUCCESS_REPORT_KIND, CLAUDE_COUNT_TOKENS_SYNC_PLAN_KIND,
|
||||
CLAUDE_COUNT_TOKENS_SYNC_SUCCESS_REPORT_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_STREAM_SUCCESS_REPORT_KIND, GEMINI_CHAT_SYNC_ERROR_REPORT_KIND,
|
||||
@@ -127,7 +128,9 @@ pub use crate::formats::shared::response::{
|
||||
pub use crate::formats::shared::routing::{
|
||||
is_matching_stream_http_request, is_matching_stream_request,
|
||||
request_path_implies_stream_request, resolve_execution_runtime_stream_plan_kind,
|
||||
resolve_execution_runtime_sync_plan_kind, sanitize_request_path,
|
||||
resolve_execution_runtime_stream_plan_kind_with_client_surface,
|
||||
resolve_execution_runtime_sync_plan_kind,
|
||||
resolve_execution_runtime_sync_plan_kind_with_client_surface, sanitize_request_path,
|
||||
sanitize_request_path_and_query, sanitize_request_query_string,
|
||||
supports_stream_execution_decision_kind, supports_sync_execution_decision_kind,
|
||||
};
|
||||
|
||||
@@ -3,6 +3,7 @@ mod auth_context;
|
||||
mod control_payloads;
|
||||
mod plan_kinds;
|
||||
mod report_kinds;
|
||||
mod request_dimensions;
|
||||
|
||||
pub use actions::{
|
||||
EXECUTION_RUNTIME_STREAM_ACTION, EXECUTION_RUNTIME_STREAM_DECISION_ACTION,
|
||||
@@ -13,19 +14,20 @@ pub use control_payloads::{build_ai_control_plan_request, AiControlPlanRequest};
|
||||
pub use plan_kinds::{
|
||||
is_openai_responses_stream_plan_kind, is_openai_responses_sync_plan_kind,
|
||||
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, GEMINI_EMBEDDING_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_INTERACTIONS_STREAM_PLAN_KIND, GEMINI_INTERACTIONS_SYNC_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_EMBEDDING_SYNC_PLAN_KIND,
|
||||
OPENAI_IMAGE_STREAM_PLAN_KIND, OPENAI_IMAGE_SYNC_PLAN_KIND, OPENAI_RERANK_SYNC_PLAN_KIND,
|
||||
OPENAI_RESPONSES_COMPACT_STREAM_PLAN_KIND, OPENAI_RESPONSES_COMPACT_SYNC_PLAN_KIND,
|
||||
OPENAI_RESPONSES_STREAM_PLAN_KIND, OPENAI_RESPONSES_SYNC_PLAN_KIND,
|
||||
OPENAI_SEARCH_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,
|
||||
CLAUDE_CLI_SYNC_PLAN_KIND, CLAUDE_COUNT_TOKENS_SYNC_PLAN_KIND, GEMINI_CHAT_STREAM_PLAN_KIND,
|
||||
GEMINI_CHAT_SYNC_PLAN_KIND, GEMINI_CLI_STREAM_PLAN_KIND, GEMINI_CLI_SYNC_PLAN_KIND,
|
||||
GEMINI_EMBEDDING_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_INTERACTIONS_STREAM_PLAN_KIND,
|
||||
GEMINI_INTERACTIONS_SYNC_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_EMBEDDING_SYNC_PLAN_KIND, OPENAI_IMAGE_STREAM_PLAN_KIND, OPENAI_IMAGE_SYNC_PLAN_KIND,
|
||||
OPENAI_RERANK_SYNC_PLAN_KIND, OPENAI_RESPONSES_COMPACT_STREAM_PLAN_KIND,
|
||||
OPENAI_RESPONSES_COMPACT_SYNC_PLAN_KIND, OPENAI_RESPONSES_STREAM_PLAN_KIND,
|
||||
OPENAI_RESPONSES_SYNC_PLAN_KIND, OPENAI_SEARCH_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 use report_kinds::{
|
||||
core_error_background_report_kind, core_error_default_client_api_format,
|
||||
@@ -34,20 +36,20 @@ pub use report_kinds::{
|
||||
CLAUDE_CHAT_SYNC_ERROR_REPORT_KIND, CLAUDE_CHAT_SYNC_FINALIZE_REPORT_KIND,
|
||||
CLAUDE_CHAT_SYNC_SUCCESS_REPORT_KIND, CLAUDE_CLI_STREAM_SUCCESS_REPORT_KIND,
|
||||
CLAUDE_CLI_SYNC_ERROR_REPORT_KIND, CLAUDE_CLI_SYNC_FINALIZE_REPORT_KIND,
|
||||
CLAUDE_CLI_SYNC_SUCCESS_REPORT_KIND, GEMINI_CHAT_STREAM_SUCCESS_REPORT_KIND,
|
||||
GEMINI_CHAT_SYNC_ERROR_REPORT_KIND, GEMINI_CHAT_SYNC_FINALIZE_REPORT_KIND,
|
||||
GEMINI_CHAT_SYNC_SUCCESS_REPORT_KIND, GEMINI_CLI_STREAM_SUCCESS_REPORT_KIND,
|
||||
GEMINI_CLI_SYNC_ERROR_REPORT_KIND, GEMINI_CLI_SYNC_FINALIZE_REPORT_KIND,
|
||||
GEMINI_CLI_SYNC_SUCCESS_REPORT_KIND, GEMINI_EMBEDDING_SYNC_SUCCESS_REPORT_KIND,
|
||||
GEMINI_INTERACTIONS_STREAM_SUCCESS_REPORT_KIND, GEMINI_INTERACTIONS_SYNC_ERROR_REPORT_KIND,
|
||||
GEMINI_INTERACTIONS_SYNC_FINALIZE_REPORT_KIND, GEMINI_INTERACTIONS_SYNC_SUCCESS_REPORT_KIND,
|
||||
GEMINI_VIDEO_CREATE_SYNC_FINALIZE_REPORT_KIND, OPENAI_CHAT_STREAM_SUCCESS_REPORT_KIND,
|
||||
OPENAI_CHAT_SYNC_ERROR_REPORT_KIND, OPENAI_CHAT_SYNC_FINALIZE_REPORT_KIND,
|
||||
OPENAI_CHAT_SYNC_SUCCESS_REPORT_KIND, OPENAI_EMBEDDING_SYNC_ERROR_REPORT_KIND,
|
||||
OPENAI_EMBEDDING_SYNC_FINALIZE_REPORT_KIND, OPENAI_EMBEDDING_SYNC_SUCCESS_REPORT_KIND,
|
||||
OPENAI_IMAGE_STREAM_SUCCESS_REPORT_KIND, OPENAI_IMAGE_SYNC_ERROR_REPORT_KIND,
|
||||
OPENAI_IMAGE_SYNC_FINALIZE_REPORT_KIND, OPENAI_IMAGE_SYNC_SUCCESS_REPORT_KIND,
|
||||
OPENAI_RESPONSES_COMPACT_STREAM_SUCCESS_REPORT_KIND,
|
||||
CLAUDE_CLI_SYNC_SUCCESS_REPORT_KIND, CLAUDE_COUNT_TOKENS_SYNC_SUCCESS_REPORT_KIND,
|
||||
GEMINI_CHAT_STREAM_SUCCESS_REPORT_KIND, GEMINI_CHAT_SYNC_ERROR_REPORT_KIND,
|
||||
GEMINI_CHAT_SYNC_FINALIZE_REPORT_KIND, GEMINI_CHAT_SYNC_SUCCESS_REPORT_KIND,
|
||||
GEMINI_CLI_STREAM_SUCCESS_REPORT_KIND, GEMINI_CLI_SYNC_ERROR_REPORT_KIND,
|
||||
GEMINI_CLI_SYNC_FINALIZE_REPORT_KIND, GEMINI_CLI_SYNC_SUCCESS_REPORT_KIND,
|
||||
GEMINI_EMBEDDING_SYNC_SUCCESS_REPORT_KIND, GEMINI_INTERACTIONS_STREAM_SUCCESS_REPORT_KIND,
|
||||
GEMINI_INTERACTIONS_SYNC_ERROR_REPORT_KIND, GEMINI_INTERACTIONS_SYNC_FINALIZE_REPORT_KIND,
|
||||
GEMINI_INTERACTIONS_SYNC_SUCCESS_REPORT_KIND, GEMINI_VIDEO_CREATE_SYNC_FINALIZE_REPORT_KIND,
|
||||
OPENAI_CHAT_STREAM_SUCCESS_REPORT_KIND, OPENAI_CHAT_SYNC_ERROR_REPORT_KIND,
|
||||
OPENAI_CHAT_SYNC_FINALIZE_REPORT_KIND, OPENAI_CHAT_SYNC_SUCCESS_REPORT_KIND,
|
||||
OPENAI_EMBEDDING_SYNC_ERROR_REPORT_KIND, OPENAI_EMBEDDING_SYNC_FINALIZE_REPORT_KIND,
|
||||
OPENAI_EMBEDDING_SYNC_SUCCESS_REPORT_KIND, OPENAI_IMAGE_STREAM_SUCCESS_REPORT_KIND,
|
||||
OPENAI_IMAGE_SYNC_ERROR_REPORT_KIND, OPENAI_IMAGE_SYNC_FINALIZE_REPORT_KIND,
|
||||
OPENAI_IMAGE_SYNC_SUCCESS_REPORT_KIND, OPENAI_RESPONSES_COMPACT_STREAM_SUCCESS_REPORT_KIND,
|
||||
OPENAI_RESPONSES_COMPACT_SYNC_ERROR_REPORT_KIND,
|
||||
OPENAI_RESPONSES_COMPACT_SYNC_FINALIZE_REPORT_KIND,
|
||||
OPENAI_RESPONSES_COMPACT_SYNC_SUCCESS_REPORT_KIND, OPENAI_RESPONSES_STREAM_SUCCESS_REPORT_KIND,
|
||||
@@ -55,3 +57,4 @@ pub use report_kinds::{
|
||||
OPENAI_RESPONSES_SYNC_SUCCESS_REPORT_KIND, OPENAI_SEARCH_SYNC_SUCCESS_REPORT_KIND,
|
||||
OPENAI_VIDEO_CREATE_SYNC_FINALIZE_REPORT_KIND,
|
||||
};
|
||||
pub use request_dimensions::{ApiOperation, ClientSurface};
|
||||
|
||||
@@ -13,6 +13,7 @@ pub const GEMINI_VIDEO_CREATE_SYNC_PLAN_KIND: &str = "gemini_video_create_sync";
|
||||
pub const GEMINI_VIDEO_CANCEL_SYNC_PLAN_KIND: &str = "gemini_video_cancel_sync";
|
||||
pub const OPENAI_CHAT_STREAM_PLAN_KIND: &str = "openai_chat_stream";
|
||||
pub const CLAUDE_CHAT_STREAM_PLAN_KIND: &str = "claude_chat_stream";
|
||||
pub const CLAUDE_COUNT_TOKENS_SYNC_PLAN_KIND: &str = "claude_count_tokens_sync";
|
||||
pub const GEMINI_CHAT_STREAM_PLAN_KIND: &str = "gemini_chat_stream";
|
||||
pub const GEMINI_INTERACTIONS_STREAM_PLAN_KIND: &str = "gemini_interactions_stream";
|
||||
pub const OPENAI_RESPONSES_STREAM_PLAN_KIND: &str = "openai_responses_stream";
|
||||
|
||||
@@ -23,6 +23,7 @@ const LEGACY_OPENAI_COMPACT_SYNC_FINALIZE_REPORT_KIND: &str = "openai_compact_sy
|
||||
|
||||
pub const OPENAI_CHAT_SYNC_SUCCESS_REPORT_KIND: &str = "openai_chat_sync_success";
|
||||
pub const CLAUDE_CHAT_SYNC_SUCCESS_REPORT_KIND: &str = "claude_chat_sync_success";
|
||||
pub const CLAUDE_COUNT_TOKENS_SYNC_SUCCESS_REPORT_KIND: &str = "claude_count_tokens_sync_success";
|
||||
pub const GEMINI_CHAT_SYNC_SUCCESS_REPORT_KIND: &str = "gemini_chat_sync_success";
|
||||
pub const GEMINI_INTERACTIONS_SYNC_SUCCESS_REPORT_KIND: &str = "gemini_interactions_sync_success";
|
||||
pub const OPENAI_RESPONSES_SYNC_SUCCESS_REPORT_KIND: &str = "openai_responses_sync_success";
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// Client behavior profile detected at the ingress boundary.
|
||||
///
|
||||
/// This is deliberately independent from the credential carrier: an
|
||||
/// Anthropic SDK may use bearer auth, and Claude Code may be authenticated by
|
||||
/// an Aether API key.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum ClientSurface {
|
||||
ClaudeCode,
|
||||
AnthropicSdk,
|
||||
GenericCompatible,
|
||||
}
|
||||
|
||||
impl ClientSurface {
|
||||
pub const fn as_str(self) -> &'static str {
|
||||
match self {
|
||||
Self::ClaudeCode => "claude_code",
|
||||
Self::AnthropicSdk => "anthropic_sdk",
|
||||
Self::GenericCompatible => "generic_compatible",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Semantic operation carried over an API wire format.
|
||||
///
|
||||
/// Operations must not be represented as additional API formats: both
|
||||
/// Anthropic message creation and token counting use the `claude:messages`
|
||||
/// request contract.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum ApiOperation {
|
||||
ClaudeMessagesCreate,
|
||||
ClaudeCountTokens,
|
||||
OpenAiResponsesCompact,
|
||||
}
|
||||
|
||||
impl ApiOperation {
|
||||
pub const fn as_str(self) -> &'static str {
|
||||
match self {
|
||||
Self::ClaudeMessagesCreate => "messages",
|
||||
Self::ClaudeCountTokens => "count_tokens",
|
||||
Self::OpenAiResponsesCompact => "compact",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{ApiOperation, ClientSurface};
|
||||
|
||||
#[test]
|
||||
fn request_dimensions_have_stable_external_names() {
|
||||
assert_eq!(ClientSurface::ClaudeCode.as_str(), "claude_code");
|
||||
assert_eq!(ApiOperation::ClaudeMessagesCreate.as_str(), "messages");
|
||||
assert_eq!(ApiOperation::ClaudeCountTokens.as_str(), "count_tokens");
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,7 @@ pub enum LocalCoreSyncErrorKind {
|
||||
NotFound,
|
||||
RateLimit,
|
||||
ContextLengthExceeded,
|
||||
RequestTooLarge,
|
||||
Overloaded,
|
||||
ServerError,
|
||||
}
|
||||
@@ -93,7 +94,9 @@ fn map_local_sync_error_kind_to_openai_type(kind: LocalCoreSyncErrorKind) -> &'s
|
||||
LocalCoreSyncErrorKind::PermissionDenied => "permission_error",
|
||||
LocalCoreSyncErrorKind::NotFound => "not_found_error",
|
||||
LocalCoreSyncErrorKind::RateLimit => "rate_limit_error",
|
||||
LocalCoreSyncErrorKind::ContextLengthExceeded => "context_length_exceeded",
|
||||
LocalCoreSyncErrorKind::ContextLengthExceeded | LocalCoreSyncErrorKind::RequestTooLarge => {
|
||||
"context_length_exceeded"
|
||||
}
|
||||
LocalCoreSyncErrorKind::Overloaded | LocalCoreSyncErrorKind::ServerError => "server_error",
|
||||
}
|
||||
}
|
||||
@@ -103,19 +106,21 @@ fn map_local_sync_error_kind_to_claude_type(kind: LocalCoreSyncErrorKind) -> &'s
|
||||
LocalCoreSyncErrorKind::InvalidRequest | LocalCoreSyncErrorKind::ContextLengthExceeded => {
|
||||
"invalid_request_error"
|
||||
}
|
||||
LocalCoreSyncErrorKind::RequestTooLarge => "request_too_large",
|
||||
LocalCoreSyncErrorKind::Authentication => "authentication_error",
|
||||
LocalCoreSyncErrorKind::PermissionDenied => "permission_error",
|
||||
LocalCoreSyncErrorKind::NotFound => "not_found_error",
|
||||
LocalCoreSyncErrorKind::RateLimit => "rate_limit_error",
|
||||
LocalCoreSyncErrorKind::Overloaded | LocalCoreSyncErrorKind::ServerError => "api_error",
|
||||
LocalCoreSyncErrorKind::Overloaded => "overloaded_error",
|
||||
LocalCoreSyncErrorKind::ServerError => "api_error",
|
||||
}
|
||||
}
|
||||
|
||||
fn map_local_sync_error_kind_to_gemini_code(kind: LocalCoreSyncErrorKind) -> u16 {
|
||||
match kind {
|
||||
LocalCoreSyncErrorKind::InvalidRequest | LocalCoreSyncErrorKind::ContextLengthExceeded => {
|
||||
400
|
||||
}
|
||||
LocalCoreSyncErrorKind::InvalidRequest
|
||||
| LocalCoreSyncErrorKind::ContextLengthExceeded
|
||||
| LocalCoreSyncErrorKind::RequestTooLarge => 400,
|
||||
LocalCoreSyncErrorKind::Authentication => 401,
|
||||
LocalCoreSyncErrorKind::PermissionDenied => 403,
|
||||
LocalCoreSyncErrorKind::NotFound => 404,
|
||||
@@ -127,9 +132,9 @@ fn map_local_sync_error_kind_to_gemini_code(kind: LocalCoreSyncErrorKind) -> u16
|
||||
|
||||
fn map_local_sync_error_kind_to_gemini_status(kind: LocalCoreSyncErrorKind) -> &'static str {
|
||||
match kind {
|
||||
LocalCoreSyncErrorKind::InvalidRequest | LocalCoreSyncErrorKind::ContextLengthExceeded => {
|
||||
"INVALID_ARGUMENT"
|
||||
}
|
||||
LocalCoreSyncErrorKind::InvalidRequest
|
||||
| LocalCoreSyncErrorKind::ContextLengthExceeded
|
||||
| LocalCoreSyncErrorKind::RequestTooLarge => "INVALID_ARGUMENT",
|
||||
LocalCoreSyncErrorKind::Authentication => "UNAUTHENTICATED",
|
||||
LocalCoreSyncErrorKind::PermissionDenied => "PERMISSION_DENIED",
|
||||
LocalCoreSyncErrorKind::NotFound => "NOT_FOUND",
|
||||
@@ -176,6 +181,51 @@ mod tests {
|
||||
assert_eq!(body["error"]["code"], "upstream_unavailable");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn builds_claude_overloaded_error_body() {
|
||||
let body = build_core_error_body_for_client_format(
|
||||
"claude:messages",
|
||||
"busy",
|
||||
None,
|
||||
LocalCoreSyncErrorKind::Overloaded,
|
||||
)
|
||||
.expect("body should build");
|
||||
|
||||
assert_eq!(body["type"], "error");
|
||||
assert_eq!(body["error"]["type"], "overloaded_error");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn maps_request_too_large_for_each_client_format() {
|
||||
let claude = build_core_error_body_for_client_format(
|
||||
"claude:messages",
|
||||
"too large",
|
||||
None,
|
||||
LocalCoreSyncErrorKind::RequestTooLarge,
|
||||
)
|
||||
.expect("Claude body should build");
|
||||
assert_eq!(claude["error"]["type"], "request_too_large");
|
||||
|
||||
let openai = build_core_error_body_for_client_format(
|
||||
"openai:chat",
|
||||
"too large",
|
||||
None,
|
||||
LocalCoreSyncErrorKind::RequestTooLarge,
|
||||
)
|
||||
.expect("OpenAI body should build");
|
||||
assert_eq!(openai["error"]["type"], "context_length_exceeded");
|
||||
|
||||
let gemini = build_core_error_body_for_client_format(
|
||||
"gemini:generate_content",
|
||||
"too large",
|
||||
None,
|
||||
LocalCoreSyncErrorKind::RequestTooLarge,
|
||||
)
|
||||
.expect("Gemini body should build");
|
||||
assert_eq!(gemini["error"]["code"], 400);
|
||||
assert_eq!(gemini["error"]["status"], "INVALID_ARGUMENT");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn recognizes_finalize_kind_and_success_mapping() {
|
||||
assert!(is_core_error_finalize_kind("openai_chat_sync_finalize"));
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
use crate::contracts::{
|
||||
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, GEMINI_EMBEDDING_SYNC_PLAN_KIND,
|
||||
GEMINI_EMBEDDING_SYNC_SUCCESS_REPORT_KIND, GEMINI_INTERACTIONS_STREAM_PLAN_KIND,
|
||||
GEMINI_INTERACTIONS_STREAM_SUCCESS_REPORT_KIND, GEMINI_INTERACTIONS_SYNC_PLAN_KIND,
|
||||
GEMINI_INTERACTIONS_SYNC_SUCCESS_REPORT_KIND, OPENAI_EMBEDDING_SYNC_PLAN_KIND,
|
||||
OPENAI_RERANK_SYNC_PLAN_KIND, OPENAI_SEARCH_SYNC_PLAN_KIND,
|
||||
ApiOperation, CLAUDE_CHAT_STREAM_PLAN_KIND, CLAUDE_CHAT_SYNC_PLAN_KIND,
|
||||
CLAUDE_CLI_STREAM_PLAN_KIND, CLAUDE_CLI_SYNC_PLAN_KIND, CLAUDE_COUNT_TOKENS_SYNC_PLAN_KIND,
|
||||
CLAUDE_COUNT_TOKENS_SYNC_SUCCESS_REPORT_KIND, GEMINI_CHAT_STREAM_PLAN_KIND,
|
||||
GEMINI_CHAT_SYNC_PLAN_KIND, GEMINI_CLI_STREAM_PLAN_KIND, GEMINI_CLI_SYNC_PLAN_KIND,
|
||||
GEMINI_EMBEDDING_SYNC_PLAN_KIND, GEMINI_EMBEDDING_SYNC_SUCCESS_REPORT_KIND,
|
||||
GEMINI_INTERACTIONS_STREAM_PLAN_KIND, GEMINI_INTERACTIONS_STREAM_SUCCESS_REPORT_KIND,
|
||||
GEMINI_INTERACTIONS_SYNC_PLAN_KIND, GEMINI_INTERACTIONS_SYNC_SUCCESS_REPORT_KIND,
|
||||
OPENAI_EMBEDDING_SYNC_PLAN_KIND, OPENAI_RERANK_SYNC_PLAN_KIND, OPENAI_SEARCH_SYNC_PLAN_KIND,
|
||||
OPENAI_SEARCH_SYNC_SUCCESS_REPORT_KIND,
|
||||
};
|
||||
|
||||
@@ -22,6 +23,7 @@ pub struct LocalSameFormatProviderSpec {
|
||||
pub report_kind: &'static str,
|
||||
pub family: LocalSameFormatProviderFamily,
|
||||
pub require_streaming: bool,
|
||||
pub operation: Option<ApiOperation>,
|
||||
}
|
||||
|
||||
pub fn resolve_sync_spec(plan_kind: &str) -> Option<LocalSameFormatProviderSpec> {
|
||||
@@ -32,6 +34,7 @@ pub fn resolve_sync_spec(plan_kind: &str) -> Option<LocalSameFormatProviderSpec>
|
||||
report_kind: "claude_chat_sync_success",
|
||||
family: LocalSameFormatProviderFamily::Standard,
|
||||
require_streaming: false,
|
||||
operation: Some(ApiOperation::ClaudeMessagesCreate),
|
||||
}),
|
||||
CLAUDE_CLI_SYNC_PLAN_KIND => Some(LocalSameFormatProviderSpec {
|
||||
api_format: "claude:messages",
|
||||
@@ -39,6 +42,15 @@ pub fn resolve_sync_spec(plan_kind: &str) -> Option<LocalSameFormatProviderSpec>
|
||||
report_kind: "claude_cli_sync_success",
|
||||
family: LocalSameFormatProviderFamily::Standard,
|
||||
require_streaming: false,
|
||||
operation: Some(ApiOperation::ClaudeMessagesCreate),
|
||||
}),
|
||||
CLAUDE_COUNT_TOKENS_SYNC_PLAN_KIND => Some(LocalSameFormatProviderSpec {
|
||||
api_format: "claude:messages",
|
||||
decision_kind: CLAUDE_COUNT_TOKENS_SYNC_PLAN_KIND,
|
||||
report_kind: CLAUDE_COUNT_TOKENS_SYNC_SUCCESS_REPORT_KIND,
|
||||
family: LocalSameFormatProviderFamily::Standard,
|
||||
require_streaming: false,
|
||||
operation: Some(ApiOperation::ClaudeCountTokens),
|
||||
}),
|
||||
GEMINI_CHAT_SYNC_PLAN_KIND => Some(LocalSameFormatProviderSpec {
|
||||
api_format: "gemini:generate_content",
|
||||
@@ -46,6 +58,7 @@ pub fn resolve_sync_spec(plan_kind: &str) -> Option<LocalSameFormatProviderSpec>
|
||||
report_kind: "gemini_chat_sync_success",
|
||||
family: LocalSameFormatProviderFamily::Gemini,
|
||||
require_streaming: false,
|
||||
operation: None,
|
||||
}),
|
||||
GEMINI_CLI_SYNC_PLAN_KIND => Some(LocalSameFormatProviderSpec {
|
||||
api_format: "gemini:generate_content",
|
||||
@@ -53,6 +66,7 @@ pub fn resolve_sync_spec(plan_kind: &str) -> Option<LocalSameFormatProviderSpec>
|
||||
report_kind: "gemini_cli_sync_success",
|
||||
family: LocalSameFormatProviderFamily::Gemini,
|
||||
require_streaming: false,
|
||||
operation: None,
|
||||
}),
|
||||
GEMINI_EMBEDDING_SYNC_PLAN_KIND => Some(LocalSameFormatProviderSpec {
|
||||
api_format: "gemini:embedding",
|
||||
@@ -60,6 +74,7 @@ pub fn resolve_sync_spec(plan_kind: &str) -> Option<LocalSameFormatProviderSpec>
|
||||
report_kind: GEMINI_EMBEDDING_SYNC_SUCCESS_REPORT_KIND,
|
||||
family: LocalSameFormatProviderFamily::Gemini,
|
||||
require_streaming: false,
|
||||
operation: None,
|
||||
}),
|
||||
GEMINI_INTERACTIONS_SYNC_PLAN_KIND => Some(LocalSameFormatProviderSpec {
|
||||
api_format: "gemini:interactions",
|
||||
@@ -67,6 +82,7 @@ pub fn resolve_sync_spec(plan_kind: &str) -> Option<LocalSameFormatProviderSpec>
|
||||
report_kind: GEMINI_INTERACTIONS_SYNC_SUCCESS_REPORT_KIND,
|
||||
family: LocalSameFormatProviderFamily::Gemini,
|
||||
require_streaming: false,
|
||||
operation: None,
|
||||
}),
|
||||
OPENAI_EMBEDDING_SYNC_PLAN_KIND => Some(LocalSameFormatProviderSpec {
|
||||
api_format: "openai:embedding",
|
||||
@@ -74,6 +90,7 @@ pub fn resolve_sync_spec(plan_kind: &str) -> Option<LocalSameFormatProviderSpec>
|
||||
report_kind: "openai_embedding_sync_success",
|
||||
family: LocalSameFormatProviderFamily::Standard,
|
||||
require_streaming: false,
|
||||
operation: None,
|
||||
}),
|
||||
OPENAI_RERANK_SYNC_PLAN_KIND => Some(LocalSameFormatProviderSpec {
|
||||
api_format: "openai:rerank",
|
||||
@@ -81,6 +98,7 @@ pub fn resolve_sync_spec(plan_kind: &str) -> Option<LocalSameFormatProviderSpec>
|
||||
report_kind: "openai_rerank_sync_success",
|
||||
family: LocalSameFormatProviderFamily::Standard,
|
||||
require_streaming: false,
|
||||
operation: None,
|
||||
}),
|
||||
OPENAI_SEARCH_SYNC_PLAN_KIND => Some(LocalSameFormatProviderSpec {
|
||||
api_format: "openai:search",
|
||||
@@ -88,6 +106,7 @@ pub fn resolve_sync_spec(plan_kind: &str) -> Option<LocalSameFormatProviderSpec>
|
||||
report_kind: OPENAI_SEARCH_SYNC_SUCCESS_REPORT_KIND,
|
||||
family: LocalSameFormatProviderFamily::Standard,
|
||||
require_streaming: false,
|
||||
operation: None,
|
||||
}),
|
||||
_ => None,
|
||||
}
|
||||
@@ -101,6 +120,7 @@ pub fn resolve_stream_spec(plan_kind: &str) -> Option<LocalSameFormatProviderSpe
|
||||
report_kind: "claude_chat_stream_success",
|
||||
family: LocalSameFormatProviderFamily::Standard,
|
||||
require_streaming: true,
|
||||
operation: Some(ApiOperation::ClaudeMessagesCreate),
|
||||
}),
|
||||
CLAUDE_CLI_STREAM_PLAN_KIND => Some(LocalSameFormatProviderSpec {
|
||||
api_format: "claude:messages",
|
||||
@@ -108,6 +128,7 @@ pub fn resolve_stream_spec(plan_kind: &str) -> Option<LocalSameFormatProviderSpe
|
||||
report_kind: "claude_cli_stream_success",
|
||||
family: LocalSameFormatProviderFamily::Standard,
|
||||
require_streaming: true,
|
||||
operation: Some(ApiOperation::ClaudeMessagesCreate),
|
||||
}),
|
||||
GEMINI_CHAT_STREAM_PLAN_KIND => Some(LocalSameFormatProviderSpec {
|
||||
api_format: "gemini:generate_content",
|
||||
@@ -115,6 +136,7 @@ pub fn resolve_stream_spec(plan_kind: &str) -> Option<LocalSameFormatProviderSpe
|
||||
report_kind: "gemini_chat_stream_success",
|
||||
family: LocalSameFormatProviderFamily::Gemini,
|
||||
require_streaming: true,
|
||||
operation: None,
|
||||
}),
|
||||
GEMINI_CLI_STREAM_PLAN_KIND => Some(LocalSameFormatProviderSpec {
|
||||
api_format: "gemini:generate_content",
|
||||
@@ -122,6 +144,7 @@ pub fn resolve_stream_spec(plan_kind: &str) -> Option<LocalSameFormatProviderSpe
|
||||
report_kind: "gemini_cli_stream_success",
|
||||
family: LocalSameFormatProviderFamily::Gemini,
|
||||
require_streaming: true,
|
||||
operation: None,
|
||||
}),
|
||||
GEMINI_INTERACTIONS_STREAM_PLAN_KIND => Some(LocalSameFormatProviderSpec {
|
||||
api_format: "gemini:interactions",
|
||||
@@ -129,6 +152,7 @@ pub fn resolve_stream_spec(plan_kind: &str) -> Option<LocalSameFormatProviderSpe
|
||||
report_kind: GEMINI_INTERACTIONS_STREAM_SUCCESS_REPORT_KIND,
|
||||
family: LocalSameFormatProviderFamily::Gemini,
|
||||
require_streaming: true,
|
||||
operation: None,
|
||||
}),
|
||||
_ => None,
|
||||
}
|
||||
|
||||
@@ -2,20 +2,21 @@ use http::Method;
|
||||
use url::form_urlencoded;
|
||||
|
||||
use crate::contracts::{
|
||||
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, GEMINI_EMBEDDING_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_INTERACTIONS_STREAM_PLAN_KIND, GEMINI_INTERACTIONS_SYNC_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_EMBEDDING_SYNC_PLAN_KIND,
|
||||
OPENAI_IMAGE_STREAM_PLAN_KIND, OPENAI_IMAGE_SYNC_PLAN_KIND, OPENAI_RERANK_SYNC_PLAN_KIND,
|
||||
OPENAI_RESPONSES_COMPACT_STREAM_PLAN_KIND, OPENAI_RESPONSES_COMPACT_SYNC_PLAN_KIND,
|
||||
OPENAI_RESPONSES_STREAM_PLAN_KIND, OPENAI_RESPONSES_SYNC_PLAN_KIND,
|
||||
OPENAI_SEARCH_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,
|
||||
ClientSurface, CLAUDE_CHAT_STREAM_PLAN_KIND, CLAUDE_CHAT_SYNC_PLAN_KIND,
|
||||
CLAUDE_CLI_STREAM_PLAN_KIND, CLAUDE_CLI_SYNC_PLAN_KIND, CLAUDE_COUNT_TOKENS_SYNC_PLAN_KIND,
|
||||
GEMINI_CHAT_STREAM_PLAN_KIND, GEMINI_CHAT_SYNC_PLAN_KIND, GEMINI_CLI_STREAM_PLAN_KIND,
|
||||
GEMINI_CLI_SYNC_PLAN_KIND, GEMINI_EMBEDDING_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_INTERACTIONS_STREAM_PLAN_KIND,
|
||||
GEMINI_INTERACTIONS_SYNC_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_EMBEDDING_SYNC_PLAN_KIND, OPENAI_IMAGE_STREAM_PLAN_KIND, OPENAI_IMAGE_SYNC_PLAN_KIND,
|
||||
OPENAI_RERANK_SYNC_PLAN_KIND, OPENAI_RESPONSES_COMPACT_STREAM_PLAN_KIND,
|
||||
OPENAI_RESPONSES_COMPACT_SYNC_PLAN_KIND, OPENAI_RESPONSES_STREAM_PLAN_KIND,
|
||||
OPENAI_RESPONSES_SYNC_PLAN_KIND, OPENAI_SEARCH_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,
|
||||
};
|
||||
use crate::formats::openai::image::request::is_openai_image_stream_request;
|
||||
|
||||
@@ -26,6 +27,26 @@ pub fn resolve_execution_runtime_stream_plan_kind(
|
||||
request_auth_channel: Option<&str>,
|
||||
method: &Method,
|
||||
path: &str,
|
||||
) -> Option<&'static str> {
|
||||
resolve_execution_runtime_stream_plan_kind_with_client_surface(
|
||||
route_class,
|
||||
route_family,
|
||||
route_kind,
|
||||
legacy_client_surface_from_auth_channel(request_auth_channel),
|
||||
request_auth_channel,
|
||||
method,
|
||||
path,
|
||||
)
|
||||
}
|
||||
|
||||
pub fn resolve_execution_runtime_stream_plan_kind_with_client_surface(
|
||||
route_class: Option<&str>,
|
||||
route_family: Option<&str>,
|
||||
route_kind: Option<&str>,
|
||||
client_surface: Option<ClientSurface>,
|
||||
request_auth_channel: Option<&str>,
|
||||
method: &Method,
|
||||
path: &str,
|
||||
) -> Option<&'static str> {
|
||||
if route_class != Some("ai_public") {
|
||||
return None;
|
||||
@@ -53,7 +74,7 @@ pub fn resolve_execution_runtime_stream_plan_kind(
|
||||
&& path == "/v1/messages"
|
||||
{
|
||||
return Some(resolve_claude_messages_plan_kind(
|
||||
request_auth_channel,
|
||||
client_surface,
|
||||
CLAUDE_CHAT_STREAM_PLAN_KIND,
|
||||
CLAUDE_CLI_STREAM_PLAN_KIND,
|
||||
));
|
||||
@@ -121,6 +142,26 @@ pub fn resolve_execution_runtime_sync_plan_kind(
|
||||
request_auth_channel: Option<&str>,
|
||||
method: &Method,
|
||||
path: &str,
|
||||
) -> Option<&'static str> {
|
||||
resolve_execution_runtime_sync_plan_kind_with_client_surface(
|
||||
route_class,
|
||||
route_family,
|
||||
route_kind,
|
||||
legacy_client_surface_from_auth_channel(request_auth_channel),
|
||||
request_auth_channel,
|
||||
method,
|
||||
path,
|
||||
)
|
||||
}
|
||||
|
||||
pub fn resolve_execution_runtime_sync_plan_kind_with_client_surface(
|
||||
route_class: Option<&str>,
|
||||
route_family: Option<&str>,
|
||||
route_kind: Option<&str>,
|
||||
client_surface: Option<ClientSurface>,
|
||||
request_auth_channel: Option<&str>,
|
||||
method: &Method,
|
||||
path: &str,
|
||||
) -> Option<&'static str> {
|
||||
if route_class != Some("ai_public") {
|
||||
return None;
|
||||
@@ -248,13 +289,21 @@ pub fn resolve_execution_runtime_sync_plan_kind(
|
||||
return Some(OPENAI_RESPONSES_COMPACT_SYNC_PLAN_KIND);
|
||||
}
|
||||
|
||||
if route_family == Some("claude")
|
||||
&& route_kind == Some("count_tokens")
|
||||
&& *method == Method::POST
|
||||
&& path == "/v1/messages/count_tokens"
|
||||
{
|
||||
return Some(CLAUDE_COUNT_TOKENS_SYNC_PLAN_KIND);
|
||||
}
|
||||
|
||||
if route_family == Some("claude")
|
||||
&& is_claude_messages_route_kind(route_kind)
|
||||
&& *method == Method::POST
|
||||
&& path == "/v1/messages"
|
||||
{
|
||||
return Some(resolve_claude_messages_plan_kind(
|
||||
request_auth_channel,
|
||||
client_surface,
|
||||
CLAUDE_CHAT_SYNC_PLAN_KIND,
|
||||
CLAUDE_CLI_SYNC_PLAN_KIND,
|
||||
));
|
||||
@@ -313,17 +362,27 @@ fn is_gemini_generate_content_route_kind(route_kind: Option<&str>) -> bool {
|
||||
}
|
||||
|
||||
fn resolve_claude_messages_plan_kind(
|
||||
request_auth_channel: Option<&str>,
|
||||
client_surface: Option<ClientSurface>,
|
||||
chat_plan_kind: &'static str,
|
||||
cli_plan_kind: &'static str,
|
||||
) -> &'static str {
|
||||
if request_auth_channel == Some("bearer_like") {
|
||||
if client_surface == Some(ClientSurface::ClaudeCode) {
|
||||
cli_plan_kind
|
||||
} else {
|
||||
chat_plan_kind
|
||||
}
|
||||
}
|
||||
|
||||
fn legacy_client_surface_from_auth_channel(
|
||||
request_auth_channel: Option<&str>,
|
||||
) -> Option<ClientSurface> {
|
||||
Some(if request_auth_channel == Some("bearer_like") {
|
||||
ClientSurface::ClaudeCode
|
||||
} else {
|
||||
ClientSurface::GenericCompatible
|
||||
})
|
||||
}
|
||||
|
||||
fn resolve_gemini_generate_content_plan_kind(
|
||||
request_auth_channel: Option<&str>,
|
||||
chat_plan_kind: &'static str,
|
||||
@@ -447,6 +506,7 @@ pub fn supports_sync_execution_decision_kind(plan_kind: &str) -> bool {
|
||||
| OPENAI_RESPONSES_COMPACT_SYNC_PLAN_KIND
|
||||
| CLAUDE_CHAT_SYNC_PLAN_KIND
|
||||
| CLAUDE_CLI_SYNC_PLAN_KIND
|
||||
| CLAUDE_COUNT_TOKENS_SYNC_PLAN_KIND
|
||||
| GEMINI_CHAT_SYNC_PLAN_KIND
|
||||
| GEMINI_CLI_SYNC_PLAN_KIND
|
||||
| GEMINI_EMBEDDING_SYNC_PLAN_KIND
|
||||
|
||||
@@ -10,6 +10,7 @@ use crate::formats::shared::response::{
|
||||
};
|
||||
use crate::formats::shared::sse::encode_json_sse;
|
||||
use crate::formats::shared::stream_core::StreamingStandardFormatMatrix;
|
||||
use crate::formats::shared::sync_products::anthropic_legacy_compatibility_enabled;
|
||||
use crate::formats::shared::AiSurfaceFinalizeError;
|
||||
use crate::provider_compat::kiro_stream::KiroToClaudeCliStreamState;
|
||||
use crate::provider_compat::private_envelope::transform_provider_private_stream_line;
|
||||
@@ -117,7 +118,12 @@ pub fn resolve_finalize_stream_rewrite_mode(
|
||||
if provider_stream_event_api_format == "claude:messages"
|
||||
&& client_api_format == "claude:messages"
|
||||
{
|
||||
return Some(FinalizeStreamRewriteMode::ClaudeReadToolSanitize);
|
||||
return if anthropic_legacy_compatibility_enabled(report_context) {
|
||||
Some(FinalizeStreamRewriteMode::ClaudeReadToolSanitize)
|
||||
} else {
|
||||
model_directive_display_model_from_report_context(report_context)
|
||||
.map(|_| FinalizeStreamRewriteMode::ModelDirectiveDisplay)
|
||||
};
|
||||
}
|
||||
return model_directive_display_model_from_report_context(report_context)
|
||||
.map(|_| FinalizeStreamRewriteMode::ModelDirectiveDisplay);
|
||||
@@ -144,7 +150,11 @@ pub fn resolve_finalize_stream_rewrite_mode(
|
||||
)
|
||||
{
|
||||
if provider_stream_event_api_format == "claude:messages" {
|
||||
return Some(FinalizeStreamRewriteMode::ClaudeReadToolSanitize);
|
||||
return Some(if anthropic_legacy_compatibility_enabled(report_context) {
|
||||
FinalizeStreamRewriteMode::ClaudeReadToolSanitize
|
||||
} else {
|
||||
FinalizeStreamRewriteMode::ModelDirectiveDisplay
|
||||
});
|
||||
}
|
||||
return Some(FinalizeStreamRewriteMode::ModelDirectiveDisplay);
|
||||
}
|
||||
@@ -152,7 +162,8 @@ pub fn resolve_finalize_stream_rewrite_mode(
|
||||
if provider_stream_event_api_format == "claude:messages"
|
||||
&& client_api_format == "claude:messages"
|
||||
{
|
||||
return Some(FinalizeStreamRewriteMode::ClaudeReadToolSanitize);
|
||||
return anthropic_legacy_compatibility_enabled(report_context)
|
||||
.then_some(FinalizeStreamRewriteMode::ClaudeReadToolSanitize);
|
||||
}
|
||||
|
||||
if is_same_format_family(
|
||||
@@ -1160,13 +1171,19 @@ data: {\"type\":\"response.completed\",\"response\":{\"id\":\"resp_123\",\"objec
|
||||
"needs_conversion": true,
|
||||
"model": "claude-sonnet-4.5-high",
|
||||
"mapped_model": "claude-sonnet-4.5",
|
||||
"provider_type": "claude_code",
|
||||
"anthropic_compatibility_profile": "native_transparent",
|
||||
});
|
||||
assert_eq!(
|
||||
resolve_finalize_stream_rewrite_mode(&report_context),
|
||||
Some(FinalizeStreamRewriteMode::ModelDirectiveDisplay)
|
||||
);
|
||||
let mut rewriter = maybe_build_ai_surface_stream_rewriter(Some(&report_context))
|
||||
.expect("rewriter should exist");
|
||||
let output = rewriter
|
||||
.push_chunk(
|
||||
b"event: content_block_delta\n\
|
||||
data: {\"type\":\"content_block_delta\",\"index\":1,\"delta\":{\"type\":\"thinking_delta\",\"thinking\":\"Let me reason...\"}}\n\n",
|
||||
data: {\"type\":\"content_block_delta\",\"index\":1,\"future_event_field\":{\"keep\":true},\"delta\":{\"type\":\"thinking_delta\",\"thinking\":\"Let me reason...\",\"future_delta_field\":42}}\n\n",
|
||||
)
|
||||
.expect("rewrite should succeed");
|
||||
let output = String::from_utf8(output).expect("output should be utf8");
|
||||
@@ -1175,20 +1192,51 @@ data: {\"type\":\"content_block_delta\",\"index\":1,\"delta\":{\"type\":\"thinki
|
||||
assert!(output.contains("event: content_block_delta"));
|
||||
assert!(output.contains("\"thinking\":\"Let me reason...\""));
|
||||
assert!(output.contains("\"type\":\"thinking_delta\""));
|
||||
assert!(output.contains("\"future_event_field\":{\"keep\":true}"));
|
||||
assert!(output.contains("\"future_delta_field\":42"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn same_format_claude_uses_read_tool_sanitizer_without_display_model() {
|
||||
// Claude→Claude needs a narrow sanitizer for Claude Code Read input.
|
||||
fn native_same_format_claude_without_display_model_is_verbatim() {
|
||||
let report_context = json!({
|
||||
"provider_api_format": "claude:messages",
|
||||
"client_api_format": "claude:messages",
|
||||
"needs_conversion": true,
|
||||
});
|
||||
assert_eq!(
|
||||
resolve_finalize_stream_rewrite_mode(&report_context),
|
||||
Some(FinalizeStreamRewriteMode::ClaudeReadToolSanitize)
|
||||
);
|
||||
assert_eq!(resolve_finalize_stream_rewrite_mode(&report_context), None);
|
||||
assert!(maybe_build_ai_surface_stream_rewriter(Some(&report_context)).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn claude_code_legacy_profile_and_provider_fallback_enable_read_tool_sanitizer() {
|
||||
for report_context in [
|
||||
json!({
|
||||
"provider_api_format": "claude:messages",
|
||||
"client_api_format": "claude:messages",
|
||||
"needs_conversion": false,
|
||||
"anthropic_compatibility_profile": "claude_code_legacy",
|
||||
}),
|
||||
json!({
|
||||
"provider_api_format": "claude:messages",
|
||||
"client_api_format": "claude:messages",
|
||||
"needs_conversion": false,
|
||||
"provider_type": "claude_code",
|
||||
}),
|
||||
] {
|
||||
assert_eq!(
|
||||
resolve_finalize_stream_rewrite_mode(&report_context),
|
||||
Some(FinalizeStreamRewriteMode::ClaudeReadToolSanitize)
|
||||
);
|
||||
}
|
||||
|
||||
let explicit_native = json!({
|
||||
"provider_api_format": "claude:messages",
|
||||
"client_api_format": "claude:messages",
|
||||
"needs_conversion": false,
|
||||
"provider_type": "claude_code",
|
||||
"anthropic_compatibility_profile": "native_transparent",
|
||||
});
|
||||
assert_eq!(resolve_finalize_stream_rewrite_mode(&explicit_native), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -1197,6 +1245,7 @@ data: {\"type\":\"content_block_delta\",\"index\":1,\"delta\":{\"type\":\"thinki
|
||||
"provider_api_format": "claude:messages",
|
||||
"client_api_format": "claude:messages",
|
||||
"needs_conversion": false,
|
||||
"anthropic_compatibility_profile": "claude_code_legacy",
|
||||
});
|
||||
let mut rewriter = maybe_build_ai_surface_stream_rewriter(Some(&report_context))
|
||||
.expect("same-format claude sanitizer should exist");
|
||||
@@ -1219,6 +1268,7 @@ data: {\"type\":\"content_block_start\",\"index\":0,\"content_block\":{\"type\":
|
||||
"provider_api_format": "claude:messages",
|
||||
"client_api_format": "claude:messages",
|
||||
"needs_conversion": false,
|
||||
"anthropic_compatibility_profile": "claude_code_legacy",
|
||||
});
|
||||
let mut rewriter = maybe_build_ai_surface_stream_rewriter(Some(&report_context))
|
||||
.expect("same-format claude sanitizer should exist");
|
||||
@@ -1263,6 +1313,7 @@ data: {\"type\":\"content_block_stop\",\"index\":0}\n\n",
|
||||
"provider_api_format": "claude:messages",
|
||||
"client_api_format": "claude:messages",
|
||||
"needs_conversion": false,
|
||||
"anthropic_compatibility_profile": "claude_code_legacy",
|
||||
});
|
||||
let mut rewriter = maybe_build_ai_surface_stream_rewriter(Some(&report_context))
|
||||
.expect("same-format claude sanitizer should exist");
|
||||
|
||||
@@ -25,10 +25,7 @@ use crate::formats::claude::messages::stream::ClaudeProviderState;
|
||||
use crate::formats::gemini::generate_content::stream::GeminiProviderState;
|
||||
use crate::formats::openai::chat::stream::{OpenAIChatProviderState, OpenAIResponsesProviderState};
|
||||
use crate::formats::shared::model_directives::model_directive_display_model_from_report_context;
|
||||
use crate::formats::shared::response::{
|
||||
remove_empty_pages_from_tool_arguments, remove_empty_pages_from_tool_input_value,
|
||||
sanitize_claude_read_tool_inputs,
|
||||
};
|
||||
use crate::formats::shared::response::sanitize_claude_read_tool_inputs;
|
||||
use crate::formats::shared::stream_core::common::{
|
||||
content_part_from_openai_image_generation_item, gemini_usage_metadata_from_usage,
|
||||
map_openai_finish_reason_to_gemini, parse_json_arguments_value,
|
||||
@@ -685,7 +682,9 @@ fn maybe_build_standard_same_format_sync_body(
|
||||
}
|
||||
|
||||
let mut body_json = body_json.clone();
|
||||
if expected_api_format == "claude:messages" {
|
||||
if expected_api_format == "claude:messages"
|
||||
&& anthropic_legacy_compatibility_enabled(report_context)
|
||||
{
|
||||
sanitize_claude_read_tool_inputs(&mut body_json);
|
||||
}
|
||||
|
||||
@@ -744,13 +743,18 @@ fn maybe_build_standard_same_format_stream_sync_body(
|
||||
let body_bytes = base64::engine::general_purpose::STANDARD.decode(body_base64)?;
|
||||
let provider_stream_event_api_format =
|
||||
provider_stream_event_api_format_for_report_context(report_context, &provider_api_format);
|
||||
let Some(body) = try_aggregate_standard_chat_stream_sync_response(
|
||||
let Some(mut body) = try_aggregate_standard_chat_stream_sync_response(
|
||||
&body_bytes,
|
||||
&provider_stream_event_api_format,
|
||||
)?
|
||||
else {
|
||||
return Ok(None);
|
||||
};
|
||||
if provider_stream_event_api_format == "claude:messages"
|
||||
&& anthropic_legacy_compatibility_enabled(report_context)
|
||||
{
|
||||
sanitize_claude_read_tool_inputs(&mut body);
|
||||
}
|
||||
if api_format_is_gemini_generate_content(&provider_stream_event_api_format)
|
||||
&& !gemini_generate_content_body_has_visible_output(&body)
|
||||
{
|
||||
@@ -772,6 +776,21 @@ fn maybe_build_standard_same_format_stream_sync_body(
|
||||
)))
|
||||
}
|
||||
|
||||
pub(super) fn anthropic_legacy_compatibility_enabled(report_context: &Value) -> bool {
|
||||
if let Some(profile) = report_context.get("anthropic_compatibility_profile") {
|
||||
return profile
|
||||
.as_str()
|
||||
.map(str::trim)
|
||||
.is_some_and(|profile| profile.eq_ignore_ascii_case("claude_code_legacy"));
|
||||
}
|
||||
|
||||
report_context
|
||||
.get("provider_type")
|
||||
.and_then(Value::as_str)
|
||||
.map(str::trim)
|
||||
.is_some_and(|provider_type| provider_type.eq_ignore_ascii_case("claude_code"))
|
||||
}
|
||||
|
||||
fn maybe_build_openai_responses_same_family_sync_body(
|
||||
report_kind: &str,
|
||||
status_code: u16,
|
||||
@@ -3400,22 +3419,9 @@ pub fn aggregate_claude_stream_sync_response(body: &[u8]) -> Option<Value> {
|
||||
}
|
||||
}
|
||||
"tool_use" => {
|
||||
let tool_name = block
|
||||
.get("name")
|
||||
.and_then(Value::as_str)
|
||||
.unwrap_or_default()
|
||||
.to_string();
|
||||
if let Some(input) = block.get("input") {
|
||||
let sanitized = remove_empty_pages_from_tool_input_value(&tool_name, input);
|
||||
if sanitized != *input {
|
||||
block.insert("input".to_string(), sanitized);
|
||||
}
|
||||
}
|
||||
if !state.partial_json.is_empty() {
|
||||
let arguments =
|
||||
remove_empty_pages_from_tool_arguments(&tool_name, &state.partial_json);
|
||||
let input = serde_json::from_str::<Value>(&arguments)
|
||||
.unwrap_or(Value::String(arguments));
|
||||
let input = serde_json::from_str::<Value>(&state.partial_json)
|
||||
.unwrap_or(Value::String(state.partial_json));
|
||||
block.insert("input".to_string(), input);
|
||||
}
|
||||
}
|
||||
@@ -4094,7 +4100,7 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn aggregates_claude_stream_removes_empty_pages_from_tool_input() {
|
||||
fn aggregates_native_claude_stream_preserves_empty_pages_from_tool_input() {
|
||||
let body = concat!(
|
||||
"event: message_start\n",
|
||||
"data: {\"type\":\"message_start\",\"message\":{\"id\":\"msg_123\",\"type\":\"message\",\"role\":\"assistant\",\"model\":\"claude-sonnet-4-5\",\"content\":[],\"stop_reason\":null,\"stop_sequence\":null}}\n\n",
|
||||
@@ -4118,12 +4124,13 @@ mod tests {
|
||||
"file_path": "/tmp/a.txt",
|
||||
"offset": 1,
|
||||
"limit": 20,
|
||||
"pages": "",
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn aggregates_claude_stream_removes_empty_pages_from_start_tool_input() {
|
||||
fn aggregates_native_claude_stream_preserves_empty_pages_from_start_tool_input() {
|
||||
let body = concat!(
|
||||
"event: message_start\n",
|
||||
"data: {\"type\":\"message_start\",\"message\":{\"id\":\"msg_123\",\"type\":\"message\",\"role\":\"assistant\",\"model\":\"claude-sonnet-4-5\",\"content\":[],\"stop_reason\":null,\"stop_sequence\":null}}\n\n",
|
||||
@@ -4143,6 +4150,7 @@ mod tests {
|
||||
json!({
|
||||
"file_path": "/tmp/a.txt",
|
||||
"limit": 20,
|
||||
"pages": "",
|
||||
})
|
||||
);
|
||||
}
|
||||
@@ -4496,8 +4504,8 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn same_format_claude_sync_body_sanitizes_read_tool_input() {
|
||||
let report_context = json!({
|
||||
fn same_format_claude_sync_body_sanitizes_only_for_legacy_compatibility() {
|
||||
let native_report_context = json!({
|
||||
"provider_api_format": "claude:messages",
|
||||
"client_api_format": "claude:messages",
|
||||
"needs_conversion": false,
|
||||
@@ -4507,6 +4515,7 @@ mod tests {
|
||||
"type": "message",
|
||||
"role": "assistant",
|
||||
"model": "claude-sonnet-4-6",
|
||||
"future_message_field": {"preserve": true},
|
||||
"content": [
|
||||
{
|
||||
"type": "tool_use",
|
||||
@@ -4515,8 +4524,10 @@ mod tests {
|
||||
"input": {
|
||||
"file_path": "/tmp/a.txt",
|
||||
"limit": 20,
|
||||
"pages": ""
|
||||
}
|
||||
"pages": "",
|
||||
"future_input_field": 42
|
||||
},
|
||||
"future_block_field": "keep"
|
||||
},
|
||||
{
|
||||
"type": "tool_use",
|
||||
@@ -4530,10 +4541,28 @@ mod tests {
|
||||
]
|
||||
});
|
||||
|
||||
let native_body = maybe_build_standard_same_format_sync_body_from_normalized_payload(
|
||||
"claude_chat_sync_finalize",
|
||||
200,
|
||||
Some(&native_report_context),
|
||||
Some(&provider_body_json),
|
||||
None,
|
||||
)
|
||||
.expect("native same-format sync body should succeed")
|
||||
.expect("native body should exist");
|
||||
assert_eq!(native_body, provider_body_json);
|
||||
|
||||
let legacy_report_context = json!({
|
||||
"provider_api_format": "claude:messages",
|
||||
"client_api_format": "claude:messages",
|
||||
"needs_conversion": false,
|
||||
"anthropic_compatibility_profile": "claude_code_legacy",
|
||||
});
|
||||
|
||||
let body_json = maybe_build_standard_same_format_sync_body_from_normalized_payload(
|
||||
"claude_chat_sync_finalize",
|
||||
200,
|
||||
Some(&report_context),
|
||||
Some(&legacy_report_context),
|
||||
Some(&provider_body_json),
|
||||
None,
|
||||
)
|
||||
@@ -4545,8 +4574,11 @@ mod tests {
|
||||
json!({
|
||||
"file_path": "/tmp/a.txt",
|
||||
"limit": 20,
|
||||
"future_input_field": 42,
|
||||
})
|
||||
);
|
||||
assert_eq!(body_json["content"][0]["future_block_field"], "keep");
|
||||
assert_eq!(body_json["future_message_field"]["preserve"], true);
|
||||
assert_eq!(
|
||||
body_json["content"][1]["input"],
|
||||
json!({
|
||||
@@ -4556,6 +4588,60 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn same_format_claude_stream_sync_body_sanitizes_only_for_legacy_compatibility() {
|
||||
let stream = concat!(
|
||||
"event: message_start\n",
|
||||
"data: {\"type\":\"message_start\",\"message\":{\"id\":\"msg_read\",\"type\":\"message\",\"role\":\"assistant\",\"model\":\"claude-sonnet-4-6\",\"content\":[],\"future_message_field\":{\"keep\":true}}}\n\n",
|
||||
"event: content_block_start\n",
|
||||
"data: {\"type\":\"content_block_start\",\"index\":0,\"content_block\":{\"type\":\"tool_use\",\"id\":\"toolu_read\",\"name\":\"Read\",\"input\":{},\"future_block_field\":\"keep\"}}\n\n",
|
||||
"event: content_block_delta\n",
|
||||
"data: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"input_json_delta\",\"partial_json\":\"{\\\"file_path\\\":\\\"/tmp/a.txt\\\",\\\"pages\\\":\\\"\\\"}\"}}\n\n",
|
||||
"event: content_block_stop\n",
|
||||
"data: {\"type\":\"content_block_stop\",\"index\":0}\n\n",
|
||||
"event: message_stop\n",
|
||||
"data: {\"type\":\"message_stop\"}\n\n",
|
||||
);
|
||||
let body_base64 = base64::engine::general_purpose::STANDARD.encode(stream);
|
||||
let native_report_context = json!({
|
||||
"provider_api_format": "claude:messages",
|
||||
"client_api_format": "claude:messages",
|
||||
"needs_conversion": false,
|
||||
});
|
||||
|
||||
let native_body = maybe_build_standard_same_format_sync_body_from_normalized_payload(
|
||||
"claude_chat_sync_finalize",
|
||||
200,
|
||||
Some(&native_report_context),
|
||||
None,
|
||||
Some(&body_base64),
|
||||
)
|
||||
.expect("native stream sync body should succeed")
|
||||
.expect("native stream sync body should exist");
|
||||
assert_eq!(native_body["content"][0]["input"]["pages"], "");
|
||||
assert_eq!(native_body["content"][0]["future_block_field"], "keep");
|
||||
assert_eq!(native_body["future_message_field"]["keep"], true);
|
||||
|
||||
let legacy_report_context = json!({
|
||||
"provider_api_format": "claude:messages",
|
||||
"client_api_format": "claude:messages",
|
||||
"needs_conversion": false,
|
||||
"provider_type": "claude_code",
|
||||
});
|
||||
let legacy_body = maybe_build_standard_same_format_sync_body_from_normalized_payload(
|
||||
"claude_chat_sync_finalize",
|
||||
200,
|
||||
Some(&legacy_report_context),
|
||||
None,
|
||||
Some(&body_base64),
|
||||
)
|
||||
.expect("legacy stream sync body should succeed")
|
||||
.expect("legacy stream sync body should exist");
|
||||
assert!(legacy_body["content"][0]["input"].get("pages").is_none());
|
||||
assert_eq!(legacy_body["content"][0]["future_block_field"], "keep");
|
||||
assert_eq!(legacy_body["future_message_field"]["keep"], true);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn same_format_sync_response_restores_model_directive_display_model() {
|
||||
let report_context = json!({
|
||||
|
||||
@@ -1814,6 +1814,12 @@ mod tests {
|
||||
"event: message_stop\n",
|
||||
"data: {\"type\":\"message_stop\"}\n\n",
|
||||
);
|
||||
let report_context = json!({
|
||||
"provider_api_format": "claude:messages",
|
||||
"client_api_format": "claude:messages",
|
||||
"needs_conversion": false,
|
||||
"anthropic_compatibility_profile": "claude_code_legacy",
|
||||
});
|
||||
let outcome = maybe_bridge_standard_sync_json_to_stream(
|
||||
&json!({
|
||||
"status_code": 200,
|
||||
@@ -1825,7 +1831,7 @@ mod tests {
|
||||
}),
|
||||
"openai:responses",
|
||||
"claude:messages",
|
||||
None,
|
||||
Some(&report_context),
|
||||
)
|
||||
.expect("bridge should succeed")
|
||||
.expect("capture should bridge");
|
||||
|
||||
@@ -6,6 +6,8 @@ pub mod formats;
|
||||
pub mod protocol;
|
||||
pub mod provider_compat;
|
||||
|
||||
pub use contracts::{ApiOperation, ClientSurface};
|
||||
|
||||
pub use formats::context::{
|
||||
ConversionFieldRecord, ConversionFieldStatus, ConversionReport, Converted, FormatContext,
|
||||
FormatError,
|
||||
|
||||
Reference in New Issue
Block a user