@@ -1128,3 +1128,114 @@ mod tests {
11281128 assert_eq ! ( request. messages. as_ref( ) . unwrap( ) . len( ) , 1 ) ;
11291129 }
11301130}
1131+
1132+ #[ cfg( feature = "vision" ) ]
1133+ #[ axum:: debug_handler]
1134+ pub async fn vision (
1135+ State ( state) : State < Arc < AppState > > ,
1136+ Json ( mut req) : Json < crate :: vision:: VisionRequest > ,
1137+ ) -> impl IntoResponse {
1138+ // Extract license from environment or request
1139+ if req. license . is_none ( ) {
1140+ req. license = std:: env:: var ( "SHIMMY_LICENSE_KEY" ) . ok ( ) ;
1141+ }
1142+
1143+ // Use default vision model or specified one
1144+ let env_model = std:: env:: var ( "SHIMMY_VISION_MODEL" ) . ok ( ) ;
1145+ let model_name = req
1146+ . model
1147+ . as_deref ( )
1148+ . or ( env_model. as_deref ( ) )
1149+ . unwrap_or ( "minicpm-v" )
1150+ . to_string ( ) ;
1151+
1152+ let Some ( license_manager) = state. vision_license_manager . as_ref ( ) else {
1153+ tracing:: error!( "Vision license manager not initialized" ) ;
1154+ return (
1155+ axum:: http:: StatusCode :: INTERNAL_SERVER_ERROR ,
1156+ Json ( serde_json:: json!( {
1157+ "error" : {
1158+ "code" : "VISION_LICENSE_MANAGER_MISSING" ,
1159+ "message" : "Vision subsystem not initialized" ,
1160+ }
1161+ } ) ) ,
1162+ )
1163+ . into_response ( ) ;
1164+ } ;
1165+
1166+ fn map_vision_error_status ( message : & str ) -> axum:: http:: StatusCode {
1167+ if message. contains ( "Either image_base64 or url must be provided" ) {
1168+ return axum:: http:: StatusCode :: BAD_REQUEST ;
1169+ }
1170+ if message. starts_with ( "Failed to decode base64 image" ) {
1171+ return axum:: http:: StatusCode :: BAD_REQUEST ;
1172+ }
1173+ if message. starts_with ( "Failed to preprocess image" ) {
1174+ return axum:: http:: StatusCode :: UNPROCESSABLE_ENTITY ;
1175+ }
1176+ if message. contains ( "Vision model '" ) && message. contains ( "not available in Ollama" ) {
1177+ return axum:: http:: StatusCode :: UNPROCESSABLE_ENTITY ;
1178+ }
1179+ if message. contains ( "Failed to fetch image from URL" ) {
1180+ if message. to_lowercase ( ) . contains ( "timed out" ) {
1181+ return axum:: http:: StatusCode :: GATEWAY_TIMEOUT ;
1182+ }
1183+ return axum:: http:: StatusCode :: BAD_GATEWAY ;
1184+ }
1185+ if message. contains ( "Vision inference timed out" ) {
1186+ return axum:: http:: StatusCode :: GATEWAY_TIMEOUT ;
1187+ }
1188+ if message. contains ( "Failed to load vision model" )
1189+ || message. contains ( "Vision inference failed" )
1190+ {
1191+ return axum:: http:: StatusCode :: BAD_GATEWAY ;
1192+ }
1193+
1194+ axum:: http:: StatusCode :: INTERNAL_SERVER_ERROR
1195+ }
1196+
1197+ match crate :: vision:: process_vision_request (
1198+ req,
1199+ & model_name,
1200+ license_manager,
1201+ & state,
1202+ )
1203+ . await
1204+ {
1205+ Ok ( response) => Json ( response) . into_response ( ) ,
1206+ Err ( e) => {
1207+ // Check if it's a license error
1208+ if let Some ( license_err) = e. downcast_ref :: < crate :: vision_license:: VisionLicenseError > ( )
1209+ {
1210+ return (
1211+ license_err. to_status_code ( ) ,
1212+ Json ( license_err. to_json_error ( ) ) ,
1213+ )
1214+ . into_response ( ) ;
1215+ }
1216+
1217+ let full_message = e. to_string ( ) ;
1218+ let status = map_vision_error_status ( & full_message) ;
1219+
1220+ tracing:: error!( status = %status, "Vision processing error: {}" , full_message) ;
1221+ // Expose client error messages (4xx) to help users fix their requests.
1222+ // Hide server error details (5xx) unless running in dev mode.
1223+ let message = if status. is_client_error ( ) || std:: env:: var ( "SHIMMY_VISION_DEV_MODE" ) . is_ok ( ) {
1224+ full_message
1225+ } else {
1226+ "Vision processing error" . to_string ( )
1227+ } ;
1228+
1229+ (
1230+ status,
1231+ Json ( serde_json:: json!( {
1232+ "error" : {
1233+ "code" : "VISION_PROCESSING_ERROR" ,
1234+ "message" : message,
1235+ }
1236+ } ) ) ,
1237+ )
1238+ . into_response ( )
1239+ }
1240+ }
1241+ }
0 commit comments