Skip to content

Commit 27eecf0

Browse files
authored
Merge pull request #785 from superlinked/fix/decision-model-aliases
fix(gateway): resolve configured aliases for typed decisions
2 parents 04f8080 + f124a99 commit 27eecf0

2 files changed

Lines changed: 227 additions & 2 deletions

File tree

‎packages/sie_gateway/src/handlers/decisions.rs‎

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -824,6 +824,104 @@ mod tests {
824824
}
825825
}
826826

827+
#[tokio::test]
828+
async fn configured_aliases_preserve_caller_spelling_and_dispatch_identity() {
829+
for model in ["decision", "DECISION", "profiled", "bundled"] {
830+
let model_yaml = format!(
831+
"{LOCAL_DECISION_MODEL} alternate:\n adapter_path: sie_server.adapters.gliner2.decide:GLiNER2DecideAdapter\n max_batch_tokens: 8192\n"
832+
);
833+
let mut gateway = TestGateway::new(&[&model_yaml]).await;
834+
gateway.add_model_alias("decision", "acme/decide");
835+
gateway.add_model_alias("profiled", "acme/decide:alternate");
836+
gateway.add_model_alias("bundled", "default:/acme/decide");
837+
gateway
838+
.add_verified_worker(
839+
"local-1",
840+
LOCAL_LANE,
841+
&["acme/decide", "acme/decide:alternate"],
842+
)
843+
.await;
844+
gateway.dispatcher.return_extract_data(json!({
845+
"question_0":{"type":"noul", "noul":0.9}
846+
}));
847+
let mut body = predicate_request();
848+
body["model"] = json!(model);
849+
let response = proxy_decisions(State(Arc::clone(&gateway.state)), request(body)).await;
850+
let status = response.status();
851+
let body = body_json(response).await;
852+
assert_eq!(status, StatusCode::OK, "{model}: {body}");
853+
assert_eq!(body["model"], model);
854+
assert_eq!(body["answers"][0]["probability"], 0.9);
855+
let dispatched = gateway.dispatcher.dispatched();
856+
assert_eq!(dispatched.len(), 1);
857+
assert_eq!(
858+
dispatched[0].model,
859+
if model == "profiled" {
860+
"acme/decide:alternate"
861+
} else {
862+
"acme/decide"
863+
}
864+
);
865+
assert_eq!(dispatched[0].endpoint, "extract");
866+
}
867+
}
868+
869+
#[tokio::test]
870+
async fn hidden_and_absent_aliases_have_identical_not_found_responses() {
871+
for populated in [false, true] {
872+
let hidden_models = if populated {
873+
vec![LOCAL_DECISION_MODEL]
874+
} else {
875+
vec![]
876+
};
877+
let absent_models = if populated {
878+
vec![LOCAL_ENCODE_MODEL]
879+
} else {
880+
vec![]
881+
};
882+
let mut hidden = TestGateway::new(&hidden_models).await;
883+
let mut absent = TestGateway::new(&absent_models).await;
884+
hidden.add_model_alias("decision", "acme/decide");
885+
absent.add_model_alias("decision", "acme/decide");
886+
hidden.install_policy(Arc::new(RequireCaller));
887+
for model in ["decision", "DECISION"] {
888+
let mut payload = predicate_request();
889+
payload["model"] = json!(model);
890+
let denied =
891+
proxy_decisions(State(Arc::clone(&hidden.state)), request(payload.clone()))
892+
.await;
893+
let missing =
894+
proxy_decisions(State(Arc::clone(&absent.state)), request(payload)).await;
895+
assert_eq!(denied.status(), StatusCode::NOT_FOUND);
896+
assert_eq!(missing.status(), StatusCode::NOT_FOUND);
897+
let denied = body_json(denied).await;
898+
assert_eq!(denied["error"]["code"], "MODEL_NOT_FOUND");
899+
assert_eq!(denied, body_json(missing).await);
900+
assert!(!denied.to_string().contains("acme/"));
901+
}
902+
assert!(hidden.dispatcher.dispatched().is_empty());
903+
assert!(absent.dispatcher.dispatched().is_empty());
904+
}
905+
}
906+
907+
#[tokio::test]
908+
async fn alias_visibility_precedes_decision_capability_checks() {
909+
let mut gateway = TestGateway::new(&[LOCAL_DECISION_MODEL, LOCAL_ENCODE_MODEL]).await;
910+
gateway.add_model_alias("decision", "acme/decide");
911+
gateway.add_model_alias("encode", "acme/local");
912+
gateway.install_policy(Arc::new(RequireCaller));
913+
for model in ["decision", "encode"] {
914+
let mut body = predicate_request();
915+
body["model"] = json!(model);
916+
let response = proxy_decisions(State(Arc::clone(&gateway.state)), request(body)).await;
917+
assert_eq!(response.status(), StatusCode::NOT_FOUND);
918+
let body = body_json(response).await;
919+
assert!(body.get("error").is_some());
920+
assert!(!body.to_string().contains("acme/"));
921+
}
922+
assert!(gateway.dispatcher.dispatched().is_empty());
923+
}
924+
827925
#[tokio::test]
828926
async fn authenticated_extensions_reach_the_native_model_policy() {
829927
let mut gateway = TestGateway::new(&[LOCAL_DECISION_MODEL]).await;

‎packages/sie_gateway/src/handlers/systemone.rs‎

Lines changed: 129 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ use crate::server::AppState;
3636
use super::proxy::{
3737
check_sdk_version, compatibility_model_uri, is_openai_compat_forwarded_header,
3838
is_openai_compat_inner_request_header, is_valid_compat_model_id, proxy_request,
39-
request_body_error, CompatibilityRoute,
39+
request_body_error, resolve_model_and_bundle, CompatibilityRoute,
4040
};
4141

4242
/// Request body cap. A System One request is one text state and its
@@ -459,6 +459,13 @@ async fn translate_error(response: Response) -> Response {
459459
Err(_) => Value::Null,
460460
};
461461
let (code, message) = error_parts(&parsed);
462+
let code = code.map(|code| {
463+
if parts.status == StatusCode::NOT_FOUND && code.eq_ignore_ascii_case("MODEL_NOT_FOUND") {
464+
"MODEL_NOT_FOUND"
465+
} else {
466+
code
467+
}
468+
});
462469
if message.is_none() {
463470
warn!(
464471
status = parts.status.as_u16(),
@@ -542,7 +549,12 @@ pub async fn proxy_systemone(State(state): State<Arc<AppState>>, req: Request) -
542549
if !is_valid_compat_model_id(&request.model) {
543550
return validation_error(&invalid(&["model"], "invalid model id"), None);
544551
}
545-
match is_decision_model(&state, &request.model) {
552+
let (resolved_model, _) =
553+
match resolve_model_and_bundle(&state, &request.model, &parts.extensions) {
554+
Ok(resolved) => resolved,
555+
Err(response) => return translate_error(response).await,
556+
};
557+
match is_decision_model(&state, &resolved_model) {
546558
None => {
547559
return api_error(
548560
StatusCode::NOT_FOUND,
@@ -944,6 +956,121 @@ mod tests {
944956
assert_eq!(dispatched[0].model, "acme/decide");
945957
}
946958

959+
#[tokio::test]
960+
async fn configured_aliases_reach_the_native_extract_queue() {
961+
for model in ["decision", "DECISION", "profiled", "bundled"] {
962+
let model_yaml = format!(
963+
"{LOCAL_DECISION_MODEL} alternate:\n adapter_path: sie_server.adapters.gliner2.decide:GLiNER2DecideAdapter\n max_batch_tokens: 8192\n"
964+
);
965+
let mut gateway = TestGateway::new(&[&model_yaml]).await;
966+
gateway.add_model_alias("decision", "acme/decide");
967+
gateway.add_model_alias("profiled", "acme/decide:alternate");
968+
gateway.add_model_alias("bundled", "default:/acme/decide");
969+
gateway
970+
.add_verified_worker(
971+
"local-1",
972+
LOCAL_LANE,
973+
&["acme/decide", "acme/decide:alternate"],
974+
)
975+
.await;
976+
gateway.dispatcher.return_extract_data(json!({
977+
"q": {"type":"noul", "noul":0.9, "answer":true}
978+
}));
979+
let response = proxy_systemone(
980+
State(Arc::clone(&gateway.state)),
981+
systemone_request(json!({
982+
"model":model, "state":"Continue?", "questions":{"q":{"type":"noul"}}
983+
})),
984+
)
985+
.await;
986+
let status = response.status();
987+
let body = body_json(response).await;
988+
assert_eq!(status, StatusCode::OK, "{model}: {body}");
989+
assert_eq!(body["model"], model);
990+
assert_eq!(body["answers"]["q"]["answer"], true);
991+
let dispatched = gateway.dispatcher.dispatched();
992+
assert_eq!(dispatched.len(), 1);
993+
assert_eq!(
994+
dispatched[0].model,
995+
if model == "profiled" {
996+
"acme/decide:alternate"
997+
} else {
998+
"acme/decide"
999+
}
1000+
);
1001+
assert_eq!(dispatched[0].endpoint, "extract");
1002+
}
1003+
}
1004+
1005+
struct HiddenModels;
1006+
1007+
impl crate::server::ModelAccessPolicy for HiddenModels {
1008+
fn visible(&self, _: &str, _: &axum::http::Extensions) -> bool {
1009+
false
1010+
}
1011+
}
1012+
1013+
#[tokio::test]
1014+
async fn hidden_and_absent_aliases_have_identical_not_found_responses() {
1015+
for populated in [false, true] {
1016+
let hidden_models = if populated {
1017+
vec![LOCAL_DECISION_MODEL]
1018+
} else {
1019+
vec![]
1020+
};
1021+
let absent_models = if populated {
1022+
vec![LOCAL_ENCODE_MODEL]
1023+
} else {
1024+
vec![]
1025+
};
1026+
let mut hidden = TestGateway::new(&hidden_models).await;
1027+
let mut absent = TestGateway::new(&absent_models).await;
1028+
hidden.add_model_alias("decision", "acme/decide");
1029+
absent.add_model_alias("decision", "acme/decide");
1030+
hidden.install_policy(Arc::new(HiddenModels));
1031+
for model in ["decision", "DECISION"] {
1032+
let payload = json!({"model": model, "state": "Continue?", "questions": {"q": {"type": "noul"}}});
1033+
let denied = proxy_systemone(
1034+
State(Arc::clone(&hidden.state)),
1035+
systemone_request(payload.clone()),
1036+
)
1037+
.await;
1038+
let missing =
1039+
proxy_systemone(State(Arc::clone(&absent.state)), systemone_request(payload))
1040+
.await;
1041+
assert_eq!(denied.status(), StatusCode::NOT_FOUND);
1042+
assert_eq!(missing.status(), StatusCode::NOT_FOUND);
1043+
let denied = body_json(denied).await;
1044+
assert_eq!(denied["detail"]["code"], "MODEL_NOT_FOUND");
1045+
assert_eq!(denied, body_json(missing).await);
1046+
assert!(!denied.to_string().contains("acme/"));
1047+
}
1048+
assert!(hidden.dispatcher.dispatched().is_empty());
1049+
assert!(absent.dispatcher.dispatched().is_empty());
1050+
}
1051+
}
1052+
1053+
#[tokio::test]
1054+
async fn configured_aliases_preserve_decision_capability_checks() {
1055+
let mut gateway = TestGateway::new(&[LOCAL_DECISION_MODEL, LOCAL_ENCODE_MODEL]).await;
1056+
gateway.add_model_alias("encode", "acme/local");
1057+
gateway.add_model_alias("missing", "acme/missing");
1058+
for (model, status) in [
1059+
("encode", StatusCode::UNPROCESSABLE_ENTITY),
1060+
("missing", StatusCode::NOT_FOUND),
1061+
] {
1062+
let response = proxy_systemone(
1063+
State(Arc::clone(&gateway.state)),
1064+
systemone_request(json!({
1065+
"model":model, "state":"Continue?", "questions":{"q":{"type":"noul"}}
1066+
})),
1067+
)
1068+
.await;
1069+
assert_eq!(response.status(), status, "{model}");
1070+
}
1071+
assert!(gateway.dispatcher.dispatched().is_empty());
1072+
}
1073+
9471074
#[tokio::test]
9481075
async fn an_undelivered_result_is_a_gateway_owned_fault() {
9491076
let gateway = TestGateway::new(&[LOCAL_DECISION_MODEL]).await;

0 commit comments

Comments
 (0)