mirror of
https://github.com/fawney19/Aether.git
synced 2026-10-12 12:09:50 +08:00
refactor: separate request auth channel from route kind
Extract request_auth_channel as a distinct routing dimension to distinguish between api_key and bearer_like authentication methods. This improves routing clarity by decoupling authentication mechanism from route classification.
This commit is contained in:
@@ -66,6 +66,7 @@ pub(crate) fn resolve_execution_runtime_stream_plan_kind(
|
|||||||
decision.route_class.as_deref(),
|
decision.route_class.as_deref(),
|
||||||
decision.route_family.as_deref(),
|
decision.route_family.as_deref(),
|
||||||
decision.route_kind.as_deref(),
|
decision.route_kind.as_deref(),
|
||||||
|
decision.request_auth_channel.as_deref(),
|
||||||
&parts.method,
|
&parts.method,
|
||||||
parts.uri.path(),
|
parts.uri.path(),
|
||||||
)
|
)
|
||||||
@@ -79,6 +80,7 @@ pub(crate) fn resolve_execution_runtime_sync_plan_kind(
|
|||||||
decision.route_class.as_deref(),
|
decision.route_class.as_deref(),
|
||||||
decision.route_family.as_deref(),
|
decision.route_family.as_deref(),
|
||||||
decision.route_kind.as_deref(),
|
decision.route_kind.as_deref(),
|
||||||
|
decision.request_auth_channel.as_deref(),
|
||||||
&parts.method,
|
&parts.method,
|
||||||
parts.uri.path(),
|
parts.uri.path(),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ fn test_decision() -> GatewayControlDecision {
|
|||||||
route_class: Some("ai_public".to_string()),
|
route_class: Some("ai_public".to_string()),
|
||||||
route_family: Some("openai".to_string()),
|
route_family: Some("openai".to_string()),
|
||||||
route_kind: Some("compact".to_string()),
|
route_kind: Some("compact".to_string()),
|
||||||
|
request_auth_channel: None,
|
||||||
auth_endpoint_signature: Some("openai:responses:compact".to_string()),
|
auth_endpoint_signature: Some("openai:responses:compact".to_string()),
|
||||||
execution_runtime_candidate: true,
|
execution_runtime_candidate: true,
|
||||||
auth_context: None,
|
auth_context: None,
|
||||||
@@ -1747,6 +1748,7 @@ fn local_finalize_handles_claude_chat_cross_format_sync_response_from_openai_cha
|
|||||||
route_class: Some("ai_public".to_string()),
|
route_class: Some("ai_public".to_string()),
|
||||||
route_family: Some("claude".to_string()),
|
route_family: Some("claude".to_string()),
|
||||||
route_kind: Some("chat".to_string()),
|
route_kind: Some("chat".to_string()),
|
||||||
|
request_auth_channel: None,
|
||||||
auth_endpoint_signature: Some("claude:messages".to_string()),
|
auth_endpoint_signature: Some("claude:messages".to_string()),
|
||||||
execution_runtime_candidate: true,
|
execution_runtime_candidate: true,
|
||||||
auth_context: None,
|
auth_context: None,
|
||||||
@@ -1813,6 +1815,7 @@ fn local_finalize_handles_gemini_cli_cross_format_sync_response_from_claude_cli(
|
|||||||
route_class: Some("ai_public".to_string()),
|
route_class: Some("ai_public".to_string()),
|
||||||
route_family: Some("gemini".to_string()),
|
route_family: Some("gemini".to_string()),
|
||||||
route_kind: Some("cli".to_string()),
|
route_kind: Some("cli".to_string()),
|
||||||
|
request_auth_channel: None,
|
||||||
auth_endpoint_signature: Some("gemini:generate_content".to_string()),
|
auth_endpoint_signature: Some("gemini:generate_content".to_string()),
|
||||||
execution_runtime_candidate: true,
|
execution_runtime_candidate: true,
|
||||||
auth_context: None,
|
auth_context: None,
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ pub(crate) fn resolve_execution_runtime_stream_plan_kind(
|
|||||||
decision.route_class.as_deref(),
|
decision.route_class.as_deref(),
|
||||||
decision.route_family.as_deref(),
|
decision.route_family.as_deref(),
|
||||||
decision.route_kind.as_deref(),
|
decision.route_kind.as_deref(),
|
||||||
|
decision.request_auth_channel.as_deref(),
|
||||||
&parts.method,
|
&parts.method,
|
||||||
parts.uri.path(),
|
parts.uri.path(),
|
||||||
)
|
)
|
||||||
@@ -28,6 +29,7 @@ pub(crate) fn resolve_execution_runtime_sync_plan_kind(
|
|||||||
decision.route_class.as_deref(),
|
decision.route_class.as_deref(),
|
||||||
decision.route_family.as_deref(),
|
decision.route_family.as_deref(),
|
||||||
decision.route_kind.as_deref(),
|
decision.route_kind.as_deref(),
|
||||||
|
decision.request_auth_channel.as_deref(),
|
||||||
&parts.method,
|
&parts.method,
|
||||||
parts.uri.path(),
|
parts.uri.path(),
|
||||||
)
|
)
|
||||||
@@ -69,6 +71,7 @@ mod tests {
|
|||||||
route_class: Some("ai_public".to_string()),
|
route_class: Some("ai_public".to_string()),
|
||||||
route_family: Some(route_family.to_string()),
|
route_family: Some(route_family.to_string()),
|
||||||
route_kind: Some(route_kind.to_string()),
|
route_kind: Some(route_kind.to_string()),
|
||||||
|
request_auth_channel: None,
|
||||||
auth_context: None,
|
auth_context: None,
|
||||||
admin_principal: None,
|
admin_principal: None,
|
||||||
auth_endpoint_signature: None,
|
auth_endpoint_signature: None,
|
||||||
@@ -77,6 +80,16 @@ mod tests {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn sample_decision_with_auth_channel(
|
||||||
|
route_family: &str,
|
||||||
|
route_kind: &str,
|
||||||
|
request_auth_channel: &str,
|
||||||
|
) -> GatewayControlDecision {
|
||||||
|
let mut decision = sample_decision(route_family, route_kind);
|
||||||
|
decision.request_auth_channel = Some(request_auth_channel.to_string());
|
||||||
|
decision
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn resolves_openai_chat_plan_kinds_via_format_crate() {
|
fn resolves_openai_chat_plan_kinds_via_format_crate() {
|
||||||
let request = Request::builder()
|
let request = Request::builder()
|
||||||
@@ -97,6 +110,47 @@ mod tests {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn resolves_endpoint_route_kinds_by_request_auth_channel_via_format_crate() {
|
||||||
|
let claude_request = Request::builder()
|
||||||
|
.method(Method::POST)
|
||||||
|
.uri("/v1/messages")
|
||||||
|
.body(())
|
||||||
|
.expect("request should build");
|
||||||
|
let (claude_parts, _) = claude_request.into_parts();
|
||||||
|
|
||||||
|
let claude_api_key = sample_decision_with_auth_channel("claude", "messages", "api_key");
|
||||||
|
let claude_bearer = sample_decision_with_auth_channel("claude", "messages", "bearer_like");
|
||||||
|
assert_eq!(
|
||||||
|
resolve_execution_runtime_sync_plan_kind(&claude_parts, &claude_api_key),
|
||||||
|
Some("claude_chat_sync")
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
resolve_execution_runtime_stream_plan_kind(&claude_parts, &claude_bearer),
|
||||||
|
Some("claude_cli_stream")
|
||||||
|
);
|
||||||
|
|
||||||
|
let gemini_request = Request::builder()
|
||||||
|
.method(Method::POST)
|
||||||
|
.uri("/v1beta/models/gemini-2.5-pro:generateContent")
|
||||||
|
.body(())
|
||||||
|
.expect("request should build");
|
||||||
|
let (gemini_parts, _) = gemini_request.into_parts();
|
||||||
|
|
||||||
|
let gemini_api_key =
|
||||||
|
sample_decision_with_auth_channel("gemini", "generate_content", "api_key");
|
||||||
|
let gemini_bearer =
|
||||||
|
sample_decision_with_auth_channel("gemini", "generate_content", "bearer_like");
|
||||||
|
assert_eq!(
|
||||||
|
resolve_execution_runtime_sync_plan_kind(&gemini_parts, &gemini_api_key),
|
||||||
|
Some("gemini_chat_sync")
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
resolve_execution_runtime_sync_plan_kind(&gemini_parts, &gemini_bearer),
|
||||||
|
Some("gemini_cli_sync")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn stream_matching_uses_surface_route_logic() {
|
fn stream_matching_uses_surface_route_logic() {
|
||||||
let request = Request::builder()
|
let request = Request::builder()
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
use super::{
|
use super::{
|
||||||
classified, is_claude_cli_request, is_gemini_cli_request, is_gemini_models_route,
|
classified, classified_with_request_auth_channel, is_claude_cli_request, is_gemini_cli_request,
|
||||||
is_gemini_operation_route, ClassifiedRoute,
|
is_gemini_models_route, is_gemini_operation_route, ClassifiedRoute,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub(super) fn classify_ai_public_route(
|
pub(super) fn classify_ai_public_route(
|
||||||
@@ -58,15 +58,16 @@ pub(super) fn classify_ai_public_route(
|
|||||||
false,
|
false,
|
||||||
))
|
))
|
||||||
} else if method == http::Method::POST && normalized_path == "/v1/messages" {
|
} else if method == http::Method::POST && normalized_path == "/v1/messages" {
|
||||||
let route_kind = if is_claude_cli_request(headers) {
|
let request_auth_channel = if is_claude_cli_request(headers) {
|
||||||
"cli"
|
"bearer_like"
|
||||||
} else {
|
} else {
|
||||||
"messages"
|
"api_key"
|
||||||
};
|
};
|
||||||
Some(classified(
|
Some(classified_with_request_auth_channel(
|
||||||
"ai_public",
|
"ai_public",
|
||||||
"claude",
|
"claude",
|
||||||
route_kind,
|
"messages",
|
||||||
|
request_auth_channel,
|
||||||
"claude:messages",
|
"claude:messages",
|
||||||
true,
|
true,
|
||||||
))
|
))
|
||||||
@@ -88,18 +89,20 @@ pub(super) fn classify_ai_public_route(
|
|||||||
true,
|
true,
|
||||||
))
|
))
|
||||||
} else if is_gemini_cli_request(headers) {
|
} else if is_gemini_cli_request(headers) {
|
||||||
Some(classified(
|
Some(classified_with_request_auth_channel(
|
||||||
"ai_public",
|
"ai_public",
|
||||||
"gemini",
|
"gemini",
|
||||||
"cli",
|
"generate_content",
|
||||||
|
"bearer_like",
|
||||||
"gemini:generate_content",
|
"gemini:generate_content",
|
||||||
true,
|
true,
|
||||||
))
|
))
|
||||||
} else {
|
} else {
|
||||||
Some(classified(
|
Some(classified_with_request_auth_channel(
|
||||||
"ai_public",
|
"ai_public",
|
||||||
"gemini",
|
"gemini",
|
||||||
"generate_content",
|
"generate_content",
|
||||||
|
"api_key",
|
||||||
"gemini:generate_content",
|
"gemini:generate_content",
|
||||||
true,
|
true,
|
||||||
))
|
))
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ pub(crate) struct GatewayControlDecision {
|
|||||||
pub(crate) route_class: Option<String>,
|
pub(crate) route_class: Option<String>,
|
||||||
pub(crate) route_family: Option<String>,
|
pub(crate) route_family: Option<String>,
|
||||||
pub(crate) route_kind: Option<String>,
|
pub(crate) route_kind: Option<String>,
|
||||||
|
pub(crate) request_auth_channel: Option<String>,
|
||||||
pub(crate) auth_endpoint_signature: Option<String>,
|
pub(crate) auth_endpoint_signature: Option<String>,
|
||||||
pub(crate) execution_runtime_candidate: bool,
|
pub(crate) execution_runtime_candidate: bool,
|
||||||
pub(crate) auth_context: Option<GatewayControlAuthContext>,
|
pub(crate) auth_context: Option<GatewayControlAuthContext>,
|
||||||
@@ -40,6 +41,7 @@ impl GatewayControlDecision {
|
|||||||
route_class,
|
route_class,
|
||||||
route_family,
|
route_family,
|
||||||
route_kind,
|
route_kind,
|
||||||
|
request_auth_channel: None,
|
||||||
auth_endpoint_signature,
|
auth_endpoint_signature,
|
||||||
execution_runtime_candidate: false,
|
execution_runtime_candidate: false,
|
||||||
auth_context: None,
|
auth_context: None,
|
||||||
@@ -75,6 +77,7 @@ pub(super) struct ClassifiedRoute {
|
|||||||
route_class: &'static str,
|
route_class: &'static str,
|
||||||
route_family: &'static str,
|
route_family: &'static str,
|
||||||
route_kind: &'static str,
|
route_kind: &'static str,
|
||||||
|
request_auth_channel: Option<&'static str>,
|
||||||
auth_endpoint_signature: String,
|
auth_endpoint_signature: String,
|
||||||
execution_runtime_candidate: bool,
|
execution_runtime_candidate: bool,
|
||||||
}
|
}
|
||||||
@@ -90,6 +93,25 @@ pub(super) fn classified(
|
|||||||
route_class,
|
route_class,
|
||||||
route_family,
|
route_family,
|
||||||
route_kind,
|
route_kind,
|
||||||
|
request_auth_channel: None,
|
||||||
|
auth_endpoint_signature: auth_endpoint_signature.into(),
|
||||||
|
execution_runtime_candidate,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn classified_with_request_auth_channel(
|
||||||
|
route_class: &'static str,
|
||||||
|
route_family: &'static str,
|
||||||
|
route_kind: &'static str,
|
||||||
|
request_auth_channel: &'static str,
|
||||||
|
auth_endpoint_signature: impl Into<String>,
|
||||||
|
execution_runtime_candidate: bool,
|
||||||
|
) -> ClassifiedRoute {
|
||||||
|
ClassifiedRoute {
|
||||||
|
route_class,
|
||||||
|
route_family,
|
||||||
|
route_kind,
|
||||||
|
request_auth_channel: Some(request_auth_channel),
|
||||||
auth_endpoint_signature: auth_endpoint_signature.into(),
|
auth_endpoint_signature: auth_endpoint_signature.into(),
|
||||||
execution_runtime_candidate,
|
execution_runtime_candidate,
|
||||||
}
|
}
|
||||||
@@ -103,6 +125,7 @@ impl ClassifiedRoute {
|
|||||||
route_class: Some(self.route_class.to_string()),
|
route_class: Some(self.route_class.to_string()),
|
||||||
route_family: Some(self.route_family.to_string()),
|
route_family: Some(self.route_family.to_string()),
|
||||||
route_kind: Some(self.route_kind.to_string()),
|
route_kind: Some(self.route_kind.to_string()),
|
||||||
|
request_auth_channel: self.request_auth_channel.map(str::to_string),
|
||||||
auth_endpoint_signature: Some(self.auth_endpoint_signature),
|
auth_endpoint_signature: Some(self.auth_endpoint_signature),
|
||||||
execution_runtime_candidate: self.execution_runtime_candidate,
|
execution_runtime_candidate: self.execution_runtime_candidate,
|
||||||
auth_context: None,
|
auth_context: None,
|
||||||
|
|||||||
@@ -44,7 +44,11 @@ fn classifies_claude_messages_cli_when_bearer_without_api_key() {
|
|||||||
classify_control_route(&http::Method::POST, &uri, &headers).expect("route should classify");
|
classify_control_route(&http::Method::POST, &uri, &headers).expect("route should classify");
|
||||||
|
|
||||||
assert_eq!(decision.route_family.as_deref(), Some("claude"));
|
assert_eq!(decision.route_family.as_deref(), Some("claude"));
|
||||||
assert_eq!(decision.route_kind.as_deref(), Some("cli"));
|
assert_eq!(decision.route_kind.as_deref(), Some("messages"));
|
||||||
|
assert_eq!(
|
||||||
|
decision.request_auth_channel.as_deref(),
|
||||||
|
Some("bearer_like")
|
||||||
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
decision.auth_endpoint_signature.as_deref(),
|
decision.auth_endpoint_signature.as_deref(),
|
||||||
Some("claude:messages")
|
Some("claude:messages")
|
||||||
@@ -63,7 +67,11 @@ fn classifies_claude_messages_cli_when_bearer_is_present_even_with_api_key() {
|
|||||||
classify_control_route(&http::Method::POST, &uri, &headers).expect("route should classify");
|
classify_control_route(&http::Method::POST, &uri, &headers).expect("route should classify");
|
||||||
|
|
||||||
assert_eq!(decision.route_family.as_deref(), Some("claude"));
|
assert_eq!(decision.route_family.as_deref(), Some("claude"));
|
||||||
assert_eq!(decision.route_kind.as_deref(), Some("cli"));
|
assert_eq!(decision.route_kind.as_deref(), Some("messages"));
|
||||||
|
assert_eq!(
|
||||||
|
decision.request_auth_channel.as_deref(),
|
||||||
|
Some("bearer_like")
|
||||||
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
decision.auth_endpoint_signature.as_deref(),
|
decision.auth_endpoint_signature.as_deref(),
|
||||||
Some("claude:messages")
|
Some("claude:messages")
|
||||||
@@ -80,6 +88,7 @@ fn classifies_claude_messages_when_api_key_without_bearer() {
|
|||||||
|
|
||||||
assert_eq!(decision.route_family.as_deref(), Some("claude"));
|
assert_eq!(decision.route_family.as_deref(), Some("claude"));
|
||||||
assert_eq!(decision.route_kind.as_deref(), Some("messages"));
|
assert_eq!(decision.route_kind.as_deref(), Some("messages"));
|
||||||
|
assert_eq!(decision.request_auth_channel.as_deref(), Some("api_key"));
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
decision.auth_endpoint_signature.as_deref(),
|
decision.auth_endpoint_signature.as_deref(),
|
||||||
Some("claude:messages")
|
Some("claude:messages")
|
||||||
@@ -97,7 +106,30 @@ fn classifies_gemini_cli_generate_content_when_x_app_contains_cli() {
|
|||||||
classify_control_route(&http::Method::POST, &uri, &headers).expect("route should classify");
|
classify_control_route(&http::Method::POST, &uri, &headers).expect("route should classify");
|
||||||
|
|
||||||
assert_eq!(decision.route_family.as_deref(), Some("gemini"));
|
assert_eq!(decision.route_family.as_deref(), Some("gemini"));
|
||||||
assert_eq!(decision.route_kind.as_deref(), Some("cli"));
|
assert_eq!(decision.route_kind.as_deref(), Some("generate_content"));
|
||||||
|
assert_eq!(
|
||||||
|
decision.request_auth_channel.as_deref(),
|
||||||
|
Some("bearer_like")
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
decision.auth_endpoint_signature.as_deref(),
|
||||||
|
Some("gemini:generate_content")
|
||||||
|
);
|
||||||
|
assert!(decision.is_execution_runtime_candidate());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn classifies_gemini_generate_content_api_key_without_cli_marker() {
|
||||||
|
let headers = headers(&[("x-goog-api-key", "gemini-key")]);
|
||||||
|
let uri: Uri = "/v1beta/models/gemini-2.5-pro:generateContent"
|
||||||
|
.parse()
|
||||||
|
.expect("uri should parse");
|
||||||
|
let decision =
|
||||||
|
classify_control_route(&http::Method::POST, &uri, &headers).expect("route should classify");
|
||||||
|
|
||||||
|
assert_eq!(decision.route_family.as_deref(), Some("gemini"));
|
||||||
|
assert_eq!(decision.route_kind.as_deref(), Some("generate_content"));
|
||||||
|
assert_eq!(decision.request_auth_channel.as_deref(), Some("api_key"));
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
decision.auth_endpoint_signature.as_deref(),
|
decision.auth_endpoint_signature.as_deref(),
|
||||||
Some("gemini:generate_content")
|
Some("gemini:generate_content")
|
||||||
|
|||||||
@@ -61,7 +61,14 @@ fn builds_public_request_context_from_request_parts() {
|
|||||||
.control_decision
|
.control_decision
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.and_then(|value| value.route_kind.as_deref()),
|
.and_then(|value| value.route_kind.as_deref()),
|
||||||
Some("cli")
|
Some("generate_content")
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
context
|
||||||
|
.control_decision
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|value| value.request_auth_channel.as_deref()),
|
||||||
|
Some("bearer_like")
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -173,6 +173,7 @@ mod tests {
|
|||||||
route_class: Some("ai_public".to_string()),
|
route_class: Some("ai_public".to_string()),
|
||||||
route_family: Some("openai".to_string()),
|
route_family: Some("openai".to_string()),
|
||||||
route_kind: Some("chat".to_string()),
|
route_kind: Some("chat".to_string()),
|
||||||
|
request_auth_channel: None,
|
||||||
auth_endpoint_signature: None,
|
auth_endpoint_signature: None,
|
||||||
execution_runtime_candidate: true,
|
execution_runtime_candidate: true,
|
||||||
auth_context: None,
|
auth_context: None,
|
||||||
|
|||||||
@@ -74,6 +74,12 @@ pub(crate) fn build_internal_gateway_resolve_payload(
|
|||||||
if let Some(route_kind) = decision.route_kind {
|
if let Some(route_kind) = decision.route_kind {
|
||||||
payload.insert("route_kind".to_string(), json!(route_kind));
|
payload.insert("route_kind".to_string(), json!(route_kind));
|
||||||
}
|
}
|
||||||
|
if let Some(request_auth_channel) = decision.request_auth_channel {
|
||||||
|
payload.insert(
|
||||||
|
"request_auth_channel".to_string(),
|
||||||
|
json!(request_auth_channel),
|
||||||
|
);
|
||||||
|
}
|
||||||
if let Some(signature) = decision.auth_endpoint_signature {
|
if let Some(signature) = decision.auth_endpoint_signature {
|
||||||
payload.insert("auth_endpoint_signature".to_string(), json!(signature));
|
payload.insert("auth_endpoint_signature".to_string(), json!(signature));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -536,6 +536,7 @@ mod tests {
|
|||||||
route_class: Some("ai_public".to_string()),
|
route_class: Some("ai_public".to_string()),
|
||||||
route_family: Some("openai".to_string()),
|
route_family: Some("openai".to_string()),
|
||||||
route_kind: Some("chat".to_string()),
|
route_kind: Some("chat".to_string()),
|
||||||
|
request_auth_channel: None,
|
||||||
auth_endpoint_signature: Some("openai:chat".to_string()),
|
auth_endpoint_signature: Some("openai:chat".to_string()),
|
||||||
execution_runtime_candidate: true,
|
execution_runtime_candidate: true,
|
||||||
auth_context: Some(auth_context),
|
auth_context: Some(auth_context),
|
||||||
|
|||||||
@@ -269,7 +269,8 @@ async fn gateway_executes_kiro_claude_cli_stream_via_local_provider_catalog_cand
|
|||||||
"action": "proxy_public",
|
"action": "proxy_public",
|
||||||
"route_class": "ai_public",
|
"route_class": "ai_public",
|
||||||
"route_family": "claude",
|
"route_family": "claude",
|
||||||
"route_kind": "cli",
|
"route_kind": "messages",
|
||||||
|
"request_auth_channel": "bearer_like",
|
||||||
"auth_endpoint_signature": "claude:messages",
|
"auth_endpoint_signature": "claude:messages",
|
||||||
"execution_runtime_candidate": true,
|
"execution_runtime_candidate": true,
|
||||||
"auth_context": {
|
"auth_context": {
|
||||||
|
|||||||
@@ -288,7 +288,8 @@ async fn gateway_executes_kiro_claude_cli_sync_via_local_provider_catalog_candid
|
|||||||
"action": "proxy_public",
|
"action": "proxy_public",
|
||||||
"route_class": "ai_public",
|
"route_class": "ai_public",
|
||||||
"route_family": "claude",
|
"route_family": "claude",
|
||||||
"route_kind": "cli",
|
"route_kind": "messages",
|
||||||
|
"request_auth_channel": "bearer_like",
|
||||||
"auth_endpoint_signature": "claude:messages",
|
"auth_endpoint_signature": "claude:messages",
|
||||||
"execution_runtime_candidate": true,
|
"execution_runtime_candidate": true,
|
||||||
"auth_context": {
|
"auth_context": {
|
||||||
@@ -872,7 +873,8 @@ async fn gateway_executes_kiro_claude_cli_sync_via_local_provider_catalog_candid
|
|||||||
"action": "proxy_public",
|
"action": "proxy_public",
|
||||||
"route_class": "ai_public",
|
"route_class": "ai_public",
|
||||||
"route_family": "claude",
|
"route_family": "claude",
|
||||||
"route_kind": "cli",
|
"route_kind": "messages",
|
||||||
|
"request_auth_channel": "bearer_like",
|
||||||
"auth_endpoint_signature": "claude:messages",
|
"auth_endpoint_signature": "claude:messages",
|
||||||
"execution_runtime_candidate": true,
|
"execution_runtime_candidate": true,
|
||||||
"auth_context": {
|
"auth_context": {
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ fn sample_decision() -> crate::control::GatewayControlDecision {
|
|||||||
route_class: Some("ai_public".to_string()),
|
route_class: Some("ai_public".to_string()),
|
||||||
route_family: Some("openai".to_string()),
|
route_family: Some("openai".to_string()),
|
||||||
route_kind: Some("chat".to_string()),
|
route_kind: Some("chat".to_string()),
|
||||||
|
request_auth_channel: None,
|
||||||
auth_endpoint_signature: None,
|
auth_endpoint_signature: None,
|
||||||
execution_runtime_candidate: true,
|
execution_runtime_candidate: true,
|
||||||
auth_context: None,
|
auth_context: None,
|
||||||
|
|||||||
@@ -50,6 +50,28 @@ async fn gateway_handles_internal_gateway_resolve_without_proxying_upstream() {
|
|||||||
assert_eq!(payload["route_kind"], "chat");
|
assert_eq!(payload["route_kind"], "chat");
|
||||||
assert_eq!(*upstream_hits.lock().expect("mutex should lock"), 0);
|
assert_eq!(*upstream_hits.lock().expect("mutex should lock"), 0);
|
||||||
|
|
||||||
|
let response = reqwest::Client::new()
|
||||||
|
.post(format!("{gateway_url}/api/internal/gateway/resolve"))
|
||||||
|
.json(&json!({
|
||||||
|
"method": "POST",
|
||||||
|
"path": "/v1/messages",
|
||||||
|
"headers": {
|
||||||
|
"authorization": "Bearer local-token",
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
.expect("request should succeed");
|
||||||
|
|
||||||
|
assert_eq!(response.status(), StatusCode::OK);
|
||||||
|
let payload: serde_json::Value = response.json().await.expect("json body should parse");
|
||||||
|
assert_eq!(payload["route_class"], "ai_public");
|
||||||
|
assert_eq!(payload["route_family"], "claude");
|
||||||
|
assert_eq!(payload["route_kind"], "messages");
|
||||||
|
assert_eq!(payload["request_auth_channel"], "bearer_like");
|
||||||
|
assert_eq!(payload["auth_endpoint_signature"], "claude:messages");
|
||||||
|
assert_eq!(*upstream_hits.lock().expect("mutex should lock"), 0);
|
||||||
|
|
||||||
gateway_handle.abort();
|
gateway_handle.abort();
|
||||||
upstream_handle.abort();
|
upstream_handle.abort();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1645,7 +1645,7 @@ async fn gateway_records_failed_usage_when_all_local_claude_cli_candidates_are_s
|
|||||||
assert_eq!(stored_usage.routing_key_name(), None);
|
assert_eq!(stored_usage.routing_key_name(), None);
|
||||||
assert_eq!(stored_usage.routing_planner_kind(), Some("claude_cli_sync"));
|
assert_eq!(stored_usage.routing_planner_kind(), Some("claude_cli_sync"));
|
||||||
assert_eq!(stored_usage.routing_route_family(), Some("claude"));
|
assert_eq!(stored_usage.routing_route_family(), Some("claude"));
|
||||||
assert_eq!(stored_usage.routing_route_kind(), Some("cli"));
|
assert_eq!(stored_usage.routing_route_kind(), Some("messages"));
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
stored_usage.routing_execution_path(),
|
stored_usage.routing_execution_path(),
|
||||||
Some("local_execution_runtime_miss")
|
Some("local_execution_runtime_miss")
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ pub fn resolve_execution_runtime_stream_plan_kind(
|
|||||||
route_class: Option<&str>,
|
route_class: Option<&str>,
|
||||||
route_family: Option<&str>,
|
route_family: Option<&str>,
|
||||||
route_kind: Option<&str>,
|
route_kind: Option<&str>,
|
||||||
|
request_auth_channel: Option<&str>,
|
||||||
method: &Method,
|
method: &Method,
|
||||||
path: &str,
|
path: &str,
|
||||||
) -> Option<&'static str> {
|
) -> Option<&'static str> {
|
||||||
@@ -48,15 +49,11 @@ pub fn resolve_execution_runtime_stream_plan_kind(
|
|||||||
&& *method == Method::POST
|
&& *method == Method::POST
|
||||||
&& path == "/v1/messages"
|
&& path == "/v1/messages"
|
||||||
{
|
{
|
||||||
return Some(CLAUDE_CHAT_STREAM_PLAN_KIND);
|
return Some(resolve_claude_messages_plan_kind(
|
||||||
}
|
request_auth_channel,
|
||||||
|
CLAUDE_CHAT_STREAM_PLAN_KIND,
|
||||||
if route_family == Some("claude")
|
CLAUDE_CLI_STREAM_PLAN_KIND,
|
||||||
&& route_kind == Some("cli")
|
));
|
||||||
&& *method == Method::POST
|
|
||||||
&& path == "/v1/messages"
|
|
||||||
{
|
|
||||||
return Some(CLAUDE_CLI_STREAM_PLAN_KIND);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if route_family == Some("gemini")
|
if route_family == Some("gemini")
|
||||||
@@ -64,15 +61,11 @@ pub fn resolve_execution_runtime_stream_plan_kind(
|
|||||||
&& *method == Method::POST
|
&& *method == Method::POST
|
||||||
&& path.ends_with(":streamGenerateContent")
|
&& path.ends_with(":streamGenerateContent")
|
||||||
{
|
{
|
||||||
return Some(GEMINI_CHAT_STREAM_PLAN_KIND);
|
return Some(resolve_gemini_generate_content_plan_kind(
|
||||||
}
|
request_auth_channel,
|
||||||
|
GEMINI_CHAT_STREAM_PLAN_KIND,
|
||||||
if route_family == Some("gemini")
|
GEMINI_CLI_STREAM_PLAN_KIND,
|
||||||
&& route_kind == Some("cli")
|
));
|
||||||
&& *method == Method::POST
|
|
||||||
&& path.ends_with(":streamGenerateContent")
|
|
||||||
{
|
|
||||||
return Some(GEMINI_CLI_STREAM_PLAN_KIND);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if route_family == Some("openai")
|
if route_family == Some("openai")
|
||||||
@@ -114,6 +107,7 @@ pub fn resolve_execution_runtime_sync_plan_kind(
|
|||||||
route_class: Option<&str>,
|
route_class: Option<&str>,
|
||||||
route_family: Option<&str>,
|
route_family: Option<&str>,
|
||||||
route_kind: Option<&str>,
|
route_kind: Option<&str>,
|
||||||
|
request_auth_channel: Option<&str>,
|
||||||
method: &Method,
|
method: &Method,
|
||||||
path: &str,
|
path: &str,
|
||||||
) -> Option<&'static str> {
|
) -> Option<&'static str> {
|
||||||
@@ -211,15 +205,11 @@ pub fn resolve_execution_runtime_sync_plan_kind(
|
|||||||
&& *method == Method::POST
|
&& *method == Method::POST
|
||||||
&& path == "/v1/messages"
|
&& path == "/v1/messages"
|
||||||
{
|
{
|
||||||
return Some(CLAUDE_CHAT_SYNC_PLAN_KIND);
|
return Some(resolve_claude_messages_plan_kind(
|
||||||
}
|
request_auth_channel,
|
||||||
|
CLAUDE_CHAT_SYNC_PLAN_KIND,
|
||||||
if route_family == Some("claude")
|
CLAUDE_CLI_SYNC_PLAN_KIND,
|
||||||
&& route_kind == Some("cli")
|
));
|
||||||
&& *method == Method::POST
|
|
||||||
&& path == "/v1/messages"
|
|
||||||
{
|
|
||||||
return Some(CLAUDE_CLI_SYNC_PLAN_KIND);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if route_family == Some("gemini")
|
if route_family == Some("gemini")
|
||||||
@@ -227,15 +217,11 @@ pub fn resolve_execution_runtime_sync_plan_kind(
|
|||||||
&& *method == Method::POST
|
&& *method == Method::POST
|
||||||
&& path.ends_with(":generateContent")
|
&& path.ends_with(":generateContent")
|
||||||
{
|
{
|
||||||
return Some(GEMINI_CHAT_SYNC_PLAN_KIND);
|
return Some(resolve_gemini_generate_content_plan_kind(
|
||||||
}
|
request_auth_channel,
|
||||||
|
GEMINI_CHAT_SYNC_PLAN_KIND,
|
||||||
if route_family == Some("gemini")
|
GEMINI_CLI_SYNC_PLAN_KIND,
|
||||||
&& route_kind == Some("cli")
|
));
|
||||||
&& *method == Method::POST
|
|
||||||
&& path.ends_with(":generateContent")
|
|
||||||
{
|
|
||||||
return Some(GEMINI_CLI_SYNC_PLAN_KIND);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if route_family == Some("gemini") && route_kind == Some("files") {
|
if route_family == Some("gemini") && route_kind == Some("files") {
|
||||||
@@ -278,6 +264,30 @@ fn is_gemini_generate_content_route_kind(route_kind: Option<&str>) -> bool {
|
|||||||
matches!(route_kind, Some("generate_content") | Some("chat"))
|
matches!(route_kind, Some("generate_content") | Some("chat"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn resolve_claude_messages_plan_kind(
|
||||||
|
request_auth_channel: Option<&str>,
|
||||||
|
chat_plan_kind: &'static str,
|
||||||
|
cli_plan_kind: &'static str,
|
||||||
|
) -> &'static str {
|
||||||
|
if request_auth_channel == Some("bearer_like") {
|
||||||
|
cli_plan_kind
|
||||||
|
} else {
|
||||||
|
chat_plan_kind
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn resolve_gemini_generate_content_plan_kind(
|
||||||
|
request_auth_channel: Option<&str>,
|
||||||
|
chat_plan_kind: &'static str,
|
||||||
|
cli_plan_kind: &'static str,
|
||||||
|
) -> &'static str {
|
||||||
|
if request_auth_channel == Some("bearer_like") {
|
||||||
|
cli_plan_kind
|
||||||
|
} else {
|
||||||
|
chat_plan_kind
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn is_matching_stream_request(
|
pub fn is_matching_stream_request(
|
||||||
plan_kind: &str,
|
plan_kind: &str,
|
||||||
path: &str,
|
path: &str,
|
||||||
@@ -364,10 +374,12 @@ mod tests {
|
|||||||
supports_stream_execution_decision_kind, supports_sync_execution_decision_kind,
|
supports_stream_execution_decision_kind, supports_sync_execution_decision_kind,
|
||||||
};
|
};
|
||||||
use crate::contracts::{
|
use crate::contracts::{
|
||||||
OPENAI_CHAT_STREAM_PLAN_KIND, OPENAI_CHAT_SYNC_PLAN_KIND, OPENAI_IMAGE_STREAM_PLAN_KIND,
|
CLAUDE_CHAT_STREAM_PLAN_KIND, CLAUDE_CHAT_SYNC_PLAN_KIND, CLAUDE_CLI_STREAM_PLAN_KIND,
|
||||||
OPENAI_IMAGE_SYNC_PLAN_KIND, OPENAI_RESPONSES_COMPACT_STREAM_PLAN_KIND,
|
CLAUDE_CLI_SYNC_PLAN_KIND, GEMINI_CHAT_STREAM_PLAN_KIND, GEMINI_CHAT_SYNC_PLAN_KIND,
|
||||||
OPENAI_RESPONSES_COMPACT_SYNC_PLAN_KIND, OPENAI_RESPONSES_STREAM_PLAN_KIND,
|
GEMINI_CLI_STREAM_PLAN_KIND, GEMINI_CLI_SYNC_PLAN_KIND, OPENAI_CHAT_STREAM_PLAN_KIND,
|
||||||
OPENAI_RESPONSES_SYNC_PLAN_KIND,
|
OPENAI_CHAT_SYNC_PLAN_KIND, OPENAI_IMAGE_STREAM_PLAN_KIND, OPENAI_IMAGE_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,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -377,6 +389,7 @@ mod tests {
|
|||||||
Some("ai_public"),
|
Some("ai_public"),
|
||||||
Some("openai"),
|
Some("openai"),
|
||||||
Some("chat"),
|
Some("chat"),
|
||||||
|
None,
|
||||||
&Method::POST,
|
&Method::POST,
|
||||||
"/v1/chat/completions",
|
"/v1/chat/completions",
|
||||||
),
|
),
|
||||||
@@ -387,6 +400,7 @@ mod tests {
|
|||||||
Some("ai_public"),
|
Some("ai_public"),
|
||||||
Some("openai"),
|
Some("openai"),
|
||||||
Some("chat"),
|
Some("chat"),
|
||||||
|
None,
|
||||||
&Method::POST,
|
&Method::POST,
|
||||||
"/v1/chat/completions",
|
"/v1/chat/completions",
|
||||||
),
|
),
|
||||||
@@ -401,6 +415,7 @@ mod tests {
|
|||||||
Some("ai_public"),
|
Some("ai_public"),
|
||||||
Some("openai"),
|
Some("openai"),
|
||||||
Some("responses"),
|
Some("responses"),
|
||||||
|
None,
|
||||||
&Method::POST,
|
&Method::POST,
|
||||||
"/v1/responses",
|
"/v1/responses",
|
||||||
),
|
),
|
||||||
@@ -411,6 +426,7 @@ mod tests {
|
|||||||
Some("ai_public"),
|
Some("ai_public"),
|
||||||
Some("openai"),
|
Some("openai"),
|
||||||
Some("responses"),
|
Some("responses"),
|
||||||
|
None,
|
||||||
&Method::POST,
|
&Method::POST,
|
||||||
"/v1/responses",
|
"/v1/responses",
|
||||||
),
|
),
|
||||||
@@ -421,6 +437,7 @@ mod tests {
|
|||||||
Some("ai_public"),
|
Some("ai_public"),
|
||||||
Some("openai"),
|
Some("openai"),
|
||||||
Some("cli"),
|
Some("cli"),
|
||||||
|
None,
|
||||||
&Method::POST,
|
&Method::POST,
|
||||||
"/v1/responses",
|
"/v1/responses",
|
||||||
),
|
),
|
||||||
@@ -441,6 +458,7 @@ mod tests {
|
|||||||
Some("ai_public"),
|
Some("ai_public"),
|
||||||
Some("openai"),
|
Some("openai"),
|
||||||
Some("responses:compact"),
|
Some("responses:compact"),
|
||||||
|
None,
|
||||||
&Method::POST,
|
&Method::POST,
|
||||||
"/v1/responses/compact",
|
"/v1/responses/compact",
|
||||||
),
|
),
|
||||||
@@ -451,6 +469,7 @@ mod tests {
|
|||||||
Some("ai_public"),
|
Some("ai_public"),
|
||||||
Some("openai"),
|
Some("openai"),
|
||||||
Some("responses:compact"),
|
Some("responses:compact"),
|
||||||
|
None,
|
||||||
&Method::POST,
|
&Method::POST,
|
||||||
"/v1/responses/compact",
|
"/v1/responses/compact",
|
||||||
),
|
),
|
||||||
@@ -461,6 +480,7 @@ mod tests {
|
|||||||
Some("ai_public"),
|
Some("ai_public"),
|
||||||
Some("openai"),
|
Some("openai"),
|
||||||
Some("compact"),
|
Some("compact"),
|
||||||
|
None,
|
||||||
&Method::POST,
|
&Method::POST,
|
||||||
"/v1/responses/compact",
|
"/v1/responses/compact",
|
||||||
),
|
),
|
||||||
@@ -474,6 +494,102 @@ mod tests {
|
|||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn resolves_claude_messages_plan_kinds_by_request_auth_channel() {
|
||||||
|
assert_eq!(
|
||||||
|
resolve_execution_runtime_sync_plan_kind(
|
||||||
|
Some("ai_public"),
|
||||||
|
Some("claude"),
|
||||||
|
Some("messages"),
|
||||||
|
Some("api_key"),
|
||||||
|
&Method::POST,
|
||||||
|
"/v1/messages",
|
||||||
|
),
|
||||||
|
Some(CLAUDE_CHAT_SYNC_PLAN_KIND)
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
resolve_execution_runtime_stream_plan_kind(
|
||||||
|
Some("ai_public"),
|
||||||
|
Some("claude"),
|
||||||
|
Some("messages"),
|
||||||
|
Some("api_key"),
|
||||||
|
&Method::POST,
|
||||||
|
"/v1/messages",
|
||||||
|
),
|
||||||
|
Some(CLAUDE_CHAT_STREAM_PLAN_KIND)
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
resolve_execution_runtime_sync_plan_kind(
|
||||||
|
Some("ai_public"),
|
||||||
|
Some("claude"),
|
||||||
|
Some("messages"),
|
||||||
|
Some("bearer_like"),
|
||||||
|
&Method::POST,
|
||||||
|
"/v1/messages",
|
||||||
|
),
|
||||||
|
Some(CLAUDE_CLI_SYNC_PLAN_KIND)
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
resolve_execution_runtime_stream_plan_kind(
|
||||||
|
Some("ai_public"),
|
||||||
|
Some("claude"),
|
||||||
|
Some("messages"),
|
||||||
|
Some("bearer_like"),
|
||||||
|
&Method::POST,
|
||||||
|
"/v1/messages",
|
||||||
|
),
|
||||||
|
Some(CLAUDE_CLI_STREAM_PLAN_KIND)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn resolves_gemini_generate_content_plan_kinds_by_request_auth_channel() {
|
||||||
|
assert_eq!(
|
||||||
|
resolve_execution_runtime_sync_plan_kind(
|
||||||
|
Some("ai_public"),
|
||||||
|
Some("gemini"),
|
||||||
|
Some("generate_content"),
|
||||||
|
Some("api_key"),
|
||||||
|
&Method::POST,
|
||||||
|
"/v1beta/models/gemini-2.5-pro:generateContent",
|
||||||
|
),
|
||||||
|
Some(GEMINI_CHAT_SYNC_PLAN_KIND)
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
resolve_execution_runtime_stream_plan_kind(
|
||||||
|
Some("ai_public"),
|
||||||
|
Some("gemini"),
|
||||||
|
Some("generate_content"),
|
||||||
|
Some("api_key"),
|
||||||
|
&Method::POST,
|
||||||
|
"/v1beta/models/gemini-2.5-pro:streamGenerateContent",
|
||||||
|
),
|
||||||
|
Some(GEMINI_CHAT_STREAM_PLAN_KIND)
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
resolve_execution_runtime_sync_plan_kind(
|
||||||
|
Some("ai_public"),
|
||||||
|
Some("gemini"),
|
||||||
|
Some("generate_content"),
|
||||||
|
Some("bearer_like"),
|
||||||
|
&Method::POST,
|
||||||
|
"/v1beta/models/gemini-2.5-pro:generateContent",
|
||||||
|
),
|
||||||
|
Some(GEMINI_CLI_SYNC_PLAN_KIND)
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
resolve_execution_runtime_stream_plan_kind(
|
||||||
|
Some("ai_public"),
|
||||||
|
Some("gemini"),
|
||||||
|
Some("generate_content"),
|
||||||
|
Some("bearer_like"),
|
||||||
|
&Method::POST,
|
||||||
|
"/v1beta/models/gemini-2.5-pro:streamGenerateContent",
|
||||||
|
),
|
||||||
|
Some(GEMINI_CLI_STREAM_PLAN_KIND)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn stream_matching_requires_openai_stream_flag() {
|
fn stream_matching_requires_openai_stream_flag() {
|
||||||
assert!(!is_matching_stream_request(
|
assert!(!is_matching_stream_request(
|
||||||
@@ -501,6 +617,7 @@ mod tests {
|
|||||||
Some("ai_public"),
|
Some("ai_public"),
|
||||||
Some("openai"),
|
Some("openai"),
|
||||||
Some("image"),
|
Some("image"),
|
||||||
|
None,
|
||||||
&Method::POST,
|
&Method::POST,
|
||||||
"/v1/images/generations",
|
"/v1/images/generations",
|
||||||
),
|
),
|
||||||
@@ -511,6 +628,7 @@ mod tests {
|
|||||||
Some("ai_public"),
|
Some("ai_public"),
|
||||||
Some("openai"),
|
Some("openai"),
|
||||||
Some("image"),
|
Some("image"),
|
||||||
|
None,
|
||||||
&Method::POST,
|
&Method::POST,
|
||||||
"/v1/images/edits",
|
"/v1/images/edits",
|
||||||
),
|
),
|
||||||
@@ -521,6 +639,7 @@ mod tests {
|
|||||||
Some("ai_public"),
|
Some("ai_public"),
|
||||||
Some("openai"),
|
Some("openai"),
|
||||||
Some("image"),
|
Some("image"),
|
||||||
|
None,
|
||||||
&Method::POST,
|
&Method::POST,
|
||||||
"/v1/images/variations",
|
"/v1/images/variations",
|
||||||
),
|
),
|
||||||
@@ -538,6 +657,7 @@ mod tests {
|
|||||||
Some("ai_public"),
|
Some("ai_public"),
|
||||||
Some("openai"),
|
Some("openai"),
|
||||||
Some("image"),
|
Some("image"),
|
||||||
|
None,
|
||||||
&Method::POST,
|
&Method::POST,
|
||||||
"/v1/images/generations",
|
"/v1/images/generations",
|
||||||
),
|
),
|
||||||
@@ -548,6 +668,7 @@ mod tests {
|
|||||||
Some("ai_public"),
|
Some("ai_public"),
|
||||||
Some("openai"),
|
Some("openai"),
|
||||||
Some("image"),
|
Some("image"),
|
||||||
|
None,
|
||||||
&Method::POST,
|
&Method::POST,
|
||||||
"/v1/images/edits",
|
"/v1/images/edits",
|
||||||
),
|
),
|
||||||
|
|||||||
Reference in New Issue
Block a user